@zeyos/zx 4.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 (49) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/DESIGN-SYSTEM.md +59 -3
  3. package/dist/types/components/app-icon/app-icon.d.ts +88 -0
  4. package/dist/types/components/filter/filter-model.d.ts +87 -0
  5. package/dist/types/components/filter/filter.d.ts +331 -0
  6. package/dist/types/components/select/select.d.ts +46 -4
  7. package/dist/types/components/table/table.d.ts +103 -0
  8. package/dist/types/components/tag-picker/tag-picker.d.ts +16 -0
  9. package/dist/types/core/dom.d.ts +1 -1
  10. package/dist/types/core/util.d.ts +9 -0
  11. package/dist/types/index.d.ts +4 -1
  12. package/dist/types/zeyos/icons.d.ts +19 -2
  13. package/dist/types/zeyos/index.d.ts +1 -1
  14. package/dist/types/zeyos/select.d.ts +2 -0
  15. package/dist/zx-zeyos.esm.js +702 -75
  16. package/dist/zx-zeyos.esm.min.js +1 -1
  17. package/dist/zx.css +415 -4
  18. package/dist/zx.esm.js +1568 -115
  19. package/dist/zx.esm.min.js +3 -3
  20. package/dist/zx.global.js +1568 -115
  21. package/dist/zx.global.min.js +3 -3
  22. package/dist/zx.min.css +1 -1
  23. package/docs/api.json +379 -0
  24. package/docs/llms.md +93 -16
  25. package/docs/llms.txt +11 -9
  26. package/package.json +1 -1
  27. package/src/components/app-icon/app-icon.css +105 -0
  28. package/src/components/app-icon/app-icon.js +134 -0
  29. package/src/components/filter/filter-model.js +226 -0
  30. package/src/components/filter/filter.css +147 -0
  31. package/src/components/filter/filter.js +554 -0
  32. package/src/components/grid/grid.css +1 -1
  33. package/src/components/grid/grid.js +4 -1
  34. package/src/components/message/message.css +36 -3
  35. package/src/components/select/select.css +17 -0
  36. package/src/components/select/select.js +105 -24
  37. package/src/components/table/table.css +85 -0
  38. package/src/components/table/table.js +418 -24
  39. package/src/components/tag-picker/tag-picker.css +19 -0
  40. package/src/components/tag-picker/tag-picker.js +42 -5
  41. package/src/core/dom.js +1 -1
  42. package/src/core/util.js +20 -0
  43. package/src/index.js +7 -1
  44. package/src/zeyos/icons.js +34 -21
  45. package/src/zeyos/index.js +1 -1
  46. package/src/zeyos/select.js +6 -5
  47. package/styles/tokens/semantic.css +9 -0
  48. package/styles/tokens/themes.css +2 -2
  49. package/styles/zx.css +2 -0
