@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.global.js CHANGED
@@ -21,10 +21,11 @@ var zx = (() => {
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
23
  AccountMenu: () => AccountMenu,
24
- AppRail: () => AppRail,
24
+ AppIcon: () => AppIcon,
25
25
  AppSidebar: () => AppSidebar,
26
26
  Avatar: () => Avatar,
27
27
  Breadcrumb: () => Breadcrumb,
28
+ Card: () => Card,
28
29
  Chart: () => Chart,
29
30
  ChartJsAdapter: () => ChartJsAdapter,
30
31
  CheckButton: () => CheckButton,
@@ -44,6 +45,7 @@ var zx = (() => {
44
45
  Field: () => Field,
45
46
  FieldUpload: () => FieldUpload,
46
47
  Fieldset: () => Fieldset,
48
+ Filter: () => Filter,
47
49
  Finder: () => Finder,
48
50
  Form: () => Form,
49
51
  Grid: () => Grid,
@@ -84,6 +86,7 @@ var zx = (() => {
84
86
  __: () => __,
85
87
  addDays: () => addDays,
86
88
  addMonths: () => addMonths,
89
+ appIcon: () => appIcon,
87
90
  appendCompact: () => appendCompact,
88
91
  appendCompactChild: () => appendCompactChild,
89
92
  appendContent: () => appendContent,
@@ -103,6 +106,7 @@ var zx = (() => {
103
106
  clampRange: () => clampRange,
104
107
  clampSize: () => clampSize,
105
108
  cloneChartValue: () => cloneChartValue,
109
+ cloneFilterAst: () => cloneFilterAst,
106
110
  compactElement: () => compactElement,
107
111
  configureIcons: () => configureIcons,
108
112
  copyButton: () => copyButton,
@@ -113,12 +117,16 @@ var zx = (() => {
113
117
  defineElements: () => defineElements,
114
118
  describe: () => describe,
115
119
  downloadBlob: () => downloadBlob,
120
+ emptyFilterAst: () => emptyFilterAst,
116
121
  emptyState: () => emptyState,
117
122
  escapeRegExp: () => escapeRegExp,
118
123
  faFamilies: () => faFamilies,
119
124
  faIconClasses: () => faIconClasses,
120
125
  faNames: () => faNames,
121
126
  faStyles: () => faStyles,
127
+ filterCondition: () => filterCondition,
128
+ filterGroup: () => filterGroup,
129
+ filterOperators: () => filterOperators,
122
130
  fireCompactEvent: () => fireCompactEvent,
123
131
  focusTrap: () => focusTrap,
124
132
  formatCurrency: () => formatCurrency,
@@ -158,6 +166,7 @@ var zx = (() => {
158
166
  paginationRange: () => paginationRange,
159
167
  parseCompactTag: () => parseCompactTag,
160
168
  parseDate: () => parseDate,
169
+ parseFilterAst: () => parseFilterAst,
161
170
  parseIconSpec: () => parseIconSpec,
162
171
  parseNumber: () => parseNumber,
163
172
  parseRangeText: () => parseRangeText,
@@ -169,6 +178,7 @@ var zx = (() => {
169
178
  rankLauncherItems: () => rankLauncherItems,
170
179
  registerFieldAdapters: () => registerFieldAdapters,
171
180
  registerIcons: () => registerIcons,
181
+ reorderTableRows: () => reorderTableRows,
172
182
  resolveElement: () => resolveElement,
173
183
  resolveSize: () => resolveSize,
174
184
  rovingTabindex: () => rovingTabindex,
@@ -185,6 +195,7 @@ var zx = (() => {
185
195
  stack: () => stack,
186
196
  stepPrecision: () => stepPrecision,
187
197
  storage: () => storage,
198
+ stringifyFilterAst: () => stringifyFilterAst,
188
199
  throttle: () => throttle,
189
200
  toArray: () => toArray,
190
201
  toCsv: () => toCsv,
@@ -196,6 +207,7 @@ var zx = (() => {
196
207
  uniqueBy: () => uniqueBy,
197
208
  useBuiltinIcons: () => useBuiltinIcons,
198
209
  useFontAwesome: () => useFontAwesome,
210
+ validateFilterAst: () => validateFilterAst,
199
211
  zeyosService: () => zeyosService
200
212
  });
201
213
 
@@ -274,6 +286,20 @@ var zx = (() => {
274
286
  return Boolean(value && typeof value === "object" && /** @type {{nodeType?: unknown}} */
275
287
  value.nodeType === 1);
276
288
  }
289
+ function isCssColor(value) {
290
+ if (typeof value !== "string") return false;
291
+ const text4 = value.trim();
292
+ if (!text4 || text4.length > 256 || /[;{}\u0000-\u001f\u007f]/.test(text4) || /(?:url|image|image-set|cross-fade|element|var)\s*\(/i.test(text4)) return false;
293
+ const supports = globalThis.CSS?.supports;
294
+ if (typeof supports === "function") {
295
+ try {
296
+ return supports.call(globalThis.CSS, "color", text4);
297
+ } catch {
298
+ return false;
299
+ }
300
+ }
301
+ return /^(?:#[\da-f]{3,8}|[a-z]+|(?:rgba?|hsla?|hwb|lab|lch|oklab|oklch|color|color-mix)\([^;{}]+\))$/i.test(text4);
302
+ }
277
303
  function clamp(n, min, max) {
278
304
  return Math.min(max, Math.max(min, n));
279
305
  }
@@ -468,6 +494,18 @@ var zx = (() => {
468
494
  return element;
469
495
  }
470
496
  var __ = compactElement;
497
+ function safeHref(value) {
498
+ if (typeof value !== "string") return null;
499
+ const href = value.trim();
500
+ if (!href) return null;
501
+ try {
502
+ const protocol = new URL(href, globalThis.location?.href ?? "https://zx.invalid/").protocol;
503
+ if (["javascript:", "data:", "vbscript:"].includes(protocol.toLowerCase())) return null;
504
+ } catch {
505
+ return null;
506
+ }
507
+ return href;
508
+ }
471
509
  function applyCompactProperties(element, properties) {
472
510
  for (const name in properties ?? {}) {
473
511
  const value = properties[name];
@@ -899,9 +937,9 @@ var zx = (() => {
899
937
  let rendered;
900
938
  try {
901
939
  rendered = h.scope(this.refs, () => this.render());
902
- } catch (error) {
940
+ } catch (error2) {
903
941
  this.#abort.abort();
904
- throw error;
942
+ throw error2;
905
943
  }
906
944
  if (this.#created) {
907
945
  const root = isElement(rendered) ? rendered : this.el;
@@ -1192,8 +1230,8 @@ var zx = (() => {
1192
1230
  /** @type {Readonly<FieldOptions>} */
1193
1231
  this.options
1194
1232
  ));
1195
- } catch (error) {
1196
- const message = error instanceof Error ? error.message : String(error);
1233
+ } catch (error2) {
1234
+ const message = error2 instanceof Error ? error2.message : String(error2);
1197
1235
  console.warn(`[zx.Field] Adapter "${type}" failed: ${message}`);
1198
1236
  this.adapter = errorAdapter(message, this.controlId);
1199
1237
  }
@@ -5176,8 +5214,8 @@ var zx = (() => {
5176
5214
  return response;
5177
5215
  } catch (caught) {
5178
5216
  if (runId !== this._runId) return null;
5179
- const error = caught instanceof Error ? caught : new Error(String(caught));
5180
- if (error.name === "AbortError") {
5217
+ const error2 = caught instanceof Error ? caught : new Error(String(caught));
5218
+ if (error2.name === "AbortError") {
5181
5219
  this._finishAbort();
5182
5220
  return null;
5183
5221
  }
@@ -5186,8 +5224,8 @@ var zx = (() => {
5186
5224
  this._transportController = null;
5187
5225
  this._abort.hidden = true;
5188
5226
  this._setState("error");
5189
- this._status.textContent = error.message;
5190
- this.emit("error", { error });
5227
+ this._status.textContent = error2.message;
5228
+ this.emit("error", { error: error2 });
5191
5229
  return null;
5192
5230
  }
5193
5231
  }
@@ -5335,10 +5373,10 @@ var zx = (() => {
5335
5373
  this.emit("abort", {});
5336
5374
  }
5337
5375
  /** @param {Error} error @returns {void} */
5338
- _selectionError(error) {
5376
+ _selectionError(error2) {
5339
5377
  this._setState("error");
5340
- this._status.textContent = error.message;
5341
- this.emit("error", { error });
5378
+ this._status.textContent = error2.message;
5379
+ this.emit("error", { error: error2 });
5342
5380
  }
5343
5381
  /** @returns {void} */
5344
5382
  _renderPreview() {
@@ -5421,9 +5459,9 @@ var zx = (() => {
5421
5459
  }
5422
5460
  function abortError() {
5423
5461
  if (typeof DOMException === "function") return new DOMException("Upload aborted", "AbortError");
5424
- const error = new Error("Upload aborted");
5425
- error.name = "AbortError";
5426
- return error;
5462
+ const error2 = new Error("Upload aborted");
5463
+ error2.name = "AbortError";
5464
+ return error2;
5427
5465
  }
5428
5466
 
5429
5467
  // src/components/field-upload/field-adapter.js
@@ -6549,8 +6587,8 @@ var zx = (() => {
6549
6587
  const shown = this._preview ?? this._value;
6550
6588
  const symbols = this.refs.symbols.querySelectorAll(".zx-rating__symbol");
6551
6589
  symbols.forEach((symbol, index) => {
6552
- const filled = Math.min(1, Math.max(0, shown - index));
6553
- symbol.style.setProperty("--zx-rating-fill", `${filled * 100}%`);
6590
+ const filled2 = Math.min(1, Math.max(0, shown - index));
6591
+ symbol.style.setProperty("--zx-rating-fill", `${filled2 * 100}%`);
6554
6592
  symbol.toggleAttribute("data-preview", this._preview !== null);
6555
6593
  });
6556
6594
  this.el.dataset.value = String(shown);
@@ -6818,17 +6856,33 @@ var zx = (() => {
6818
6856
  "priority.high",
6819
6857
  "priority.highest"
6820
6858
  ];
6821
- var PRIORITY_FALLBACKS = ["Lowest", "Low", "Normal", "High", "Highest"];
6859
+ var PRIORITY_FALLBACKS = ["Lowest", "Low", "Medium", "High", "Highest"];
6860
+ var DEFAULT_STATUSES = Object.freeze([
6861
+ ["backlog", "status.backlog", "Backlog", "dotted", "muted"],
6862
+ ["todo", "status.todo", "Todo", "ring", "neutral"],
6863
+ ["in-progress", "status.inProgress", "In Progress", "progress", "warning"],
6864
+ ["in-review", "status.inReview", "In Review", "review", "info"],
6865
+ ["ready", "status.ready", "Ready to Merge", "ready", "accent"],
6866
+ ["waiting", "status.waiting", "Waiting for feedback", "waiting", "warning"],
6867
+ ["done", "status.done", "Done", "done", "info"],
6868
+ ["canceled", "status.canceled", "Canceled", "canceled", "muted"],
6869
+ ["duplicate", "status.duplicate", "Duplicate", "duplicate", "muted"]
6870
+ ]);
6822
6871
  var Select = class _Select extends Component {
6823
6872
  static cssName = "select";
6824
6873
  /** @type {SelectOptions} */
6825
6874
  static defaults = {
6826
6875
  items: [],
6827
6876
  fixedItems: [],
6877
+ fixedLabel: null,
6828
6878
  valueKey: "ID",
6829
6879
  labelKey: "name",
6830
6880
  renderItem: null,
6831
6881
  renderValue: null,
6882
+ renderValueAdornment: null,
6883
+ renderNone: null,
6884
+ noneLabel: null,
6885
+ actions: [],
6832
6886
  value: null,
6833
6887
  disabled: false,
6834
6888
  placeholder: "",
@@ -6846,6 +6900,7 @@ var zx = (() => {
6846
6900
  this._visibleItems = [];
6847
6901
  this._fixedItems = [];
6848
6902
  this._visibleFixed = [];
6903
+ this._actions = [];
6849
6904
  this._selected = null;
6850
6905
  this._optionEntries = [];
6851
6906
  this._activeIndex = -1;
@@ -6857,8 +6912,7 @@ var zx = (() => {
6857
6912
  this._position = null;
6858
6913
  this._createdRoot = false;
6859
6914
  this._root = null;
6860
- this._originalChildren = [];
6861
- this._originalAttributes = /* @__PURE__ */ new Map();
6915
+ this._snapshot = null;
6862
6916
  this._createdRoot = this.el === null;
6863
6917
  const root = (
6864
6918
  /** @type {HTMLElement} */
@@ -6866,10 +6920,7 @@ var zx = (() => {
6866
6920
  );
6867
6921
  this.el = root;
6868
6922
  this._root = root;
6869
- if (!this._createdRoot) this._originalChildren = Array.from(root.childNodes);
6870
- this._rememberAttribute(root, "data-state");
6871
- this._rememberAttribute(root, "data-loading");
6872
- this._rememberAttribute(root, "aria-disabled");
6923
+ this._snapshot = this._createdRoot ? null : snapshotTarget(root);
6873
6924
  const listId = uid("zx-select-list");
6874
6925
  const input = h("input", {
6875
6926
  ref: "input",
@@ -6884,6 +6935,12 @@ var zx = (() => {
6884
6935
  placeholder: String(this.options.placeholder ?? ""),
6885
6936
  readOnly: !this.options.filter
6886
6937
  });
6938
+ const valueAdornment = h("span", {
6939
+ ref: "valueAdornment",
6940
+ class: "zx-select__value-adornment",
6941
+ ariaHidden: "true",
6942
+ hidden: true
6943
+ });
6887
6944
  const clear = h("button", {
6888
6945
  ref: "clear",
6889
6946
  class: "zx-icon-btn zx-select__clear",
@@ -6909,6 +6966,7 @@ var zx = (() => {
6909
6966
  const control = h(
6910
6967
  "div",
6911
6968
  { ref: "control", class: "zx-select__control" },
6969
+ valueAdornment,
6912
6970
  input,
6913
6971
  clear,
6914
6972
  loading,
@@ -6931,9 +6989,10 @@ var zx = (() => {
6931
6989
  this._visibleItems = this._items.slice();
6932
6990
  this._fixedItems = Array.isArray(this.options.fixedItems) ? this.options.fixedItems.slice() : [];
6933
6991
  this._visibleFixed = this._fixedItems.slice();
6992
+ this._actions = normalizeSelectActions(this.options.actions);
6934
6993
  this._disabled = Boolean(this.options.disabled);
6935
6994
  this._handleTypeahead = typeahead(
6936
- () => this._optionEntries.filter((entry) => !entry.clear).map((entry) => entry.element),
6995
+ () => this._optionEntries.filter((entry) => !entry.clear && !entry.action).map((entry) => entry.element),
6937
6996
  (_option, index) => {
6938
6997
  if (!this._open) this.open();
6939
6998
  this._setActive(this._itemEntryIndex(index));
@@ -7028,6 +7087,13 @@ var zx = (() => {
7028
7087
  this._position?.update();
7029
7088
  return this;
7030
7089
  }
7090
+ /** Replaces command rows without changing the selected value. @param {SelectAction[]} actions @returns {this} */
7091
+ setActions(actions2) {
7092
+ this._actions = normalizeSelectActions(actions2);
7093
+ this._renderOptions();
7094
+ this._position?.update();
7095
+ return this;
7096
+ }
7031
7097
  /**
7032
7098
  * Restores the configured initial value.
7033
7099
  * @param {{silent?: boolean}} [options={}] Reset behavior.
@@ -7112,24 +7178,109 @@ var zx = (() => {
7112
7178
  value: null,
7113
7179
  items: [],
7114
7180
  renderItem(item) {
7181
+ return priorityContent(item, true);
7182
+ },
7183
+ renderValueAdornment(item) {
7184
+ return priorityIndicator(item.level);
7185
+ }
7186
+ });
7187
+ select.setItems(PRIORITY_KEYS.map((key, level) => {
7188
+ let name = level === 2 ? select.msg("priority.medium") : key;
7189
+ if (name === "priority.medium") name = select._message(key, PRIORITY_FALLBACKS[level]);
7190
+ else if (level !== 2) name = select._message(key, PRIORITY_FALLBACKS[level]);
7191
+ return { ID: level, name, level };
7192
+ }));
7193
+ select.set(requestedValue, { silent: true });
7194
+ return select;
7195
+ }
7196
+ /**
7197
+ * Creates a status picker with shape- and color-distinct workflow indicators.
7198
+ * @param {Element|string|null} target Component target.
7199
+ * @param {SelectOptions & {items?: Array<Record<string, any>>}} [options={}] Select overrides and status items.
7200
+ * @returns {Select}
7201
+ */
7202
+ static status(target, options = {}) {
7203
+ const { items = null, value = null, ...rest } = options;
7204
+ const select = new _Select(target, {
7205
+ ...rest,
7206
+ value: null,
7207
+ items: [],
7208
+ renderItem(item) {
7209
+ return statusContent(item);
7210
+ },
7211
+ renderValueAdornment(item) {
7212
+ return statusIndicator(item);
7213
+ }
7214
+ });
7215
+ const statuses = Array.isArray(items) ? items.map(normalizeStatusItem).filter(Boolean) : DEFAULT_STATUSES.map(([ID, key, fallback, indicator, tone], index) => ({
7216
+ ID,
7217
+ name: select._message(key, fallback),
7218
+ indicator,
7219
+ tone,
7220
+ shortcut: String(index + 1)
7221
+ }));
7222
+ select.setItems(statuses);
7223
+ select.set(value, { silent: true });
7224
+ return select;
7225
+ }
7226
+ /**
7227
+ * Creates a rich entity picker with optional Recent choices and a create command.
7228
+ * @param {Element|string|null} target Component target.
7229
+ * @param {SelectOptions & EntitySelectOptions} [options={}] Entity-preset options.
7230
+ * @returns {Select}
7231
+ */
7232
+ static entity(target, options = {}) {
7233
+ const {
7234
+ recent = [],
7235
+ recentLabel = "Recent",
7236
+ create = false,
7237
+ iconKey = "icon",
7238
+ colorKey = "color",
7239
+ subtitleKey = "description",
7240
+ renderIcon = null,
7241
+ fixedItems = [],
7242
+ actions: actions2 = [],
7243
+ renderItem = null,
7244
+ renderValueAdornment = null,
7245
+ ...rest
7246
+ } = options;
7247
+ const visual = (item, selected = false) => {
7248
+ if (typeof renderIcon === "function") return renderIcon(item, { selected });
7249
+ const value = readSelectItem(item, iconKey);
7250
+ if (value && typeof value === "object" && typeof value.nodeType === "number") return value.cloneNode(true);
7251
+ if (typeof value !== "string" || !value) return null;
7252
+ const color = readSelectItem(item, colorKey);
7253
+ return h("span", {
7254
+ class: "zx-select__entity-glyph",
7255
+ style: isCssColor(color) ? { "--zx-select-entity-color": String(color) } : null,
7256
+ ariaHidden: "true"
7257
+ }, icon(value, { size: 14 }));
7258
+ };
7259
+ const createAction = create ? normalizeEntityCreateAction(create) : null;
7260
+ const select = new _Select(target, {
7261
+ ...rest,
7262
+ fixedItems: [...Array.isArray(recent) ? recent : [], ...Array.isArray(fixedItems) ? fixedItems : []],
7263
+ fixedLabel: Array.isArray(recent) && recent.length ? recentLabel : rest.fixedLabel ?? null,
7264
+ actions: [...Array.isArray(actions2) ? actions2 : [], ...createAction ? [createAction] : []],
7265
+ renderItem: renderItem ?? ((item) => {
7266
+ const decoration = visual(item, false);
7267
+ const subtitle = readSelectItem(item, subtitleKey);
7268
+ const copy = h(
7269
+ "span",
7270
+ { class: "zx-select__entity-copy" },
7271
+ h("span", { class: "zx-select__entity-label" }, String(readSelectItem(item, rest.labelKey ?? "name") ?? "")),
7272
+ subtitle == null || subtitle === "" ? null : h("span", { class: "zx-select__entity-subtitle" }, String(subtitle))
7273
+ );
7115
7274
  return h(
7116
7275
  "span",
7117
- { class: "zx-select__priority" },
7118
- h("span", {
7119
- class: "zx-select__priority-icon",
7120
- dataset: { level: item.level },
7121
- ariaHidden: "true"
7122
- }),
7123
- h("span", {}, String(item.name))
7276
+ { class: "zx-select__entity" },
7277
+ decoration ? h("span", { class: "zx-select__entity-icon", ariaHidden: "true" }, decoration) : null,
7278
+ copy
7124
7279
  );
7125
- }
7280
+ }),
7281
+ renderValueAdornment: renderValueAdornment ?? ((item) => visual(item, true))
7126
7282
  });
7127
- select.setItems(PRIORITY_KEYS.map((key, level) => ({
7128
- ID: level,
7129
- name: select._message(key, PRIORITY_FALLBACKS[level]),
7130
- level
7131
- })));
7132
- select.set(requestedValue, { silent: true });
7283
+ select.el.dataset.preset = "entity";
7133
7284
  return select;
7134
7285
  }
7135
7286
  /**
@@ -7144,12 +7295,21 @@ var zx = (() => {
7144
7295
  * @returns {Select}
7145
7296
  */
7146
7297
  static permission(target, options = {}) {
7147
- const { groups = [], value = true, valueKey = "ID", labelKey = "name", ...rest } = options;
7298
+ const {
7299
+ groups = [],
7300
+ value = true,
7301
+ valueKey = "ID",
7302
+ labelKey = "name",
7303
+ filter = "local",
7304
+ ...rest
7305
+ } = options;
7148
7306
  const loader = typeof groups === "function" ? groups : null;
7149
7307
  const shareable = loader ? [] : groups;
7150
7308
  const choices = [{ ID: false, name: "Private" }, { ID: true, name: "Public" }];
7151
7309
  const select = new _Select(target, {
7152
- filter: loader ?? "local",
7310
+ // An async group loader is the data source and cannot be disconnected by an incidental
7311
+ // `filter` override. Array-backed groups still honor local/disabled filtering.
7312
+ filter: loader ?? filter,
7153
7313
  ...rest,
7154
7314
  value: null,
7155
7315
  items: shareable,
@@ -7173,18 +7333,24 @@ var zx = (() => {
7173
7333
  /** @type {HTMLElement} */
7174
7334
  this.el
7175
7335
  );
7176
- if (!this._createdRoot && root) {
7177
- root.replaceChildren(...this._originalChildren);
7178
- for (const [name, value] of this._originalAttributes) {
7179
- if (value === null) root.removeAttribute(name);
7180
- else root.setAttribute(name, value);
7181
- }
7182
- }
7336
+ if (!this._createdRoot && root) restoreTarget(root, this._snapshot);
7183
7337
  super.destroy();
7184
7338
  }
7185
7339
  /** @param {KeyboardEvent} event @returns {void} */
7186
7340
  _onKeydown(event) {
7187
7341
  if (this._disabled) return;
7342
+ if (this._open && isPrintable(event)) {
7343
+ const shortcut = this._optionEntries.findIndex((entry) => !entry.clear && !entry.action && String(
7344
+ /** @type {Record<string, any>|null} */
7345
+ entry.item?.shortcut ?? ""
7346
+ ) === event.key);
7347
+ if (shortcut >= 0) {
7348
+ event.preventDefault();
7349
+ this._setActive(shortcut);
7350
+ this._chooseEntry(this._optionEntries[shortcut]);
7351
+ return;
7352
+ }
7353
+ }
7188
7354
  if (event.key === "ArrowDown") {
7189
7355
  event.preventDefault();
7190
7356
  if (!this._open) this.open();
@@ -7232,6 +7398,7 @@ var zx = (() => {
7232
7398
  /** @type {HTMLInputElement} */
7233
7399
  this.refs.input.value
7234
7400
  );
7401
+ this._syncValueAdornment();
7235
7402
  if (!this._open) this.open();
7236
7403
  if (this.options.filter === "local") {
7237
7404
  this._visibleFixed = this._matchFixed(query);
@@ -7255,6 +7422,22 @@ var zx = (() => {
7255
7422
  /** @param {SelectOptionEntry|undefined} entry @returns {void} */
7256
7423
  _chooseEntry(entry) {
7257
7424
  if (!entry) return;
7425
+ if (entry.action) {
7426
+ const detail = {
7427
+ id: entry.action.id,
7428
+ action: entry.action,
7429
+ query: (
7430
+ /** @type {HTMLInputElement} */
7431
+ this.refs.input.value
7432
+ ),
7433
+ select: this
7434
+ };
7435
+ const action = this.emit("action", detail);
7436
+ if (!action.defaultPrevented) entry.action.invoke?.(detail);
7437
+ this.close();
7438
+ this.focus();
7439
+ return;
7440
+ }
7258
7441
  this.set(entry.clear ? null : entry.item);
7259
7442
  this.close();
7260
7443
  this.focus();
@@ -7287,14 +7470,19 @@ var zx = (() => {
7287
7470
  list.replaceChildren();
7288
7471
  this._optionEntries = [];
7289
7472
  let lastGroup = /* @__PURE__ */ Symbol("initial-group");
7473
+ const fixed = this._uniqueItems(this._visibleFixed);
7474
+ const items = this._uniqueItems(this._visibleItems, fixed);
7290
7475
  if (this.options.clearable) {
7291
- this._appendOption(null, true, this.options.placeholder || this._message("select.none", "No selection"));
7476
+ this._appendOption(null, true, this.options.noneLabel ?? this._message("select.none", "No selection"));
7292
7477
  }
7293
- for (const item of this._visibleFixed) this._appendOption(item, false);
7294
- if (this._visibleFixed.length > 0 && this._visibleItems.length > 0 && !this.options.groupKey) {
7478
+ if (fixed.length > 0 && this.options.fixedLabel) {
7479
+ list.append(h("div", { class: "zx-select__group", role: "presentation" }, String(this.options.fixedLabel)));
7480
+ }
7481
+ for (const item of fixed) this._appendOption(item, false);
7482
+ if (fixed.length > 0 && items.length > 0 && !this.options.groupKey) {
7295
7483
  list.append(h("div", { class: "zx-select__separator", role: "presentation" }));
7296
7484
  }
7297
- for (const item of this._visibleItems) {
7485
+ for (const item of items) {
7298
7486
  if (this.options.groupKey) {
7299
7487
  const group = this._read(item, this.options.groupKey);
7300
7488
  if (!Object.is(group, lastGroup)) {
@@ -7307,7 +7495,23 @@ var zx = (() => {
7307
7495
  }
7308
7496
  this._appendOption(item, false);
7309
7497
  }
7310
- if (this._visibleItems.length === 0 && this._visibleFixed.length === 0 && !this._loading) {
7498
+ if (this._actions.length > 0) {
7499
+ if (fixed.length > 0 || items.length > 0 || this.options.clearable) {
7500
+ list.append(h("div", { class: "zx-select__separator", role: "presentation" }));
7501
+ }
7502
+ let actionGroup = /* @__PURE__ */ Symbol("initial-action-group");
7503
+ for (const action of this._actions) {
7504
+ if (action.group && action.group !== actionGroup) {
7505
+ list.append(h("div", {
7506
+ class: "zx-select__group",
7507
+ role: "presentation"
7508
+ }, action.group));
7509
+ actionGroup = action.group;
7510
+ }
7511
+ this._appendAction(action);
7512
+ }
7513
+ }
7514
+ if (items.length === 0 && fixed.length === 0 && this._actions.length === 0 && !this._loading) {
7311
7515
  list.append(h("div", {
7312
7516
  class: "zx-select__empty",
7313
7517
  role: "status"
@@ -7319,7 +7523,7 @@ var zx = (() => {
7319
7523
  _appendOption(item, clear, label) {
7320
7524
  const selected = clear ? this._selected === null : this._selected !== null && Object.is(this._itemValue(item), this.value);
7321
7525
  const index = this._optionEntries.length;
7322
- const content = clear ? String(label ?? "") : this._renderItem(item);
7526
+ const content = clear && typeof this.options.renderNone === "function" ? normalizeRenderedContent(this.options.renderNone(String(label ?? ""))) : clear ? String(label ?? "") : this._renderItem(item);
7323
7527
  const option = h("div", {
7324
7528
  class: "zx-select__option",
7325
7529
  id: uid("zx-select-option"),
@@ -7329,16 +7533,37 @@ var zx = (() => {
7329
7533
  }, h("span", { class: "zx-select__option-content" }, content));
7330
7534
  if (selected) option.append(icon("check", { size: 15 }));
7331
7535
  this.refs.list.append(option);
7332
- this._optionEntries.push({ item, clear, element: option });
7536
+ this._optionEntries.push({ item, clear, action: null, element: option });
7537
+ }
7538
+ /** @param {SelectAction} action @returns {void} */
7539
+ _appendAction(action) {
7540
+ const index = this._optionEntries.length;
7541
+ const visual = selectActionIcon(action.icon);
7542
+ const option = h("div", {
7543
+ class: "zx-select__option zx-select__action",
7544
+ id: uid("zx-select-option"),
7545
+ role: "option",
7546
+ ariaSelected: "false",
7547
+ dataset: { index, kind: "action" }
7548
+ }, h(
7549
+ "span",
7550
+ { class: "zx-select__option-content zx-select__action-content" },
7551
+ visual ? h("span", { class: "zx-select__action-icon", ariaHidden: "true" }, visual) : null,
7552
+ h(
7553
+ "span",
7554
+ { class: "zx-select__action-copy" },
7555
+ h("span", { class: "zx-select__action-label" }, action.label),
7556
+ action.description ? h("span", { class: "zx-select__action-description" }, action.description) : null
7557
+ )
7558
+ ));
7559
+ this.refs.list.append(option);
7560
+ this._optionEntries.push({ item: null, clear: false, action, element: option });
7333
7561
  }
7334
7562
  /** @param {SelectItem|null} item @returns {Node|string} */
7335
7563
  _renderItem(item) {
7336
7564
  if (typeof this.options.renderItem === "function") {
7337
7565
  const rendered = this.options.renderItem(item);
7338
- return rendered && typeof rendered === "object" && "nodeType" in rendered ? (
7339
- /** @type {Node} */
7340
- rendered
7341
- ) : String(rendered ?? "");
7566
+ return normalizeRenderedContent(rendered);
7342
7567
  }
7343
7568
  return this._itemLabel(item);
7344
7569
  }
@@ -7409,9 +7634,29 @@ var zx = (() => {
7409
7634
  this.refs.input
7410
7635
  );
7411
7636
  input.value = this._selected === null ? "" : typeof this.options.renderValue === "function" ? String(this.options.renderValue(this._selected) ?? "") : this._itemLabel(this._selected);
7637
+ this._syncValueAdornment();
7412
7638
  this.refs.clear.hidden = !this.options.clearable || this._selected === null;
7413
7639
  }
7414
7640
  /** @returns {void} */
7641
+ _syncValueAdornment() {
7642
+ const host = this.refs.valueAdornment;
7643
+ if (!host) return;
7644
+ const selectedLabel = this._selected === null ? "" : typeof this.options.renderValue === "function" ? String(this.options.renderValue(this._selected) ?? "") : this._itemLabel(this._selected);
7645
+ const query = String(
7646
+ /** @type {HTMLInputElement} */
7647
+ this.refs.input?.value ?? ""
7648
+ );
7649
+ if (this._selected === null || typeof this.options.renderValueAdornment !== "function" || Boolean(this.options.filter && query !== selectedLabel)) {
7650
+ host.replaceChildren();
7651
+ host.hidden = true;
7652
+ return;
7653
+ }
7654
+ const rendered = this.options.renderValueAdornment(this._selected);
7655
+ const content = normalizeRenderedContent(rendered);
7656
+ host.replaceChildren(content instanceof Node ? content : document.createTextNode(content));
7657
+ host.hidden = rendered == null || typeof content === "string" && content === "";
7658
+ }
7659
+ /** @returns {void} */
7415
7660
  _syncDisabled() {
7416
7661
  const input = (
7417
7662
  /** @type {HTMLInputElement} */
@@ -7467,6 +7712,16 @@ var zx = (() => {
7467
7712
  _matchFixed(query) {
7468
7713
  return matchItems(this._fixedItems, query, this._searchKeys());
7469
7714
  }
7715
+ /** De-duplicates choices by public value while preserving caller order. @param {SelectItem[]} items @param {SelectItem[]} [excluded=[]] @returns {SelectItem[]} */
7716
+ _uniqueItems(items, excluded = []) {
7717
+ const values = excluded.map((item) => this._itemValue(item));
7718
+ return items.filter((item) => {
7719
+ const value = this._itemValue(item);
7720
+ if (values.some((candidate) => Object.is(candidate, value))) return false;
7721
+ values.push(value);
7722
+ return true;
7723
+ });
7724
+ }
7470
7725
  /** @returns {number} */
7471
7726
  _selectedEntryIndex() {
7472
7727
  if (this._selected === null) return this.options.clearable ? 0 : -1;
@@ -7478,16 +7733,104 @@ var zx = (() => {
7478
7733
  _itemEntryIndex(itemIndex) {
7479
7734
  return itemIndex + (this.options.clearable ? 1 : 0);
7480
7735
  }
7481
- /** @param {string} name @returns {void} */
7482
- _rememberAttribute(element, name) {
7483
- this._originalAttributes.set(name, element.getAttribute(name));
7484
- }
7485
7736
  /** @param {string} key @param {string} fallback @returns {string} */
7486
7737
  _message(key, fallback) {
7487
7738
  const message = this.msg(key);
7488
7739
  return message === key ? fallback : message;
7489
7740
  }
7490
7741
  };
7742
+ function readSelectItem(item, reader) {
7743
+ if (typeof reader === "function") return reader(item);
7744
+ if (item !== null && typeof item === "object" && typeof reader === "string") return item[reader];
7745
+ return null;
7746
+ }
7747
+ function normalizeEntityCreateAction(value) {
7748
+ const descriptor = value === true ? {} : value;
7749
+ return {
7750
+ id: descriptor.id ?? "create",
7751
+ label: descriptor.label ?? "Create new\u2026",
7752
+ description: descriptor.description,
7753
+ group: descriptor.group ?? "New",
7754
+ icon: descriptor.icon ?? "plus",
7755
+ invoke: descriptor.invoke
7756
+ };
7757
+ }
7758
+ function normalizeRenderedContent(rendered) {
7759
+ return rendered && typeof rendered === "object" && "nodeType" in rendered ? (
7760
+ /** @type {Node} */
7761
+ rendered
7762
+ ) : String(rendered ?? "");
7763
+ }
7764
+ function normalizeSelectActions(actions2) {
7765
+ if (!Array.isArray(actions2)) return [];
7766
+ return actions2.filter((action) => action && typeof action === "object" && action.id != null && action.label != null).map((action) => ({
7767
+ ...action,
7768
+ label: String(action.label),
7769
+ description: action.description == null ? void 0 : String(action.description),
7770
+ group: action.group == null ? void 0 : String(action.group)
7771
+ }));
7772
+ }
7773
+ function selectActionIcon(visual) {
7774
+ if (typeof visual === "string") return icon(visual);
7775
+ if (visual instanceof Node) return visual.cloneNode(true);
7776
+ return null;
7777
+ }
7778
+ function priorityContent(item, label) {
7779
+ return h(
7780
+ "span",
7781
+ { class: "zx-select__priority" },
7782
+ priorityIndicator(item.level),
7783
+ label ? h("span", {}, String(item.name)) : null
7784
+ );
7785
+ }
7786
+ function priorityIndicator(level) {
7787
+ const normalized = Math.max(0, Math.min(4, Math.trunc(Number(level) || 0)));
7788
+ return h("span", {
7789
+ class: "zx-select__priority-icon",
7790
+ dataset: { level: normalized },
7791
+ ariaHidden: "true"
7792
+ }, Array.from({ length: 5 }, (_, index) => h("span", {
7793
+ dataset: { filled: String(index <= normalized) }
7794
+ })));
7795
+ }
7796
+ function normalizeStatusItem(input) {
7797
+ if (!input || typeof input !== "object" || input.ID == null || input.name == null) return null;
7798
+ const indicators = ["dotted", "ring", "progress", "review", "ready", "waiting", "done", "canceled", "duplicate"];
7799
+ const tones = ["muted", "neutral", "warning", "info", "accent", "success", "danger"];
7800
+ return {
7801
+ ...input,
7802
+ name: String(input.name),
7803
+ indicator: indicators.includes(String(input.indicator)) ? String(input.indicator) : "ring",
7804
+ tone: tones.includes(String(input.tone)) ? String(input.tone) : "muted",
7805
+ description: input.description == null ? "" : String(input.description),
7806
+ shortcut: input.shortcut == null ? "" : String(input.shortcut)
7807
+ };
7808
+ }
7809
+ function statusContent(item) {
7810
+ return h(
7811
+ "span",
7812
+ { class: "zx-select__status" },
7813
+ statusIndicator(item),
7814
+ h(
7815
+ "span",
7816
+ { class: "zx-select__status-copy" },
7817
+ h("span", { class: "zx-select__status-label" }, String(item.name)),
7818
+ item.description ? h("span", { class: "zx-select__status-description" }, item.description) : null
7819
+ ),
7820
+ item.shortcut ? h("kbd", { class: "zx-select__status-shortcut" }, item.shortcut) : null
7821
+ );
7822
+ }
7823
+ function statusIndicator(item) {
7824
+ const indicator = String(item.indicator ?? "ring");
7825
+ const marker = h("span", {
7826
+ class: "zx-select__status-indicator",
7827
+ dataset: { indicator, tone: String(item.tone ?? "muted") },
7828
+ ariaHidden: "true"
7829
+ });
7830
+ if (indicator === "done") marker.append(icon("check", { size: 8 }));
7831
+ if (indicator === "canceled") marker.append(icon("x", { size: 7 }));
7832
+ return marker;
7833
+ }
7491
7834
  function readItem(item, reader) {
7492
7835
  if (typeof reader === "function") return reader(item);
7493
7836
  if (item === null || typeof item !== "object") return item;
@@ -7500,10 +7843,11 @@ var zx = (() => {
7500
7843
  // src/components/select/field-adapter.js
7501
7844
  function registerSelectFieldAdapter() {
7502
7845
  Field.register("zxselect", (field, options) => {
7503
- const select = field.own(new Select(null, {
7504
- ...options.props ?? {},
7505
- value: options.value
7506
- }));
7846
+ const props = options.props ?? {};
7847
+ const preset = props.preset;
7848
+ const selectOptions = { ...props, value: options.value };
7849
+ delete selectOptions.preset;
7850
+ const select = field.own(preset === "priority" ? Select.priority(null, selectOptions) : preset === "status" ? Select.status(null, selectOptions) : new Select(null, selectOptions));
7507
7851
  field.listen(select.el, "change", (event) => event.stopImmediatePropagation());
7508
7852
  select.on("change", (event) => field.emit("change", { value: event.detail.value }));
7509
7853
  return {
@@ -7887,6 +8231,8 @@ var zx = (() => {
7887
8231
  values: [],
7888
8232
  valueKey: "ID",
7889
8233
  labelKey: "name",
8234
+ iconKey: "icon",
8235
+ colorKey: "color",
7890
8236
  searchKeys: null,
7891
8237
  filter: "local",
7892
8238
  minQuery: 0,
@@ -8227,12 +8573,13 @@ var zx = (() => {
8227
8573
  _renderTags() {
8228
8574
  const tags = this._selected.map((item) => {
8229
8575
  const label = this._labelOf(item);
8230
- const content = this.options.renderTag ? this.options.renderTag(item) : label;
8231
- const tag = h(
8232
- "span",
8233
- { class: "zx-tag-picker__tag" },
8234
- h("span", { class: "zx-tag-picker__tag-label" }, content)
8235
- );
8576
+ const color = this._colorOf(item);
8577
+ const content = this.options.renderTag ? this.options.renderTag(item) : [this._iconOf(item), h("span", { class: "zx-tag-picker__tag-label" }, label)];
8578
+ const tag = h("span", {
8579
+ class: "zx-tag-picker__tag",
8580
+ dataset: { colored: String(Boolean(color)) },
8581
+ style: color ? { "--zx-tag-color": color } : null
8582
+ }, content);
8236
8583
  if (!this._readonly && !this._disabled) {
8237
8584
  tag.append(h("button", {
8238
8585
  class: "zx-icon-btn zx-tag-picker__remove",
@@ -8270,7 +8617,7 @@ var zx = (() => {
8270
8617
  h(
8271
8618
  "span",
8272
8619
  { class: "zx-tag-picker__option-label" },
8273
- this.options.renderItem ? this.options.renderItem(item) : this._labelOf(item)
8620
+ this.options.renderItem ? this.options.renderItem(item) : [this._iconOf(item), h("span", {}, this._labelOf(item))]
8274
8621
  )
8275
8622
  );
8276
8623
  if (!selected && this.isFull()) option.setAttribute("aria-disabled", "true");
@@ -8495,10 +8842,10 @@ var zx = (() => {
8495
8842
  if (sequence !== this._requestSequence) return;
8496
8843
  this._items = Array.isArray(result) ? result.slice() : [];
8497
8844
  this._visible = this._items.slice();
8498
- } catch (error) {
8845
+ } catch (error2) {
8499
8846
  if (sequence !== this._requestSequence) return;
8500
8847
  this._visible = [];
8501
- this.emit("error", { error });
8848
+ this.emit("error", { error: error2 });
8502
8849
  } finally {
8503
8850
  if (sequence === this._requestSequence) {
8504
8851
  this._setLoading(false);
@@ -8591,6 +8938,34 @@ var zx = (() => {
8591
8938
  if (item !== null && typeof item === "object") return String(item[key] ?? "");
8592
8939
  return String(item ?? "");
8593
8940
  }
8941
+ /** @param {unknown} item @returns {Node|null} */
8942
+ _iconOf(item) {
8943
+ const value = this._readItem(item, this.options.iconKey);
8944
+ const visual = value && typeof value === "object" && typeof value.nodeType === "number" ? (
8945
+ /** @type {Node} */
8946
+ value.cloneNode(true)
8947
+ ) : typeof value === "string" && value.trim() ? icon(value, { size: 12 }) : null;
8948
+ if (!visual) return null;
8949
+ return h("span", {
8950
+ class: "zx-tag-picker__visual",
8951
+ style: this._colorOf(item) ? { "--zx-tag-color": this._colorOf(item) } : null,
8952
+ ariaHidden: "true"
8953
+ }, visual);
8954
+ }
8955
+ /** @param {unknown} item @returns {string|null} */
8956
+ _colorOf(item) {
8957
+ const value = this._readItem(item, this.options.colorKey);
8958
+ return isCssColor(value) ? value : null;
8959
+ }
8960
+ /** @param {unknown} item @param {unknown} reader @returns {unknown} */
8961
+ _readItem(item, reader) {
8962
+ if (typeof reader === "function") return reader(
8963
+ /** @type {TagItem} */
8964
+ item
8965
+ );
8966
+ if (item !== null && typeof item === "object" && typeof reader === "string") return item[reader];
8967
+ return null;
8968
+ }
8594
8969
  /** @returns {void} */
8595
8970
  _emitChange() {
8596
8971
  this.emit("change", { values: this.getValues(), items: this.getItems() });
@@ -9475,38 +9850,38 @@ var zx = (() => {
9475
9850
  try {
9476
9851
  json2 = JSON.parse(text4);
9477
9852
  } catch (cause) {
9478
- const error = (
9853
+ const error2 = (
9479
9854
  /** @type {Error & {response?: Response}} */
9480
9855
  new Error("Invalid JSON response", { cause })
9481
9856
  );
9482
- error.response = response;
9483
- throw error;
9857
+ error2.response = response;
9858
+ throw error2;
9484
9859
  }
9485
9860
  }
9486
9861
  if (!response.ok) {
9487
9862
  try {
9488
9863
  parseResult(json2);
9489
- } catch (error2) {
9490
- error2.response = response;
9491
- throw error2;
9864
+ } catch (error3) {
9865
+ error3.response = response;
9866
+ throw error3;
9492
9867
  }
9493
- const error = (
9868
+ const error2 = (
9494
9869
  /** @type {Error & {response?: Response}} */
9495
9870
  new Error(`HTTP ${response.status} ${response.statusText}`.trim())
9496
9871
  );
9497
- error.response = response;
9498
- throw error;
9872
+ error2.response = response;
9873
+ throw error2;
9499
9874
  }
9500
9875
  return parseResult(json2);
9501
9876
  } catch (caught) {
9502
- const error = caught instanceof Error ? caught : new Error(String(caught));
9877
+ const error2 = caught instanceof Error ? caught : new Error(String(caught));
9503
9878
  if (this.onError) {
9504
9879
  try {
9505
- this.onError(error, { path, options });
9880
+ this.onError(error2, { path, options });
9506
9881
  } catch {
9507
9882
  }
9508
9883
  }
9509
- throw error;
9884
+ throw error2;
9510
9885
  } finally {
9511
9886
  if (timeoutId !== null) clearTimeout(timeoutId);
9512
9887
  }
@@ -10223,9 +10598,9 @@ var zx = (() => {
10223
10598
  this.el.returnValue = "";
10224
10599
  try {
10225
10600
  this._show();
10226
- } catch (error) {
10601
+ } catch (error2) {
10227
10602
  delete this.el.dataset.zxOverlayOrder;
10228
- throw error;
10603
+ throw error2;
10229
10604
  }
10230
10605
  this.el.dataset.state = "open";
10231
10606
  this.emit("open");
@@ -10708,9 +11083,9 @@ var zx = (() => {
10708
11083
  }
10709
11084
  try {
10710
11085
  super(normalizedTarget, options);
10711
- } catch (error) {
11086
+ } catch (error2) {
10712
11087
  if (adoption) restoreAdoption(adoption);
10713
- throw error;
11088
+ throw error2;
10714
11089
  }
10715
11090
  this._adoption = adoption;
10716
11091
  }
@@ -11438,7 +11813,11 @@ var zx = (() => {
11438
11813
  rowClass: null,
11439
11814
  zebra: true,
11440
11815
  editMode: false,
11441
- hierarchy: false
11816
+ editTrigger: "double",
11817
+ hierarchy: false,
11818
+ rowReorder: false,
11819
+ columnVisibility: false,
11820
+ hiddenColumns: []
11442
11821
  };
11443
11822
  /**
11444
11823
  * Creates a table around a target or creates a new root when target is null.
@@ -11450,12 +11829,14 @@ var zx = (() => {
11450
11829
  }
11451
11830
  /** @returns {HTMLElement} Table root. */
11452
11831
  render() {
11832
+ const created = this.el === null;
11453
11833
  const root = (
11454
11834
  /** @type {HTMLElement} */
11455
11835
  this.el ?? h("div")
11456
11836
  );
11457
11837
  this.el = root;
11458
- this._originalChildren = this.el ? Array.from(this.el.childNodes) : null;
11838
+ this._createdRoot = created;
11839
+ this._original = created ? null : snapshotTarget(root);
11459
11840
  this._columns = Array.isArray(this.options.columns) ? this.options.columns.map((column) => ({ ...column })) : [];
11460
11841
  this._data = Array.isArray(this.options.data) ? [...this.options.data] : [];
11461
11842
  this._selected = /* @__PURE__ */ new Set();
@@ -11465,6 +11846,7 @@ var zx = (() => {
11465
11846
  this._rowElements = /* @__PURE__ */ new Map();
11466
11847
  this._restored = false;
11467
11848
  this._editMode = normalizeEditMode(this.options.editMode);
11849
+ this._editTrigger = this.options.editTrigger === "single" ? "single" : "double";
11468
11850
  this._edit = null;
11469
11851
  this._editMessage = null;
11470
11852
  this._stacked = false;
@@ -11476,6 +11858,10 @@ var zx = (() => {
11476
11858
  this._rovingCell = null;
11477
11859
  this._rovingMatched = false;
11478
11860
  this._hierarchy = normalizeHierarchy(this.options.hierarchy, this._columns);
11861
+ this._hiddenColumns = new Set((Array.isArray(this.options.hiddenColumns) ? this.options.hiddenColumns : []).map(String).filter((id) => this._columns.some((column) => column.id === id)));
11862
+ if (this._hiddenColumns.size >= this._columns.length && this._columns[0]) this._hiddenColumns.delete(this._columns[0].id);
11863
+ this._dragId = null;
11864
+ this._grabbedId = null;
11479
11865
  this._expanded = initialExpanded(this._hierarchy?.expanded);
11480
11866
  if (this._sort && this.options.sortMode === "local") this._sortData();
11481
11867
  if (this._hierarchy?.expanded === true) {
@@ -11497,11 +11883,17 @@ var zx = (() => {
11497
11883
  zebra: String(Boolean(this.options.zebra))
11498
11884
  }
11499
11885
  }, this._table);
11886
+ this._rowMoveMessage = this.options.rowReorder ? h("div", {
11887
+ class: "zx-table__reorder-status",
11888
+ role: "status",
11889
+ ariaLive: "polite"
11890
+ }) : null;
11500
11891
  if (this.options.height != null) {
11501
11892
  this._scroll.style.blockSize = typeof this.options.height === "number" ? `${this.options.height}px` : String(this.options.height);
11502
11893
  }
11894
+ this._columnControls = this.options.columnVisibility ? this._createColumnControls() : null;
11503
11895
  if (this._editMode === false) {
11504
- root.replaceChildren(this._scroll);
11896
+ root.replaceChildren(...[this._columnControls, this._scroll, this._rowMoveMessage].filter(Boolean));
11505
11897
  } else {
11506
11898
  root.dataset.editMode = this._editMode;
11507
11899
  this._editMessage = h("div", {
@@ -11510,7 +11902,7 @@ var zx = (() => {
11510
11902
  role: "status",
11511
11903
  ariaLive: "polite"
11512
11904
  });
11513
- root.replaceChildren(this._scroll, this._editMessage);
11905
+ root.replaceChildren(...[this._columnControls, this._scroll, this._editMessage, this._rowMoveMessage].filter(Boolean));
11514
11906
  }
11515
11907
  this._renderColumns();
11516
11908
  this._renderHeader();
@@ -11518,6 +11910,34 @@ var zx = (() => {
11518
11910
  this._watchWidth();
11519
11911
  this.listen(this._tbody, "click", (event) => this._handleBodyClick(event));
11520
11912
  this.listen(this._tbody, "dblclick", (event) => this._handleBodyDoubleClick(event));
11913
+ if (this.options.rowReorder) {
11914
+ this.listen(this._tbody, "dragstart", (event) => this._handleRowDragStart(
11915
+ /** @type {DragEvent} */
11916
+ event
11917
+ ));
11918
+ this.listen(this._tbody, "dragover", (event) => this._handleRowDragOver(
11919
+ /** @type {DragEvent} */
11920
+ event
11921
+ ));
11922
+ this.listen(this._tbody, "drop", (event) => this._handleRowDrop(
11923
+ /** @type {DragEvent} */
11924
+ event
11925
+ ));
11926
+ this.listen(this._tbody, "dragend", () => this._clearRowDrag());
11927
+ this.listen(this._tbody, "keydown", (event) => this._handleRowHandleKeydown(
11928
+ /** @type {KeyboardEvent} */
11929
+ event
11930
+ ));
11931
+ }
11932
+ if (this._columnControls) {
11933
+ this.listen(this._columnControls, "change", (event) => {
11934
+ const input = (
11935
+ /** @type {HTMLInputElement|null} */
11936
+ event.target
11937
+ );
11938
+ if (input?.classList.contains("zx-table__column-toggle")) this.setColumnVisible(input.value, input.checked);
11939
+ });
11940
+ }
11521
11941
  if (this._editMode !== false) {
11522
11942
  this.listen(this._tbody, "keydown", (event) => this._handleEditKeydown(
11523
11943
  /** @type {KeyboardEvent} */
@@ -11530,8 +11950,8 @@ var zx = (() => {
11530
11950
  }
11531
11951
  /**
11532
11952
  * Toggles a loading state. While loading, the body is dimmed, an indeterminate progress bar is
11533
- * shown, and `aria-busy` is set — a lightweight take on the busy/skeleton pattern used by
11534
- * enterprise tables (SAP UI5, Salesforce SLDS). Call `setLoading(false)` (or `setData`) when done.
11953
+ * shown, and `aria-busy` is set — a lightweight enterprise-table busy and skeleton pattern.
11954
+ * Call `setLoading(false)` (or `setData`) when done.
11535
11955
  * @param {boolean} [loading=true] Whether the table is loading.
11536
11956
  * @returns {this}
11537
11957
  */
@@ -11633,6 +12053,81 @@ var zx = (() => {
11633
12053
  getData() {
11634
12054
  return [...this._data];
11635
12055
  }
12056
+ /** Returns visible column IDs in caller order. @returns {string[]} */
12057
+ getVisibleColumns() {
12058
+ return this._visibleColumns().map((column) => column.id);
12059
+ }
12060
+ /** Returns hidden column IDs in caller order. @returns {string[]} */
12061
+ getHiddenColumns() {
12062
+ return this._columns.filter((column) => this._hiddenColumns.has(column.id)).map((column) => column.id);
12063
+ }
12064
+ /**
12065
+ * Shows or hides one column. The final visible column cannot be hidden.
12066
+ * @param {string} id Column ID.
12067
+ * @param {boolean} [visible=true] Desired visibility.
12068
+ * @param {{silent?:boolean}} [options={}] Event behavior.
12069
+ * @returns {this}
12070
+ */
12071
+ setColumnVisible(id, visible = true, options = {}) {
12072
+ const column = this._columns.find((candidate) => candidate.id === id);
12073
+ if (!column) throw new RangeError(`Unknown table column: ${id}`);
12074
+ const active = (
12075
+ /** @type {HTMLElement|null} */
12076
+ document.activeElement
12077
+ );
12078
+ const restoreFocus = active?.classList.contains("zx-table__column-toggle") && this._columnList?.contains(active) ? (
12079
+ /** @type {HTMLInputElement} */
12080
+ active.value
12081
+ ) : null;
12082
+ if (!visible && !this._hiddenColumns.has(id) && this._visibleColumns().length <= 1) {
12083
+ this._renderColumnControls(restoreFocus);
12084
+ return this;
12085
+ }
12086
+ const changed = visible ? this._hiddenColumns.delete(id) : !this._hiddenColumns.has(id);
12087
+ if (!visible && changed) this._hiddenColumns.add(id);
12088
+ if (!changed) return this;
12089
+ this._renderColumns();
12090
+ this._renderHeader();
12091
+ this._renderBody();
12092
+ this._renderColumnControls(restoreFocus);
12093
+ if (!options.silent) this.emit("columnvisibilitychange", {
12094
+ visible: this.getVisibleColumns(),
12095
+ hidden: this.getHiddenColumns()
12096
+ });
12097
+ return this;
12098
+ }
12099
+ /** Toggles one column. @param {string} id @param {{silent?:boolean}} [options={}] @returns {this} */
12100
+ toggleColumn(id, options = {}) {
12101
+ return this.setColumnVisible(id, this._hiddenColumns.has(id), options);
12102
+ }
12103
+ /**
12104
+ * Reorders one row relative to another and emits a cancelable `rowmove` before committing.
12105
+ * Hierarchical rows may only move among siblings; an active local sort disables manual order.
12106
+ * @param {unknown} id Moving row ID.
12107
+ * @param {unknown} targetId Target row ID.
12108
+ * @param {'before'|'after'} [position='before'] Target edge.
12109
+ * @returns {this}
12110
+ */
12111
+ moveRow(id, targetId, position2 = "before") {
12112
+ if (!this.options.rowReorder || this._sort && this.options.sortMode === "local") return this;
12113
+ const row2 = this.getRow(id);
12114
+ const target = this.getRow(targetId);
12115
+ if (!row2 || !target || Object.is(id, targetId)) return this;
12116
+ const next = reorderTableRows(this._data, this.options.rowId, id, targetId, position2, this._hierarchy?.parentId);
12117
+ if (!next) return this;
12118
+ const event = this.emit("rowmove", { row: row2, id, target, targetId, position: position2 });
12119
+ if (event.defaultPrevented) return this;
12120
+ this._data = next;
12121
+ this._renderBody();
12122
+ if (this._rowMoveMessage) {
12123
+ const siblings = this._siblingIds(id);
12124
+ const index = siblings.findIndex((candidate) => Object.is(candidate, id));
12125
+ this._rowMoveMessage.textContent = `Moved row to position ${index + 1} of ${siblings.length}.`;
12126
+ }
12127
+ this._emitDataChange();
12128
+ queueMicrotask(() => this._rowHandle(id)?.focus());
12129
+ return this;
12130
+ }
11636
12131
  /**
11637
12132
  * Removes all rows.
11638
12133
  * @returns {this}
@@ -11801,14 +12296,17 @@ var zx = (() => {
11801
12296
  */
11802
12297
  startEdit(rowId, columnId) {
11803
12298
  if (this._editMode === false) return this;
11804
- const column = this._columns.find((candidate) => candidate.id === columnId);
11805
- if (!column) return this;
12299
+ const requested = this._columns.find((candidate) => candidate.id === columnId);
12300
+ if (!requested) return this;
11806
12301
  const current = this._data.find((candidate) => Object.is(this._idFor(candidate), rowId));
11807
- if (current === void 0 || resolveEditable(column, current) === false) return this;
12302
+ if (current === void 0) return this;
12303
+ const column = this._editMode === "row" && this._hiddenColumns.has(requested.id) ? this._visibleColumns().find((candidate) => resolveEditable(candidate, current) !== false) ?? null : requested;
12304
+ if (!column || resolveEditable(column, current) === false) return this;
12305
+ const activeColumnId = column.id;
11808
12306
  if (this._edit) {
11809
- if (Object.is(this._edit.id, rowId) && this._edit.cells.has(columnId)) {
11810
- this._edit.columnId = columnId;
11811
- this._focusEditor(columnId);
12307
+ if (Object.is(this._edit.id, rowId) && this._edit.cells.has(activeColumnId)) {
12308
+ this._edit.columnId = activeColumnId;
12309
+ this._focusEditor(activeColumnId);
11812
12310
  return this;
11813
12311
  }
11814
12312
  this.commitEdit();
@@ -11819,7 +12317,7 @@ var zx = (() => {
11819
12317
  const row2 = this._data[index];
11820
12318
  const tr = this._rowElements.get(rowId)?.[0];
11821
12319
  if (!tr) return this;
11822
- const targets = this._editMode === "row" ? this._columns.filter((candidate) => resolveEditable(candidate, row2) !== false) : [column];
12320
+ const targets = this._editMode === "row" ? this._visibleColumns().filter((candidate) => resolveEditable(candidate, row2) !== false) : [column];
11823
12321
  const cells = /* @__PURE__ */ new Map();
11824
12322
  for (const target of targets) {
11825
12323
  const td = cellIn(tr, target.id);
@@ -11828,10 +12326,10 @@ var zx = (() => {
11828
12326
  if (entry) cells.set(target.id, entry);
11829
12327
  }
11830
12328
  if (cells.size === 0) return this;
11831
- this._edit = { id: rowId, columnId, row: row2, cells, tr };
12329
+ this._edit = { id: rowId, columnId: activeColumnId, row: row2, cells, tr };
11832
12330
  tr.dataset.editing = "true";
11833
- this.emit("editstart", { row: row2, id: rowId, column, columnId, value: row2?.[columnId] });
11834
- this._focusEditor(columnId);
12331
+ this.emit("editstart", { row: row2, id: rowId, column, columnId: activeColumnId, value: row2?.[activeColumnId] });
12332
+ this._focusEditor(activeColumnId);
11835
12333
  return this;
11836
12334
  }
11837
12335
  /**
@@ -12080,60 +12578,245 @@ var zx = (() => {
12080
12578
  this._teardownEdit();
12081
12579
  this._width?.destroy();
12082
12580
  this._width = null;
12083
- if (this.el) delete this.el.dataset.editMode;
12084
- if (!this._restored && this._originalChildren) {
12085
- this.el.replaceChildren(...this._originalChildren);
12581
+ if (!this._restored && !this._createdRoot) {
12582
+ restoreTarget(this.el, this._original);
12086
12583
  this._restored = true;
12087
12584
  }
12088
12585
  super.destroy();
12089
12586
  }
12090
- /** @returns {void} */
12091
- _renderColumns() {
12092
- const fragment2 = document.createDocumentFragment();
12093
- if (this.options.selectable === "multi") {
12094
- fragment2.append(h("col", { class: "zx-table__selection-column" }));
12095
- }
12096
- const fractions = this._columns.map((column) => parseFraction(column.width));
12097
- const total = fractions.reduce((sum, value) => sum + value, 0);
12098
- this._table.dataset.layout = total > 0 ? "fixed" : "auto";
12099
- this._columns.forEach((column, index) => {
12100
- const col = h("col");
12101
- if (fractions[index] > 0) col.style.width = `${fractions[index] / total * 100}%`;
12102
- else if (column.width && column.width !== "auto") col.style.width = String(column.width);
12103
- fragment2.append(col);
12104
- });
12105
- this._colgroup.replaceChildren(fragment2);
12587
+ /** @returns {TableColumn[]} */
12588
+ _visibleColumns() {
12589
+ return this._columns.filter((column) => !this._hiddenColumns.has(column.id));
12106
12590
  }
12107
- /** @returns {void} */
12108
- _renderHeader() {
12109
- const row2 = h("tr", { class: "zx-table__header-row" });
12110
- if (this._growStep) row2.setAttribute("aria-rowindex", "1");
12111
- if (this.options.selectable === "multi") {
12112
- this._selectAll = h("input", {
12113
- class: "zx-table__checkbox zx-table__select-all",
12114
- type: "checkbox",
12115
- ariaLabel: "Select all rows"
12116
- });
12117
- const selectionHeader = h("th", {
12118
- class: "zx-table__selection-header",
12119
- scope: "col"
12120
- }, this._selectAll);
12121
- row2.append(selectionHeader);
12122
- this.listen(this._selectAll, "click", () => {
12123
- if (this._selectAll.checked) {
12124
- for (const dataRow of this._visibleData()) this._selected.add(this._idFor(dataRow));
12125
- } else {
12126
- for (const dataRow of this._visibleData()) this._selected.delete(this._idFor(dataRow));
12127
- }
12128
- this._selectionAnchorId = null;
12129
- this._syncSelection();
12130
- this._emitSelectionChange();
12591
+ /** @returns {HTMLElement} */
12592
+ _createColumnControls() {
12593
+ const list = h("div", { class: "zx-table__column-list" });
12594
+ this._columnList = list;
12595
+ const details = h(
12596
+ "details",
12597
+ { class: "zx-table__column-controls" },
12598
+ h("summary", { class: "zx-button zx-button--sm" }, icon("fields", { size: 13 }), "Columns"),
12599
+ list
12600
+ );
12601
+ this._renderColumnControls();
12602
+ return details;
12603
+ }
12604
+ /** @param {string|null} [focusColumn=null] Column checkbox that keeps focus. @returns {void} */
12605
+ _renderColumnControls(focusColumn = null) {
12606
+ if (!this._columnList) return;
12607
+ const visible = this._visibleColumns();
12608
+ const current = [...this._columnList.querySelectorAll(".zx-table__column-toggle")];
12609
+ const reusable = current.length === this._columns.length && current.every((control, index) => (
12610
+ /** @type {HTMLInputElement} */
12611
+ control.value === this._columns[index].id
12612
+ ));
12613
+ if (reusable) {
12614
+ current.forEach((control, index) => {
12615
+ const input = (
12616
+ /** @type {HTMLInputElement} */
12617
+ control
12618
+ );
12619
+ const checked = !this._hiddenColumns.has(this._columns[index].id);
12620
+ input.checked = checked;
12621
+ input.disabled = checked && visible.length === 1;
12131
12622
  });
12132
- } else {
12133
- this._selectAll = null;
12623
+ return;
12624
+ }
12625
+ this._columnList.replaceChildren(...this._columns.map((column) => {
12626
+ const checked = !this._hiddenColumns.has(column.id);
12627
+ return h(
12628
+ "label",
12629
+ { class: "zx-table__column-option" },
12630
+ h("input", {
12631
+ class: "zx-table__column-toggle",
12632
+ type: "checkbox",
12633
+ value: column.id,
12634
+ checked,
12635
+ disabled: checked && visible.length === 1
12636
+ }),
12637
+ h("span", {}, column.label)
12638
+ );
12639
+ }));
12640
+ if (focusColumn !== null) {
12641
+ const control = [...this._columnList.querySelectorAll(".zx-table__column-toggle")].find((candidate) => (
12642
+ /** @type {HTMLInputElement} */
12643
+ candidate.value === focusColumn
12644
+ ));
12645
+ queueMicrotask(() => {
12646
+ if (control?.isConnected) control.focus();
12647
+ });
12648
+ }
12649
+ }
12650
+ /** @param {DragEvent} event @returns {void} */
12651
+ _handleRowDragStart(event) {
12652
+ const handle = (
12653
+ /** @type {Element|null} */
12654
+ event.target?.closest?.(".zx-table__row-handle")
12655
+ );
12656
+ const rowElement = handle?.closest("tr[data-row]");
12657
+ const meta = rowElement ? this._rowMeta.get(rowElement) : null;
12658
+ if (!handle || !meta || this._sort && this.options.sortMode === "local") {
12659
+ event.preventDefault();
12660
+ return;
12661
+ }
12662
+ this._dragId = meta.id;
12663
+ rowElement.dataset.dragging = "true";
12664
+ event.dataTransfer?.setData("text/plain", "zx-table-row");
12665
+ if (event.dataTransfer) event.dataTransfer.effectAllowed = "move";
12666
+ }
12667
+ /** @param {DragEvent} event @returns {void} */
12668
+ _handleRowDragOver(event) {
12669
+ if (this._dragId == null) return;
12670
+ const rowElement = (
12671
+ /** @type {Element|null} */
12672
+ event.target?.closest?.("tr[data-row]")
12673
+ );
12674
+ const meta = rowElement ? this._rowMeta.get(rowElement) : null;
12675
+ if (!meta || Object.is(meta.id, this._dragId) || !this._canMoveTogether(this._dragId, meta.id)) return;
12676
+ event.preventDefault();
12677
+ if (event.dataTransfer) event.dataTransfer.dropEffect = "move";
12678
+ this._clearDropMarkers();
12679
+ const rectangle = rowElement.getBoundingClientRect();
12680
+ rowElement.dataset.drop = event.clientY >= rectangle.top + rectangle.height / 2 ? "after" : "before";
12681
+ }
12682
+ /** @param {DragEvent} event @returns {void} */
12683
+ _handleRowDrop(event) {
12684
+ if (this._dragId == null) return;
12685
+ const rowElement = (
12686
+ /** @type {Element|null} */
12687
+ event.target?.closest?.("tr[data-row]")
12688
+ );
12689
+ const meta = rowElement ? this._rowMeta.get(rowElement) : null;
12690
+ const position2 = rowElement?.dataset.drop === "after" ? "after" : "before";
12691
+ if (meta && this._canMoveTogether(this._dragId, meta.id)) {
12692
+ event.preventDefault();
12693
+ this.moveRow(this._dragId, meta.id, position2);
12694
+ }
12695
+ this._clearRowDrag();
12696
+ }
12697
+ /** @param {KeyboardEvent} event @returns {void} */
12698
+ _handleRowHandleKeydown(event) {
12699
+ const handle = (
12700
+ /** @type {Element|null} */
12701
+ event.target?.closest?.(".zx-table__row-handle")
12702
+ );
12703
+ const rowElement = handle?.closest("tr[data-row]");
12704
+ const meta = rowElement ? this._rowMeta.get(rowElement) : null;
12705
+ if (!handle || !meta) return;
12706
+ if (event.key === " " || event.key === "Enter") {
12707
+ event.preventDefault();
12708
+ this._grabbedId = Object.is(this._grabbedId, meta.id) ? null : meta.id;
12709
+ handle.setAttribute("aria-pressed", String(this._grabbedId != null));
12710
+ const status = this._rowMoveMessage ?? this._editMessage;
12711
+ if (status) status.textContent = this._grabbedId == null ? "Row dropped" : "Row grabbed. Use the arrow keys to move it.";
12712
+ return;
12713
+ }
12714
+ if (event.key === "Escape" && this._grabbedId != null) {
12715
+ event.preventDefault();
12716
+ this._grabbedId = null;
12717
+ handle.setAttribute("aria-pressed", "false");
12718
+ return;
12719
+ }
12720
+ if (!Object.is(this._grabbedId, meta.id) || !["ArrowUp", "ArrowDown"].includes(event.key)) return;
12721
+ event.preventDefault();
12722
+ const siblings = this._siblingIds(meta.id);
12723
+ const index = siblings.findIndex((id) => Object.is(id, meta.id));
12724
+ const targetId = siblings[index + (event.key === "ArrowUp" ? -1 : 1)];
12725
+ if (targetId !== void 0) this.moveRow(meta.id, targetId, event.key === "ArrowUp" ? "before" : "after");
12726
+ }
12727
+ /** @param {unknown} id @returns {unknown[]} */
12728
+ _siblingIds(id) {
12729
+ const row2 = this.getRow(id);
12730
+ if (!row2) return [];
12731
+ const parent = this._hierarchy ? readRowValue(this._hierarchy.parentId, row2) : null;
12732
+ return this._data.filter((candidate) => !this._hierarchy || sameHierarchyParent(readRowValue(this._hierarchy.parentId, candidate), parent)).map((candidate) => this._idFor(candidate));
12733
+ }
12734
+ /** @param {unknown} left @param {unknown} right @returns {boolean} */
12735
+ _canMoveTogether(left, right) {
12736
+ if (!this._hierarchy) return true;
12737
+ const a = this.getRow(left);
12738
+ const b = this.getRow(right);
12739
+ return Boolean(a && b && sameHierarchyParent(
12740
+ readRowValue(this._hierarchy.parentId, a),
12741
+ readRowValue(this._hierarchy.parentId, b)
12742
+ ));
12743
+ }
12744
+ /** @param {unknown} id @returns {HTMLElement|null} */
12745
+ _rowHandle(id) {
12746
+ return (
12747
+ /** @type {HTMLElement|null} */
12748
+ this._rowElements.get(id)?.[0]?.querySelector(".zx-table__row-handle") ?? null
12749
+ );
12750
+ }
12751
+ /** @returns {void} */
12752
+ _clearDropMarkers() {
12753
+ for (const row2 of this._tbody.querySelectorAll("tr[data-drop]")) delete row2.dataset.drop;
12754
+ }
12755
+ /** @returns {void} */
12756
+ _clearRowDrag() {
12757
+ for (const row2 of this._tbody.querySelectorAll("tr[data-dragging]")) delete row2.dataset.dragging;
12758
+ this._clearDropMarkers();
12759
+ this._dragId = null;
12760
+ }
12761
+ /** @returns {string|null} */
12762
+ _hierarchyColumn() {
12763
+ if (!this._hierarchy) return null;
12764
+ return this._visibleColumns().some((column) => column.id === this._hierarchy.column) ? this._hierarchy.column : this._visibleColumns()[0]?.id ?? null;
12765
+ }
12766
+ /** @returns {void} */
12767
+ _renderColumns() {
12768
+ const fragment2 = document.createDocumentFragment();
12769
+ if (this.options.rowReorder) fragment2.append(h("col", { class: "zx-table__reorder-column" }));
12770
+ if (this.options.selectable === "multi") {
12771
+ fragment2.append(h("col", { class: "zx-table__selection-column" }));
12772
+ }
12773
+ const columns = this._visibleColumns();
12774
+ const fractions = columns.map((column) => parseFraction(column.width));
12775
+ const total = fractions.reduce((sum, value) => sum + value, 0);
12776
+ this._table.dataset.layout = total > 0 ? "fixed" : "auto";
12777
+ columns.forEach((column, index) => {
12778
+ const col = h("col");
12779
+ if (fractions[index] > 0) col.style.width = `${fractions[index] / total * 100}%`;
12780
+ else if (column.width && column.width !== "auto") col.style.width = String(column.width);
12781
+ fragment2.append(col);
12782
+ });
12783
+ this._colgroup.replaceChildren(fragment2);
12784
+ }
12785
+ /** @returns {void} */
12786
+ _renderHeader() {
12787
+ const row2 = h("tr", { class: "zx-table__header-row" });
12788
+ if (this._growStep) row2.setAttribute("aria-rowindex", "1");
12789
+ if (this.options.rowReorder) row2.append(h("th", {
12790
+ class: "zx-table__reorder-header",
12791
+ scope: "col",
12792
+ ariaLabel: "Row order"
12793
+ }));
12794
+ if (this.options.selectable === "multi") {
12795
+ this._selectAll = h("input", {
12796
+ class: "zx-table__checkbox zx-table__select-all",
12797
+ type: "checkbox",
12798
+ ariaLabel: "Select all rows"
12799
+ });
12800
+ const selectionHeader = h("th", {
12801
+ class: "zx-table__selection-header",
12802
+ scope: "col"
12803
+ }, this._selectAll);
12804
+ row2.append(selectionHeader);
12805
+ this.listen(this._selectAll, "click", () => {
12806
+ if (this._selectAll.checked) {
12807
+ for (const dataRow of this._visibleData()) this._selected.add(this._idFor(dataRow));
12808
+ } else {
12809
+ for (const dataRow of this._visibleData()) this._selected.delete(this._idFor(dataRow));
12810
+ }
12811
+ this._selectionAnchorId = null;
12812
+ this._syncSelection();
12813
+ this._emitSelectionChange();
12814
+ });
12815
+ } else {
12816
+ this._selectAll = null;
12134
12817
  }
12135
12818
  this._headers = /* @__PURE__ */ new Map();
12136
- for (const column of this._columns) {
12819
+ for (const column of this._visibleColumns()) {
12137
12820
  const th = h("th", { scope: "col" });
12138
12821
  const align = column.align ?? (isNumericColumn(column) ? "end" : null);
12139
12822
  if (align) th.dataset.align = align;
@@ -12173,7 +12856,7 @@ var zx = (() => {
12173
12856
  { class: "zx-table__empty-row" },
12174
12857
  h("td", {
12175
12858
  class: "zx-table__empty",
12176
- colspan: this._columns.length + (this.options.selectable === "multi" ? 1 : 0)
12859
+ colspan: this._visibleColumns().length + (this.options.selectable === "multi" ? 1 : 0) + (this.options.rowReorder ? 1 : 0)
12177
12860
  }, this._emptyContent())
12178
12861
  ));
12179
12862
  } else {
@@ -12182,6 +12865,7 @@ var zx = (() => {
12182
12865
  });
12183
12866
  }
12184
12867
  this._tbody.replaceChildren(fragment2);
12868
+ this._table.setAttribute("aria-colcount", String(this._visibleColumns().length + (this.options.selectable === "multi" ? 1 : 0) + (this.options.rowReorder ? 1 : 0)));
12185
12869
  if (this._growStep) this._table.setAttribute("aria-rowcount", String(projected.length + 1));
12186
12870
  this._syncSelectAll();
12187
12871
  this._syncGrowing();
@@ -12225,6 +12909,19 @@ var zx = (() => {
12225
12909
  const elements = this._rowElements.get(id) ?? [];
12226
12910
  elements.push(tr);
12227
12911
  this._rowElements.set(id, elements);
12912
+ if (this.options.rowReorder) {
12913
+ const handle = h("button", {
12914
+ class: "zx-table__row-handle",
12915
+ type: "button",
12916
+ draggable: true,
12917
+ dataset: { rowHandle: "" },
12918
+ ariaLabel: `Move row ${index + 1}`,
12919
+ ariaPressed: String(Object.is(this._grabbedId, id)),
12920
+ disabled: Boolean(this._sort && this.options.sortMode === "local"),
12921
+ title: this._sort && this.options.sortMode === "local" ? "Clear sorting to reorder rows" : null
12922
+ }, icon("drag", { size: 14 }));
12923
+ tr.append(h("td", { class: "zx-table__reorder-cell" }, handle));
12924
+ }
12228
12925
  if (this.options.selectable === "multi") {
12229
12926
  const checkbox = h("input", {
12230
12927
  class: "zx-table__checkbox zx-table__row-checkbox",
@@ -12234,7 +12931,7 @@ var zx = (() => {
12234
12931
  });
12235
12932
  tr.append(h("td", { class: "zx-table__selection-cell" }, checkbox));
12236
12933
  }
12237
- this._columns.forEach((column) => {
12934
+ this._visibleColumns().forEach((column) => {
12238
12935
  const cell = (
12239
12936
  /** @type {HTMLTableCellElement} */
12240
12937
  h("td")
@@ -12270,7 +12967,7 @@ var zx = (() => {
12270
12967
  const content = h("span", { class: "zx-table__cell-content" });
12271
12968
  if (value && typeof value === "object" && typeof value.nodeType === "number") content.append(value);
12272
12969
  else if (value != null) content.append(document.createTextNode(String(value)));
12273
- if (!this._hierarchy || column.id !== this._hierarchy.column || !tree) {
12970
+ if (!this._hierarchy || column.id !== this._hierarchyColumn() || !tree) {
12274
12971
  cell.replaceChildren(...content.childNodes);
12275
12972
  return;
12276
12973
  }
@@ -12301,6 +12998,7 @@ var zx = (() => {
12301
12998
  if (!rowElement || !this._tbody.contains(rowElement)) return;
12302
12999
  const meta = this._rowMeta.get(rowElement);
12303
13000
  if (!meta) return;
13001
+ if (target.closest(".zx-table__row-handle")) return;
12304
13002
  if (this._edit) {
12305
13003
  this.commitEdit();
12306
13004
  if (this._edit) return;
@@ -12320,6 +13018,14 @@ var zx = (() => {
12320
13018
  );
12321
13019
  return;
12322
13020
  }
13021
+ if (isInteractiveCellContent(target)) return;
13022
+ if (this._editMode !== false && this._editTrigger === "single") {
13023
+ const cell = target?.closest('td[data-editable="true"]');
13024
+ if (cell && this._tbody.contains(cell)) {
13025
+ this.startEdit(meta.id, String(cell.dataset.column));
13026
+ return;
13027
+ }
13028
+ }
12323
13029
  this.emit("rowclick", { ...meta, event });
12324
13030
  if (this.options.selectable === "single" && !this._selected.has(meta.id)) {
12325
13031
  this._selected = /* @__PURE__ */ new Set([meta.id]);
@@ -12340,7 +13046,8 @@ var zx = (() => {
12340
13046
  if (!rowElement || !this._tbody.contains(rowElement)) return;
12341
13047
  const meta = this._rowMeta.get(rowElement);
12342
13048
  if (!meta) return;
12343
- if (this._editMode !== false) {
13049
+ if (isInteractiveCellContent(target)) return;
13050
+ if (this._editMode !== false && this._editTrigger === "double") {
12344
13051
  const cell = target?.closest('td[data-editable="true"]');
12345
13052
  if (cell && this._tbody.contains(cell)) {
12346
13053
  this.startEdit(meta.id, String(cell.dataset.column));
@@ -12642,13 +13349,14 @@ var zx = (() => {
12642
13349
  * @returns {{id: unknown, columnId: string, index: number}|null} Next cell, or null at the end.
12643
13350
  */
12644
13351
  _nextEditableCell(rowIndex, columnId, direction) {
12645
- const columnCount = this._columns.length;
13352
+ const columns = this._visibleColumns();
13353
+ const columnCount = columns.length;
12646
13354
  if (rowIndex < 0 || columnCount === 0) return null;
12647
13355
  const visible = this._visibleData();
12648
13356
  const startingId = this._idFor(this._data[rowIndex]);
12649
13357
  let row2 = visible.findIndex((candidate) => Object.is(this._idFor(candidate), startingId));
12650
13358
  if (row2 < 0) return null;
12651
- let column = this._columns.findIndex((candidate) => candidate.id === columnId);
13359
+ let column = columns.findIndex((candidate) => candidate.id === columnId);
12652
13360
  const limit = visible.length * columnCount + columnCount;
12653
13361
  for (let step = 0; step < limit; step += 1) {
12654
13362
  column += direction;
@@ -12661,7 +13369,7 @@ var zx = (() => {
12661
13369
  }
12662
13370
  if (row2 < 0 || row2 >= visible.length) return null;
12663
13371
  const candidateRow = visible[row2];
12664
- const candidateColumn = this._columns[column];
13372
+ const candidateColumn = columns[column];
12665
13373
  if (resolveEditable(candidateColumn, candidateRow) !== false) {
12666
13374
  return {
12667
13375
  id: this._idFor(candidateRow),
@@ -12904,6 +13612,53 @@ var zx = (() => {
12904
13612
  }
12905
13613
  return result;
12906
13614
  }
13615
+ function reorderTableRows(rows, rowId, id, targetId, position2 = "before", parentId = null) {
13616
+ if (!Array.isArray(rows) || Object.is(id, targetId)) return null;
13617
+ const from = rows.findIndex((row2) => Object.is(readRowValue(rowId, row2), id));
13618
+ const target = rows.findIndex((row2) => Object.is(readRowValue(rowId, row2), targetId));
13619
+ if (from < 0 || target < 0) return null;
13620
+ if (parentId && !sameHierarchyParent(
13621
+ readRowValue(parentId, rows[from]),
13622
+ readRowValue(parentId, rows[target])
13623
+ )) return null;
13624
+ if (parentId) {
13625
+ const forest = hierarchyForest(rows, rowId, parentId);
13626
+ const movingNode = forest.nodes[from];
13627
+ const targetNode = forest.nodes[target];
13628
+ if (!movingNode || !targetNode || movingNode.parent !== targetNode.parent) return null;
13629
+ const branchNodes = (root) => {
13630
+ const result = [];
13631
+ const stack2 = [root];
13632
+ while (stack2.length) {
13633
+ const node = (
13634
+ /** @type {TableHierarchyNode} */
13635
+ stack2.pop()
13636
+ );
13637
+ result.push(node);
13638
+ for (let index = node.children.length - 1; index >= 0; index -= 1) stack2.push(node.children[index]);
13639
+ }
13640
+ return result;
13641
+ };
13642
+ const moving2 = new Set(branchNodes(movingNode).map((node) => node.index));
13643
+ const targetBranch = new Set(branchNodes(targetNode).map((node) => node.index));
13644
+ const block = rows.filter((_row, index) => moving2.has(index));
13645
+ const remaining = rows.filter((_row, index) => !moving2.has(index));
13646
+ const remainingSourceIndexes = rows.map((_row, index) => index).filter((index) => !moving2.has(index));
13647
+ let insertion = remainingSourceIndexes.findIndex((index) => index === target);
13648
+ if (position2 === "after") {
13649
+ const targetPositions = remainingSourceIndexes.map((sourceIndex, index) => targetBranch.has(sourceIndex) ? index : -1).filter((index) => index >= 0);
13650
+ insertion = Math.max(...targetPositions) + 1;
13651
+ }
13652
+ if (insertion < 0) return null;
13653
+ remaining.splice(insertion, 0, ...block);
13654
+ return remaining;
13655
+ }
13656
+ const next = [...rows];
13657
+ const [moving] = next.splice(from, 1);
13658
+ const adjustedTarget = next.findIndex((row2) => Object.is(readRowValue(rowId, row2), targetId));
13659
+ next.splice(adjustedTarget + (position2 === "after" ? 1 : 0), 0, moving);
13660
+ return next;
13661
+ }
12907
13662
  function resolveEditable(column, row2) {
12908
13663
  if (!column || typeof column !== "object") return false;
12909
13664
  if (typeof column.editor === "function") {
@@ -12984,7 +13739,8 @@ var zx = (() => {
12984
13739
  for (const node of nodes) if (!byId.has(node.id)) byId.set(node.id, node);
12985
13740
  const parentOf = /* @__PURE__ */ new Map();
12986
13741
  for (const node of nodes) {
12987
- const parent = byId.get(readRowValue(parentId, node.row)) ?? null;
13742
+ const parentValue = readRowValue(parentId, node.row);
13743
+ const parent = parentValue == null ? null : byId.get(parentValue) ?? null;
12988
13744
  parentOf.set(node, parent === node ? null : parent);
12989
13745
  }
12990
13746
  for (const node of nodes) {
@@ -13013,6 +13769,9 @@ var zx = (() => {
13013
13769
  function readRowValue(accessor, row2) {
13014
13770
  return typeof accessor === "function" ? accessor(row2) : row2?.[accessor];
13015
13771
  }
13772
+ function sameHierarchyParent(left, right) {
13773
+ return left == null && right == null || Object.is(left, right);
13774
+ }
13016
13775
  function normalizeHierarchy(option, columns) {
13017
13776
  if (!option || typeof option !== "object") return null;
13018
13777
  const parentId = option.parentId;
@@ -13032,6 +13791,23 @@ var zx = (() => {
13032
13791
  function isInsideEditor(target) {
13033
13792
  return Boolean(target?.closest?.(".zx-table__editor"));
13034
13793
  }
13794
+ function isInteractiveCellContent(target) {
13795
+ const cell = target?.closest?.("td[data-column]");
13796
+ const interactive = target?.closest?.([
13797
+ "a[href]",
13798
+ "button",
13799
+ "input",
13800
+ "select",
13801
+ "textarea",
13802
+ "summary",
13803
+ '[contenteditable="true"]',
13804
+ '[role="button"]',
13805
+ '[role="link"]',
13806
+ '[tabindex]:not([tabindex="-1"])',
13807
+ "[data-zx-table-no-edit]"
13808
+ ].join(","));
13809
+ return Boolean(cell && interactive && interactive !== cell && cell.contains(interactive));
13810
+ }
13035
13811
  function deferToEditor(target) {
13036
13812
  if (target.closest("[popover]")) return true;
13037
13813
  return target.getAttribute?.("aria-expanded") === "true";
@@ -15443,6 +16219,244 @@ var zx = (() => {
15443
16219
  ) : "start";
15444
16220
  }
15445
16221
 
16222
+ // src/components/card/card.js
16223
+ var Card = class extends Component {
16224
+ static cssName = "card";
16225
+ /** @type {Readonly<CardOptions>} */
16226
+ static defaults = {
16227
+ title: "",
16228
+ media: null,
16229
+ content: null,
16230
+ actions: [],
16231
+ footer: null,
16232
+ link: null,
16233
+ variant: "outlined",
16234
+ orientation: "vertical",
16235
+ headingLevel: 3
16236
+ };
16237
+ /**
16238
+ * Creates or enhances one card.
16239
+ * @param {Element|string|null} [target=null] Existing card container, selector, or null.
16240
+ * @param {CardOptions} [options={}] Card options.
16241
+ */
16242
+ constructor(target = null, options = {}) {
16243
+ super(target, options);
16244
+ }
16245
+ /** @returns {HTMLElement} */
16246
+ render() {
16247
+ const created = !this.el;
16248
+ const root = (
16249
+ /** @type {HTMLElement} */
16250
+ this.el ?? h("article")
16251
+ );
16252
+ this.el = root;
16253
+ this._createdRoot = created;
16254
+ this._original = created ? null : snapshotTarget(root);
16255
+ this._cleaned = false;
16256
+ this._title = "";
16257
+ this._link = null;
16258
+ this._actionAbort = null;
16259
+ this._linkAbort = null;
16260
+ const originalContent = created ? [] : Array.from(root.childNodes);
16261
+ try {
16262
+ const headingLevel = normalizeHeadingLevel(this.options.headingLevel);
16263
+ const headingId = uid("zx-card-heading");
16264
+ const heading = h(`h${headingLevel}`, {
16265
+ ref: "heading",
16266
+ class: "zx-card__heading",
16267
+ id: headingId
16268
+ });
16269
+ const media = h("div", { ref: "media", class: "zx-card__media", hidden: true });
16270
+ const actions2 = h("div", {
16271
+ ref: "actions",
16272
+ class: "zx-card__actions",
16273
+ role: "group",
16274
+ ariaLabel: "Card actions",
16275
+ hidden: true
16276
+ });
16277
+ const content = h("div", { ref: "content", class: "zx-card__content", hidden: true });
16278
+ const footer = h("footer", { ref: "footer", class: "zx-card__footer", hidden: true });
16279
+ const header = h("header", { ref: "header", class: "zx-card__header" }, heading, actions2);
16280
+ root.replaceChildren(h(
16281
+ "div",
16282
+ { class: "zx-card__layout" },
16283
+ media,
16284
+ h("div", { class: "zx-card__inner" }, header, content, footer)
16285
+ ));
16286
+ root.dataset.variant = normalizeVariant2(this.options.variant);
16287
+ root.dataset.orientation = normalizeOrientation(this.options.orientation);
16288
+ this.setTitle(this.options.title);
16289
+ this.setMedia(this.options.media);
16290
+ if (this.options.content !== null) this.setContent(this.options.content);
16291
+ else if (originalContent.length) {
16292
+ content.replaceChildren(...originalContent);
16293
+ content.hidden = false;
16294
+ }
16295
+ this.setActions(this.options.actions);
16296
+ this.setFooter(this.options.footer);
16297
+ this.setLink(this.options.link);
16298
+ return root;
16299
+ } catch (error2) {
16300
+ this._actionAbort?.abort();
16301
+ this._linkAbort?.abort();
16302
+ if (!created) restoreTarget(root, this._original);
16303
+ throw error2;
16304
+ }
16305
+ }
16306
+ /** Replaces the card title. @param {string} title Next title. @returns {this} */
16307
+ setTitle(title) {
16308
+ const next = String(title ?? "");
16309
+ if (this._link && !next.trim()) throw new TypeError("A linked Card requires a non-empty title");
16310
+ this._title = next;
16311
+ this._renderHeading();
16312
+ return this;
16313
+ }
16314
+ /** Replaces or removes the media region. @param {CardContent} content Media. @returns {this} */
16315
+ setMedia(content) {
16316
+ replaceCardContent(this.refs.media, content, "Card media");
16317
+ this.refs.media.hidden = content === null;
16318
+ return this;
16319
+ }
16320
+ /** Replaces or clears the main content. @param {CardContent} content Content. @returns {this} */
16321
+ setContent(content) {
16322
+ replaceCardContent(this.refs.content, content, "Card content");
16323
+ this.refs.content.hidden = content === null;
16324
+ return this;
16325
+ }
16326
+ /** Replaces the secondary action controls. @param {CardAction[]} actions Actions. @returns {this} */
16327
+ setActions(actions2) {
16328
+ if (!Array.isArray(actions2)) throw new TypeError("Card actions must be an array");
16329
+ const nextAbort = new AbortController();
16330
+ let controls;
16331
+ try {
16332
+ controls = actions2.map((action) => {
16333
+ if (isElement(action)) return action;
16334
+ if (!action || typeof action !== "object" || Array.isArray(action)) {
16335
+ throw new TypeError("Card actions must be Elements or button descriptors");
16336
+ }
16337
+ const descriptor = { ...action };
16338
+ const onclick = descriptor.onclick;
16339
+ delete descriptor.onclick;
16340
+ const control = button(descriptor);
16341
+ if (onclick !== void 0 && typeof onclick !== "function") {
16342
+ throw new TypeError("Card action onclick must be a function");
16343
+ }
16344
+ if (onclick) control.addEventListener("click", onclick, { signal: nextAbort.signal });
16345
+ return control;
16346
+ });
16347
+ } catch (error2) {
16348
+ nextAbort.abort();
16349
+ throw error2;
16350
+ }
16351
+ this._actionAbort?.abort();
16352
+ this._actionAbort = nextAbort;
16353
+ this.refs.actions.replaceChildren(...controls);
16354
+ this.refs.actions.hidden = controls.length === 0;
16355
+ this._syncHeader();
16356
+ return this;
16357
+ }
16358
+ /** Replaces or removes the footer. @param {CardContent} content Footer. @returns {this} */
16359
+ setFooter(content) {
16360
+ replaceCardContent(this.refs.footer, content, "Card footer");
16361
+ this.refs.footer.hidden = content === null;
16362
+ return this;
16363
+ }
16364
+ /** Sets or removes the title's native primary link. @param {CardLink} link Link. @returns {this} */
16365
+ setLink(link) {
16366
+ const next = normalizeCardLink(link);
16367
+ if (next && !this._title.trim()) throw new TypeError("A linked Card requires a non-empty title");
16368
+ this._link = next;
16369
+ this.el.dataset.linked = next ? "true" : "false";
16370
+ this._renderHeading();
16371
+ return this;
16372
+ }
16373
+ /** Restores an enhanced target exactly, or removes an owned card. @returns {void} */
16374
+ destroy() {
16375
+ if (this._cleaned) return;
16376
+ this._cleaned = true;
16377
+ this._actionAbort?.abort();
16378
+ this._linkAbort?.abort();
16379
+ super.destroy();
16380
+ if (!this._createdRoot) restoreTarget(this.el, this._original);
16381
+ }
16382
+ /** @returns {void} */
16383
+ _renderHeading() {
16384
+ const title = this._title;
16385
+ const nextAbort = new AbortController();
16386
+ const node = this._link ? h("a", {
16387
+ class: "zx-card__primary",
16388
+ href: this._link.href,
16389
+ target: this._link.target,
16390
+ rel: this._link.rel
16391
+ }, title) : h("span", { class: "zx-card__title" }, title);
16392
+ if (this._link?.onclick) node.addEventListener("click", this._link.onclick, { signal: nextAbort.signal });
16393
+ this._linkAbort?.abort();
16394
+ this._linkAbort = nextAbort;
16395
+ this.refs.heading.replaceChildren(node);
16396
+ this.refs.heading.hidden = title === "";
16397
+ if (title) this.el.setAttribute("aria-labelledby", this.refs.heading.id);
16398
+ else this.el.removeAttribute("aria-labelledby");
16399
+ this._syncHeader();
16400
+ }
16401
+ /** @returns {void} */
16402
+ _syncHeader() {
16403
+ if (!this.refs.header) return;
16404
+ this.refs.header.hidden = this._title === "" && this.refs.actions.childElementCount === 0;
16405
+ }
16406
+ };
16407
+ function replaceCardContent(host, content, label) {
16408
+ if (content === null) {
16409
+ host.replaceChildren();
16410
+ return;
16411
+ }
16412
+ if (typeof content === "string") {
16413
+ host.textContent = content;
16414
+ return;
16415
+ }
16416
+ const node = content instanceof Component ? content.toElement() : content;
16417
+ if (node instanceof Node) {
16418
+ host.replaceChildren(node);
16419
+ return;
16420
+ }
16421
+ throw new TypeError(`${label} must be a string, Node, Component, or null`);
16422
+ }
16423
+ function normalizeCardLink(value) {
16424
+ if (value === null) return null;
16425
+ const descriptor = typeof value === "string" ? { href: value } : value;
16426
+ if (!descriptor || typeof descriptor !== "object" || Array.isArray(descriptor)) {
16427
+ throw new TypeError("Card link must be a URL string, descriptor, or null");
16428
+ }
16429
+ const href = safeHref(descriptor.href);
16430
+ if (href === null) {
16431
+ throw new TypeError("Card link requires a safe non-empty href");
16432
+ }
16433
+ if (descriptor.onclick !== void 0 && typeof descriptor.onclick !== "function") {
16434
+ throw new TypeError("Card link onclick must be a function");
16435
+ }
16436
+ const target = descriptor.target == null ? void 0 : String(descriptor.target);
16437
+ let rel = descriptor.rel == null ? "" : String(descriptor.rel).trim();
16438
+ if (target === "_blank" && !/(^|\s)noopener(\s|$)/i.test(rel)) {
16439
+ rel = `${rel} noopener`.trim();
16440
+ }
16441
+ return { href, target, rel: rel || void 0, onclick: descriptor.onclick };
16442
+ }
16443
+ function normalizeVariant2(value) {
16444
+ return ["raised", "filled"].includes(String(value)) ? (
16445
+ /** @type {'raised'|'filled'} */
16446
+ value
16447
+ ) : "outlined";
16448
+ }
16449
+ function normalizeOrientation(value) {
16450
+ return value === "horizontal" ? "horizontal" : "vertical";
16451
+ }
16452
+ function normalizeHeadingLevel(value) {
16453
+ const level = Math.trunc(Number(value));
16454
+ return (
16455
+ /** @type {1|2|3|4|5|6} */
16456
+ level >= 1 && level <= 6 ? level : 3
16457
+ );
16458
+ }
16459
+
15446
16460
  // src/components/panel/panel.js
15447
16461
  var Panel = class extends Component {
15448
16462
  static cssName = "panel";
@@ -18773,6 +19787,106 @@ var zx = (() => {
18773
19787
  return value.toLocaleString();
18774
19788
  }
18775
19789
 
19790
+ // src/components/app-icon/app-icon.js
19791
+ var AppIcon = class extends Component {
19792
+ static cssName = "app-icon";
19793
+ /** @type {Readonly<AppIconOptions>} */
19794
+ static defaults = {
19795
+ icon: null,
19796
+ color: null,
19797
+ size: 36,
19798
+ iconSize: "52%",
19799
+ label: null,
19800
+ badge: null,
19801
+ selected: false,
19802
+ glass: "subtle",
19803
+ class: []
19804
+ };
19805
+ /** @returns {HTMLElement} */
19806
+ render() {
19807
+ this._createdRoot = this.el === null;
19808
+ const root = (
19809
+ /** @type {HTMLElement} */
19810
+ this.el ?? h("span")
19811
+ );
19812
+ this.el = root;
19813
+ this._snapshot = this._createdRoot ? null : snapshotTarget(root);
19814
+ this._state = { ...this.options };
19815
+ this._appliedClasses = [];
19816
+ this._sync();
19817
+ return root;
19818
+ }
19819
+ /** Updates identity or presentation without replacing the root. @param {Partial<AppIconOptions>} values @returns {this} */
19820
+ set(values = {}) {
19821
+ if (!values || typeof values !== "object") return this;
19822
+ Object.assign(this._state, values);
19823
+ this._sync();
19824
+ return this;
19825
+ }
19826
+ /** Restores an enhanced target. @returns {void} */
19827
+ destroy() {
19828
+ const root = this.el;
19829
+ const snapshot10 = this._snapshot;
19830
+ super.destroy();
19831
+ if (!this._createdRoot && root) restoreTarget(root, snapshot10);
19832
+ }
19833
+ /** @returns {void} */
19834
+ _sync() {
19835
+ const state = this._state;
19836
+ this.el.classList.remove(...this._appliedClasses);
19837
+ this._appliedClasses = [state.class].flat().flatMap((value) => String(value ?? "").split(/\s+/)).filter(Boolean);
19838
+ this.el.classList.add(...this._appliedClasses);
19839
+ const size2 = cssLength2(state.size, "36px");
19840
+ const glyphSize = cssLength2(state.iconSize, "52%");
19841
+ this.el.style.setProperty("--zx-app-icon-size", size2);
19842
+ this.el.style.setProperty("--zx-app-icon-glyph-size", glyphSize);
19843
+ if (isCssColor(state.color)) this.el.style.setProperty("--zx-app-icon-color", String(state.color));
19844
+ else this.el.style.removeProperty("--zx-app-icon-color");
19845
+ this.el.dataset.glass = normalizeGlass(state.glass);
19846
+ this.el.toggleAttribute("data-selected", Boolean(state.selected));
19847
+ const label = state.label == null ? "" : String(state.label).trim();
19848
+ if (label) {
19849
+ this.el.setAttribute("role", "img");
19850
+ this.el.setAttribute("aria-label", label);
19851
+ this.el.removeAttribute("aria-hidden");
19852
+ } else {
19853
+ this.el.removeAttribute("role");
19854
+ this.el.removeAttribute("aria-label");
19855
+ this.el.setAttribute("aria-hidden", "true");
19856
+ }
19857
+ const visual = resolveVisual(state.icon);
19858
+ const badge2 = state.badge == null || state.badge === "" ? null : h("span", {
19859
+ class: "zx-app-icon__badge",
19860
+ ariaHidden: "true"
19861
+ }, String(state.badge));
19862
+ this.el.replaceChildren(
19863
+ h("span", { class: "zx-app-icon__shine", ariaHidden: "true" }),
19864
+ h("span", { class: "zx-app-icon__glyph", ariaHidden: "true" }, visual),
19865
+ badge2
19866
+ );
19867
+ }
19868
+ };
19869
+ function appIcon(options = {}) {
19870
+ return new AppIcon(null, options).toElement();
19871
+ }
19872
+ function resolveVisual(value) {
19873
+ const resolved = typeof value === "function" ? value() : value;
19874
+ if (typeof resolved === "string") return icon(resolved, { size: "1em" });
19875
+ return resolved && typeof resolved === "object" && typeof resolved.nodeType === "number" ? (
19876
+ /** @type {Node} */
19877
+ resolved
19878
+ ) : null;
19879
+ }
19880
+ function cssLength2(value, fallback) {
19881
+ if (typeof value === "number" && Number.isFinite(value)) return `${Math.max(1, value)}px`;
19882
+ const text4 = String(value ?? "").trim();
19883
+ return text4 && !/[;{}]/.test(text4) ? text4 : fallback;
19884
+ }
19885
+ function normalizeGlass(value) {
19886
+ if (value === false || value === "none") return "none";
19887
+ return value === "strong" ? "strong" : "subtle";
19888
+ }
19889
+
18776
19890
  // src/components/launcher/launcher.js
18777
19891
  var Launcher = class extends Component {
18778
19892
  static cssName = "launcher";
@@ -18786,9 +19900,12 @@ var zx = (() => {
18786
19900
  maxResults: 100,
18787
19901
  placeholder: "Search applications and records",
18788
19902
  label: "Launcher",
19903
+ closeLabel: "Close launcher",
19904
+ resultsLabel: "Launcher results",
18789
19905
  emptyText: "No results",
18790
19906
  loadingText: "Searching\u2026",
18791
- shortcut: "mod+k"
19907
+ shortcut: "mod+k",
19908
+ hints: { move: "Move", open: "Open", close: "Close" }
18792
19909
  };
18793
19910
  /**
18794
19911
  * Creates an owned launcher dialog or enhances an existing `<dialog>` target.
@@ -18817,6 +19934,7 @@ var zx = (() => {
18817
19934
  this._sources = normalizeSources(this.options.sources);
18818
19935
  this._sourceItems = /* @__PURE__ */ new Map();
18819
19936
  this._query = String(this.options.query ?? "");
19937
+ this._renderedQuery = null;
18820
19938
  this._results = [];
18821
19939
  this._active = -1;
18822
19940
  this._request = null;
@@ -18843,7 +19961,7 @@ var zx = (() => {
18843
19961
  ref: "close",
18844
19962
  class: "zx-icon-btn zx-launcher__close",
18845
19963
  type: "button",
18846
- ariaLabel: "Close launcher"
19964
+ ariaLabel: this.options.closeLabel
18847
19965
  }, icon("x"));
18848
19966
  const status = h("div", {
18849
19967
  ref: "status",
@@ -18856,16 +19974,25 @@ var zx = (() => {
18856
19974
  class: "zx-launcher__results",
18857
19975
  id: listId,
18858
19976
  role: "listbox",
18859
- ariaLabel: "Launcher results"
19977
+ ariaLabel: this.options.resultsLabel
18860
19978
  });
19979
+ const shortcut = this.options.shortcut === "mod+k" ? h("kbd", {
19980
+ class: "zx-launcher__shortcut",
19981
+ ariaHidden: "true"
19982
+ }, launcherShortcutLabel()) : null;
19983
+ const footer = launcherHints(this.options.hints);
18861
19984
  dialog.setAttribute("aria-label", String(this.options.label));
18862
19985
  dialog.replaceChildren(
18863
19986
  h(
18864
19987
  "div",
18865
19988
  { class: "zx-launcher__surface" },
18866
- h("div", { class: "zx-launcher__search" }, icon("search"), input, close),
19989
+ h("div", {
19990
+ class: "zx-launcher__search",
19991
+ dataset: { shortcut: shortcut ? "true" : "false" }
19992
+ }, icon("search"), input, shortcut, close),
18867
19993
  status,
18868
- results
19994
+ results,
19995
+ footer
18869
19996
  )
18870
19997
  );
18871
19998
  dialog.dataset.state = "closed";
@@ -18889,6 +20016,17 @@ var zx = (() => {
18889
20016
  if (!option || !results.contains(option)) return;
18890
20017
  this._activate(Number(option.getAttribute("data-launcher-index")), event);
18891
20018
  });
20019
+ this.listen(results, "pointermove", (event) => {
20020
+ const option = (
20021
+ /** @type {Element|null} */
20022
+ event.target?.closest?.("[data-launcher-index]")
20023
+ );
20024
+ if (!option || !results.contains(option)) return;
20025
+ const index = Number(option.getAttribute("data-launcher-index"));
20026
+ if (!Number.isInteger(index) || this._results[index]?.disabled || index === this._active) return;
20027
+ this._active = index;
20028
+ this._syncActive();
20029
+ });
18892
20030
  this.listen(dialog, "cancel", (event) => {
18893
20031
  event.preventDefault();
18894
20032
  this.close();
@@ -18958,8 +20096,15 @@ var zx = (() => {
18958
20096
  this._renderResults();
18959
20097
  return this;
18960
20098
  }
18961
- /** Replaces asynchronous sources and aborts work from the old set. @param {LauncherSource[]} sources @returns {this} */
18962
- setSources(sources) {
20099
+ /** Marks one local item as the current destination. @param {string|number|null} id Item id. @returns {this} */
20100
+ setCurrent(id) {
20101
+ const key = id == null ? null : String(id);
20102
+ this._items = this._items.map((item) => ({ ...item, current: key !== null && String(item.id) === key }));
20103
+ this._renderResults();
20104
+ return this;
20105
+ }
20106
+ /** Replaces asynchronous sources and aborts work from the old set. @param {LauncherSource[]} sources @returns {this} */
20107
+ setSources(sources) {
18963
20108
  this._abortRequest();
18964
20109
  this._sources = normalizeSources(sources);
18965
20110
  this._sourceItems.clear();
@@ -18993,7 +20138,7 @@ var zx = (() => {
18993
20138
  async _loadSources() {
18994
20139
  this._queryTimer = null;
18995
20140
  const query = this._query.trim();
18996
- const sources = this._sources.filter((source) => query.length >= Math.max(Number(this.options.minQuery) || 0, Number(source.minQuery) || 0));
20141
+ const sources = this._sources.filter((source) => visibleForQuery(source.when, query) && query.length >= Math.max(Number(this.options.minQuery) || 0, Number(source.minQuery) || 0));
18997
20142
  if (sources.length === 0) return;
18998
20143
  const request = new AbortController();
18999
20144
  const sequence = ++this._requestSequence;
@@ -19003,15 +20148,17 @@ var zx = (() => {
19003
20148
  try {
19004
20149
  const loaded = await source.load(query, { signal: request.signal });
19005
20150
  if (request.signal.aborted || sequence !== this._requestSequence) return;
19006
- this._sourceItems.set(source.id, normalizeItems2(loaded).map((item) => ({
20151
+ this._sourceItems.set(source.id, normalizeItems2(loaded).map((item, itemOrder) => ({
19007
20152
  ...item,
19008
20153
  _source: source.id,
19009
- group: item.group ?? source.label ?? ""
20154
+ _sourceOrder: source.order === "source",
20155
+ group: item.group ?? source.label ?? "",
20156
+ itemOrder: item.itemOrder ?? (source.order === "source" ? itemOrder : void 0)
19010
20157
  })));
19011
20158
  this._renderResults();
19012
- } catch (error) {
20159
+ } catch (error2) {
19013
20160
  if (request.signal.aborted || sequence !== this._requestSequence) return;
19014
- this.emit("error", { source, error });
20161
+ this.emit("error", { source, error: error2 });
19015
20162
  }
19016
20163
  }));
19017
20164
  if (request.signal.aborted || sequence !== this._requestSequence) return;
@@ -19035,23 +20182,36 @@ var zx = (() => {
19035
20182
  }
19036
20183
  /** @returns {void} */
19037
20184
  _renderResults() {
19038
- const remote = orderedLauncherSourceItems(this._sources, this._sourceItems);
20185
+ const queryKey = this._query;
20186
+ const activeId = this._renderedQuery === queryKey && this._active >= 0 && this._results[this._active] ? String(this._results[this._active].id) : null;
20187
+ const query = this._query.trim();
20188
+ const local = this._items.filter((item) => visibleForQuery(item.when, query));
20189
+ const rankedLocal = rankLauncherItems(local, query, this.options.maxResults);
20190
+ const remote = this._sources.flatMap((source) => {
20191
+ const loaded = this._sourceItems.get(String(source.id)) ?? [];
20192
+ return source.order === "source" ? loaded.filter((item) => visibleForQuery(item.when, query)) : rankLauncherItems(
20193
+ loaded.filter((item) => visibleForQuery(item.when, query)),
20194
+ query,
20195
+ this.options.maxResults
20196
+ );
20197
+ });
19039
20198
  const seen = /* @__PURE__ */ new Set();
19040
- const combined = [...this._items.map((item) => ({ ...item, _source: null })), ...remote].filter((item) => {
20199
+ const combined = [...rankedLocal.map((item) => ({ ...item, _source: null })), ...remote].filter((item) => {
19041
20200
  const key = String(item.id);
19042
20201
  if (seen.has(key)) return false;
19043
20202
  seen.add(key);
19044
20203
  return true;
19045
- });
19046
- const ranked = rankLauncherItems(combined, this._query, this.options.maxResults);
20204
+ }).slice(0, Math.max(0, Number(this.options.maxResults) || 0));
19047
20205
  const groups = /* @__PURE__ */ new Map();
19048
- for (const item of ranked) {
20206
+ for (const item of combined) {
19049
20207
  const group = String(item.group ?? "");
19050
20208
  if (!groups.has(group)) groups.set(group, []);
19051
20209
  groups.get(group).push(item);
19052
20210
  }
19053
20211
  this._results = [...groups.values()].flat();
19054
- this._active = this._results.findIndex((item) => !item.disabled);
20212
+ this._renderedQuery = queryKey;
20213
+ const preserved = activeId === null ? -1 : this._results.findIndex((item) => !item.disabled && String(item.id) === activeId);
20214
+ this._active = preserved >= 0 ? preserved : this._results.findIndex((item) => !item.disabled);
19055
20215
  const nodes = [];
19056
20216
  let index = 0;
19057
20217
  for (const [group, items] of groups) {
@@ -19059,7 +20219,8 @@ var zx = (() => {
19059
20219
  if (group) nodes.push(h("div", {
19060
20220
  class: "zx-launcher__group",
19061
20221
  role: "group",
19062
- ariaLabel: group
20222
+ ariaLabel: group,
20223
+ dataset: { layout: items.every((item) => item.kind === "application") ? "grid" : "list" }
19063
20224
  }, h("div", { class: "zx-launcher__group-label", ariaHidden: "true" }, group), options));
19064
20225
  else nodes.push(...options);
19065
20226
  }
@@ -19069,9 +20230,14 @@ var zx = (() => {
19069
20230
  }
19070
20231
  /** @param {RankedLauncherItem} item @param {number} index @returns {HTMLElement} */
19071
20232
  _option(item, index) {
19072
- const tag = item.href ? "a" : "button";
20233
+ const href = safeHref(item.href);
20234
+ const tag = href ? "a" : "button";
19073
20235
  const children = [];
19074
- if (item.icon) children.push(h("span", { class: "zx-launcher__icon" }, icon(item.icon)));
20236
+ const visual = launcherItemIcon(item);
20237
+ if (visual) children.push(h("span", {
20238
+ class: "zx-launcher__icon",
20239
+ ariaHidden: "true"
20240
+ }, visual));
19075
20241
  children.push(h(
19076
20242
  "span",
19077
20243
  { class: "zx-launcher__copy" },
@@ -19084,14 +20250,20 @@ var zx = (() => {
19084
20250
  h(tag, {
19085
20251
  class: "zx-launcher__option",
19086
20252
  type: tag === "button" ? "button" : null,
19087
- href: item.href ?? null,
20253
+ href,
19088
20254
  target: item.target ?? null,
20255
+ rel: tag === "a" ? "noopener" : null,
19089
20256
  role: "option",
19090
20257
  id: uid("zx-launcher-option"),
19091
20258
  tabIndex: -1,
19092
20259
  ariaSelected: "false",
19093
20260
  ariaDisabled: item.disabled ? "true" : null,
19094
- dataset: { launcherIndex: index, pinned: item.pinned ? "true" : null }
20261
+ ariaCurrent: item.current ? "page" : null,
20262
+ dataset: {
20263
+ launcherIndex: index,
20264
+ pinned: item.pinned !== void 0 && item.pinned !== false ? "true" : null,
20265
+ kind: normalizeLauncherKind(item.kind)
20266
+ }
19095
20267
  }, children)
19096
20268
  );
19097
20269
  }
@@ -19128,6 +20300,17 @@ var zx = (() => {
19128
20300
  }
19129
20301
  const enabled = this._results.map((item, index) => item.disabled ? -1 : index).filter((index) => index >= 0);
19130
20302
  if (enabled.length === 0) return;
20303
+ if (/^Arrow(?:Left|Right|Up|Down)$/.test(event.key)) {
20304
+ const next = this._spatialIndex(event.key, enabled);
20305
+ if (next !== null) {
20306
+ event.preventDefault();
20307
+ this._active = next;
20308
+ this._syncActive();
20309
+ this.refs.results.querySelector(`[data-launcher-index="${this._active}"]`)?.scrollIntoView({ block: "nearest", inline: "nearest" });
20310
+ return;
20311
+ }
20312
+ if (event.key === "ArrowLeft" || event.key === "ArrowRight") return;
20313
+ }
19131
20314
  let position2 = enabled.indexOf(this._active);
19132
20315
  if (event.key === "Home" || event.key === "PageUp") position2 = 0;
19133
20316
  else if (event.key === "End" || event.key === "PageDown") position2 = enabled.length - 1;
@@ -19139,6 +20322,15 @@ var zx = (() => {
19139
20322
  this._syncActive();
19140
20323
  this.refs.results.querySelector(`[data-launcher-index="${this._active}"]`)?.scrollIntoView({ block: "nearest" });
19141
20324
  }
20325
+ /** @param {string} direction @param {number[]} enabled @returns {number|null} */
20326
+ _spatialIndex(direction, enabled) {
20327
+ const current = this.refs.results.querySelector(`[data-launcher-index="${this._active}"]`);
20328
+ if (!(current instanceof HTMLElement)) return null;
20329
+ if ((direction === "ArrowLeft" || direction === "ArrowRight") && this._results[this._active]?.kind !== "application") return null;
20330
+ 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"));
20331
+ const next = findSpatialLauncherOption(candidates, current, direction);
20332
+ return next ? Number(next.getAttribute("data-launcher-index")) : null;
20333
+ }
19142
20334
  /** @param {number} index @param {Event} event @returns {void} */
19143
20335
  _activate(index, event) {
19144
20336
  const item = this._results[index];
@@ -19164,25 +20356,103 @@ var zx = (() => {
19164
20356
  }
19165
20357
  /** @param {KeyboardEvent} event @returns {void} */
19166
20358
  _onDocumentKeydown(event) {
19167
- if (this.options.shortcut !== "mod+k" || event.isComposing || event.key.toLowerCase() !== "k" || !event.metaKey && !event.ctrlKey || event.altKey) return;
20359
+ if (this.options.shortcut !== "mod+k" || event.isComposing || event.key.toLowerCase() !== "k" || Number(event.metaKey) + Number(event.ctrlKey) !== 1 || event.altKey || event.shiftKey) return;
19168
20360
  const active = document.activeElement;
19169
- if (active instanceof HTMLInputElement || active instanceof HTMLTextAreaElement || active instanceof HTMLElement && active.isContentEditable) return;
20361
+ if (active instanceof HTMLInputElement || active instanceof HTMLTextAreaElement || active instanceof HTMLSelectElement || active instanceof HTMLElement && active.isContentEditable) return;
19170
20362
  const modal = document.querySelector("dialog:modal");
19171
20363
  if (modal && modal !== this.el) return;
19172
20364
  event.preventDefault();
19173
20365
  this.open();
19174
20366
  }
19175
20367
  };
20368
+ function launcherShortcutLabel() {
20369
+ const platform = String(
20370
+ /** @type {any} */
20371
+ navigator.userAgentData?.platform ?? navigator.platform ?? ""
20372
+ );
20373
+ return /mac|iphone|ipad/i.test(platform) ? "\u2318 K" : "Ctrl K";
20374
+ }
20375
+ function launcherHints(hints) {
20376
+ if (hints === false) return null;
20377
+ const labels = hints && typeof hints === "object" ? hints : {};
20378
+ return h(
20379
+ "footer",
20380
+ { class: "zx-launcher__hints", ariaHidden: "true" },
20381
+ h("span", {}, h("kbd", {}, "\u2191\u2193\u2190\u2192"), labels.move ?? "Move"),
20382
+ h("span", {}, h("kbd", {}, "\u21B5"), labels.open ?? "Open"),
20383
+ h("span", {}, h("kbd", {}, "Esc"), labels.close ?? "Close")
20384
+ );
20385
+ }
20386
+ function launcherItemIcon(item) {
20387
+ let visual = item.icon;
20388
+ if (typeof visual === "function") visual = visual(item);
20389
+ if (typeof visual === "string") return icon(visual);
20390
+ if (visual instanceof Node) return visual.cloneNode(true);
20391
+ if (visual && typeof visual === "object" && typeof /** @type {any} */
20392
+ visual.toElement === "function") {
20393
+ const element = (
20394
+ /** @type {any} */
20395
+ visual.toElement()
20396
+ );
20397
+ return element instanceof Node ? element.cloneNode(true) : null;
20398
+ }
20399
+ return null;
20400
+ }
20401
+ function normalizeLauncherKind(kind) {
20402
+ return kind === "application" || kind === "record" ? kind : "action";
20403
+ }
20404
+ function visibleForQuery(when, query) {
20405
+ if (when === "empty") return query === "";
20406
+ if (when === "query") return query !== "";
20407
+ return true;
20408
+ }
20409
+ function findSpatialLauncherOption(options, current, direction) {
20410
+ const origin = current.getBoundingClientRect();
20411
+ if (origin.width === 0 && origin.height === 0) return null;
20412
+ const originX = origin.left + origin.width / 2;
20413
+ const originY = origin.top + origin.height / 2;
20414
+ let best = null;
20415
+ let bestScore = Infinity;
20416
+ for (const option of options) {
20417
+ if (option === current) continue;
20418
+ const rect = option.getBoundingClientRect();
20419
+ const dx = rect.left + rect.width / 2 - originX;
20420
+ const dy = rect.top + rect.height / 2 - originY;
20421
+ let valid = false;
20422
+ let score = Infinity;
20423
+ if (direction === "ArrowLeft") {
20424
+ valid = dx < -2 && Math.abs(dy) < Math.max(origin.height, rect.height) / 2;
20425
+ score = Math.abs(dx) + Math.abs(dy) * 4;
20426
+ } else if (direction === "ArrowRight") {
20427
+ valid = dx > 2 && Math.abs(dy) < Math.max(origin.height, rect.height) / 2;
20428
+ score = Math.abs(dx) + Math.abs(dy) * 4;
20429
+ } else if (direction === "ArrowUp") {
20430
+ valid = dy < -2;
20431
+ score = Math.abs(dy) * 10 + Math.abs(dx);
20432
+ } else if (direction === "ArrowDown") {
20433
+ valid = dy > 2;
20434
+ score = Math.abs(dy) * 10 + Math.abs(dx);
20435
+ }
20436
+ if (valid && score < bestScore) {
20437
+ best = option;
20438
+ bestScore = score;
20439
+ }
20440
+ }
20441
+ return best;
20442
+ }
19176
20443
  function rankLauncherItems(items, query, limit = 100) {
19177
20444
  const needle = searchable(query).trim();
19178
20445
  const ranked = items.map((item, order) => ({ item, order, score: launcherScore(item, needle) })).filter(({ score }) => score < Infinity);
19179
20446
  ranked.sort((a, b) => {
20447
+ const aGroup = finiteOrder(a.item.groupOrder);
20448
+ const bGroup = finiteOrder(b.item.groupOrder);
20449
+ if (aGroup !== bGroup) return aGroup - bGroup;
19180
20450
  if (needle === "") {
19181
20451
  const aPinned = pinOrder(a.item.pinned, a.order);
19182
20452
  const bPinned = pinOrder(b.item.pinned, b.order);
19183
20453
  if (aPinned !== bPinned) return aPinned - bPinned;
19184
20454
  }
19185
- return a.score - b.score || compareLauncherText(a.item.label, b.item.label) || a.order - b.order;
20455
+ return a.score - b.score || finiteOrder(a.item.itemOrder) - finiteOrder(b.item.itemOrder) || compareLauncherText(a.item.label, b.item.label) || a.order - b.order;
19186
20456
  });
19187
20457
  const maximum = Math.max(0, Number(limit) || 0);
19188
20458
  return ranked.slice(0, maximum || 0).map(({ item, score }) => ({ ...item, _score: score }));
@@ -19206,6 +20476,9 @@ var zx = (() => {
19206
20476
  if (pinned) return order;
19207
20477
  return Number.MAX_SAFE_INTEGER;
19208
20478
  }
20479
+ function finiteOrder(value) {
20480
+ return typeof value === "number" && Number.isFinite(value) ? value : Number.MAX_SAFE_INTEGER;
20481
+ }
19209
20482
  function searchable(value) {
19210
20483
  return String(value ?? "").normalize("NFKD").replace(/\p{M}/gu, "").toLowerCase();
19211
20484
  }
@@ -19220,15 +20493,23 @@ var zx = (() => {
19220
20493
  }
19221
20494
  function normalizeItems2(items) {
19222
20495
  if (!Array.isArray(items)) return [];
19223
- return items.filter((item) => item && typeof item === "object" && item.id != null && item.label != null).map((item) => ({ ...item, label: String(item.label) }));
20496
+ return items.filter((item) => item && typeof item === "object" && item.id != null && item.label != null).map((item) => normalizeLauncherItem(item));
20497
+ }
20498
+ function normalizeLauncherItem(item) {
20499
+ const normalized = { ...item, label: String(item.label) };
20500
+ if (item.href == null) return normalized;
20501
+ const href = safeHref(item.href);
20502
+ if (href !== null) normalized.href = href;
20503
+ else {
20504
+ delete normalized.href;
20505
+ if (typeof normalized.invoke !== "function") normalized.disabled = true;
20506
+ }
20507
+ return normalized;
19224
20508
  }
19225
20509
  function normalizeSources(sources) {
19226
20510
  if (!Array.isArray(sources)) return [];
19227
20511
  return sources.filter((source) => source && typeof source === "object" && source.id != null && typeof source.load === "function").map((source) => ({ ...source, id: String(source.id) }));
19228
20512
  }
19229
- function orderedLauncherSourceItems(sources, sourceItems) {
19230
- return sources.flatMap((source) => sourceItems.get(String(source.id)) ?? []);
19231
- }
19232
20513
 
19233
20514
  // src/components/avatar/avatar.js
19234
20515
  var Avatar = class extends Component {
@@ -19647,7 +20928,7 @@ var zx = (() => {
19647
20928
  this.#content = this.options.content;
19648
20929
  this.#disabled = Boolean(this.options.disabled);
19649
20930
  this.#describedBy = resolved.getAttribute("aria-describedby");
19650
- this.el.style.setProperty("--zx-tooltip-max-inline-size", cssLength2(this.options.maxWidth));
20931
+ this.el.style.setProperty("--zx-tooltip-max-inline-size", cssLength3(this.options.maxWidth));
19651
20932
  const trigger = TRIGGERS.has(this.options.trigger) ? this.options.trigger : "both";
19652
20933
  if (trigger === "manual") return;
19653
20934
  if (trigger === "hover" || trigger === "both") {
@@ -19905,7 +21186,7 @@ var zx = (() => {
19905
21186
  if (!tokens.includes(token)) tokens.push(token);
19906
21187
  return tokens.join(" ");
19907
21188
  }
19908
- function cssLength2(value) {
21189
+ function cssLength3(value) {
19909
21190
  return typeof value === "number" && Number.isFinite(value) ? `${value}px` : String(value);
19910
21191
  }
19911
21192
  function isFocusVisible(element) {
@@ -19916,10 +21197,10 @@ var zx = (() => {
19916
21197
  }
19917
21198
  }
19918
21199
 
19919
- // src/components/app-rail/app-rail.js
19920
- var AppRail = class extends Component {
21200
+ // src/internal/app-rail.js
21201
+ var AppRailPresenter = class extends Component {
19921
21202
  static cssName = "app-rail";
19922
- /** @type {Readonly<AppRailOptions>} */
21203
+ /** @type {Readonly<AppRailPresenterOptions>} */
19923
21204
  static defaults = {
19924
21205
  items: [],
19925
21206
  active: null,
@@ -19929,12 +21210,13 @@ var zx = (() => {
19929
21210
  header: null,
19930
21211
  footer: null,
19931
21212
  openDelay: 80,
19932
- closeDelay: 160
21213
+ closeDelay: 160,
21214
+ renderIcon: null
19933
21215
  };
19934
21216
  /**
19935
21217
  * Creates or enhances a rail host.
19936
21218
  * @param {Element|string|null} target Rail target.
19937
- * @param {AppRailOptions} [options={}] Rail options.
21219
+ * @param {AppRailPresenterOptions} [options={}] Rail options.
19938
21220
  */
19939
21221
  constructor(target = null, options = {}) {
19940
21222
  super(target, options);
@@ -20084,7 +21366,14 @@ var zx = (() => {
20084
21366
  index: String(item.id),
20085
21367
  iconOnly: true,
20086
21368
  disclosure: hasChildren,
20087
- disclosureIcon: railDisclosureIcon(this.el.dataset.side)
21369
+ disclosureIcon: railDisclosureIcon(this.el.dataset.side),
21370
+ visual: appNavIcon(item, this.options.renderIcon, {
21371
+ orientation: this.el.dataset.orientation,
21372
+ collapsed: true,
21373
+ side: this.el.dataset.side,
21374
+ level: 1,
21375
+ location: "root"
21376
+ })
20088
21377
  });
20089
21378
  const row2 = h("li", { class: "zx-app-rail__row" }, control);
20090
21379
  this.refs.list.append(row2);
@@ -20125,7 +21414,14 @@ var zx = (() => {
20125
21414
  index: childKey,
20126
21415
  iconOnly: false,
20127
21416
  disclosure: hasChildren,
20128
- disclosureIcon: nestedDisclosureIcon(this.el.dataset.side)
21417
+ disclosureIcon: nestedDisclosureIcon(this.el.dataset.side),
21418
+ visual: appNavIcon(child, this.options.renderIcon, {
21419
+ orientation: this.el.dataset.orientation,
21420
+ collapsed: true,
21421
+ side: this.el.dataset.side,
21422
+ level: childKey.split("/").length,
21423
+ location: "flyout"
21424
+ })
20129
21425
  });
20130
21426
  list.append(h("li", {}, control));
20131
21427
  if (hasChildren) this._buildFlyout(child, control, childKey, key);
@@ -20179,7 +21475,6 @@ var zx = (() => {
20179
21475
  this._interaction.listen(trigger, "click", (event) => {
20180
21476
  event.preventDefault();
20181
21477
  if (event instanceof MouseEvent && event.detail === 0) this.openFlyout(key, { focus: true });
20182
- else if (dropdown.isOpen()) this.closeFlyout(key);
20183
21478
  else this.openFlyout(key);
20184
21479
  });
20185
21480
  this._interaction.listen(actualPanel, "pointerenter", () => this._cancelFlyoutTimers(flyout, true));
@@ -20434,14 +21729,43 @@ var zx = (() => {
20434
21729
  };
20435
21730
  function normalizeAppItems(items) {
20436
21731
  if (!Array.isArray(items)) return [];
20437
- return items.filter((item) => item && typeof item === "object" && item.id != null && item.label != null).map((item) => ({
20438
- ...item,
20439
- label: String(item.label),
20440
- children: Array.isArray(item.children) ? normalizeAppItems(item.children) : []
20441
- }));
21732
+ return items.filter((item) => item && typeof item === "object" && item.id != null && item.label != null).map((item) => {
21733
+ const normalized = {
21734
+ ...item,
21735
+ label: String(item.label),
21736
+ children: Array.isArray(item.children) ? normalizeAppItems(item.children) : []
21737
+ };
21738
+ if (item.href == null) return normalized;
21739
+ const href = safeHref(item.href);
21740
+ if (href !== null) normalized.href = href;
21741
+ else {
21742
+ delete normalized.href;
21743
+ if (normalized.children.length === 0 && typeof normalized.invoke !== "function") {
21744
+ normalized.disabled = true;
21745
+ }
21746
+ }
21747
+ return normalized;
21748
+ });
20442
21749
  }
20443
- function navControl(item, { className, index, iconOnly, disclosure, disclosureIcon }) {
20444
- const tag = item.href && !disclosure ? "a" : "button";
21750
+ function appNavIcon(item, renderer, context) {
21751
+ let visual = typeof renderer === "function" ? renderer(item, context) : null;
21752
+ if (visual == null) visual = item.icon;
21753
+ if (typeof visual === "function") visual = visual(context, item);
21754
+ if (typeof visual === "string") return icon(visual);
21755
+ if (visual instanceof Node) return visual.cloneNode(true);
21756
+ if (visual && typeof visual === "object" && typeof /** @type {any} */
21757
+ visual.toElement === "function") {
21758
+ const element = (
21759
+ /** @type {any} */
21760
+ visual.toElement()
21761
+ );
21762
+ return element instanceof Node ? element.cloneNode(true) : null;
21763
+ }
21764
+ return null;
21765
+ }
21766
+ function navControl(item, { className, index, iconOnly, disclosure, disclosureIcon, visual }) {
21767
+ const href = safeHref(item.href);
21768
+ const tag = href && !disclosure ? "a" : "button";
20445
21769
  return (
20446
21770
  /** @type {HTMLElement} */
20447
21771
  h(
@@ -20449,13 +21773,14 @@ var zx = (() => {
20449
21773
  {
20450
21774
  class: className,
20451
21775
  type: tag === "button" ? "button" : null,
20452
- href: tag === "a" ? item.href : null,
21776
+ href: tag === "a" ? href : null,
20453
21777
  target: tag === "a" ? item.target ?? null : null,
21778
+ rel: tag === "a" ? "noopener" : null,
20454
21779
  ariaLabel: iconOnly ? item.label : null,
20455
21780
  ariaDisabled: item.disabled ? "true" : null,
20456
21781
  dataset: { appNavId: index, appNavValueId: String(item.id) }
20457
21782
  },
20458
- item.icon ? h("span", { class: `${className}-icon`, ariaHidden: "true" }, icon(item.icon)) : null,
21783
+ visual ? h("span", { class: `${className}-icon`, ariaHidden: "true" }, visual) : null,
20459
21784
  h("span", { class: `${className}-label` }, item.label),
20460
21785
  item.badge != null ? h("span", { class: `${className}-badge` }, String(item.badge)) : null,
20461
21786
  disclosure ? h("span", { class: `${className}-disclosure`, ariaHidden: "true" }, icon(disclosureIcon)) : null
@@ -20519,47 +21844,28 @@ var zx = (() => {
20519
21844
  active: null,
20520
21845
  expanded: [],
20521
21846
  collapsed: false,
21847
+ collapsible: true,
21848
+ orientation: "vertical",
20522
21849
  side: "left",
20523
21850
  label: "Applications",
20524
21851
  header: null,
20525
21852
  footer: null,
20526
21853
  railHeader: null,
20527
- railFooter: null
21854
+ railFooter: null,
21855
+ openDelay: 80,
21856
+ closeDelay: 160,
21857
+ renderIcon: null
20528
21858
  };
20529
21859
  /**
20530
- * Creates or enhances an application-sidebar host.
20531
- * @param {Element|string|null} target Sidebar target.
20532
- * @param {AppSidebarOptions} [options={}] Sidebar options.
21860
+ * Creates or enhances an application-navigation host.
21861
+ * @param {Element|string|null} [target=null] Navigation target.
21862
+ * @param {AppSidebarOptions} [options={}] Navigation options.
20533
21863
  */
20534
21864
  constructor(target = null, options = {}) {
20535
21865
  super(target, options);
20536
- this.listen(this.refs.rail, "zx-select", (event) => event.stopPropagation());
20537
- const expandButton = h("button", {
20538
- class: "zx-app-sidebar__rail-toggle zx-icon-btn",
20539
- type: "button",
20540
- ariaLabel: "Expand application sidebar"
20541
- }, icon(this.el.dataset.side === "right" ? "chevron-left" : "chevron-right"));
20542
- this._railExpandButton = expandButton;
20543
- const railHeader = h("div", { class: "zx-app-sidebar__rail-header" }, expandButton);
20544
- appendSlot2(railHeader, this.options.railHeader);
20545
- this._rail = new AppRail(this.refs.rail, {
20546
- items: this._items,
20547
- active: this._active,
20548
- orientation: "vertical",
20549
- side: this.el.dataset.side,
20550
- label: this.options.label,
20551
- header: railHeader,
20552
- footer: this.options.railFooter,
20553
- onselect: (event) => {
20554
- const selected = this.emit("select", event.detail);
20555
- if (selected.defaultPrevented) event.preventDefault();
20556
- }
20557
- });
20558
- this.listen(expandButton, "click", () => {
20559
- this.expand();
20560
- });
20561
- this._renderTree();
20562
- this.setCollapsed(Boolean(this.options.collapsed), { silent: true });
21866
+ this._rail = null;
21867
+ this._railExpandButton = null;
21868
+ this._renderMode();
20563
21869
  }
20564
21870
  /** @returns {HTMLElement} */
20565
21871
  render() {
@@ -20577,57 +21883,39 @@ var zx = (() => {
20577
21883
  this._active = this.options.active;
20578
21884
  this._expanded = new Set(Array.isArray(this.options.expanded) ? this.options.expanded.map(String) : []);
20579
21885
  this._collapsed = Boolean(this.options.collapsed);
20580
- root.dataset.side = this.options.side === "right" ? "right" : "left";
20581
- const headerContent = h("div", { class: "zx-app-sidebar__header-content" });
20582
- appendSlot2(headerContent, this.options.header);
20583
- const collapseButton = h("button", {
20584
- ref: "collapse",
20585
- class: "zx-app-sidebar__collapse zx-icon-btn",
20586
- type: "button",
20587
- ariaLabel: "Minimize application sidebar"
20588
- }, icon(root.dataset.side === "right" ? "chevron-right" : "chevron-left"));
20589
- const footer = h("footer", { class: "zx-app-sidebar__footer" });
20590
- appendSlot2(footer, this.options.footer);
20591
- footer.hidden = !footer.hasChildNodes();
20592
- const expanded = h(
20593
- "div",
20594
- { ref: "expanded", class: "zx-app-sidebar__expanded" },
20595
- h("header", { class: "zx-app-sidebar__header" }, headerContent, collapseButton),
20596
- h(
20597
- "nav",
20598
- { class: "zx-app-sidebar__nav", ariaLabel: this.options.label },
20599
- h("ul", { ref: "list", class: "zx-app-sidebar__list" })
20600
- ),
20601
- footer
20602
- );
20603
- const rail = h("div", { ref: "rail", class: "zx-app-sidebar__rail" });
20604
- root.replaceChildren(expanded, rail);
20605
- this.listen(collapseButton, "click", () => {
20606
- this.collapse();
20607
- });
20608
- this.listen(this.refs.list, "click", (event) => this._onTreeClick(event));
21886
+ this._orientation = this.options.orientation === "horizontal" ? "horizontal" : "vertical";
21887
+ this._side = normalizeSide(this._orientation, this.options.side);
21888
+ this._modeAbort = null;
21889
+ root.replaceChildren();
20609
21890
  return root;
20610
21891
  }
20611
- /** Replaces the navigation tree in both expanded and rail presentations. @param {AppNavItem[]} items @returns {this} */
21892
+ /** Replaces the navigation tree in the active presentation. @param {AppNavItem[]} items @returns {this} */
20612
21893
  setItems(items) {
20613
21894
  this._items = normalizeAppItems(items);
20614
21895
  this._reconcileExpanded();
20615
- this._renderTree();
20616
- this._rail.setItems(this._items);
21896
+ if (this._rail) this._rail.setItems(this._items);
21897
+ else this._renderTree();
20617
21898
  return this;
20618
21899
  }
20619
- /** Sets the active id in both presentations without routing. @param {string|number|null} id @returns {this} */
21900
+ /** Returns a shallow copy of normalized items. @returns {AppNavItem[]} */
21901
+ getItems() {
21902
+ return this._items.map((item) => ({
21903
+ ...item,
21904
+ children: item.children?.map((child) => ({ ...child }))
21905
+ }));
21906
+ }
21907
+ /** Sets the active id without routing. @param {string|number|null} id @returns {this} */
20620
21908
  setActive(id) {
20621
21909
  this._active = id;
20622
- this._syncActive();
20623
- this._rail.setActive(id);
21910
+ if (this._rail) this._rail.setActive(id);
21911
+ else this._syncActive();
20624
21912
  return this;
20625
21913
  }
20626
21914
  /** Replaces expanded branch ids. @param {Array<string|number>} ids @param {{silent?: boolean}} [options={}] @returns {this} */
20627
21915
  setExpanded(ids, { silent = false } = {}) {
20628
21916
  this._expanded = new Set(Array.isArray(ids) ? ids.map(String) : []);
20629
21917
  this._reconcileExpanded();
20630
- this._renderTree();
21918
+ if (!this._rail) this._renderTree();
20631
21919
  if (!silent) this.emit("branchchange", {
20632
21920
  item: null,
20633
21921
  id: null,
@@ -20652,47 +21940,105 @@ var zx = (() => {
20652
21940
  const expanded = !this._expanded.has(key);
20653
21941
  if (expanded) this._expanded.add(key);
20654
21942
  else this._expanded.delete(key);
20655
- this._renderTree();
20656
- if (!silent) this.emit("branchchange", { item, id: item.id, expanded, ids: this.getExpanded() });
21943
+ if (!this._rail) this._renderTree();
21944
+ if (!silent) this.emit("branchchange", {
21945
+ item,
21946
+ id: item.id,
21947
+ expanded,
21948
+ ids: this.getExpanded()
21949
+ });
20657
21950
  return this;
20658
21951
  }
20659
- /** Shows the expanded sidebar presentation. @param {{silent?: boolean}} [options={}] @returns {this} */
21952
+ /** Shows the expanded vertical presentation. @param {{silent?: boolean}} [options={}] @returns {this} */
20660
21953
  expand(options = {}) {
20661
21954
  return this.setCollapsed(false, options);
20662
21955
  }
20663
- /** Shows the minimized AppRail presentation. @param {{silent?: boolean}} [options={}] @returns {this} */
21956
+ /** Shows the minimized presentation. @param {{silent?: boolean}} [options={}] @returns {this} */
20664
21957
  collapse(options = {}) {
20665
21958
  return this.setCollapsed(true, options);
20666
21959
  }
20667
- /** Sets expanded/minimized presentation while preserving branch state. @param {boolean} collapsed @param {{silent?: boolean}} [options={}] @returns {this} */
21960
+ /** Sets the saved vertical expanded/minimized state. @param {boolean} collapsed @param {{silent?: boolean}} [options={}] @returns {this} */
20668
21961
  setCollapsed(collapsed, { silent = false } = {}) {
20669
- const next = Boolean(collapsed);
20670
- const changed = next !== this._collapsed;
21962
+ const previous = this.isCollapsed();
20671
21963
  const active = document.activeElement;
20672
- const focusRailToggle = next && Boolean(active && this.refs.expanded.contains(active));
20673
- const focusSidebarToggle = !next && this._rail?._containsNode(active);
20674
- if (!next) this._rail?.closeAllFlyouts();
20675
- this._collapsed = next;
20676
- this.el.dataset.collapsed = String(next);
20677
- this.refs.expanded.hidden = next;
20678
- this.refs.rail.hidden = !next;
20679
- if (focusRailToggle) this._railExpandButton?.focus();
20680
- else if (focusSidebarToggle) this.refs.collapse.focus();
20681
- if (changed && !silent) this.emit("collapsechange", { collapsed: next });
20682
- return this;
20683
- }
20684
- /** Reports whether the rail presentation is active. @returns {boolean} */
21964
+ const outgoingOwnedFocus = Boolean(active && (this.el.contains(active) || this._rail?._containsNode(active)));
21965
+ this._collapsed = Boolean(collapsed);
21966
+ const next = this.isCollapsed();
21967
+ if (previous !== next) {
21968
+ this._renderMode();
21969
+ if (outgoingOwnedFocus) {
21970
+ if (next) this._railExpandButton?.focus();
21971
+ else this.refs.collapse?.focus();
21972
+ }
21973
+ if (!silent) this.emit("collapsechange", { collapsed: next });
21974
+ }
21975
+ return this;
21976
+ }
21977
+ /** Reports the effective presentation state; horizontal navigation is always minimized. @returns {boolean} */
20685
21978
  isCollapsed() {
20686
- return this._collapsed;
21979
+ return this._orientation === "horizontal" || this._collapsed;
21980
+ }
21981
+ /**
21982
+ * Changes navigation flow/edge without replacing state.
21983
+ * @param {{orientation?: 'vertical'|'horizontal', side?: 'left'|'right'|'top'|'bottom'}} layout
21984
+ * @param {{silent?: boolean}} [options]
21985
+ * @returns {this}
21986
+ */
21987
+ setLayout(layout = {}, { silent = false } = {}) {
21988
+ const orientation = layout.orientation === "horizontal" ? "horizontal" : layout.orientation === "vertical" ? "vertical" : this._orientation;
21989
+ const side = normalizeSide(orientation, layout.side ?? this._side);
21990
+ if (orientation === this._orientation && side === this._side) return this;
21991
+ const previous = this.isCollapsed();
21992
+ const active = document.activeElement;
21993
+ const outgoingOwnedFocus = Boolean(active && (this.el.contains(active) || this._rail?._containsNode(active)));
21994
+ const focusedId = outgoingOwnedFocus && active instanceof Element ? active.closest("[data-app-nav-id]")?.getAttribute("data-app-nav-id") ?? null : null;
21995
+ this._orientation = orientation;
21996
+ this._side = side;
21997
+ this._renderMode();
21998
+ const next = this.isCollapsed();
21999
+ if (outgoingOwnedFocus) {
22000
+ const replacement = focusedId === null ? null : [...this.el.querySelectorAll("[data-app-nav-id]")].find((control) => control.getAttribute("data-app-nav-id") === focusedId) ?? null;
22001
+ if (replacement instanceof HTMLElement) replacement.focus();
22002
+ else if (next) this.focus();
22003
+ else if (this.refs.collapse instanceof HTMLElement && !this.refs.collapse.hidden) {
22004
+ this.refs.collapse.focus();
22005
+ } else visibleTreeControls(this.refs.list)[0]?.focus();
22006
+ }
22007
+ if (previous !== next && !silent) this.emit("collapsechange", { collapsed: next });
22008
+ return this;
20687
22009
  }
20688
- /** Returns the composed minimized rail. @returns {AppRail} */
20689
- getRail() {
20690
- return this._rail;
22010
+ /** Opens a minimized descendant flyout. @param {string|number} id @param {{focus?: boolean}} [options={}] @returns {this} */
22011
+ openFlyout(id, options = {}) {
22012
+ this._rail?.openFlyout(id, options);
22013
+ return this;
22014
+ }
22015
+ /** Closes a minimized descendant flyout. @param {string|number} id @returns {this} */
22016
+ closeFlyout(id) {
22017
+ this._rail?.closeFlyout(id);
22018
+ return this;
20691
22019
  }
20692
- /** Destroys the rail and restores/removes the sidebar host. @returns {void} */
22020
+ /** Closes every minimized flyout. @returns {this} */
22021
+ closeAllFlyouts() {
22022
+ this._rail?.closeAllFlyouts();
22023
+ return this;
22024
+ }
22025
+ /** Reports whether a minimized flyout is open. @param {string|number} id @returns {boolean} */
22026
+ isFlyoutOpen(id) {
22027
+ return this._rail?.isFlyoutOpen(id) ?? false;
22028
+ }
22029
+ /** Focuses the first navigation action. @returns {this} */
22030
+ focus() {
22031
+ if (this._rail) this._rail.focus();
22032
+ else visibleTreeControls(this.refs.list)[0]?.focus();
22033
+ return this;
22034
+ }
22035
+ /** Destroys flyouts and restores/removes the host. @returns {void} */
20693
22036
  destroy() {
20694
22037
  const original = this._original;
20695
- this._rail.destroy();
22038
+ this._modeAbort?.abort();
22039
+ this._modeAbort = null;
22040
+ this._rail?.destroy();
22041
+ this._rail = null;
20696
22042
  super.destroy();
20697
22043
  if (!this._createdRoot && original) {
20698
22044
  for (const attribute of Array.from(this.el.attributes)) this.el.removeAttribute(attribute.name);
@@ -20700,8 +22046,99 @@ var zx = (() => {
20700
22046
  this.el.replaceChildren(...original.children);
20701
22047
  }
20702
22048
  }
22049
+ /** Mounts exactly one expanded tree or minimized flyout presentation. @returns {void} */
22050
+ _renderMode() {
22051
+ this._modeAbort?.abort();
22052
+ this._modeAbort = new AbortController();
22053
+ this._rail?.destroy();
22054
+ this._rail = null;
22055
+ this._railExpandButton = null;
22056
+ for (const key of ["expanded", "list", "collapse", "rail"]) delete this.refs[key];
22057
+ this.el.replaceChildren();
22058
+ this.el.dataset.orientation = this._orientation;
22059
+ this.el.dataset.side = this._side;
22060
+ this.el.dataset.collapsed = String(this.isCollapsed());
22061
+ if (this.isCollapsed()) this._renderRail();
22062
+ else this._renderExpanded();
22063
+ }
22064
+ /** @returns {void} */
22065
+ _renderExpanded() {
22066
+ const headerContent = h("div", { class: "zx-app-sidebar__header-content" });
22067
+ appendSlot2(headerContent, this.options.header);
22068
+ const collapseButton = h("button", {
22069
+ class: "zx-app-sidebar__collapse zx-icon-btn",
22070
+ type: "button",
22071
+ ariaLabel: "Minimize application sidebar"
22072
+ }, icon(this._side === "right" ? "chevron-right" : "chevron-left"));
22073
+ collapseButton.hidden = !this.options.collapsible;
22074
+ const footer = h("footer", { class: "zx-app-sidebar__footer" });
22075
+ appendSlot2(footer, this.options.footer);
22076
+ footer.hidden = !footer.hasChildNodes();
22077
+ const list = h("ul", { class: "zx-app-sidebar__list" });
22078
+ const expanded = h(
22079
+ "div",
22080
+ { class: "zx-app-sidebar__expanded" },
22081
+ h("header", { class: "zx-app-sidebar__header" }, headerContent, collapseButton),
22082
+ h("nav", { class: "zx-app-sidebar__nav", ariaLabel: this.options.label }, list),
22083
+ footer
22084
+ );
22085
+ this.refs.expanded = expanded;
22086
+ this.refs.list = list;
22087
+ this.refs.collapse = collapseButton;
22088
+ this.el.append(expanded);
22089
+ this._listenMode(collapseButton, "click", () => this.collapse());
22090
+ this._listenMode(list, "click", (event) => this._onTreeClick(event));
22091
+ this._listenMode(list, "keydown", (event) => this._onTreeKeydown(
22092
+ /** @type {KeyboardEvent} */
22093
+ event
22094
+ ));
22095
+ this._renderTree();
22096
+ }
22097
+ /** @returns {void} */
22098
+ _renderRail() {
22099
+ const railHost = h("nav", { class: "zx-app-sidebar__rail" });
22100
+ this.el.append(railHost);
22101
+ this.refs.rail = railHost;
22102
+ const showToggle = this._orientation === "vertical" && Boolean(this.options.collapsible);
22103
+ const railHeader = h("div", { class: "zx-app-sidebar__rail-header" });
22104
+ if (showToggle) {
22105
+ const expandButton = h("button", {
22106
+ class: "zx-app-sidebar__rail-toggle zx-icon-btn",
22107
+ type: "button",
22108
+ ariaLabel: "Expand application sidebar"
22109
+ }, icon(this._side === "right" ? "chevron-left" : "chevron-right"));
22110
+ railHeader.append(expandButton);
22111
+ this._railExpandButton = expandButton;
22112
+ this._listenMode(expandButton, "click", () => this.expand());
22113
+ }
22114
+ appendSlot2(railHeader, this.options.railHeader);
22115
+ this._rail = new AppRailPresenter(railHost, {
22116
+ items: this._items,
22117
+ active: this._active,
22118
+ orientation: this._orientation,
22119
+ side: this._side,
22120
+ label: this.options.label,
22121
+ header: railHeader.hasChildNodes() ? railHeader : null,
22122
+ footer: this.options.railFooter,
22123
+ openDelay: this.options.openDelay,
22124
+ closeDelay: this.options.closeDelay,
22125
+ renderIcon: this.options.renderIcon,
22126
+ onselect: (event) => {
22127
+ const selected = this.emit("select", event.detail);
22128
+ if (selected.defaultPrevented) event.preventDefault();
22129
+ },
22130
+ onflyoutchange: (event) => this.emit("flyoutchange", event.detail)
22131
+ });
22132
+ this._listenMode(railHost, "zx-select", (event) => event.stopPropagation());
22133
+ this._listenMode(railHost, "zx-flyoutchange", (event) => event.stopPropagation());
22134
+ }
22135
+ /** @param {EventTarget} target @param {string} type @param {EventListener} listener @returns {void} */
22136
+ _listenMode(target, type, listener) {
22137
+ target.addEventListener(type, listener, { signal: this._modeAbort.signal });
22138
+ }
20703
22139
  /** @returns {void} */
20704
22140
  _renderTree() {
22141
+ if (!this.refs.list) return;
20705
22142
  const focused = this.refs.list.contains(document.activeElement) ? (
20706
22143
  /** @type {Element} */
20707
22144
  document.activeElement.closest("[data-app-nav-id]")?.getAttribute("data-app-nav-id")
@@ -20720,7 +22157,14 @@ var zx = (() => {
20720
22157
  const key = String(item.id);
20721
22158
  const expanded = hasChildren && this._expanded.has(key);
20722
22159
  const childId = hasChildren ? uid("zx-app-sidebar-branch") : null;
20723
- const control = navControl2(item, hasChildren, expanded, childId);
22160
+ const visual = appNavIcon(item, this.options.renderIcon, {
22161
+ orientation: this._orientation,
22162
+ collapsed: false,
22163
+ side: this._side,
22164
+ level,
22165
+ location: level === 1 ? "root" : "inline"
22166
+ });
22167
+ const control = navControl2(item, hasChildren, expanded, childId, visual);
20724
22168
  const row2 = h("li", {
20725
22169
  class: "zx-app-sidebar__row",
20726
22170
  dataset: { level }
@@ -20734,7 +22178,7 @@ var zx = (() => {
20734
22178
  }
20735
22179
  return row2;
20736
22180
  }
20737
- /** Handles dynamic tree controls without retaining detached nodes after a rerender. @param {Event} event @returns {void} */
22181
+ /** @param {Event} event @returns {void} */
20738
22182
  _onTreeClick(event) {
20739
22183
  const control = (
20740
22184
  /** @type {Element|null} */
@@ -20749,6 +22193,45 @@ var zx = (() => {
20749
22193
  if (item.children?.length) this.toggleBranch(item.id);
20750
22194
  else this._select(item, event);
20751
22195
  }
22196
+ /** @param {KeyboardEvent} event @returns {void} */
22197
+ _onTreeKeydown(event) {
22198
+ const current = (
22199
+ /** @type {Element|null} */
22200
+ event.target?.closest?.("[data-app-nav-id]")
22201
+ );
22202
+ if (!(current instanceof HTMLElement)) return;
22203
+ const item = findItem(this._items, current.getAttribute("data-app-nav-id"));
22204
+ const controls = visibleTreeControls(this.refs.list);
22205
+ const index = controls.indexOf(current);
22206
+ if (event.key === "ArrowRight" && item?.children?.length) {
22207
+ event.preventDefault();
22208
+ if (!this._expanded.has(String(item.id))) this.toggleBranch(item.id);
22209
+ else visibleTreeControls(current.parentElement?.querySelector(".zx-app-sidebar__sublist"))[0]?.focus();
22210
+ return;
22211
+ }
22212
+ if (event.key === "ArrowLeft") {
22213
+ if (item?.children?.length && this._expanded.has(String(item.id))) {
22214
+ event.preventDefault();
22215
+ this.toggleBranch(item.id);
22216
+ return;
22217
+ }
22218
+ const parentList = current.closest(".zx-app-sidebar__sublist");
22219
+ const parentControl = parentList?.parentElement?.querySelector(":scope > [data-app-nav-id]");
22220
+ if (parentControl instanceof HTMLElement) {
22221
+ event.preventDefault();
22222
+ parentControl.focus();
22223
+ }
22224
+ return;
22225
+ }
22226
+ let nextIndex;
22227
+ if (event.key === "Home") nextIndex = 0;
22228
+ else if (event.key === "End") nextIndex = controls.length - 1;
22229
+ else if (event.key === "ArrowDown") nextIndex = Math.min(controls.length - 1, index + 1);
22230
+ else if (event.key === "ArrowUp") nextIndex = Math.max(0, index - 1);
22231
+ else return;
22232
+ event.preventDefault();
22233
+ controls[nextIndex]?.focus();
22234
+ }
20752
22235
  /** @param {AppNavItem} item @param {Event} event @returns {void} */
20753
22236
  _select(item, event) {
20754
22237
  if (item.disabled) {
@@ -20770,14 +22253,14 @@ var zx = (() => {
20770
22253
  }
20771
22254
  /** @returns {void} */
20772
22255
  _syncActive() {
22256
+ if (!this.refs.list) return;
20773
22257
  const activeId = this._active == null ? null : String(this._active);
20774
22258
  for (const control of this.refs.list.querySelectorAll("[data-app-nav-id]")) {
20775
22259
  const item = findItem(this._items, control.getAttribute("data-app-nav-id"));
20776
22260
  const direct = activeId !== null && control.getAttribute("data-app-nav-id") === activeId;
20777
22261
  const descendant = activeId !== null && item ? containsItemId2(item.children ?? [], activeId) : false;
20778
- if (direct) {
20779
- control.setAttribute("aria-current", "page");
20780
- } else control.removeAttribute("aria-current");
22262
+ if (direct) control.setAttribute("aria-current", "page");
22263
+ else control.removeAttribute("aria-current");
20781
22264
  if (descendant) control.dataset.activeDescendant = "true";
20782
22265
  else delete control.dataset.activeDescendant;
20783
22266
  }
@@ -20791,7 +22274,7 @@ var zx = (() => {
20791
22274
  for (const id of this._expanded) if (!branchIds.has(id)) this._expanded.delete(id);
20792
22275
  }
20793
22276
  };
20794
- function navControl2(item, disclosure, expanded, controls) {
22277
+ function navControl2(item, disclosure, expanded, controls, visual) {
20795
22278
  const tag = item.href && !disclosure ? "a" : "button";
20796
22279
  return (
20797
22280
  /** @type {HTMLElement} */
@@ -20807,7 +22290,7 @@ var zx = (() => {
20807
22290
  ariaControls: controls,
20808
22291
  dataset: { appNavId: String(item.id) }
20809
22292
  },
20810
- item.icon ? h("span", { class: "zx-app-sidebar__item-icon", ariaHidden: "true" }, icon(item.icon)) : null,
22293
+ visual ? h("span", { class: "zx-app-sidebar__item-icon", ariaHidden: "true" }, visual) : null,
20811
22294
  h("span", { class: "zx-app-sidebar__item-label" }, item.label),
20812
22295
  item.badge != null ? h("span", { class: "zx-app-sidebar__item-badge" }, String(item.badge)) : null,
20813
22296
  disclosure ? h(
@@ -20818,6 +22301,13 @@ var zx = (() => {
20818
22301
  )
20819
22302
  );
20820
22303
  }
22304
+ function visibleTreeControls(root) {
22305
+ if (!root) return [];
22306
+ return (
22307
+ /** @type {HTMLElement[]} */
22308
+ [...root.querySelectorAll("[data-app-nav-id]")].filter((control) => !control.closest(".zx-app-sidebar__sublist[hidden]"))
22309
+ );
22310
+ }
20821
22311
  function visibleTreeControl(control) {
20822
22312
  let candidate = (
20823
22313
  /** @type {HTMLElement|null} */
@@ -20850,12 +22340,17 @@ var zx = (() => {
20850
22340
  }
20851
22341
  function appendSlot2(target, content) {
20852
22342
  if (content == null) return;
20853
- if (typeof content === "string" || typeof content === "number") target.append(document.createTextNode(String(content)));
20854
- else if (typeof content === "object" && typeof content.toElement === "function") {
22343
+ if (typeof content === "string" || typeof content === "number") {
22344
+ target.append(document.createTextNode(String(content)));
22345
+ } else if (typeof content === "object" && typeof content.toElement === "function") {
20855
22346
  const element = content.toElement();
20856
22347
  if (element) target.append(element);
20857
22348
  } else if (typeof content === "object" && typeof content.nodeType === "number") target.append(content);
20858
22349
  }
22350
+ function normalizeSide(orientation, side) {
22351
+ if (orientation === "horizontal") return side === "bottom" ? "bottom" : "top";
22352
+ return side === "right" ? "right" : "left";
22353
+ }
20859
22354
 
20860
22355
  // src/components/message/message.js
20861
22356
  var MessageQueue = class {
@@ -22192,7 +23687,7 @@ var zx = (() => {
22192
23687
  * @param {GridOptions & import('../table/table.js').TableOptions} [options={}] Grid/Table options.
22193
23688
  */
22194
23689
  constructor(target = null, options = {}) {
22195
- super(target, options);
23690
+ super(target, { editTrigger: "single", ...options });
22196
23691
  }
22197
23692
  /**
22198
23693
  * Creates an editable hierarchical billing-line grid.
@@ -22223,14 +23718,17 @@ var zx = (() => {
22223
23718
  } = options;
22224
23719
  const fields = { ...DEFAULT_FIELDS, ...requestedFields };
22225
23720
  const calculateLineTotal = typeof lineTotal === "function" ? lineTotal : (row2) => {
22226
- const quantity = Number(row2?.[fields.quantity]);
22227
- const price = Number(row2?.[fields.unitPrice]);
22228
- return Number.isFinite(quantity) && Number.isFinite(price) ? quantity * price : null;
23721
+ const quantity = billingNumber(row2?.[fields.quantity]);
23722
+ const price = billingNumber(row2?.[fields.unitPrice]);
23723
+ return quantity !== null && price !== null ? quantity * price : null;
22229
23724
  };
22230
23725
  const columns = Array.isArray(suppliedColumns) ? suppliedColumns.map((column) => ({ ...column })) : billingColumns(fields, { units, currencies, currency, locale, decimals, columnOverrides });
22231
23726
  return {
22232
23727
  rowId: fields.id,
22233
23728
  editMode: "cell",
23729
+ editTrigger: "single",
23730
+ rowReorder: true,
23731
+ columnVisibility: true,
22234
23732
  hierarchy: { parentId: fields.parent, column: fields.item, expanded: true },
22235
23733
  responsive: "sm",
22236
23734
  ...tableOptions,
@@ -22330,6 +23828,11 @@ var zx = (() => {
22330
23828
  function hasChoices(value) {
22331
23829
  return Array.isArray(value) ? value.length > 0 : Boolean(value && typeof value === "object" && Object.keys(value).length);
22332
23830
  }
23831
+ function billingNumber(value) {
23832
+ if (value == null || typeof value === "string" && value.trim() === "") return null;
23833
+ const number2 = Number(value);
23834
+ return Number.isFinite(number2) ? number2 : null;
23835
+ }
22333
23836
 
22334
23837
  // src/components/chart/chart.js
22335
23838
  var Chart = class extends Component {
@@ -22377,6 +23880,7 @@ var zx = (() => {
22377
23880
  this._chartOptions = cloneChartValue(this.options.chartOptions);
22378
23881
  this._loading = Boolean(this.options.loading);
22379
23882
  this._error = null;
23883
+ this._hasError = false;
22380
23884
  this._handle = null;
22381
23885
  this._destroyed = false;
22382
23886
  const descriptionId = uid("zx-chart-description");
@@ -22422,6 +23926,7 @@ var zx = (() => {
22422
23926
  setData(data) {
22423
23927
  this._data = normalizeChartData(data);
22424
23928
  this._error = null;
23929
+ this._hasError = false;
22425
23930
  this._loading = false;
22426
23931
  this._renderSummary();
22427
23932
  this._mountOrUpdate(true);
@@ -22455,8 +23960,9 @@ var zx = (() => {
22455
23960
  return this;
22456
23961
  }
22457
23962
  /** Shows an error and destroys the current engine. @param {unknown} error @returns {this} */
22458
- setError(error) {
22459
- this._error = error ?? new Error(String(this.options.errorText));
23963
+ setError(error2) {
23964
+ this._error = error2 ?? new Error(String(this.options.errorText));
23965
+ this._hasError = true;
22460
23966
  this._loading = false;
22461
23967
  this._destroyHandle();
22462
23968
  this._syncState();
@@ -22466,6 +23972,7 @@ var zx = (() => {
22466
23972
  /** Clears the error and remounts when data and an adapter are available. @returns {this} */
22467
23973
  clearError() {
22468
23974
  this._error = null;
23975
+ this._hasError = false;
22469
23976
  this._mountOrUpdate(false);
22470
23977
  return this;
22471
23978
  }
@@ -22473,8 +23980,8 @@ var zx = (() => {
22473
23980
  resize() {
22474
23981
  try {
22475
23982
  this._handle?.resize?.();
22476
- } catch (error) {
22477
- this.setError(error);
23983
+ } catch (error2) {
23984
+ this.setError(error2);
22478
23985
  }
22479
23986
  return this;
22480
23987
  }
@@ -22504,7 +24011,7 @@ var zx = (() => {
22504
24011
  /** @param {boolean} emitUpdate @returns {void} */
22505
24012
  _mountOrUpdate(emitUpdate) {
22506
24013
  this._syncState();
22507
- if (this._loading || this._error || isEmptyChartData(this._data)) {
24014
+ if (this._loading || this._hasError || isEmptyChartData(this._data)) {
22508
24015
  this._destroyHandle();
22509
24016
  return;
22510
24017
  }
@@ -22527,8 +24034,8 @@ var zx = (() => {
22527
24034
  this.emit("ready", { instance: this.getInstance() });
22528
24035
  }
22529
24036
  if (emitUpdate) this.emit("update", { data: this._data, type: this._type });
22530
- } catch (error) {
22531
- this.setError(error);
24037
+ } catch (error2) {
24038
+ this.setError(error2);
22532
24039
  }
22533
24040
  }
22534
24041
  /** @returns {ChartSpec} */
@@ -22559,7 +24066,7 @@ var zx = (() => {
22559
24066
  /** @returns {void} */
22560
24067
  _syncState() {
22561
24068
  const empty = isEmptyChartData(this._data);
22562
- const state = this._loading ? "loading" : this._error ? "error" : empty ? "empty" : "ready";
24069
+ const state = this._loading ? "loading" : this._hasError ? "error" : empty ? "empty" : "ready";
22563
24070
  this.el.dataset.state = state;
22564
24071
  this.refs.viewport.hidden = state !== "ready";
22565
24072
  let text4 = "";
@@ -22770,8 +24277,9 @@ var zx = (() => {
22770
24277
  borderColor: borderColor || void 0
22771
24278
  };
22772
24279
  }
22773
- function errorMessage(error, fallback) {
22774
- return error && typeof error === "object" && "message" in error ? String(error.message) : String(fallback);
24280
+ function errorMessage(error2, fallback) {
24281
+ const message = error2 && typeof error2 === "object" && "message" in error2 ? String(error2.message) : "";
24282
+ return message.trim() ? message : String(fallback);
22775
24283
  }
22776
24284
 
22777
24285
  // src/components/tree/hierarchy.js
@@ -23409,8 +24917,8 @@ var zx = (() => {
23409
24917
  node[this._childrenKey] = Array.isArray(children) ? children : [];
23410
24918
  if (node[this._childrenKey].length === 0) node.hasChildren = false;
23411
24919
  return true;
23412
- } catch (error) {
23413
- this.emit("error", { error, node, id });
24920
+ } catch (error2) {
24921
+ this.emit("error", { error: error2, node, id });
23414
24922
  return false;
23415
24923
  } finally {
23416
24924
  this._loading.delete(id);
@@ -23898,9 +25406,9 @@ var zx = (() => {
23898
25406
  const children = await this.options.load(node);
23899
25407
  node[this._childrenKey] = Array.isArray(children) ? children : [];
23900
25408
  if (node[this._childrenKey].length === 0) node.hasChildren = false;
23901
- } catch (error) {
25409
+ } catch (error2) {
23902
25410
  node[this._childrenKey] = [];
23903
- this.emit("error", { error, node, id });
25411
+ this.emit("error", { error: error2, node, id });
23904
25412
  } finally {
23905
25413
  this._loading.delete(id);
23906
25414
  }
@@ -24330,6 +25838,821 @@ var zx = (() => {
24330
25838
  }
24331
25839
  }
24332
25840
 
25841
+ // src/components/filter/filter-model.js
25842
+ var filterOperators = Object.freeze([
25843
+ op("contains", "contains", "single", ["text"]),
25844
+ op("notContains", "does not contain", "single", ["text"]),
25845
+ op("eq", "is", "single", ["text", "number", "money", "boolean", "enum", "status", "priority", "country", "currency", "unit", "entity", "date", "datetime"]),
25846
+ op("neq", "is not", "single", ["text", "number", "money", "boolean", "enum", "status", "priority", "country", "currency", "unit", "entity", "date", "datetime"]),
25847
+ op("startsWith", "starts with", "single", ["text"]),
25848
+ op("endsWith", "ends with", "single", ["text"]),
25849
+ op("lt", "is less than", "single", ["number", "money"]),
25850
+ op("lte", "is at most", "single", ["number", "money"]),
25851
+ op("gt", "is greater than", "single", ["number", "money"]),
25852
+ op("gte", "is at least", "single", ["number", "money"]),
25853
+ op("on", "is on", "single", ["date", "datetime"]),
25854
+ op("notOn", "is not on", "single", ["date", "datetime"]),
25855
+ op("before", "is before", "single", ["date", "datetime"]),
25856
+ op("onOrBefore", "is on or before", "single", ["date", "datetime"]),
25857
+ op("after", "is after", "single", ["date", "datetime"]),
25858
+ op("onOrAfter", "is on or after", "single", ["date", "datetime"]),
25859
+ op("between", "is between", "pair", ["number", "money", "date", "datetime"]),
25860
+ op("notBetween", "is not between", "pair", ["number", "money", "date", "datetime"]),
25861
+ op("withinLast", "is within the last", "pair", ["date", "datetime"]),
25862
+ op("withinNext", "is within the next", "pair", ["date", "datetime"]),
25863
+ op("anyOf", "is any of", "many", ["enum", "status", "priority", "country", "currency", "unit", "entity"]),
25864
+ op("noneOf", "is none of", "many", ["enum", "status", "priority", "country", "currency", "unit", "entity"]),
25865
+ op("containsAny", "contains any", "many", ["tags"]),
25866
+ op("containsAll", "contains all", "many", ["tags"]),
25867
+ op("containsNone", "contains none", "many", ["tags"]),
25868
+ op("isEmpty", "is empty", "none", ["text", "number", "money", "date", "datetime", "boolean", "enum", "status", "priority", "country", "currency", "unit", "entity", "tags"]),
25869
+ op("isNotEmpty", "is not empty", "none", ["text", "number", "money", "date", "datetime", "boolean", "enum", "status", "priority", "country", "currency", "unit", "entity", "tags"])
25870
+ ]);
25871
+ function emptyFilterAst() {
25872
+ return { version: 1, root: { kind: "group", id: uid("filter-root"), logic: "and", children: [] } };
25873
+ }
25874
+ function filterCondition(initial = {}) {
25875
+ return {
25876
+ kind: "condition",
25877
+ id: String(initial.id || uid("filter-condition")),
25878
+ field: String(initial.field ?? ""),
25879
+ operator: String(initial.operator ?? ""),
25880
+ value: cloneFilterValue(initial.value ?? null)
25881
+ };
25882
+ }
25883
+ function cloneFilterValue(value) {
25884
+ return cloneJson(value);
25885
+ }
25886
+ function filterGroup(logic = "and", children = []) {
25887
+ return { kind: "group", id: uid("filter-group"), logic, children };
25888
+ }
25889
+ function parseFilterAst(value, limits = {}) {
25890
+ const maxDepth = finiteLimit(limits.maxDepth, 3);
25891
+ const maxConditions = finiteLimit(limits.maxConditions, 50);
25892
+ if (!plainObject(value) || value.version !== 1 || !plainObject(value.root)) {
25893
+ throw new TypeError("Filter AST must have version 1 and a root group");
25894
+ }
25895
+ const ids = /* @__PURE__ */ new Set();
25896
+ let conditions = 0;
25897
+ const visit = (raw2, depth, root2 = false) => {
25898
+ if (!plainObject(raw2)) throw new TypeError("Filter nodes must be plain objects");
25899
+ const id = String(raw2.id ?? "").trim();
25900
+ if (!id || ids.has(id)) throw new TypeError("Filter node IDs must be non-empty and unique");
25901
+ ids.add(id);
25902
+ if (raw2.kind === "group") {
25903
+ if (depth > maxDepth) throw new RangeError("Filter group depth exceeds maxDepth");
25904
+ if (raw2.logic !== "and" && raw2.logic !== "or") throw new TypeError("Filter group logic must be and or or");
25905
+ if (!Array.isArray(raw2.children)) throw new TypeError("Filter group children must be an array");
25906
+ return {
25907
+ kind: "group",
25908
+ id,
25909
+ logic: raw2.logic,
25910
+ children: raw2.children.map((child) => visit(child, depth + 1, false))
25911
+ };
25912
+ }
25913
+ if (root2 || raw2.kind !== "condition") throw new TypeError("Filter root must be a group");
25914
+ conditions += 1;
25915
+ if (conditions > maxConditions) throw new RangeError("Filter condition count exceeds maxConditions");
25916
+ return {
25917
+ kind: "condition",
25918
+ id,
25919
+ field: String(raw2.field ?? ""),
25920
+ operator: String(raw2.operator ?? ""),
25921
+ value: cloneJson(raw2.value)
25922
+ };
25923
+ };
25924
+ const root = visit(value.root, 0, true);
25925
+ if (root.kind !== "group") throw new TypeError("Filter root must be a group");
25926
+ return (
25927
+ /** @type {FilterAst} */
25928
+ { version: 1, root }
25929
+ );
25930
+ }
25931
+ function cloneFilterAst(value) {
25932
+ return parseFilterAst(value, { maxDepth: Infinity, maxConditions: Infinity });
25933
+ }
25934
+ function stringifyFilterAst(value) {
25935
+ return JSON.stringify(cloneFilterAst(value));
25936
+ }
25937
+ function validateFilterAst(ast, fields, operators = filterOperators) {
25938
+ const errors = [];
25939
+ const fieldMap = new Map((Array.isArray(fields) ? fields : []).map((field) => [String(field.id), field]));
25940
+ const operatorMap = new Map((Array.isArray(operators) ? operators : []).map((operator) => [String(operator.id), operator]));
25941
+ const visit = (node, root = false) => {
25942
+ if (node.kind === "group") {
25943
+ if (!root && node.children.length === 0) errors.push(error(node.id, "empty-group", "Nested groups need at least one condition"));
25944
+ node.children.forEach((child) => visit(child));
25945
+ return;
25946
+ }
25947
+ const field = fieldMap.get(node.field);
25948
+ if (!field) errors.push(error(node.id, "unknown-field", node.field ? `Unavailable field: ${node.field}` : "Choose a field"));
25949
+ const operator = operatorMap.get(node.operator);
25950
+ if (!operator) {
25951
+ errors.push(error(node.id, "unknown-operator", node.operator ? `Unavailable operator: ${node.operator}` : "Choose an operator"));
25952
+ return;
25953
+ }
25954
+ if (field) {
25955
+ const allowed = Array.isArray(field.operators) && field.operators.length ? field.operators : operators.filter((candidate) => candidate.types.includes(field.type)).map((candidate) => candidate.id);
25956
+ if (!allowed.includes(operator.id) || !operator.types.includes(field.type)) {
25957
+ errors.push(error(node.id, "operator-type", `${operator.label} is not available for ${field.label}`));
25958
+ }
25959
+ }
25960
+ if (!validArity(operator.arity, node.value)) {
25961
+ 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"));
25962
+ }
25963
+ };
25964
+ visit(ast.root, true);
25965
+ return { valid: errors.length === 0, errors };
25966
+ }
25967
+ function op(id, label, arity, types) {
25968
+ return Object.freeze({ id, label, arity, types: Object.freeze(types) });
25969
+ }
25970
+ function error(nodeId, code, message) {
25971
+ return { nodeId, code, message };
25972
+ }
25973
+ function validArity(arity, value) {
25974
+ if (arity === "none") return value === null;
25975
+ if (arity === "pair") return Array.isArray(value) && value.length === 2 && value.every(filled);
25976
+ if (arity === "many") return Array.isArray(value) && value.length > 0 && value.every(filled);
25977
+ return filled(value);
25978
+ }
25979
+ function filled(value) {
25980
+ return value !== null && value !== void 0 && !(typeof value === "string" && value.trim() === "");
25981
+ }
25982
+ function cloneJson(value, seen = /* @__PURE__ */ new WeakSet()) {
25983
+ if (value === null || typeof value === "string" || typeof value === "boolean") return value;
25984
+ if (typeof value === "number") {
25985
+ if (!Number.isFinite(value)) throw new TypeError("Filter values cannot contain non-finite numbers");
25986
+ return value;
25987
+ }
25988
+ if (typeof value !== "object" || value instanceof Date || typeof value.nodeType === "number") {
25989
+ throw new TypeError("Filter values must be JSON-safe");
25990
+ }
25991
+ if (seen.has(value)) throw new TypeError("Filter values cannot contain cycles");
25992
+ seen.add(value);
25993
+ if (Array.isArray(value)) {
25994
+ const result2 = value.map((item) => cloneJson(item, seen));
25995
+ seen.delete(value);
25996
+ return result2;
25997
+ }
25998
+ if (!plainObject(value)) throw new TypeError("Filter values must be plain JSON objects");
25999
+ const result = {};
26000
+ for (const [key, item] of Object.entries(value)) {
26001
+ Object.defineProperty(result, key, {
26002
+ value: cloneJson(item, seen),
26003
+ enumerable: true,
26004
+ configurable: true,
26005
+ writable: true
26006
+ });
26007
+ }
26008
+ seen.delete(value);
26009
+ return result;
26010
+ }
26011
+ function plainObject(value) {
26012
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
26013
+ const prototype = Object.getPrototypeOf(value);
26014
+ return prototype === Object.prototype || prototype === null;
26015
+ }
26016
+ function finiteLimit(value, fallback) {
26017
+ if (value === Infinity) return Infinity;
26018
+ const number2 = Number(value);
26019
+ return Number.isFinite(number2) && number2 >= 0 ? Math.floor(number2) : fallback;
26020
+ }
26021
+
26022
+ // src/components/filter/filter.js
26023
+ var Filter = class extends Component {
26024
+ static cssName = "filter";
26025
+ /** @type {Readonly<FilterOptions>} */
26026
+ static defaults = {
26027
+ fields: [],
26028
+ value: null,
26029
+ operators: [],
26030
+ allowGroups: true,
26031
+ maxDepth: 3,
26032
+ maxConditions: 50,
26033
+ autoApply: false,
26034
+ readonly: false,
26035
+ disabled: false,
26036
+ applyLabel: "Apply filters",
26037
+ clearLabel: "Clear"
26038
+ };
26039
+ /** @returns {HTMLElement} */
26040
+ render() {
26041
+ this._createdRoot = this.el === null;
26042
+ const root = (
26043
+ /** @type {HTMLElement} */
26044
+ this.el ?? h("div")
26045
+ );
26046
+ this.el = root;
26047
+ this._snapshot = this._createdRoot ? null : snapshotTarget(root);
26048
+ this._fields = (Array.isArray(this.options.fields) ? this.options.fields : []).map((field) => ({ ...field }));
26049
+ this._operators = mergeOperators(filterOperators, this.options.operators);
26050
+ this._readonly = Boolean(this.options.readonly);
26051
+ this._disabled = Boolean(this.options.disabled);
26052
+ this._async = /* @__PURE__ */ new Map();
26053
+ this._customEditors = [];
26054
+ this._value = this.options.value == null ? emptyFilterAst() : parseFilterAst(this.options.value, this._limits());
26055
+ this.listen(root, "change", (event) => this._handleChange(event));
26056
+ this.listen(root, "input", (event) => this._handleInput(event));
26057
+ this.listen(root, "click", (event) => this._handleClick(event));
26058
+ this._render();
26059
+ return root;
26060
+ }
26061
+ /** Returns a defensive AST copy. @returns {FilterAst} */
26062
+ getValue() {
26063
+ return cloneFilterAst(this._value);
26064
+ }
26065
+ /** Atomically replaces the AST. @param {unknown} value @param {{silent?:boolean}} [options={}] @returns {this} */
26066
+ setValue(value, { silent = false } = {}) {
26067
+ const parsed = parseFilterAst(value, this._limits());
26068
+ this._abortAll();
26069
+ this._value = parsed;
26070
+ this._render();
26071
+ if (!silent) this._changed("set", this._value.root.id);
26072
+ return this;
26073
+ }
26074
+ /** Adds a condition to a group. @param {string|null} [parentId=null] @param {Record<string,unknown>} [initial={}] @returns {string|null} */
26075
+ addCondition(parentId = null, initial = {}) {
26076
+ if (!this._canMutate() || conditionCount(this._value.root) >= Number(this.options.maxConditions)) return null;
26077
+ const parent = findGroup(this._value.root, parentId ?? this._value.root.id);
26078
+ if (!parent) return null;
26079
+ const first = this._fields[0];
26080
+ const field = String(initial.field ?? first?.id ?? "");
26081
+ const definition = this._field(field);
26082
+ const node = filterCondition({
26083
+ ...initial,
26084
+ field,
26085
+ operator: String(initial.operator ?? this._defaultOperator(definition) ?? "")
26086
+ });
26087
+ parent.children.push(node);
26088
+ this._render();
26089
+ this._changed("add", node.id);
26090
+ this.focus(node.id);
26091
+ return node.id;
26092
+ }
26093
+ /** Adds a nested group. @param {string|null} [parentId=null] @param {'and'|'or'} [logic='and'] @returns {string|null} */
26094
+ addGroup(parentId = null, logic = "and") {
26095
+ if (!this._canMutate() || !this.options.allowGroups) return null;
26096
+ const parent = findGroup(this._value.root, parentId ?? this._value.root.id);
26097
+ if (!parent || groupDepth(this._value.root, parent.id) >= Number(this.options.maxDepth)) return null;
26098
+ const group = filterGroup(logic === "or" ? "or" : "and");
26099
+ parent.children.push(group);
26100
+ this._render();
26101
+ this._changed("add", group.id);
26102
+ this.focus(group.id);
26103
+ return group.id;
26104
+ }
26105
+ /** Updates a node by stable ID. @param {string} id @param {Record<string,unknown>} patch @returns {this} */
26106
+ update(id, patch = {}) {
26107
+ if (!this._canMutate()) return this;
26108
+ const node = findNode2(this._value.root, id);
26109
+ if (!node) return this;
26110
+ if (node.kind === "group") {
26111
+ if (patch.logic === "and" || patch.logic === "or") node.logic = patch.logic;
26112
+ } else {
26113
+ if (Object.hasOwn(patch, "field")) node.field = String(patch.field ?? "");
26114
+ if (Object.hasOwn(patch, "operator")) node.operator = String(patch.operator ?? "");
26115
+ if (Object.hasOwn(patch, "value")) node.value = parseValueJson(patch.value);
26116
+ }
26117
+ this._abort(id);
26118
+ this._render();
26119
+ this._changed("update", id);
26120
+ return this;
26121
+ }
26122
+ /** Removes a non-root node. @param {string} id @returns {this} */
26123
+ remove(id) {
26124
+ if (!this._canMutate() || id === this._value.root.id) return this;
26125
+ const parent = findParent(this._value.root, id);
26126
+ if (!parent) return this;
26127
+ const index = parent.children.findIndex((child) => child.id === id);
26128
+ const focusId = parent.children[index + 1]?.id ?? parent.children[index - 1]?.id ?? parent.id;
26129
+ const removed = removeNode(this._value.root, id);
26130
+ if (!removed) return this;
26131
+ this._abortTree(removed);
26132
+ this._render();
26133
+ const label = removed.kind === "condition" ? this._field(removed.field)?.label ?? "Filter" : "Filter group";
26134
+ this._announce(`${label} removed`);
26135
+ this._changed("remove", id);
26136
+ this.focus(focusId);
26137
+ return this;
26138
+ }
26139
+ /** Moves a node into a group while preserving its identity. @param {string} id @param {string} parentId @param {number} index @returns {this} */
26140
+ move(id, parentId, index) {
26141
+ const node = findNode2(this._value.root, id);
26142
+ const parent = findGroup(this._value.root, parentId);
26143
+ const previous = findParent(this._value.root, id);
26144
+ if (!this._canMutate() || id === this._value.root.id || !node || !parent || !previous || containsNode(node, parentId)) return this;
26145
+ const previousIndex = previous.children.findIndex((child) => child.id === id);
26146
+ const removed = previous.children.splice(previousIndex, 1)[0];
26147
+ parent.children.splice(Math.max(0, Math.min(parent.children.length, Number(index) || 0)), 0, removed);
26148
+ try {
26149
+ parseFilterAst(this._value, this._limits());
26150
+ } catch {
26151
+ parent.children.splice(parent.children.indexOf(removed), 1);
26152
+ previous.children.splice(previousIndex, 0, removed);
26153
+ return this;
26154
+ }
26155
+ this._render();
26156
+ this._changed("move", id);
26157
+ return this;
26158
+ }
26159
+ /** Returns semantic validity and errors. @returns {{valid:boolean,errors:Array<Record<string,string>>}} */
26160
+ validate() {
26161
+ return validateFilterAst(this._value, this._fields, this._operators);
26162
+ }
26163
+ /** Applies only a valid AST. @returns {FilterAst|null} */
26164
+ apply() {
26165
+ if (this._disabled) return null;
26166
+ const result = this.validate();
26167
+ if (!result.valid) {
26168
+ this.emit("invalid", { errors: result.errors });
26169
+ this.focus(result.errors[0]?.nodeId);
26170
+ return null;
26171
+ }
26172
+ const value = this.getValue();
26173
+ this.emit("apply", { value });
26174
+ return value;
26175
+ }
26176
+ /** Clears all conditions. @param {{silent?:boolean}} [options={}] @returns {this} */
26177
+ clear({ silent = false } = {}) {
26178
+ if (!this._canMutate()) return this;
26179
+ this._abortAll();
26180
+ this._value.root.children = [];
26181
+ this._render();
26182
+ if (!silent) this._changed("clear", this._value.root.id);
26183
+ this.refs.add?.focus();
26184
+ return this;
26185
+ }
26186
+ /** Focuses a condition's field or the Add button. @param {string|null} [id=null] @returns {this} */
26187
+ focus(id = null) {
26188
+ const selector = id ? `[data-node-id="${cssEscape(id)}"] :is([data-filter-field], [data-filter-logic], [data-filter-action="add"])` : '[data-filter-action="add"]';
26189
+ queueMicrotask(() => (
26190
+ /** @type {HTMLElement|null} */
26191
+ this.el.querySelector(selector)?.focus()
26192
+ ));
26193
+ return this;
26194
+ }
26195
+ /** @param {boolean} readonly @returns {this} */
26196
+ setReadonly(readonly) {
26197
+ this._readonly = Boolean(readonly);
26198
+ this._render();
26199
+ return this;
26200
+ }
26201
+ /** @returns {this} */
26202
+ enable() {
26203
+ this._disabled = false;
26204
+ this._render();
26205
+ return this;
26206
+ }
26207
+ /** @returns {this} */
26208
+ disable() {
26209
+ this._disabled = true;
26210
+ this._abortAll();
26211
+ this._render();
26212
+ return this;
26213
+ }
26214
+ /** @returns {void} */
26215
+ destroy() {
26216
+ this._abortAll();
26217
+ this._destroyEditors();
26218
+ const root = this.el;
26219
+ const snapshot10 = this._snapshot;
26220
+ super.destroy();
26221
+ if (!this._createdRoot && root) restoreTarget(root, snapshot10);
26222
+ }
26223
+ /** @returns {void} */
26224
+ _render() {
26225
+ this._destroyEditors();
26226
+ this._renderVersion = (this._renderVersion ?? 0) + 1;
26227
+ const status = h("div", { ref: "status", class: "zx-filter__status", role: "status", ariaLive: "polite" });
26228
+ const tree = this._renderGroup(this._value.root, 0, true);
26229
+ const actions2 = h(
26230
+ "div",
26231
+ { class: "zx-filter__actions" },
26232
+ actionButton("add", this._value.root.id, "plus", "Add filter", { ref: "add", disabled: this._blocked() }),
26233
+ this.options.allowGroups ? actionButton("add-group", this._value.root.id, "folder", "Add group", { disabled: this._blocked() }) : null,
26234
+ actionButton("clear", "", "x", String(this.options.clearLabel), { disabled: this._blocked() }),
26235
+ actionButton("apply", "", "check", String(this.options.applyLabel), { class: "zx-button--primary", disabled: this._disabled })
26236
+ );
26237
+ this.el.replaceChildren(status, tree, actions2);
26238
+ this.refs.status = status;
26239
+ this.refs.add = /** @type {HTMLElement} */
26240
+ this.el.querySelector('[data-filter-action="add"]');
26241
+ this.el.toggleAttribute("data-readonly", this._readonly);
26242
+ this.el.toggleAttribute("data-disabled", this._disabled);
26243
+ const result = this.validate();
26244
+ this.el.dataset.valid = String(result.valid);
26245
+ for (const error2 of result.errors) this.el.querySelector(`[data-node-id="${cssEscape(error2.nodeId)}"]`)?.setAttribute("data-invalid", "");
26246
+ }
26247
+ /** @param {any} group @param {number} depth @param {boolean} root @returns {HTMLElement} */
26248
+ _renderGroup(group, depth, root) {
26249
+ const children = h(
26250
+ "ol",
26251
+ { class: "zx-filter__children" },
26252
+ group.children.map((node) => node.kind === "group" ? this._renderGroup(node, depth + 1, false) : this._renderCondition(node))
26253
+ );
26254
+ const logic = h(
26255
+ "select",
26256
+ {
26257
+ class: "zx-filter__logic",
26258
+ dataset: { filterLogic: "", nodeId: group.id },
26259
+ ariaLabel: root ? "Root filter logic" : "Group filter logic",
26260
+ disabled: this._blocked()
26261
+ },
26262
+ h("option", { value: "and", selected: group.logic === "and" }, "Match all"),
26263
+ h("option", { value: "or", selected: group.logic === "or" }, "Match any")
26264
+ );
26265
+ const legend = h("legend", { class: "zx-filter__legend" }, logic, h("span", {}, root ? "conditions" : "in this group"));
26266
+ const controls = h(
26267
+ "div",
26268
+ { class: "zx-filter__group-actions" },
26269
+ actionButton("add", group.id, "plus", "Add filter", { disabled: this._blocked() }),
26270
+ this.options.allowGroups && depth < Number(this.options.maxDepth) ? actionButton("add-group", group.id, "folder", "Add subgroup", { disabled: this._blocked() }) : null,
26271
+ root ? null : actionButton("remove", group.id, "trash", "Remove group", { disabled: this._blocked() })
26272
+ );
26273
+ return h(root ? "fieldset" : "li", {
26274
+ class: "zx-filter__group",
26275
+ dataset: { nodeId: group.id, depth: String(depth) }
26276
+ }, root ? legend : h("fieldset", {}, legend, children, controls), root ? children : null, root ? controls : null);
26277
+ }
26278
+ /** @param {any} node @returns {HTMLElement} */
26279
+ _renderCondition(node) {
26280
+ const field = this._field(node.field);
26281
+ const operators = this._operatorsFor(field);
26282
+ const operator = this._operator(node.operator);
26283
+ const fieldSelect = h(
26284
+ "select",
26285
+ {
26286
+ class: "zx-filter__field",
26287
+ dataset: { filterField: "", nodeId: node.id },
26288
+ ariaLabel: "Filter field",
26289
+ disabled: this._blocked()
26290
+ },
26291
+ h("option", { value: "", selected: !node.field }, "Choose a field\u2026"),
26292
+ this._fields.map((candidate) => h("option", { value: candidate.id, selected: candidate.id === node.field }, candidate.label)),
26293
+ node.field && !field ? h("option", { value: node.field, selected: true }, `Unavailable: ${node.field}`) : null
26294
+ );
26295
+ const operatorSelect = h(
26296
+ "select",
26297
+ {
26298
+ class: "zx-filter__operator",
26299
+ dataset: { filterOperator: "", nodeId: node.id },
26300
+ ariaLabel: "Filter operator",
26301
+ disabled: this._blocked()
26302
+ },
26303
+ h("option", { value: "", selected: !node.operator }, "Choose an operator\u2026"),
26304
+ operators.map((candidate) => h("option", { value: candidate.id, selected: candidate.id === node.operator }, candidate.label)),
26305
+ node.operator && !operators.some((candidate) => candidate.id === node.operator) ? h("option", { value: node.operator, selected: true }, `Unavailable: ${node.operator}`) : null
26306
+ );
26307
+ return h(
26308
+ "li",
26309
+ { class: "zx-filter__condition", dataset: { nodeId: node.id } },
26310
+ fieldSelect,
26311
+ operatorSelect,
26312
+ this._renderValue(node, field, operator),
26313
+ actionButton("remove", node.id, "trash", `Remove ${field?.label ?? "filter"}`, { disabled: this._blocked() })
26314
+ );
26315
+ }
26316
+ /** @param {any} node @param {FilterField|null} field @param {any} operator @returns {HTMLElement} */
26317
+ _renderValue(node, field, operator) {
26318
+ const host = h("div", { class: "zx-filter__value" });
26319
+ if (!field || !operator) return host;
26320
+ if (operator.arity === "none") return h("div", { class: "zx-filter__value zx-filter__value--empty" }, "No value");
26321
+ if (field.type === "custom" && typeof field.editor === "function") {
26322
+ const renderVersion = this._renderVersion;
26323
+ queueMicrotask(() => {
26324
+ if (renderVersion !== this._renderVersion || !this.el.contains(host)) return;
26325
+ const handle = field.editor(host, {
26326
+ value: node.value,
26327
+ field,
26328
+ conditionId: node.id,
26329
+ setValue: (value) => this._setNodeValue(node.id, parseValueJson(value))
26330
+ });
26331
+ if (handle && typeof handle.destroy === "function") this._customEditors.push(handle);
26332
+ });
26333
+ return host;
26334
+ }
26335
+ const values = operator.arity === "pair" ? Array.isArray(node.value) ? node.value : [null, null] : [node.value];
26336
+ if (operator.arity === "many") {
26337
+ const control = choiceSelect(field, node, values[0], true, this._blocked());
26338
+ if (control) return h("div", { class: "zx-filter__value" }, control);
26339
+ return h("div", { class: "zx-filter__value" }, valueInput(field, node.id, Array.isArray(node.value) ? node.value.join(", ") : "", 0, this._blocked(), true));
26340
+ }
26341
+ const controls = values.map((value, index) => choiceSelect(field, node, value, false, this._blocked(), index) ?? valueInput(field, node.id, value, index, this._blocked()));
26342
+ return h("div", { class: "zx-filter__value", dataset: { arity: operator.arity } }, controls);
26343
+ }
26344
+ /** @param {Event} event @returns {void} */
26345
+ _handleChange(event) {
26346
+ if (!this._canMutate()) return;
26347
+ const target = (
26348
+ /** @type {HTMLInputElement|HTMLSelectElement|null} */
26349
+ event.target
26350
+ );
26351
+ const id = target?.dataset.nodeId;
26352
+ if (!target || !id) return;
26353
+ if (target.dataset.filterField !== void 0) {
26354
+ const node = findNode2(this._value.root, id);
26355
+ if (node?.kind !== "condition") return;
26356
+ node.field = target.value;
26357
+ const field = this._field(node.field);
26358
+ node.operator = this._defaultOperator(field) ?? "";
26359
+ node.value = null;
26360
+ this._abort(id);
26361
+ this._render();
26362
+ this._changed("update", id);
26363
+ this.focus(id);
26364
+ } else if (target.dataset.filterOperator !== void 0) {
26365
+ const node = findNode2(this._value.root, id);
26366
+ if (node?.kind !== "condition") return;
26367
+ node.operator = target.value;
26368
+ node.value = null;
26369
+ this._abort(id);
26370
+ this._render();
26371
+ this._changed("update", id);
26372
+ queueMicrotask(() => (
26373
+ /** @type {HTMLElement|null} */
26374
+ this.el.querySelector(`[data-node-id="${cssEscape(id)}"] [data-filter-value]`)?.focus()
26375
+ ));
26376
+ } else if (target.dataset.filterLogic !== void 0) {
26377
+ const node = findNode2(this._value.root, id);
26378
+ if (node?.kind === "group") node.logic = target.value === "or" ? "or" : "and";
26379
+ this._changed("update", id);
26380
+ } else if (target.dataset.filterValue !== void 0) this._valueInput(target);
26381
+ }
26382
+ /** @param {Event} event @returns {void} */
26383
+ _handleInput(event) {
26384
+ if (!this._canMutate()) return;
26385
+ const target = (
26386
+ /** @type {HTMLInputElement|null} */
26387
+ event.target
26388
+ );
26389
+ if (!target || target.dataset.filterValue === void 0) return;
26390
+ this._valueInput(target);
26391
+ const id = target.dataset.nodeId;
26392
+ const node = findNode2(this._value.root, id);
26393
+ const field = node?.kind === "condition" ? this._field(node.field) : null;
26394
+ if (field?.loadChoices) this._scheduleChoices(node.id, field, target.value);
26395
+ }
26396
+ /** @param {HTMLInputElement|HTMLSelectElement} target @returns {void} */
26397
+ _valueInput(target) {
26398
+ const node = findNode2(this._value.root, target.dataset.nodeId);
26399
+ if (node?.kind !== "condition") return;
26400
+ const operator = this._operator(node.operator);
26401
+ const field = this._field(node.field);
26402
+ if (!operator || !field) return;
26403
+ let value;
26404
+ if (target instanceof HTMLSelectElement && target.multiple) {
26405
+ value = Array.from(target.selectedOptions, (option) => parseScalar(option.value, field.type));
26406
+ } else if (operator.arity === "many") {
26407
+ value = target.value.split(",").map((item) => item.trim()).filter(Boolean).map((item) => parseScalar(item, field.type));
26408
+ } else if (operator.arity === "pair") {
26409
+ const pair = Array.isArray(node.value) ? [...node.value] : [null, null];
26410
+ pair[Number(target.dataset.valueIndex) || 0] = parseScalar(target.value, field.type);
26411
+ value = pair;
26412
+ } else value = parseScalar(target.value, field.type);
26413
+ this._setNodeValue(
26414
+ node.id,
26415
+ /** @type {any} */
26416
+ value
26417
+ );
26418
+ }
26419
+ /** @param {MouseEvent} event @returns {void} */
26420
+ _handleClick(event) {
26421
+ const button2 = (
26422
+ /** @type {HTMLElement|null} */
26423
+ event.target?.closest?.("[data-filter-action]")
26424
+ );
26425
+ if (!button2 || !this.el.contains(button2)) return;
26426
+ const action = button2.dataset.filterAction;
26427
+ const id = button2.dataset.nodeId || null;
26428
+ if (action === "add") this.addCondition(id);
26429
+ else if (action === "add-group") this.addGroup(id);
26430
+ else if (action === "remove" && id) this.remove(id);
26431
+ else if (action === "clear") this.clear();
26432
+ else if (action === "apply") this.apply();
26433
+ }
26434
+ /** @param {string} id @param {any} value @returns {void} */
26435
+ _setNodeValue(id, value) {
26436
+ const node = findNode2(this._value.root, id);
26437
+ if (node?.kind !== "condition") return;
26438
+ node.value = value;
26439
+ const result = this.validate();
26440
+ this.el.dataset.valid = String(result.valid);
26441
+ const row2 = this.el.querySelector(`[data-node-id="${cssEscape(id)}"]`);
26442
+ row2?.toggleAttribute("data-invalid", result.errors.some((candidate) => candidate.nodeId === id));
26443
+ this._changed("update", id);
26444
+ }
26445
+ /** @param {string} reason @param {string} nodeId @returns {void} */
26446
+ _changed(reason, nodeId) {
26447
+ const result = this.validate();
26448
+ const value = this.getValue();
26449
+ this.emit("change", { value, valid: result.valid, errors: result.errors, reason, nodeId });
26450
+ if (this.options.autoApply && result.valid) this.emit("apply", { value });
26451
+ }
26452
+ /** @param {string} id @param {FilterField} field @param {string} query @returns {void} */
26453
+ _scheduleChoices(id, field, query) {
26454
+ this._abort(id);
26455
+ if (query.length < Number(field.minQuery ?? 0)) return;
26456
+ const controller = new AbortController();
26457
+ this._setAsyncBusy(id, true);
26458
+ const entry = { controller, timer: setTimeout(async () => {
26459
+ this.emit("query", { conditionId: id, fieldId: field.id, query });
26460
+ try {
26461
+ const choices = await field.loadChoices({ query, signal: controller.signal, conditionId: id, field });
26462
+ if (controller.signal.aborted || this._async.get(id)?.controller !== controller) return;
26463
+ const input = this.el.querySelector(`[data-node-id="${cssEscape(id)}"] [data-filter-value]`);
26464
+ const listId = input?.getAttribute("list");
26465
+ const list = listId ? this.el.querySelector(`#${cssEscape(listId)}`) : null;
26466
+ if (list) list.replaceChildren(...(Array.isArray(choices) ? choices : []).map((choice) => h("option", { value: choice.value }, choice.label)));
26467
+ this.emit("loaded", { conditionId: id, fieldId: field.id, query, choices });
26468
+ } catch (error2) {
26469
+ if (!controller.signal.aborted) this.emit("error", { conditionId: id, fieldId: field.id, query, error: error2 instanceof Error ? error2 : new Error(String(error2)) });
26470
+ } finally {
26471
+ if (this._async.get(id)?.controller === controller) {
26472
+ this._async.delete(id);
26473
+ this._setAsyncBusy(id, false);
26474
+ }
26475
+ }
26476
+ }, Math.max(0, Number(field.debounce ?? 200))) };
26477
+ this._async.set(id, entry);
26478
+ }
26479
+ /** @param {string} id @returns {void} */
26480
+ _abort(id) {
26481
+ const entry = this._async.get(id);
26482
+ if (entry) {
26483
+ clearTimeout(entry.timer);
26484
+ entry.controller.abort();
26485
+ this._async.delete(id);
26486
+ this._setAsyncBusy(id, false);
26487
+ }
26488
+ }
26489
+ /** @param {string} id @param {boolean} busy @returns {void} */
26490
+ _setAsyncBusy(id, busy) {
26491
+ const input = this.el.querySelector(`[data-node-id="${cssEscape(id)}"] [data-filter-value]`);
26492
+ if (busy) input?.setAttribute("aria-busy", "true");
26493
+ else input?.removeAttribute("aria-busy");
26494
+ }
26495
+ /** @param {any} node @returns {void} */
26496
+ _abortTree(node) {
26497
+ if (node.kind === "condition") this._abort(node.id);
26498
+ else node.children.forEach((child) => this._abortTree(child));
26499
+ }
26500
+ /** @returns {void} */
26501
+ _abortAll() {
26502
+ for (const id of [...this._async.keys()]) this._abort(id);
26503
+ }
26504
+ /** @returns {void} */
26505
+ _destroyEditors() {
26506
+ for (const editor of this._customEditors ?? []) editor.destroy?.();
26507
+ this._customEditors = [];
26508
+ }
26509
+ /** @returns {boolean} */
26510
+ _blocked() {
26511
+ return this._readonly || this._disabled;
26512
+ }
26513
+ /** @returns {boolean} */
26514
+ _canMutate() {
26515
+ return !this._blocked();
26516
+ }
26517
+ /** @returns {{maxDepth:number,maxConditions:number}} */
26518
+ _limits() {
26519
+ return { maxDepth: Number(this.options.maxDepth), maxConditions: Number(this.options.maxConditions) };
26520
+ }
26521
+ /** @param {string} id @returns {FilterField|null} */
26522
+ _field(id) {
26523
+ return this._fields.find((field) => field.id === id) ?? null;
26524
+ }
26525
+ /** @param {string} id @returns {any} */
26526
+ _operator(id) {
26527
+ return this._operators.find((operator) => operator.id === id) ?? null;
26528
+ }
26529
+ /** @param {FilterField|null} field @returns {any[]} */
26530
+ _operatorsFor(field) {
26531
+ if (!field) return [];
26532
+ const ids = field.operators?.length ? field.operators : null;
26533
+ return this._operators.filter((operator) => ids ? ids.includes(operator.id) : operator.types.includes(field.type));
26534
+ }
26535
+ /** @param {FilterField|null} field @returns {string|null} */
26536
+ _defaultOperator(field) {
26537
+ return field?.defaultOperator && this._operatorsFor(field).some((operator) => operator.id === field.defaultOperator) ? field.defaultOperator : this._operatorsFor(field)[0]?.id ?? null;
26538
+ }
26539
+ /** @param {string} text @returns {void} */
26540
+ _announce(text4) {
26541
+ if (this.refs.status) this.refs.status.textContent = text4;
26542
+ }
26543
+ };
26544
+ function actionButton(action, id, iconName, label, props = {}) {
26545
+ 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));
26546
+ }
26547
+ function choiceSelect(field, node, value, multiple, disabled, index = 0) {
26548
+ const choices = Array.isArray(field.choices) && field.choices.length ? field.choices : field.type === "boolean" ? [{ value: true, label: "True" }, { value: false, label: "False" }] : null;
26549
+ if (!choices) return null;
26550
+ const values = new Set(Array.isArray(value) ? value.map(String) : [String(value ?? "")]);
26551
+ return (
26552
+ /** @type {HTMLSelectElement} */
26553
+ h(
26554
+ "select",
26555
+ {
26556
+ dataset: { filterValue: "", nodeId: node.id, valueIndex: String(index) },
26557
+ ariaLabel: `${field.label} value`,
26558
+ multiple,
26559
+ disabled
26560
+ },
26561
+ !multiple ? h("option", { value: "", selected: !values.has(String(value)) || value == null }, "Choose\u2026") : null,
26562
+ choices.map((choice) => h("option", { value: choice.value, selected: values.has(String(choice.value)) }, choice.label))
26563
+ )
26564
+ );
26565
+ }
26566
+ function valueInput(field, id, value, index, disabled, many = false) {
26567
+ const type = many ? "text" : ["number", "money"].includes(field.type) ? "number" : field.type === "date" ? "date" : field.type === "datetime" ? "datetime-local" : "text";
26568
+ const listId = field.loadChoices ? `zx-filter-choices-${id}-${index}` : null;
26569
+ const input = (
26570
+ /** @type {HTMLInputElement} */
26571
+ h("input", {
26572
+ type,
26573
+ value: value ?? "",
26574
+ dataset: { filterValue: "", nodeId: id, valueIndex: String(index) },
26575
+ ariaLabel: `${field.label} value${index ? ` ${index + 1}` : ""}`,
26576
+ disabled,
26577
+ placeholder: many ? "Comma-separated values" : "",
26578
+ list: listId
26579
+ })
26580
+ );
26581
+ return h(
26582
+ "span",
26583
+ { class: "zx-filter__value-control" },
26584
+ input,
26585
+ listId ? h("datalist", { id: listId }) : null
26586
+ );
26587
+ }
26588
+ function parseScalar(value, type) {
26589
+ if (value === "") return null;
26590
+ if (["number", "money"].includes(type)) {
26591
+ const number2 = Number(value);
26592
+ return Number.isFinite(number2) ? number2 : null;
26593
+ }
26594
+ if (type === "boolean") return value === "true";
26595
+ return value;
26596
+ }
26597
+ function parseValueJson(value) {
26598
+ return cloneFilterValue(value === void 0 ? null : value);
26599
+ }
26600
+ function findNode2(root, id) {
26601
+ if (!root || root.id === id) return root ?? null;
26602
+ if (root.kind === "group") for (const child of root.children) {
26603
+ const match = findNode2(child, id);
26604
+ if (match) return match;
26605
+ }
26606
+ return null;
26607
+ }
26608
+ function findGroup(root, id) {
26609
+ const node = findNode2(root, id);
26610
+ return node?.kind === "group" ? node : null;
26611
+ }
26612
+ function findParent(root, id) {
26613
+ if (root?.kind !== "group") return null;
26614
+ if (root.children.some((child) => child.id === id)) return root;
26615
+ for (const child of root.children) {
26616
+ const parent = findParent(child, id);
26617
+ if (parent) return parent;
26618
+ }
26619
+ return null;
26620
+ }
26621
+ function removeNode(root, id) {
26622
+ if (root.kind !== "group") return null;
26623
+ const index = root.children.findIndex((child) => child.id === id);
26624
+ if (index >= 0) return root.children.splice(index, 1)[0];
26625
+ for (const child of root.children) {
26626
+ const found = removeNode(child, id);
26627
+ if (found) return found;
26628
+ }
26629
+ return null;
26630
+ }
26631
+ function containsNode(node, id) {
26632
+ return Boolean(node && findNode2(node, id));
26633
+ }
26634
+ function conditionCount(node) {
26635
+ return node.kind === "condition" ? 1 : node.children.reduce((sum, child) => sum + conditionCount(child), 0);
26636
+ }
26637
+ function groupDepth(root, id, depth = 0) {
26638
+ if (root.id === id) return depth;
26639
+ if (root.kind === "group") for (const child of root.children) {
26640
+ if (child.kind === "group") {
26641
+ const found = groupDepth(child, id, depth + 1);
26642
+ if (found >= 0) return found;
26643
+ }
26644
+ }
26645
+ return -1;
26646
+ }
26647
+ function mergeOperators(base, extra) {
26648
+ const map = new Map(base.map((item) => [item.id, { ...item, types: [...item.types] }]));
26649
+ 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] : [] });
26650
+ return [...map.values()];
26651
+ }
26652
+ function cssEscape(value) {
26653
+ return globalThis.CSS?.escape ? CSS.escape(value) : String(value).replaceAll(/[^a-zA-Z0-9_-]/g, "\\$&");
26654
+ }
26655
+
24333
26656
  // src/components/fieldset/fieldset.js
24334
26657
  var Fieldset = class extends Component {
24335
26658
  static cssName = "fieldset";
@@ -25497,8 +27820,8 @@ var zx = (() => {
25497
27820
  this._setBusy(true);
25498
27821
  try {
25499
27822
  message = await item.validate(answer, this.getAnswers());
25500
- } catch (error) {
25501
- message = error instanceof Error ? error.message : String(error);
27823
+ } catch (error2) {
27824
+ message = error2 instanceof Error ? error2.message : String(error2);
25502
27825
  } finally {
25503
27826
  if (!this._destroyed) this._setBusy(false);
25504
27827
  }
@@ -25636,10 +27959,10 @@ var zx = (() => {
25636
27959
  }
25637
27960
  /** @param {NormalizedItem} item @returns {void} */
25638
27961
  _paintError(item) {
25639
- const error = this._itemHost.querySelector(".zx-questionnaire__error");
25640
- if (error instanceof HTMLElement) {
25641
- error.textContent = this._error;
25642
- error.hidden = !this._error;
27962
+ const error2 = this._itemHost.querySelector(".zx-questionnaire__error");
27963
+ if (error2 instanceof HTMLElement) {
27964
+ error2.textContent = this._error;
27965
+ error2.hidden = !this._error;
25643
27966
  }
25644
27967
  if (this._error) this._itemHost.setAttribute("aria-invalid", "true");
25645
27968
  else this._itemHost.removeAttribute("aria-invalid");
@@ -25647,7 +27970,7 @@ var zx = (() => {
25647
27970
  if (field) {
25648
27971
  if (this._error) field.setHighlight(this._error, "danger");
25649
27972
  else field.clearHighlight();
25650
- if (error instanceof HTMLElement) error.hidden = true;
27973
+ if (error2 instanceof HTMLElement) error2.hidden = true;
25651
27974
  }
25652
27975
  }
25653
27976
  /** @returns {void} */