package/dist/zx.global.js CHANGED
@@ -21,6 +21,7 @@ var zx = (() => {
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
23
  AccountMenu: () => AccountMenu,
24
+ AppIcon: () => AppIcon,
24
25
  AppSidebar: () => AppSidebar,
25
26
  Avatar: () => Avatar,
26
27
  Breadcrumb: () => Breadcrumb,
@@ -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
  }
@@ -911,9 +937,9 @@ var zx = (() => {
911
937
  let rendered;
912
938
  try {
913
939
  rendered = h.scope(this.refs, () => this.render());
914
- } catch (error) {
940
+ } catch (error2) {
915
941
  this.#abort.abort();
916
- throw error;
942
+ throw error2;
917
943
  }
918
944
  if (this.#created) {
919
945
  const root = isElement(rendered) ? rendered : this.el;
@@ -1204,8 +1230,8 @@ var zx = (() => {
1204
1230
  /** @type {Readonly<FieldOptions>} */
1205
1231
  this.options
1206
1232
  ));
1207
- } catch (error) {
1208
- const message = error instanceof Error ? error.message : String(error);
1233
+ } catch (error2) {
1234
+ const message = error2 instanceof Error ? error2.message : String(error2);
1209
1235
  console.warn(`[zx.Field] Adapter "${type}" failed: ${message}`);
1210
1236
  this.adapter = errorAdapter(message, this.controlId);
1211
1237
  }
@@ -5188,8 +5214,8 @@ var zx = (() => {
5188
5214
  return response;
5189
5215
  } catch (caught) {
5190
5216
  if (runId !== this._runId) return null;
5191
- const error = caught instanceof Error ? caught : new Error(String(caught));
5192
- if (error.name === "AbortError") {
5217
+ const error2 = caught instanceof Error ? caught : new Error(String(caught));
5218
+ if (error2.name === "AbortError") {
5193
5219
  this._finishAbort();
5194
5220
  return null;
5195
5221
  }
@@ -5198,8 +5224,8 @@ var zx = (() => {
5198
5224
  this._transportController = null;
5199
5225
  this._abort.hidden = true;
5200
5226
  this._setState("error");
5201
- this._status.textContent = error.message;
5202
- this.emit("error", { error });
5227
+ this._status.textContent = error2.message;
5228
+ this.emit("error", { error: error2 });
5203
5229
  return null;
5204
5230
  }
5205
5231
  }
@@ -5347,10 +5373,10 @@ var zx = (() => {
5347
5373
  this.emit("abort", {});
5348
5374
  }
5349
5375
  /** @param {Error} error @returns {void} */
5350
- _selectionError(error) {
5376
+ _selectionError(error2) {
5351
5377
  this._setState("error");
5352
- this._status.textContent = error.message;
5353
- this.emit("error", { error });
5378
+ this._status.textContent = error2.message;
5379
+ this.emit("error", { error: error2 });
5354
5380
  }
5355
5381
  /** @returns {void} */
5356
5382
  _renderPreview() {
@@ -5433,9 +5459,9 @@ var zx = (() => {
5433
5459
  }
5434
5460
  function abortError() {
5435
5461
  if (typeof DOMException === "function") return new DOMException("Upload aborted", "AbortError");
5436
- const error = new Error("Upload aborted");
5437
- error.name = "AbortError";
5438
- return error;
5462
+ const error2 = new Error("Upload aborted");
5463
+ error2.name = "AbortError";
5464
+ return error2;
5439
5465
  }
5440
5466
 
5441
5467
  // src/components/field-upload/field-adapter.js
@@ -6561,8 +6587,8 @@ var zx = (() => {
6561
6587
  const shown = this._preview ?? this._value;
6562
6588
  const symbols = this.refs.symbols.querySelectorAll(".zx-rating__symbol");
6563
6589
  symbols.forEach((symbol, index) => {
6564
- const filled = Math.min(1, Math.max(0, shown - index));
6565
- 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}%`);
6566
6592
  symbol.toggleAttribute("data-preview", this._preview !== null);
6567
6593
  });
6568
6594
  this.el.dataset.value = String(shown);
@@ -6848,6 +6874,7 @@ var zx = (() => {
6848
6874
  static defaults = {
6849
6875
  items: [],
6850
6876
  fixedItems: [],
6877
+ fixedLabel: null,
6851
6878
  valueKey: "ID",
6852
6879
  labelKey: "name",
6853
6880
  renderItem: null,
@@ -6885,8 +6912,7 @@ var zx = (() => {
6885
6912
  this._position = null;
6886
6913
  this._createdRoot = false;
6887
6914
  this._root = null;
6888
- this._originalChildren = [];
6889
- this._originalAttributes = /* @__PURE__ */ new Map();
6915
+ this._snapshot = null;
6890
6916
  this._createdRoot = this.el === null;
6891
6917
  const root = (
6892
6918
  /** @type {HTMLElement} */
@@ -6894,10 +6920,7 @@ var zx = (() => {
6894
6920
  );
6895
6921
  this.el = root;
6896
6922
  this._root = root;
6897
- if (!this._createdRoot) this._originalChildren = Array.from(root.childNodes);
6898
- this._rememberAttribute(root, "data-state");
6899
- this._rememberAttribute(root, "data-loading");
6900
- this._rememberAttribute(root, "aria-disabled");
6923
+ this._snapshot = this._createdRoot ? null : snapshotTarget(root);
6901
6924
  const listId = uid("zx-select-list");
6902
6925
  const input = h("input", {
6903
6926
  ref: "input",
@@ -7200,6 +7223,66 @@ var zx = (() => {
7200
7223
  select.set(value, { silent: true });
7201
7224
  return select;
7202
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
+ );
7274
+ return h(
7275
+ "span",
7276
+ { class: "zx-select__entity" },
7277
+ decoration ? h("span", { class: "zx-select__entity-icon", ariaHidden: "true" }, decoration) : null,
7278
+ copy
7279
+ );
7280
+ }),
7281
+ renderValueAdornment: renderValueAdornment ?? ((item) => visual(item, true))
7282
+ });
7283
+ select.el.dataset.preset = "entity";
7284
+ return select;
7285
+ }
7203
7286
  /**
7204
7287
  * Creates the record-permission picker: the fixed Private and Public choices above the groups a
7205
7288
  * record can be shared with. The value is the tri-state ZeyOS stores — `false` for private,
@@ -7250,13 +7333,7 @@ var zx = (() => {
7250
7333
  /** @type {HTMLElement} */
7251
7334
  this.el
7252
7335
  );
7253
- if (!this._createdRoot && root) {
7254
- root.replaceChildren(...this._originalChildren);
7255
- for (const [name, value] of this._originalAttributes) {
7256
- if (value === null) root.removeAttribute(name);
7257
- else root.setAttribute(name, value);
7258
- }
7259
- }
7336
+ if (!this._createdRoot && root) restoreTarget(root, this._snapshot);
7260
7337
  super.destroy();
7261
7338
  }
7262
7339
  /** @param {KeyboardEvent} event @returns {void} */
@@ -7393,14 +7470,19 @@ var zx = (() => {
7393
7470
  list.replaceChildren();
7394
7471
  this._optionEntries = [];
7395
7472
  let lastGroup = /* @__PURE__ */ Symbol("initial-group");
7473
+ const fixed = this._uniqueItems(this._visibleFixed);
7474
+ const items = this._uniqueItems(this._visibleItems, fixed);
7396
7475
  if (this.options.clearable) {
7397
7476
  this._appendOption(null, true, this.options.noneLabel ?? this._message("select.none", "No selection"));
7398
7477
  }
7399
- for (const item of this._visibleFixed) this._appendOption(item, false);
7400
- 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) {
7401
7483
  list.append(h("div", { class: "zx-select__separator", role: "presentation" }));
7402
7484
  }
7403
- for (const item of this._visibleItems) {
7485
+ for (const item of items) {
7404
7486
  if (this.options.groupKey) {
7405
7487
  const group = this._read(item, this.options.groupKey);
7406
7488
  if (!Object.is(group, lastGroup)) {
@@ -7414,7 +7496,7 @@ var zx = (() => {
7414
7496
  this._appendOption(item, false);
7415
7497
  }
7416
7498
  if (this._actions.length > 0) {
7417
- if (this._visibleFixed.length > 0 || this._visibleItems.length > 0 || this.options.clearable) {
7499
+ if (fixed.length > 0 || items.length > 0 || this.options.clearable) {
7418
7500
  list.append(h("div", { class: "zx-select__separator", role: "presentation" }));
7419
7501
  }
7420
7502
  let actionGroup = /* @__PURE__ */ Symbol("initial-action-group");
@@ -7429,7 +7511,7 @@ var zx = (() => {
7429
7511
  this._appendAction(action);
7430
7512
  }
7431
7513
  }
7432
- if (this._visibleItems.length === 0 && this._visibleFixed.length === 0 && this._actions.length === 0 && !this._loading) {
7514
+ if (items.length === 0 && fixed.length === 0 && this._actions.length === 0 && !this._loading) {
7433
7515
  list.append(h("div", {
7434
7516
  class: "zx-select__empty",
7435
7517
  role: "status"
@@ -7630,6 +7712,16 @@ var zx = (() => {
7630
7712
  _matchFixed(query) {
7631
7713
  return matchItems(this._fixedItems, query, this._searchKeys());
7632
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
+ }
7633
7725
  /** @returns {number} */
7634
7726
  _selectedEntryIndex() {
7635
7727
  if (this._selected === null) return this.options.clearable ? 0 : -1;
@@ -7641,16 +7733,28 @@ var zx = (() => {
7641
7733
  _itemEntryIndex(itemIndex) {
7642
7734
  return itemIndex + (this.options.clearable ? 1 : 0);
7643
7735
  }
7644
- /** @param {string} name @returns {void} */
7645
- _rememberAttribute(element, name) {
7646
- this._originalAttributes.set(name, element.getAttribute(name));
7647
- }
7648
7736
  /** @param {string} key @param {string} fallback @returns {string} */
7649
7737
  _message(key, fallback) {
7650
7738
  const message = this.msg(key);
7651
7739
  return message === key ? fallback : message;
7652
7740
  }
7653
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
+ }
7654
7758
  function normalizeRenderedContent(rendered) {
7655
7759
  return rendered && typeof rendered === "object" && "nodeType" in rendered ? (
7656
7760
  /** @type {Node} */
@@ -8127,6 +8231,8 @@ var zx = (() => {
8127
8231
  values: [],
8128
8232
  valueKey: "ID",
8129
8233
  labelKey: "name",
8234
+ iconKey: "icon",
8235
+ colorKey: "color",
8130
8236
  searchKeys: null,
8131
8237
  filter: "local",
8132
8238
  minQuery: 0,
@@ -8467,12 +8573,13 @@ var zx = (() => {
8467
8573
  _renderTags() {
8468
8574
  const tags = this._selected.map((item) => {
8469
8575
  const label = this._labelOf(item);
8470
- const content = this.options.renderTag ? this.options.renderTag(item) : label;
8471
- const tag = h(
8472
- "span",
8473
- { class: "zx-tag-picker__tag" },
8474
- h("span", { class: "zx-tag-picker__tag-label" }, content)
8475
- );
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);
8476
8583
  if (!this._readonly && !this._disabled) {
8477
8584
  tag.append(h("button", {
8478
8585
  class: "zx-icon-btn zx-tag-picker__remove",
@@ -8510,7 +8617,7 @@ var zx = (() => {
8510
8617
  h(
8511
8618
  "span",
8512
8619
  { class: "zx-tag-picker__option-label" },
8513
- 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))]
8514
8621
  )
8515
8622
  );
8516
8623
  if (!selected && this.isFull()) option.setAttribute("aria-disabled", "true");
@@ -8735,10 +8842,10 @@ var zx = (() => {
8735
8842
  if (sequence !== this._requestSequence) return;
8736
8843
  this._items = Array.isArray(result) ? result.slice() : [];
8737
8844
  this._visible = this._items.slice();
8738
- } catch (error) {
8845
+ } catch (error2) {
8739
8846
  if (sequence !== this._requestSequence) return;
8740
8847
  this._visible = [];
8741
- this.emit("error", { error });
8848
+ this.emit("error", { error: error2 });
8742
8849
  } finally {
8743
8850
  if (sequence === this._requestSequence) {
8744
8851
  this._setLoading(false);
@@ -8831,6 +8938,34 @@ var zx = (() => {
8831
8938
  if (item !== null && typeof item === "object") return String(item[key] ?? "");
8832
8939
  return String(item ?? "");
8833
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
+ }
8834
8969
  /** @returns {void} */
8835
8970
  _emitChange() {
8836
8971
  this.emit("change", { values: this.getValues(), items: this.getItems() });
@@ -9715,38 +9850,38 @@ var zx = (() => {
9715
9850
  try {
9716
9851
  json2 = JSON.parse(text4);
9717
9852
  } catch (cause) {
9718
- const error = (
9853
+ const error2 = (
9719
9854
  /** @type {Error & {response?: Response}} */
9720
9855
  new Error("Invalid JSON response", { cause })
9721
9856
  );
9722
- error.response = response;
9723
- throw error;
9857
+ error2.response = response;
9858
+ throw error2;
9724
9859
  }
9725
9860
  }
9726
9861
  if (!response.ok) {
9727
9862
  try {
9728
9863
  parseResult(json2);
9729
- } catch (error2) {
9730
- error2.response = response;
9731
- throw error2;
9864
+ } catch (error3) {
9865
+ error3.response = response;
9866
+ throw error3;
9732
9867
  }
9733
- const error = (
9868
+ const error2 = (
9734
9869
  /** @type {Error & {response?: Response}} */
9735
9870
  new Error(`HTTP ${response.status} ${response.statusText}`.trim())
9736
9871
  );
9737
- error.response = response;
9738
- throw error;
9872
+ error2.response = response;
9873
+ throw error2;
9739
9874
  }
9740
9875
  return parseResult(json2);
9741
9876
  } catch (caught) {
9742
- const error = caught instanceof Error ? caught : new Error(String(caught));
9877
+ const error2 = caught instanceof Error ? caught : new Error(String(caught));
9743
9878
  if (this.onError) {
9744
9879
  try {
9745
- this.onError(error, { path, options });
9880
+ this.onError(error2, { path, options });
9746
9881
  } catch {
9747
9882
  }
9748
9883
  }
9749
- throw error;
9884
+ throw error2;
9750
9885
  } finally {
9751
9886
  if (timeoutId !== null) clearTimeout(timeoutId);
9752
9887
  }
@@ -10463,9 +10598,9 @@ var zx = (() => {
10463
10598
  this.el.returnValue = "";
10464
10599
  try {
10465
10600
  this._show();
10466
- } catch (error) {
10601
+ } catch (error2) {
10467
10602
  delete this.el.dataset.zxOverlayOrder;
10468
- throw error;
10603
+ throw error2;
10469
10604
  }
10470
10605
  this.el.dataset.state = "open";
10471
10606
  this.emit("open");
@@ -10948,9 +11083,9 @@ var zx = (() => {
10948
11083
  }
10949
11084
  try {
10950
11085
  super(normalizedTarget, options);
10951
- } catch (error) {
11086
+ } catch (error2) {
10952
11087
  if (adoption) restoreAdoption(adoption);
10953
- throw error;
11088
+ throw error2;
10954
11089
  }
10955
11090
  this._adoption = adoption;
10956
11091
  }
@@ -11678,7 +11813,11 @@ var zx = (() => {
11678
11813
  rowClass: null,
11679
11814
  zebra: true,
11680
11815
  editMode: false,
11681
- hierarchy: false
11816
+ editTrigger: "double",
11817
+ hierarchy: false,
11818
+ rowReorder: false,
11819
+ columnVisibility: false,
11820
+ hiddenColumns: []
11682
11821
  };
11683
11822
  /**
11684
11823
  * Creates a table around a target or creates a new root when target is null.
@@ -11707,6 +11846,7 @@ var zx = (() => {
11707
11846
  this._rowElements = /* @__PURE__ */ new Map();
11708
11847
  this._restored = false;
11709
11848
  this._editMode = normalizeEditMode(this.options.editMode);
11849
+ this._editTrigger = this.options.editTrigger === "single" ? "single" : "double";
11710
11850
  this._edit = null;
11711
11851
  this._editMessage = null;
11712
11852
  this._stacked = false;
@@ -11718,6 +11858,10 @@ var zx = (() => {
11718
11858
  this._rovingCell = null;
11719
11859
  this._rovingMatched = false;
11720
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;
11721
11865
  this._expanded = initialExpanded(this._hierarchy?.expanded);
11722
11866
  if (this._sort && this.options.sortMode === "local") this._sortData();
11723
11867
  if (this._hierarchy?.expanded === true) {
@@ -11739,11 +11883,17 @@ var zx = (() => {
11739
11883
  zebra: String(Boolean(this.options.zebra))
11740
11884
  }
11741
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;
11742
11891
  if (this.options.height != null) {
11743
11892
  this._scroll.style.blockSize = typeof this.options.height === "number" ? `${this.options.height}px` : String(this.options.height);
11744
11893
  }
11894
+ this._columnControls = this.options.columnVisibility ? this._createColumnControls() : null;
11745
11895
  if (this._editMode === false) {
11746
- root.replaceChildren(this._scroll);
11896
+ root.replaceChildren(...[this._columnControls, this._scroll, this._rowMoveMessage].filter(Boolean));
11747
11897
  } else {
11748
11898
  root.dataset.editMode = this._editMode;
11749
11899
  this._editMessage = h("div", {
@@ -11752,7 +11902,7 @@ var zx = (() => {
11752
11902
  role: "status",
11753
11903
  ariaLive: "polite"
11754
11904
  });
11755
- root.replaceChildren(this._scroll, this._editMessage);
11905
+ root.replaceChildren(...[this._columnControls, this._scroll, this._editMessage, this._rowMoveMessage].filter(Boolean));
11756
11906
  }
11757
11907
  this._renderColumns();
11758
11908
  this._renderHeader();
@@ -11760,6 +11910,34 @@ var zx = (() => {
11760
11910
  this._watchWidth();
11761
11911
  this.listen(this._tbody, "click", (event) => this._handleBodyClick(event));
11762
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
+ }
11763
11941
  if (this._editMode !== false) {
11764
11942
  this.listen(this._tbody, "keydown", (event) => this._handleEditKeydown(
11765
11943
  /** @type {KeyboardEvent} */
@@ -11875,6 +12053,81 @@ var zx = (() => {
11875
12053
  getData() {
11876
12054
  return [...this._data];
11877
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
+ }
11878
12131
  /**
11879
12132
  * Removes all rows.
11880
12133
  * @returns {this}
@@ -12043,14 +12296,17 @@ var zx = (() => {
12043
12296
  */
12044
12297
  startEdit(rowId, columnId) {
12045
12298
  if (this._editMode === false) return this;
12046
- const column = this._columns.find((candidate) => candidate.id === columnId);
12047
- if (!column) return this;
12299
+ const requested = this._columns.find((candidate) => candidate.id === columnId);
12300
+ if (!requested) return this;
12048
12301
  const current = this._data.find((candidate) => Object.is(this._idFor(candidate), rowId));
12049
- 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;
12050
12306
  if (this._edit) {
12051
- if (Object.is(this._edit.id, rowId) && this._edit.cells.has(columnId)) {
12052
- this._edit.columnId = columnId;
12053
- 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);
12054
12310
  return this;
12055
12311
  }
12056
12312
  this.commitEdit();
@@ -12061,7 +12317,7 @@ var zx = (() => {
12061
12317
  const row2 = this._data[index];
12062
12318
  const tr = this._rowElements.get(rowId)?.[0];
12063
12319
  if (!tr) return this;
12064
- 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];
12065
12321
  const cells = /* @__PURE__ */ new Map();
12066
12322
  for (const target of targets) {
12067
12323
  const td = cellIn(tr, target.id);
@@ -12070,10 +12326,10 @@ var zx = (() => {
12070
12326
  if (entry) cells.set(target.id, entry);
12071
12327
  }
12072
12328
  if (cells.size === 0) return this;
12073
- this._edit = { id: rowId, columnId, row: row2, cells, tr };
12329
+ this._edit = { id: rowId, columnId: activeColumnId, row: row2, cells, tr };
12074
12330
  tr.dataset.editing = "true";
12075
- this.emit("editstart", { row: row2, id: rowId, column, columnId, value: row2?.[columnId] });
12076
- this._focusEditor(columnId);
12331
+ this.emit("editstart", { row: row2, id: rowId, column, columnId: activeColumnId, value: row2?.[activeColumnId] });
12332
+ this._focusEditor(activeColumnId);
12077
12333
  return this;
12078
12334
  }
12079
12335
  /**
@@ -12328,16 +12584,197 @@ var zx = (() => {
12328
12584
  }
12329
12585
  super.destroy();
12330
12586
  }
12587
+ /** @returns {TableColumn[]} */
12588
+ _visibleColumns() {
12589
+ return this._columns.filter((column) => !this._hiddenColumns.has(column.id));
12590
+ }
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;
12622
+ });
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
+ }
12331
12766
  /** @returns {void} */
12332
12767
  _renderColumns() {
12333
12768
  const fragment2 = document.createDocumentFragment();
12769
+ if (this.options.rowReorder) fragment2.append(h("col", { class: "zx-table__reorder-column" }));
12334
12770
  if (this.options.selectable === "multi") {
12335
12771
  fragment2.append(h("col", { class: "zx-table__selection-column" }));
12336
12772
  }
12337
- const fractions = this._columns.map((column) => parseFraction(column.width));
12773
+ const columns = this._visibleColumns();
12774
+ const fractions = columns.map((column) => parseFraction(column.width));
12338
12775
  const total = fractions.reduce((sum, value) => sum + value, 0);
12339
12776
  this._table.dataset.layout = total > 0 ? "fixed" : "auto";
12340
- this._columns.forEach((column, index) => {
12777
+ columns.forEach((column, index) => {
12341
12778
  const col = h("col");
12342
12779
  if (fractions[index] > 0) col.style.width = `${fractions[index] / total * 100}%`;
12343
12780
  else if (column.width && column.width !== "auto") col.style.width = String(column.width);
@@ -12349,6 +12786,11 @@ var zx = (() => {
12349
12786
  _renderHeader() {
12350
12787
  const row2 = h("tr", { class: "zx-table__header-row" });
12351
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
+ }));
12352
12794
  if (this.options.selectable === "multi") {
12353
12795
  this._selectAll = h("input", {
12354
12796
  class: "zx-table__checkbox zx-table__select-all",
@@ -12374,7 +12816,7 @@ var zx = (() => {
12374
12816
  this._selectAll = null;
12375
12817
  }
12376
12818
  this._headers = /* @__PURE__ */ new Map();
12377
- for (const column of this._columns) {
12819
+ for (const column of this._visibleColumns()) {
12378
12820
  const th = h("th", { scope: "col" });
12379
12821
  const align = column.align ?? (isNumericColumn(column) ? "end" : null);
12380
12822
  if (align) th.dataset.align = align;
@@ -12414,7 +12856,7 @@ var zx = (() => {
12414
12856
  { class: "zx-table__empty-row" },
12415
12857
  h("td", {
12416
12858
  class: "zx-table__empty",
12417
- 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)
12418
12860
  }, this._emptyContent())
12419
12861
  ));
12420
12862
  } else {
@@ -12423,6 +12865,7 @@ var zx = (() => {
12423
12865
  });
12424
12866
  }
12425
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)));
12426
12869
  if (this._growStep) this._table.setAttribute("aria-rowcount", String(projected.length + 1));
12427
12870
  this._syncSelectAll();
12428
12871
  this._syncGrowing();
@@ -12466,6 +12909,19 @@ var zx = (() => {
12466
12909
  const elements = this._rowElements.get(id) ?? [];
12467
12910
  elements.push(tr);
12468
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
+ }
12469
12925
  if (this.options.selectable === "multi") {
12470
12926
  const checkbox = h("input", {
12471
12927
  class: "zx-table__checkbox zx-table__row-checkbox",
@@ -12475,7 +12931,7 @@ var zx = (() => {
12475
12931
  });
12476
12932
  tr.append(h("td", { class: "zx-table__selection-cell" }, checkbox));
12477
12933
  }
12478
- this._columns.forEach((column) => {
12934
+ this._visibleColumns().forEach((column) => {
12479
12935
  const cell = (
12480
12936
  /** @type {HTMLTableCellElement} */
12481
12937
  h("td")
@@ -12511,7 +12967,7 @@ var zx = (() => {
12511
12967
  const content = h("span", { class: "zx-table__cell-content" });
12512
12968
  if (value && typeof value === "object" && typeof value.nodeType === "number") content.append(value);
12513
12969
  else if (value != null) content.append(document.createTextNode(String(value)));
12514
- if (!this._hierarchy || column.id !== this._hierarchy.column || !tree) {
12970
+ if (!this._hierarchy || column.id !== this._hierarchyColumn() || !tree) {
12515
12971
  cell.replaceChildren(...content.childNodes);
12516
12972
  return;
12517
12973
  }
@@ -12542,6 +12998,7 @@ var zx = (() => {
12542
12998
  if (!rowElement || !this._tbody.contains(rowElement)) return;
12543
12999
  const meta = this._rowMeta.get(rowElement);
12544
13000
  if (!meta) return;
13001
+ if (target.closest(".zx-table__row-handle")) return;
12545
13002
  if (this._edit) {
12546
13003
  this.commitEdit();
12547
13004
  if (this._edit) return;
@@ -12561,6 +13018,14 @@ var zx = (() => {
12561
13018
  );
12562
13019
  return;
12563
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
+ }
12564
13029
  this.emit("rowclick", { ...meta, event });
12565
13030
  if (this.options.selectable === "single" && !this._selected.has(meta.id)) {
12566
13031
  this._selected = /* @__PURE__ */ new Set([meta.id]);
@@ -12581,7 +13046,8 @@ var zx = (() => {
12581
13046
  if (!rowElement || !this._tbody.contains(rowElement)) return;
12582
13047
  const meta = this._rowMeta.get(rowElement);
12583
13048
  if (!meta) return;
12584
- if (this._editMode !== false) {
13049
+ if (isInteractiveCellContent(target)) return;
13050
+ if (this._editMode !== false && this._editTrigger === "double") {
12585
13051
  const cell = target?.closest('td[data-editable="true"]');
12586
13052
  if (cell && this._tbody.contains(cell)) {
12587
13053
  this.startEdit(meta.id, String(cell.dataset.column));
@@ -12883,13 +13349,14 @@ var zx = (() => {
12883
13349
  * @returns {{id: unknown, columnId: string, index: number}|null} Next cell, or null at the end.
12884
13350
  */
12885
13351
  _nextEditableCell(rowIndex, columnId, direction) {
12886
- const columnCount = this._columns.length;
13352
+ const columns = this._visibleColumns();
13353
+ const columnCount = columns.length;
12887
13354
  if (rowIndex < 0 || columnCount === 0) return null;
12888
13355
  const visible = this._visibleData();
12889
13356
  const startingId = this._idFor(this._data[rowIndex]);
12890
13357
  let row2 = visible.findIndex((candidate) => Object.is(this._idFor(candidate), startingId));
12891
13358
  if (row2 < 0) return null;
12892
- let column = this._columns.findIndex((candidate) => candidate.id === columnId);
13359
+ let column = columns.findIndex((candidate) => candidate.id === columnId);
12893
13360
  const limit = visible.length * columnCount + columnCount;
12894
13361
  for (let step = 0; step < limit; step += 1) {
12895
13362
  column += direction;
@@ -12902,7 +13369,7 @@ var zx = (() => {
12902
13369
  }
12903
13370
  if (row2 < 0 || row2 >= visible.length) return null;
12904
13371
  const candidateRow = visible[row2];
12905
- const candidateColumn = this._columns[column];
13372
+ const candidateColumn = columns[column];
12906
13373
  if (resolveEditable(candidateColumn, candidateRow) !== false) {
12907
13374
  return {
12908
13375
  id: this._idFor(candidateRow),
@@ -13145,6 +13612,53 @@ var zx = (() => {
13145
13612
  }
13146
13613
  return result;
13147
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
+ }
13148
13662
  function resolveEditable(column, row2) {
13149
13663
  if (!column || typeof column !== "object") return false;
13150
13664
  if (typeof column.editor === "function") {
@@ -13225,7 +13739,8 @@ var zx = (() => {
13225
13739
  for (const node of nodes) if (!byId.has(node.id)) byId.set(node.id, node);
13226
13740
  const parentOf = /* @__PURE__ */ new Map();
13227
13741
  for (const node of nodes) {
13228
- 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;
13229
13744
  parentOf.set(node, parent === node ? null : parent);
13230
13745
  }
13231
13746
  for (const node of nodes) {
@@ -13254,6 +13769,9 @@ var zx = (() => {
13254
13769
  function readRowValue(accessor, row2) {
13255
13770
  return typeof accessor === "function" ? accessor(row2) : row2?.[accessor];
13256
13771
  }
13772
+ function sameHierarchyParent(left, right) {
13773
+ return left == null && right == null || Object.is(left, right);
13774
+ }
13257
13775
  function normalizeHierarchy(option, columns) {
13258
13776
  if (!option || typeof option !== "object") return null;
13259
13777
  const parentId = option.parentId;
@@ -13273,6 +13791,23 @@ var zx = (() => {
13273
13791
  function isInsideEditor(target) {
13274
13792
  return Boolean(target?.closest?.(".zx-table__editor"));
13275
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
+ }
13276
13811
  function deferToEditor(target) {
13277
13812
  if (target.closest("[popover]")) return true;
13278
13813
  return target.getAttribute?.("aria-expanded") === "true";
@@ -15761,11 +16296,11 @@ var zx = (() => {
15761
16296
  this.setFooter(this.options.footer);
15762
16297
  this.setLink(this.options.link);
15763
16298
  return root;
15764
- } catch (error) {
16299
+ } catch (error2) {
15765
16300
  this._actionAbort?.abort();
15766
16301
  this._linkAbort?.abort();
15767
16302
  if (!created) restoreTarget(root, this._original);
15768
- throw error;
16303
+ throw error2;
15769
16304
  }
15770
16305
  }
15771
16306
  /** Replaces the card title. @param {string} title Next title. @returns {this} */
@@ -15809,9 +16344,9 @@ var zx = (() => {
15809
16344
  if (onclick) control.addEventListener("click", onclick, { signal: nextAbort.signal });
15810
16345
  return control;
15811
16346
  });
15812
- } catch (error) {
16347
+ } catch (error2) {
15813
16348
  nextAbort.abort();
15814
- throw error;
16349
+ throw error2;
15815
16350
  }
15816
16351
  this._actionAbort?.abort();
15817
16352
  this._actionAbort = nextAbort;
@@ -19252,6 +19787,106 @@ var zx = (() => {
19252
19787
  return value.toLocaleString();
19253
19788
  }
19254
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
+
19255
19890
  // src/components/launcher/launcher.js
19256
19891
  var Launcher = class extends Component {
19257
19892
  static cssName = "launcher";
@@ -19521,9 +20156,9 @@ var zx = (() => {
19521
20156
  itemOrder: item.itemOrder ?? (source.order === "source" ? itemOrder : void 0)
19522
20157
  })));
19523
20158
  this._renderResults();
19524
- } catch (error) {
20159
+ } catch (error2) {
19525
20160
  if (request.signal.aborted || sequence !== this._requestSequence) return;
19526
- this.emit("error", { source, error });
20161
+ this.emit("error", { source, error: error2 });
19527
20162
  }
19528
20163
  }));
19529
20164
  if (request.signal.aborted || sequence !== this._requestSequence) return;
@@ -20293,7 +20928,7 @@ var zx = (() => {
20293
20928
  this.#content = this.options.content;
20294
20929
  this.#disabled = Boolean(this.options.disabled);
20295
20930
  this.#describedBy = resolved.getAttribute("aria-describedby");
20296
- 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));
20297
20932
  const trigger = TRIGGERS.has(this.options.trigger) ? this.options.trigger : "both";
20298
20933
  if (trigger === "manual") return;
20299
20934
  if (trigger === "hover" || trigger === "both") {
@@ -20551,7 +21186,7 @@ var zx = (() => {
20551
21186
  if (!tokens.includes(token)) tokens.push(token);
20552
21187
  return tokens.join(" ");
20553
21188
  }
20554
- function cssLength2(value) {
21189
+ function cssLength3(value) {
20555
21190
  return typeof value === "number" && Number.isFinite(value) ? `${value}px` : String(value);
20556
21191
  }
20557
21192
  function isFocusVisible(element) {
@@ -23052,7 +23687,7 @@ var zx = (() => {
23052
23687
  * @param {GridOptions & import('../table/table.js').TableOptions} [options={}] Grid/Table options.
23053
23688
  */
23054
23689
  constructor(target = null, options = {}) {
23055
- super(target, options);
23690
+ super(target, { editTrigger: "single", ...options });
23056
23691
  }
23057
23692
  /**
23058
23693
  * Creates an editable hierarchical billing-line grid.
@@ -23091,6 +23726,9 @@ var zx = (() => {
23091
23726
  return {
23092
23727
  rowId: fields.id,
23093
23728
  editMode: "cell",
23729
+ editTrigger: "single",
23730
+ rowReorder: true,
23731
+ columnVisibility: true,
23094
23732
  hierarchy: { parentId: fields.parent, column: fields.item, expanded: true },
23095
23733
  responsive: "sm",
23096
23734
  ...tableOptions,
@@ -23322,8 +23960,8 @@ var zx = (() => {
23322
23960
  return this;
23323
23961
  }
23324
23962
  /** Shows an error and destroys the current engine. @param {unknown} error @returns {this} */
23325
- setError(error) {
23326
- this._error = error ?? new Error(String(this.options.errorText));
23963
+ setError(error2) {
23964
+ this._error = error2 ?? new Error(String(this.options.errorText));
23327
23965
  this._hasError = true;
23328
23966
  this._loading = false;
23329
23967
  this._destroyHandle();
@@ -23342,8 +23980,8 @@ var zx = (() => {
23342
23980
  resize() {
23343
23981
  try {
23344
23982
  this._handle?.resize?.();
23345
- } catch (error) {
23346
- this.setError(error);
23983
+ } catch (error2) {
23984
+ this.setError(error2);
23347
23985
  }
23348
23986
  return this;
23349
23987
  }
@@ -23396,8 +24034,8 @@ var zx = (() => {
23396
24034
  this.emit("ready", { instance: this.getInstance() });
23397
24035
  }
23398
24036
  if (emitUpdate) this.emit("update", { data: this._data, type: this._type });
23399
- } catch (error) {
23400
- this.setError(error);
24037
+ } catch (error2) {
24038
+ this.setError(error2);
23401
24039
  }
23402
24040
  }
23403
24041
  /** @returns {ChartSpec} */
@@ -23639,8 +24277,8 @@ var zx = (() => {
23639
24277
  borderColor: borderColor || void 0
23640
24278
  };
23641
24279
  }
23642
- function errorMessage(error, fallback) {
23643
- const message = error && typeof error === "object" && "message" in error ? String(error.message) : "";
24280
+ function errorMessage(error2, fallback) {
24281
+ const message = error2 && typeof error2 === "object" && "message" in error2 ? String(error2.message) : "";
23644
24282
  return message.trim() ? message : String(fallback);
23645
24283
  }
23646
24284
 
@@ -24279,8 +24917,8 @@ var zx = (() => {
24279
24917
  node[this._childrenKey] = Array.isArray(children) ? children : [];
24280
24918
  if (node[this._childrenKey].length === 0) node.hasChildren = false;
24281
24919
  return true;
24282
- } catch (error) {
24283
- this.emit("error", { error, node, id });
24920
+ } catch (error2) {
24921
+ this.emit("error", { error: error2, node, id });
24284
24922
  return false;
24285
24923
  } finally {
24286
24924
  this._loading.delete(id);
@@ -24768,9 +25406,9 @@ var zx = (() => {
24768
25406
  const children = await this.options.load(node);
24769
25407
  node[this._childrenKey] = Array.isArray(children) ? children : [];
24770
25408
  if (node[this._childrenKey].length === 0) node.hasChildren = false;
24771
- } catch (error) {
25409
+ } catch (error2) {
24772
25410
  node[this._childrenKey] = [];
24773
- this.emit("error", { error, node, id });
25411
+ this.emit("error", { error: error2, node, id });
24774
25412
  } finally {
24775
25413
  this._loading.delete(id);
24776
25414
  }
@@ -25200,6 +25838,821 @@ var zx = (() => {
25200
25838
  }
25201
25839
  }
25202
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
+
25203
26656
  // src/components/fieldset/fieldset.js
25204
26657
  var Fieldset = class extends Component {
25205
26658
  static cssName = "fieldset";
@@ -26367,8 +27820,8 @@ var zx = (() => {
26367
27820
  this._setBusy(true);
26368
27821
  try {
26369
27822
  message = await item.validate(answer, this.getAnswers());
26370
- } catch (error) {
26371
- message = error instanceof Error ? error.message : String(error);
27823
+ } catch (error2) {
27824
+ message = error2 instanceof Error ? error2.message : String(error2);
26372
27825
  } finally {
26373
27826
  if (!this._destroyed) this._setBusy(false);
26374
27827
  }
@@ -26506,10 +27959,10 @@ var zx = (() => {
26506
27959
  }
26507
27960
  /** @param {NormalizedItem} item @returns {void} */
26508
27961
  _paintError(item) {
26509
- const error = this._itemHost.querySelector(".zx-questionnaire__error");
26510
- if (error instanceof HTMLElement) {
26511
- error.textContent = this._error;
26512
- 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;
26513
27966
  }
26514
27967
  if (this._error) this._itemHost.setAttribute("aria-invalid", "true");
26515
27968
  else this._itemHost.removeAttribute("aria-invalid");
@@ -26517,7 +27970,7 @@ var zx = (() => {
26517
27970
  if (field) {
26518
27971
  if (this._error) field.setHighlight(this._error, "danger");
26519
27972
  else field.clearHighlight();
26520
- if (error instanceof HTMLElement) error.hidden = true;
27973
+ if (error2 instanceof HTMLElement) error2.hidden = true;
26521
27974
  }
26522
27975
  }
26523
27976
  /** @returns {void} */