@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
@@ -500,6 +500,20 @@ function isElement(value) {
500
500
  return Boolean(value && typeof value === "object" && /** @type {{nodeType?: unknown}} */
501
501
  value.nodeType === 1);
502
502
  }
503
+ function isCssColor(value) {
504
+ if (typeof value !== "string") return false;
505
+ const text3 = value.trim();
506
+ if (!text3 || text3.length > 256 || /[;{}\u0000-\u001f\u007f]/.test(text3) || /(?:url|image|image-set|cross-fade|element|var)\s*\(/i.test(text3)) return false;
507
+ const supports = globalThis.CSS?.supports;
508
+ if (typeof supports === "function") {
509
+ try {
510
+ return supports.call(globalThis.CSS, "color", text3);
511
+ } catch {
512
+ return false;
513
+ }
514
+ }
515
+ return /^(?:#[\da-f]{3,8}|[a-z]+|(?:rgba?|hsla?|hwb|lab|lch|oklab|oklch|color|color-mix)\([^;{}]+\))$/i.test(text3);
516
+ }
503
517
  function clamp(n, min, max) {
504
518
  return Math.min(max, Math.max(min, n));
505
519
  }
@@ -6563,6 +6577,7 @@ var Select = class _Select extends Component {
6563
6577
  static defaults = {
6564
6578
  items: [],
6565
6579
  fixedItems: [],
6580
+ fixedLabel: null,
6566
6581
  valueKey: "ID",
6567
6582
  labelKey: "name",
6568
6583
  renderItem: null,
@@ -6600,8 +6615,7 @@ var Select = class _Select extends Component {
6600
6615
  this._position = null;
6601
6616
  this._createdRoot = false;
6602
6617
  this._root = null;
6603
- this._originalChildren = [];
6604
- this._originalAttributes = /* @__PURE__ */ new Map();
6618
+ this._snapshot = null;
6605
6619
  this._createdRoot = this.el === null;
6606
6620
  const root = (
6607
6621
  /** @type {HTMLElement} */
@@ -6609,10 +6623,7 @@ var Select = class _Select extends Component {
6609
6623
  );
6610
6624
  this.el = root;
6611
6625
  this._root = root;
6612
- if (!this._createdRoot) this._originalChildren = Array.from(root.childNodes);
6613
- this._rememberAttribute(root, "data-state");
6614
- this._rememberAttribute(root, "data-loading");
6615
- this._rememberAttribute(root, "aria-disabled");
6626
+ this._snapshot = this._createdRoot ? null : snapshotTarget(root);
6616
6627
  const listId = uid("zx-select-list");
6617
6628
  const input = h("input", {
6618
6629
  ref: "input",
@@ -6915,6 +6926,66 @@ var Select = class _Select extends Component {
6915
6926
  select.set(value, { silent: true });
6916
6927
  return select;
6917
6928
  }
6929
+ /**
6930
+ * Creates a rich entity picker with optional Recent choices and a create command.
6931
+ * @param {Element|string|null} target Component target.
6932
+ * @param {SelectOptions & EntitySelectOptions} [options={}] Entity-preset options.
6933
+ * @returns {Select}
6934
+ */
6935
+ static entity(target, options = {}) {
6936
+ const {
6937
+ recent = [],
6938
+ recentLabel = "Recent",
6939
+ create = false,
6940
+ iconKey = "icon",
6941
+ colorKey = "color",
6942
+ subtitleKey = "description",
6943
+ renderIcon = null,
6944
+ fixedItems = [],
6945
+ actions = [],
6946
+ renderItem = null,
6947
+ renderValueAdornment = null,
6948
+ ...rest
6949
+ } = options;
6950
+ const visual = (item, selected = false) => {
6951
+ if (typeof renderIcon === "function") return renderIcon(item, { selected });
6952
+ const value = readSelectItem(item, iconKey);
6953
+ if (value && typeof value === "object" && typeof value.nodeType === "number") return value.cloneNode(true);
6954
+ if (typeof value !== "string" || !value) return null;
6955
+ const color = readSelectItem(item, colorKey);
6956
+ return h("span", {
6957
+ class: "zx-select__entity-glyph",
6958
+ style: isCssColor(color) ? { "--zx-select-entity-color": String(color) } : null,
6959
+ ariaHidden: "true"
6960
+ }, icon(value, { size: 14 }));
6961
+ };
6962
+ const createAction = create ? normalizeEntityCreateAction(create) : null;
6963
+ const select = new _Select(target, {
6964
+ ...rest,
6965
+ fixedItems: [...Array.isArray(recent) ? recent : [], ...Array.isArray(fixedItems) ? fixedItems : []],
6966
+ fixedLabel: Array.isArray(recent) && recent.length ? recentLabel : rest.fixedLabel ?? null,
6967
+ actions: [...Array.isArray(actions) ? actions : [], ...createAction ? [createAction] : []],
6968
+ renderItem: renderItem ?? ((item) => {
6969
+ const decoration = visual(item, false);
6970
+ const subtitle = readSelectItem(item, subtitleKey);
6971
+ const copy = h(
6972
+ "span",
6973
+ { class: "zx-select__entity-copy" },
6974
+ h("span", { class: "zx-select__entity-label" }, String(readSelectItem(item, rest.labelKey ?? "name") ?? "")),
6975
+ subtitle == null || subtitle === "" ? null : h("span", { class: "zx-select__entity-subtitle" }, String(subtitle))
6976
+ );
6977
+ return h(
6978
+ "span",
6979
+ { class: "zx-select__entity" },
6980
+ decoration ? h("span", { class: "zx-select__entity-icon", ariaHidden: "true" }, decoration) : null,
6981
+ copy
6982
+ );
6983
+ }),
6984
+ renderValueAdornment: renderValueAdornment ?? ((item) => visual(item, true))
6985
+ });
6986
+ select.el.dataset.preset = "entity";
6987
+ return select;
6988
+ }
6918
6989
  /**
6919
6990
  * Creates the record-permission picker: the fixed Private and Public choices above the groups a
6920
6991
  * record can be shared with. The value is the tri-state ZeyOS stores — `false` for private,
@@ -6965,13 +7036,7 @@ var Select = class _Select extends Component {
6965
7036
  /** @type {HTMLElement} */
6966
7037
  this.el
6967
7038
  );
6968
- if (!this._createdRoot && root) {
6969
- root.replaceChildren(...this._originalChildren);
6970
- for (const [name, value] of this._originalAttributes) {
6971
- if (value === null) root.removeAttribute(name);
6972
- else root.setAttribute(name, value);
6973
- }
6974
- }
7039
+ if (!this._createdRoot && root) restoreTarget(root, this._snapshot);
6975
7040
  super.destroy();
6976
7041
  }
6977
7042
  /** @param {KeyboardEvent} event @returns {void} */
@@ -7108,14 +7173,19 @@ var Select = class _Select extends Component {
7108
7173
  list.replaceChildren();
7109
7174
  this._optionEntries = [];
7110
7175
  let lastGroup = /* @__PURE__ */ Symbol("initial-group");
7176
+ const fixed = this._uniqueItems(this._visibleFixed);
7177
+ const items = this._uniqueItems(this._visibleItems, fixed);
7111
7178
  if (this.options.clearable) {
7112
7179
  this._appendOption(null, true, this.options.noneLabel ?? this._message("select.none", "No selection"));
7113
7180
  }
7114
- for (const item of this._visibleFixed) this._appendOption(item, false);
7115
- if (this._visibleFixed.length > 0 && this._visibleItems.length > 0 && !this.options.groupKey) {
7181
+ if (fixed.length > 0 && this.options.fixedLabel) {
7182
+ list.append(h("div", { class: "zx-select__group", role: "presentation" }, String(this.options.fixedLabel)));
7183
+ }
7184
+ for (const item of fixed) this._appendOption(item, false);
7185
+ if (fixed.length > 0 && items.length > 0 && !this.options.groupKey) {
7116
7186
  list.append(h("div", { class: "zx-select__separator", role: "presentation" }));
7117
7187
  }
7118
- for (const item of this._visibleItems) {
7188
+ for (const item of items) {
7119
7189
  if (this.options.groupKey) {
7120
7190
  const group = this._read(item, this.options.groupKey);
7121
7191
  if (!Object.is(group, lastGroup)) {
@@ -7129,7 +7199,7 @@ var Select = class _Select extends Component {
7129
7199
  this._appendOption(item, false);
7130
7200
  }
7131
7201
  if (this._actions.length > 0) {
7132
- if (this._visibleFixed.length > 0 || this._visibleItems.length > 0 || this.options.clearable) {
7202
+ if (fixed.length > 0 || items.length > 0 || this.options.clearable) {
7133
7203
  list.append(h("div", { class: "zx-select__separator", role: "presentation" }));
7134
7204
  }
7135
7205
  let actionGroup = /* @__PURE__ */ Symbol("initial-action-group");
@@ -7144,7 +7214,7 @@ var Select = class _Select extends Component {
7144
7214
  this._appendAction(action);
7145
7215
  }
7146
7216
  }
7147
- if (this._visibleItems.length === 0 && this._visibleFixed.length === 0 && this._actions.length === 0 && !this._loading) {
7217
+ if (items.length === 0 && fixed.length === 0 && this._actions.length === 0 && !this._loading) {
7148
7218
  list.append(h("div", {
7149
7219
  class: "zx-select__empty",
7150
7220
  role: "status"
@@ -7345,6 +7415,16 @@ var Select = class _Select extends Component {
7345
7415
  _matchFixed(query) {
7346
7416
  return matchItems(this._fixedItems, query, this._searchKeys());
7347
7417
  }
7418
+ /** De-duplicates choices by public value while preserving caller order. @param {SelectItem[]} items @param {SelectItem[]} [excluded=[]] @returns {SelectItem[]} */
7419
+ _uniqueItems(items, excluded = []) {
7420
+ const values = excluded.map((item) => this._itemValue(item));
7421
+ return items.filter((item) => {
7422
+ const value = this._itemValue(item);
7423
+ if (values.some((candidate) => Object.is(candidate, value))) return false;
7424
+ values.push(value);
7425
+ return true;
7426
+ });
7427
+ }
7348
7428
  /** @returns {number} */
7349
7429
  _selectedEntryIndex() {
7350
7430
  if (this._selected === null) return this.options.clearable ? 0 : -1;
@@ -7356,16 +7436,28 @@ var Select = class _Select extends Component {
7356
7436
  _itemEntryIndex(itemIndex) {
7357
7437
  return itemIndex + (this.options.clearable ? 1 : 0);
7358
7438
  }
7359
- /** @param {string} name @returns {void} */
7360
- _rememberAttribute(element, name) {
7361
- this._originalAttributes.set(name, element.getAttribute(name));
7362
- }
7363
7439
  /** @param {string} key @param {string} fallback @returns {string} */
7364
7440
  _message(key, fallback) {
7365
7441
  const message = this.msg(key);
7366
7442
  return message === key ? fallback : message;
7367
7443
  }
7368
7444
  };
7445
+ function readSelectItem(item, reader) {
7446
+ if (typeof reader === "function") return reader(item);
7447
+ if (item !== null && typeof item === "object" && typeof reader === "string") return item[reader];
7448
+ return null;
7449
+ }
7450
+ function normalizeEntityCreateAction(value) {
7451
+ const descriptor = value === true ? {} : value;
7452
+ return {
7453
+ id: descriptor.id ?? "create",
7454
+ label: descriptor.label ?? "Create new\u2026",
7455
+ description: descriptor.description,
7456
+ group: descriptor.group ?? "New",
7457
+ icon: descriptor.icon ?? "plus",
7458
+ invoke: descriptor.invoke
7459
+ };
7460
+ }
7369
7461
  function normalizeRenderedContent(rendered) {
7370
7462
  return rendered && typeof rendered === "object" && "nodeType" in rendered ? (
7371
7463
  /** @type {Node} */
@@ -7842,6 +7934,8 @@ var TagPicker = class extends Component {
7842
7934
  values: [],
7843
7935
  valueKey: "ID",
7844
7936
  labelKey: "name",
7937
+ iconKey: "icon",
7938
+ colorKey: "color",
7845
7939
  searchKeys: null,
7846
7940
  filter: "local",
7847
7941
  minQuery: 0,
@@ -8182,12 +8276,13 @@ var TagPicker = class extends Component {
8182
8276
  _renderTags() {
8183
8277
  const tags = this._selected.map((item) => {
8184
8278
  const label = this._labelOf(item);
8185
- const content = this.options.renderTag ? this.options.renderTag(item) : label;
8186
- const tag = h(
8187
- "span",
8188
- { class: "zx-tag-picker__tag" },
8189
- h("span", { class: "zx-tag-picker__tag-label" }, content)
8190
- );
8279
+ const color = this._colorOf(item);
8280
+ const content = this.options.renderTag ? this.options.renderTag(item) : [this._iconOf(item), h("span", { class: "zx-tag-picker__tag-label" }, label)];
8281
+ const tag = h("span", {
8282
+ class: "zx-tag-picker__tag",
8283
+ dataset: { colored: String(Boolean(color)) },
8284
+ style: color ? { "--zx-tag-color": color } : null
8285
+ }, content);
8191
8286
  if (!this._readonly && !this._disabled) {
8192
8287
  tag.append(h("button", {
8193
8288
  class: "zx-icon-btn zx-tag-picker__remove",
@@ -8225,7 +8320,7 @@ var TagPicker = class extends Component {
8225
8320
  h(
8226
8321
  "span",
8227
8322
  { class: "zx-tag-picker__option-label" },
8228
- this.options.renderItem ? this.options.renderItem(item) : this._labelOf(item)
8323
+ this.options.renderItem ? this.options.renderItem(item) : [this._iconOf(item), h("span", {}, this._labelOf(item))]
8229
8324
  )
8230
8325
  );
8231
8326
  if (!selected && this.isFull()) option.setAttribute("aria-disabled", "true");
@@ -8546,6 +8641,34 @@ var TagPicker = class extends Component {
8546
8641
  if (item !== null && typeof item === "object") return String(item[key] ?? "");
8547
8642
  return String(item ?? "");
8548
8643
  }
8644
+ /** @param {unknown} item @returns {Node|null} */
8645
+ _iconOf(item) {
8646
+ const value = this._readItem(item, this.options.iconKey);
8647
+ const visual = value && typeof value === "object" && typeof value.nodeType === "number" ? (
8648
+ /** @type {Node} */
8649
+ value.cloneNode(true)
8650
+ ) : typeof value === "string" && value.trim() ? icon(value, { size: 12 }) : null;
8651
+ if (!visual) return null;
8652
+ return h("span", {
8653
+ class: "zx-tag-picker__visual",
8654
+ style: this._colorOf(item) ? { "--zx-tag-color": this._colorOf(item) } : null,
8655
+ ariaHidden: "true"
8656
+ }, visual);
8657
+ }
8658
+ /** @param {unknown} item @returns {string|null} */
8659
+ _colorOf(item) {
8660
+ const value = this._readItem(item, this.options.colorKey);
8661
+ return isCssColor(value) ? value : null;
8662
+ }
8663
+ /** @param {unknown} item @param {unknown} reader @returns {unknown} */
8664
+ _readItem(item, reader) {
8665
+ if (typeof reader === "function") return reader(
8666
+ /** @type {TagItem} */
8667
+ item
8668
+ );
8669
+ if (item !== null && typeof item === "object" && typeof reader === "string") return item[reader];
8670
+ return null;
8671
+ }
8549
8672
  /** @returns {void} */
8550
8673
  _emitChange() {
8551
8674
  this.emit("change", { values: this.getValues(), items: this.getItems() });
@@ -9496,7 +9619,11 @@ var Table = class extends Component {
9496
9619
  rowClass: null,
9497
9620
  zebra: true,
9498
9621
  editMode: false,
9499
- hierarchy: false
9622
+ editTrigger: "double",
9623
+ hierarchy: false,
9624
+ rowReorder: false,
9625
+ columnVisibility: false,
9626
+ hiddenColumns: []
9500
9627
  };
9501
9628
  /**
9502
9629
  * Creates a table around a target or creates a new root when target is null.
@@ -9525,6 +9652,7 @@ var Table = class extends Component {
9525
9652
  this._rowElements = /* @__PURE__ */ new Map();
9526
9653
  this._restored = false;
9527
9654
  this._editMode = normalizeEditMode(this.options.editMode);
9655
+ this._editTrigger = this.options.editTrigger === "single" ? "single" : "double";
9528
9656
  this._edit = null;
9529
9657
  this._editMessage = null;
9530
9658
  this._stacked = false;
@@ -9536,6 +9664,10 @@ var Table = class extends Component {
9536
9664
  this._rovingCell = null;
9537
9665
  this._rovingMatched = false;
9538
9666
  this._hierarchy = normalizeHierarchy(this.options.hierarchy, this._columns);
9667
+ this._hiddenColumns = new Set((Array.isArray(this.options.hiddenColumns) ? this.options.hiddenColumns : []).map(String).filter((id) => this._columns.some((column) => column.id === id)));
9668
+ if (this._hiddenColumns.size >= this._columns.length && this._columns[0]) this._hiddenColumns.delete(this._columns[0].id);
9669
+ this._dragId = null;
9670
+ this._grabbedId = null;
9539
9671
  this._expanded = initialExpanded(this._hierarchy?.expanded);
9540
9672
  if (this._sort && this.options.sortMode === "local") this._sortData();
9541
9673
  if (this._hierarchy?.expanded === true) {
@@ -9557,11 +9689,17 @@ var Table = class extends Component {
9557
9689
  zebra: String(Boolean(this.options.zebra))
9558
9690
  }
9559
9691
  }, this._table);
9692
+ this._rowMoveMessage = this.options.rowReorder ? h("div", {
9693
+ class: "zx-table__reorder-status",
9694
+ role: "status",
9695
+ ariaLive: "polite"
9696
+ }) : null;
9560
9697
  if (this.options.height != null) {
9561
9698
  this._scroll.style.blockSize = typeof this.options.height === "number" ? `${this.options.height}px` : String(this.options.height);
9562
9699
  }
9700
+ this._columnControls = this.options.columnVisibility ? this._createColumnControls() : null;
9563
9701
  if (this._editMode === false) {
9564
- root.replaceChildren(this._scroll);
9702
+ root.replaceChildren(...[this._columnControls, this._scroll, this._rowMoveMessage].filter(Boolean));
9565
9703
  } else {
9566
9704
  root.dataset.editMode = this._editMode;
9567
9705
  this._editMessage = h("div", {
@@ -9570,7 +9708,7 @@ var Table = class extends Component {
9570
9708
  role: "status",
9571
9709
  ariaLive: "polite"
9572
9710
  });
9573
- root.replaceChildren(this._scroll, this._editMessage);
9711
+ root.replaceChildren(...[this._columnControls, this._scroll, this._editMessage, this._rowMoveMessage].filter(Boolean));
9574
9712
  }
9575
9713
  this._renderColumns();
9576
9714
  this._renderHeader();
@@ -9578,6 +9716,34 @@ var Table = class extends Component {
9578
9716
  this._watchWidth();
9579
9717
  this.listen(this._tbody, "click", (event) => this._handleBodyClick(event));
9580
9718
  this.listen(this._tbody, "dblclick", (event) => this._handleBodyDoubleClick(event));
9719
+ if (this.options.rowReorder) {
9720
+ this.listen(this._tbody, "dragstart", (event) => this._handleRowDragStart(
9721
+ /** @type {DragEvent} */
9722
+ event
9723
+ ));
9724
+ this.listen(this._tbody, "dragover", (event) => this._handleRowDragOver(
9725
+ /** @type {DragEvent} */
9726
+ event
9727
+ ));
9728
+ this.listen(this._tbody, "drop", (event) => this._handleRowDrop(
9729
+ /** @type {DragEvent} */
9730
+ event
9731
+ ));
9732
+ this.listen(this._tbody, "dragend", () => this._clearRowDrag());
9733
+ this.listen(this._tbody, "keydown", (event) => this._handleRowHandleKeydown(
9734
+ /** @type {KeyboardEvent} */
9735
+ event
9736
+ ));
9737
+ }
9738
+ if (this._columnControls) {
9739
+ this.listen(this._columnControls, "change", (event) => {
9740
+ const input = (
9741
+ /** @type {HTMLInputElement|null} */
9742
+ event.target
9743
+ );
9744
+ if (input?.classList.contains("zx-table__column-toggle")) this.setColumnVisible(input.value, input.checked);
9745
+ });
9746
+ }
9581
9747
  if (this._editMode !== false) {
9582
9748
  this.listen(this._tbody, "keydown", (event) => this._handleEditKeydown(
9583
9749
  /** @type {KeyboardEvent} */
@@ -9693,6 +9859,81 @@ var Table = class extends Component {
9693
9859
  getData() {
9694
9860
  return [...this._data];
9695
9861
  }
9862
+ /** Returns visible column IDs in caller order. @returns {string[]} */
9863
+ getVisibleColumns() {
9864
+ return this._visibleColumns().map((column) => column.id);
9865
+ }
9866
+ /** Returns hidden column IDs in caller order. @returns {string[]} */
9867
+ getHiddenColumns() {
9868
+ return this._columns.filter((column) => this._hiddenColumns.has(column.id)).map((column) => column.id);
9869
+ }
9870
+ /**
9871
+ * Shows or hides one column. The final visible column cannot be hidden.
9872
+ * @param {string} id Column ID.
9873
+ * @param {boolean} [visible=true] Desired visibility.
9874
+ * @param {{silent?:boolean}} [options={}] Event behavior.
9875
+ * @returns {this}
9876
+ */
9877
+ setColumnVisible(id, visible = true, options = {}) {
9878
+ const column = this._columns.find((candidate) => candidate.id === id);
9879
+ if (!column) throw new RangeError(`Unknown table column: ${id}`);
9880
+ const active = (
9881
+ /** @type {HTMLElement|null} */
9882
+ document.activeElement
9883
+ );
9884
+ const restoreFocus = active?.classList.contains("zx-table__column-toggle") && this._columnList?.contains(active) ? (
9885
+ /** @type {HTMLInputElement} */
9886
+ active.value
9887
+ ) : null;
9888
+ if (!visible && !this._hiddenColumns.has(id) && this._visibleColumns().length <= 1) {
9889
+ this._renderColumnControls(restoreFocus);
9890
+ return this;
9891
+ }
9892
+ const changed = visible ? this._hiddenColumns.delete(id) : !this._hiddenColumns.has(id);
9893
+ if (!visible && changed) this._hiddenColumns.add(id);
9894
+ if (!changed) return this;
9895
+ this._renderColumns();
9896
+ this._renderHeader();
9897
+ this._renderBody();
9898
+ this._renderColumnControls(restoreFocus);
9899
+ if (!options.silent) this.emit("columnvisibilitychange", {
9900
+ visible: this.getVisibleColumns(),
9901
+ hidden: this.getHiddenColumns()
9902
+ });
9903
+ return this;
9904
+ }
9905
+ /** Toggles one column. @param {string} id @param {{silent?:boolean}} [options={}] @returns {this} */
9906
+ toggleColumn(id, options = {}) {
9907
+ return this.setColumnVisible(id, this._hiddenColumns.has(id), options);
9908
+ }
9909
+ /**
9910
+ * Reorders one row relative to another and emits a cancelable `rowmove` before committing.
9911
+ * Hierarchical rows may only move among siblings; an active local sort disables manual order.
9912
+ * @param {unknown} id Moving row ID.
9913
+ * @param {unknown} targetId Target row ID.
9914
+ * @param {'before'|'after'} [position='before'] Target edge.
9915
+ * @returns {this}
9916
+ */
9917
+ moveRow(id, targetId, position2 = "before") {
9918
+ if (!this.options.rowReorder || this._sort && this.options.sortMode === "local") return this;
9919
+ const row = this.getRow(id);
9920
+ const target = this.getRow(targetId);
9921
+ if (!row || !target || Object.is(id, targetId)) return this;
9922
+ const next = reorderTableRows(this._data, this.options.rowId, id, targetId, position2, this._hierarchy?.parentId);
9923
+ if (!next) return this;
9924
+ const event = this.emit("rowmove", { row, id, target, targetId, position: position2 });
9925
+ if (event.defaultPrevented) return this;
9926
+ this._data = next;
9927
+ this._renderBody();
9928
+ if (this._rowMoveMessage) {
9929
+ const siblings = this._siblingIds(id);
9930
+ const index = siblings.findIndex((candidate) => Object.is(candidate, id));
9931
+ this._rowMoveMessage.textContent = `Moved row to position ${index + 1} of ${siblings.length}.`;
9932
+ }
9933
+ this._emitDataChange();
9934
+ queueMicrotask(() => this._rowHandle(id)?.focus());
9935
+ return this;
9936
+ }
9696
9937
  /**
9697
9938
  * Removes all rows.
9698
9939
  * @returns {this}
@@ -9861,14 +10102,17 @@ var Table = class extends Component {
9861
10102
  */
9862
10103
  startEdit(rowId, columnId) {
9863
10104
  if (this._editMode === false) return this;
9864
- const column = this._columns.find((candidate) => candidate.id === columnId);
9865
- if (!column) return this;
10105
+ const requested = this._columns.find((candidate) => candidate.id === columnId);
10106
+ if (!requested) return this;
9866
10107
  const current = this._data.find((candidate) => Object.is(this._idFor(candidate), rowId));
9867
- if (current === void 0 || resolveEditable(column, current) === false) return this;
10108
+ if (current === void 0) return this;
10109
+ const column = this._editMode === "row" && this._hiddenColumns.has(requested.id) ? this._visibleColumns().find((candidate) => resolveEditable(candidate, current) !== false) ?? null : requested;
10110
+ if (!column || resolveEditable(column, current) === false) return this;
10111
+ const activeColumnId = column.id;
9868
10112
  if (this._edit) {
9869
- if (Object.is(this._edit.id, rowId) && this._edit.cells.has(columnId)) {
9870
- this._edit.columnId = columnId;
9871
- this._focusEditor(columnId);
10113
+ if (Object.is(this._edit.id, rowId) && this._edit.cells.has(activeColumnId)) {
10114
+ this._edit.columnId = activeColumnId;
10115
+ this._focusEditor(activeColumnId);
9872
10116
  return this;
9873
10117
  }
9874
10118
  this.commitEdit();
@@ -9879,7 +10123,7 @@ var Table = class extends Component {
9879
10123
  const row = this._data[index];
9880
10124
  const tr = this._rowElements.get(rowId)?.[0];
9881
10125
  if (!tr) return this;
9882
- const targets = this._editMode === "row" ? this._columns.filter((candidate) => resolveEditable(candidate, row) !== false) : [column];
10126
+ const targets = this._editMode === "row" ? this._visibleColumns().filter((candidate) => resolveEditable(candidate, row) !== false) : [column];
9883
10127
  const cells = /* @__PURE__ */ new Map();
9884
10128
  for (const target of targets) {
9885
10129
  const td = cellIn(tr, target.id);
@@ -9888,10 +10132,10 @@ var Table = class extends Component {
9888
10132
  if (entry) cells.set(target.id, entry);
9889
10133
  }
9890
10134
  if (cells.size === 0) return this;
9891
- this._edit = { id: rowId, columnId, row, cells, tr };
10135
+ this._edit = { id: rowId, columnId: activeColumnId, row, cells, tr };
9892
10136
  tr.dataset.editing = "true";
9893
- this.emit("editstart", { row, id: rowId, column, columnId, value: row?.[columnId] });
9894
- this._focusEditor(columnId);
10137
+ this.emit("editstart", { row, id: rowId, column, columnId: activeColumnId, value: row?.[activeColumnId] });
10138
+ this._focusEditor(activeColumnId);
9895
10139
  return this;
9896
10140
  }
9897
10141
  /**
@@ -10146,16 +10390,197 @@ var Table = class extends Component {
10146
10390
  }
10147
10391
  super.destroy();
10148
10392
  }
10393
+ /** @returns {TableColumn[]} */
10394
+ _visibleColumns() {
10395
+ return this._columns.filter((column) => !this._hiddenColumns.has(column.id));
10396
+ }
10397
+ /** @returns {HTMLElement} */
10398
+ _createColumnControls() {
10399
+ const list = h("div", { class: "zx-table__column-list" });
10400
+ this._columnList = list;
10401
+ const details = h(
10402
+ "details",
10403
+ { class: "zx-table__column-controls" },
10404
+ h("summary", { class: "zx-button zx-button--sm" }, icon("fields", { size: 13 }), "Columns"),
10405
+ list
10406
+ );
10407
+ this._renderColumnControls();
10408
+ return details;
10409
+ }
10410
+ /** @param {string|null} [focusColumn=null] Column checkbox that keeps focus. @returns {void} */
10411
+ _renderColumnControls(focusColumn = null) {
10412
+ if (!this._columnList) return;
10413
+ const visible = this._visibleColumns();
10414
+ const current = [...this._columnList.querySelectorAll(".zx-table__column-toggle")];
10415
+ const reusable = current.length === this._columns.length && current.every((control, index) => (
10416
+ /** @type {HTMLInputElement} */
10417
+ control.value === this._columns[index].id
10418
+ ));
10419
+ if (reusable) {
10420
+ current.forEach((control, index) => {
10421
+ const input = (
10422
+ /** @type {HTMLInputElement} */
10423
+ control
10424
+ );
10425
+ const checked = !this._hiddenColumns.has(this._columns[index].id);
10426
+ input.checked = checked;
10427
+ input.disabled = checked && visible.length === 1;
10428
+ });
10429
+ return;
10430
+ }
10431
+ this._columnList.replaceChildren(...this._columns.map((column) => {
10432
+ const checked = !this._hiddenColumns.has(column.id);
10433
+ return h(
10434
+ "label",
10435
+ { class: "zx-table__column-option" },
10436
+ h("input", {
10437
+ class: "zx-table__column-toggle",
10438
+ type: "checkbox",
10439
+ value: column.id,
10440
+ checked,
10441
+ disabled: checked && visible.length === 1
10442
+ }),
10443
+ h("span", {}, column.label)
10444
+ );
10445
+ }));
10446
+ if (focusColumn !== null) {
10447
+ const control = [...this._columnList.querySelectorAll(".zx-table__column-toggle")].find((candidate) => (
10448
+ /** @type {HTMLInputElement} */
10449
+ candidate.value === focusColumn
10450
+ ));
10451
+ queueMicrotask(() => {
10452
+ if (control?.isConnected) control.focus();
10453
+ });
10454
+ }
10455
+ }
10456
+ /** @param {DragEvent} event @returns {void} */
10457
+ _handleRowDragStart(event) {
10458
+ const handle = (
10459
+ /** @type {Element|null} */
10460
+ event.target?.closest?.(".zx-table__row-handle")
10461
+ );
10462
+ const rowElement = handle?.closest("tr[data-row]");
10463
+ const meta = rowElement ? this._rowMeta.get(rowElement) : null;
10464
+ if (!handle || !meta || this._sort && this.options.sortMode === "local") {
10465
+ event.preventDefault();
10466
+ return;
10467
+ }
10468
+ this._dragId = meta.id;
10469
+ rowElement.dataset.dragging = "true";
10470
+ event.dataTransfer?.setData("text/plain", "zx-table-row");
10471
+ if (event.dataTransfer) event.dataTransfer.effectAllowed = "move";
10472
+ }
10473
+ /** @param {DragEvent} event @returns {void} */
10474
+ _handleRowDragOver(event) {
10475
+ if (this._dragId == null) return;
10476
+ const rowElement = (
10477
+ /** @type {Element|null} */
10478
+ event.target?.closest?.("tr[data-row]")
10479
+ );
10480
+ const meta = rowElement ? this._rowMeta.get(rowElement) : null;
10481
+ if (!meta || Object.is(meta.id, this._dragId) || !this._canMoveTogether(this._dragId, meta.id)) return;
10482
+ event.preventDefault();
10483
+ if (event.dataTransfer) event.dataTransfer.dropEffect = "move";
10484
+ this._clearDropMarkers();
10485
+ const rectangle = rowElement.getBoundingClientRect();
10486
+ rowElement.dataset.drop = event.clientY >= rectangle.top + rectangle.height / 2 ? "after" : "before";
10487
+ }
10488
+ /** @param {DragEvent} event @returns {void} */
10489
+ _handleRowDrop(event) {
10490
+ if (this._dragId == null) return;
10491
+ const rowElement = (
10492
+ /** @type {Element|null} */
10493
+ event.target?.closest?.("tr[data-row]")
10494
+ );
10495
+ const meta = rowElement ? this._rowMeta.get(rowElement) : null;
10496
+ const position2 = rowElement?.dataset.drop === "after" ? "after" : "before";
10497
+ if (meta && this._canMoveTogether(this._dragId, meta.id)) {
10498
+ event.preventDefault();
10499
+ this.moveRow(this._dragId, meta.id, position2);
10500
+ }
10501
+ this._clearRowDrag();
10502
+ }
10503
+ /** @param {KeyboardEvent} event @returns {void} */
10504
+ _handleRowHandleKeydown(event) {
10505
+ const handle = (
10506
+ /** @type {Element|null} */
10507
+ event.target?.closest?.(".zx-table__row-handle")
10508
+ );
10509
+ const rowElement = handle?.closest("tr[data-row]");
10510
+ const meta = rowElement ? this._rowMeta.get(rowElement) : null;
10511
+ if (!handle || !meta) return;
10512
+ if (event.key === " " || event.key === "Enter") {
10513
+ event.preventDefault();
10514
+ this._grabbedId = Object.is(this._grabbedId, meta.id) ? null : meta.id;
10515
+ handle.setAttribute("aria-pressed", String(this._grabbedId != null));
10516
+ const status = this._rowMoveMessage ?? this._editMessage;
10517
+ if (status) status.textContent = this._grabbedId == null ? "Row dropped" : "Row grabbed. Use the arrow keys to move it.";
10518
+ return;
10519
+ }
10520
+ if (event.key === "Escape" && this._grabbedId != null) {
10521
+ event.preventDefault();
10522
+ this._grabbedId = null;
10523
+ handle.setAttribute("aria-pressed", "false");
10524
+ return;
10525
+ }
10526
+ if (!Object.is(this._grabbedId, meta.id) || !["ArrowUp", "ArrowDown"].includes(event.key)) return;
10527
+ event.preventDefault();
10528
+ const siblings = this._siblingIds(meta.id);
10529
+ const index = siblings.findIndex((id) => Object.is(id, meta.id));
10530
+ const targetId = siblings[index + (event.key === "ArrowUp" ? -1 : 1)];
10531
+ if (targetId !== void 0) this.moveRow(meta.id, targetId, event.key === "ArrowUp" ? "before" : "after");
10532
+ }
10533
+ /** @param {unknown} id @returns {unknown[]} */
10534
+ _siblingIds(id) {
10535
+ const row = this.getRow(id);
10536
+ if (!row) return [];
10537
+ const parent = this._hierarchy ? readRowValue(this._hierarchy.parentId, row) : null;
10538
+ return this._data.filter((candidate) => !this._hierarchy || sameHierarchyParent(readRowValue(this._hierarchy.parentId, candidate), parent)).map((candidate) => this._idFor(candidate));
10539
+ }
10540
+ /** @param {unknown} left @param {unknown} right @returns {boolean} */
10541
+ _canMoveTogether(left, right) {
10542
+ if (!this._hierarchy) return true;
10543
+ const a = this.getRow(left);
10544
+ const b = this.getRow(right);
10545
+ return Boolean(a && b && sameHierarchyParent(
10546
+ readRowValue(this._hierarchy.parentId, a),
10547
+ readRowValue(this._hierarchy.parentId, b)
10548
+ ));
10549
+ }
10550
+ /** @param {unknown} id @returns {HTMLElement|null} */
10551
+ _rowHandle(id) {
10552
+ return (
10553
+ /** @type {HTMLElement|null} */
10554
+ this._rowElements.get(id)?.[0]?.querySelector(".zx-table__row-handle") ?? null
10555
+ );
10556
+ }
10557
+ /** @returns {void} */
10558
+ _clearDropMarkers() {
10559
+ for (const row of this._tbody.querySelectorAll("tr[data-drop]")) delete row.dataset.drop;
10560
+ }
10561
+ /** @returns {void} */
10562
+ _clearRowDrag() {
10563
+ for (const row of this._tbody.querySelectorAll("tr[data-dragging]")) delete row.dataset.dragging;
10564
+ this._clearDropMarkers();
10565
+ this._dragId = null;
10566
+ }
10567
+ /** @returns {string|null} */
10568
+ _hierarchyColumn() {
10569
+ if (!this._hierarchy) return null;
10570
+ return this._visibleColumns().some((column) => column.id === this._hierarchy.column) ? this._hierarchy.column : this._visibleColumns()[0]?.id ?? null;
10571
+ }
10149
10572
  /** @returns {void} */
10150
10573
  _renderColumns() {
10151
10574
  const fragment2 = document.createDocumentFragment();
10575
+ if (this.options.rowReorder) fragment2.append(h("col", { class: "zx-table__reorder-column" }));
10152
10576
  if (this.options.selectable === "multi") {
10153
10577
  fragment2.append(h("col", { class: "zx-table__selection-column" }));
10154
10578
  }
10155
- const fractions = this._columns.map((column) => parseFraction(column.width));
10579
+ const columns = this._visibleColumns();
10580
+ const fractions = columns.map((column) => parseFraction(column.width));
10156
10581
  const total = fractions.reduce((sum, value) => sum + value, 0);
10157
10582
  this._table.dataset.layout = total > 0 ? "fixed" : "auto";
10158
- this._columns.forEach((column, index) => {
10583
+ columns.forEach((column, index) => {
10159
10584
  const col = h("col");
10160
10585
  if (fractions[index] > 0) col.style.width = `${fractions[index] / total * 100}%`;
10161
10586
  else if (column.width && column.width !== "auto") col.style.width = String(column.width);
@@ -10167,6 +10592,11 @@ var Table = class extends Component {
10167
10592
  _renderHeader() {
10168
10593
  const row = h("tr", { class: "zx-table__header-row" });
10169
10594
  if (this._growStep) row.setAttribute("aria-rowindex", "1");
10595
+ if (this.options.rowReorder) row.append(h("th", {
10596
+ class: "zx-table__reorder-header",
10597
+ scope: "col",
10598
+ ariaLabel: "Row order"
10599
+ }));
10170
10600
  if (this.options.selectable === "multi") {
10171
10601
  this._selectAll = h("input", {
10172
10602
  class: "zx-table__checkbox zx-table__select-all",
@@ -10192,7 +10622,7 @@ var Table = class extends Component {
10192
10622
  this._selectAll = null;
10193
10623
  }
10194
10624
  this._headers = /* @__PURE__ */ new Map();
10195
- for (const column of this._columns) {
10625
+ for (const column of this._visibleColumns()) {
10196
10626
  const th = h("th", { scope: "col" });
10197
10627
  const align = column.align ?? (isNumericColumn(column) ? "end" : null);
10198
10628
  if (align) th.dataset.align = align;
@@ -10232,7 +10662,7 @@ var Table = class extends Component {
10232
10662
  { class: "zx-table__empty-row" },
10233
10663
  h("td", {
10234
10664
  class: "zx-table__empty",
10235
- colspan: this._columns.length + (this.options.selectable === "multi" ? 1 : 0)
10665
+ colspan: this._visibleColumns().length + (this.options.selectable === "multi" ? 1 : 0) + (this.options.rowReorder ? 1 : 0)
10236
10666
  }, this._emptyContent())
10237
10667
  ));
10238
10668
  } else {
@@ -10241,6 +10671,7 @@ var Table = class extends Component {
10241
10671
  });
10242
10672
  }
10243
10673
  this._tbody.replaceChildren(fragment2);
10674
+ this._table.setAttribute("aria-colcount", String(this._visibleColumns().length + (this.options.selectable === "multi" ? 1 : 0) + (this.options.rowReorder ? 1 : 0)));
10244
10675
  if (this._growStep) this._table.setAttribute("aria-rowcount", String(projected.length + 1));
10245
10676
  this._syncSelectAll();
10246
10677
  this._syncGrowing();
@@ -10284,6 +10715,19 @@ var Table = class extends Component {
10284
10715
  const elements = this._rowElements.get(id) ?? [];
10285
10716
  elements.push(tr);
10286
10717
  this._rowElements.set(id, elements);
10718
+ if (this.options.rowReorder) {
10719
+ const handle = h("button", {
10720
+ class: "zx-table__row-handle",
10721
+ type: "button",
10722
+ draggable: true,
10723
+ dataset: { rowHandle: "" },
10724
+ ariaLabel: `Move row ${index + 1}`,
10725
+ ariaPressed: String(Object.is(this._grabbedId, id)),
10726
+ disabled: Boolean(this._sort && this.options.sortMode === "local"),
10727
+ title: this._sort && this.options.sortMode === "local" ? "Clear sorting to reorder rows" : null
10728
+ }, icon("drag", { size: 14 }));
10729
+ tr.append(h("td", { class: "zx-table__reorder-cell" }, handle));
10730
+ }
10287
10731
  if (this.options.selectable === "multi") {
10288
10732
  const checkbox = h("input", {
10289
10733
  class: "zx-table__checkbox zx-table__row-checkbox",
@@ -10293,7 +10737,7 @@ var Table = class extends Component {
10293
10737
  });
10294
10738
  tr.append(h("td", { class: "zx-table__selection-cell" }, checkbox));
10295
10739
  }
10296
- this._columns.forEach((column) => {
10740
+ this._visibleColumns().forEach((column) => {
10297
10741
  const cell = (
10298
10742
  /** @type {HTMLTableCellElement} */
10299
10743
  h("td")
@@ -10329,7 +10773,7 @@ var Table = class extends Component {
10329
10773
  const content = h("span", { class: "zx-table__cell-content" });
10330
10774
  if (value && typeof value === "object" && typeof value.nodeType === "number") content.append(value);
10331
10775
  else if (value != null) content.append(document.createTextNode(String(value)));
10332
- if (!this._hierarchy || column.id !== this._hierarchy.column || !tree) {
10776
+ if (!this._hierarchy || column.id !== this._hierarchyColumn() || !tree) {
10333
10777
  cell.replaceChildren(...content.childNodes);
10334
10778
  return;
10335
10779
  }
@@ -10360,6 +10804,7 @@ var Table = class extends Component {
10360
10804
  if (!rowElement || !this._tbody.contains(rowElement)) return;
10361
10805
  const meta = this._rowMeta.get(rowElement);
10362
10806
  if (!meta) return;
10807
+ if (target.closest(".zx-table__row-handle")) return;
10363
10808
  if (this._edit) {
10364
10809
  this.commitEdit();
10365
10810
  if (this._edit) return;
@@ -10379,6 +10824,14 @@ var Table = class extends Component {
10379
10824
  );
10380
10825
  return;
10381
10826
  }
10827
+ if (isInteractiveCellContent(target)) return;
10828
+ if (this._editMode !== false && this._editTrigger === "single") {
10829
+ const cell = target?.closest('td[data-editable="true"]');
10830
+ if (cell && this._tbody.contains(cell)) {
10831
+ this.startEdit(meta.id, String(cell.dataset.column));
10832
+ return;
10833
+ }
10834
+ }
10382
10835
  this.emit("rowclick", { ...meta, event });
10383
10836
  if (this.options.selectable === "single" && !this._selected.has(meta.id)) {
10384
10837
  this._selected = /* @__PURE__ */ new Set([meta.id]);
@@ -10399,7 +10852,8 @@ var Table = class extends Component {
10399
10852
  if (!rowElement || !this._tbody.contains(rowElement)) return;
10400
10853
  const meta = this._rowMeta.get(rowElement);
10401
10854
  if (!meta) return;
10402
- if (this._editMode !== false) {
10855
+ if (isInteractiveCellContent(target)) return;
10856
+ if (this._editMode !== false && this._editTrigger === "double") {
10403
10857
  const cell = target?.closest('td[data-editable="true"]');
10404
10858
  if (cell && this._tbody.contains(cell)) {
10405
10859
  this.startEdit(meta.id, String(cell.dataset.column));
@@ -10701,13 +11155,14 @@ var Table = class extends Component {
10701
11155
  * @returns {{id: unknown, columnId: string, index: number}|null} Next cell, or null at the end.
10702
11156
  */
10703
11157
  _nextEditableCell(rowIndex, columnId, direction) {
10704
- const columnCount = this._columns.length;
11158
+ const columns = this._visibleColumns();
11159
+ const columnCount = columns.length;
10705
11160
  if (rowIndex < 0 || columnCount === 0) return null;
10706
11161
  const visible = this._visibleData();
10707
11162
  const startingId = this._idFor(this._data[rowIndex]);
10708
11163
  let row = visible.findIndex((candidate) => Object.is(this._idFor(candidate), startingId));
10709
11164
  if (row < 0) return null;
10710
- let column = this._columns.findIndex((candidate) => candidate.id === columnId);
11165
+ let column = columns.findIndex((candidate) => candidate.id === columnId);
10711
11166
  const limit = visible.length * columnCount + columnCount;
10712
11167
  for (let step = 0; step < limit; step += 1) {
10713
11168
  column += direction;
@@ -10720,7 +11175,7 @@ var Table = class extends Component {
10720
11175
  }
10721
11176
  if (row < 0 || row >= visible.length) return null;
10722
11177
  const candidateRow = visible[row];
10723
- const candidateColumn = this._columns[column];
11178
+ const candidateColumn = columns[column];
10724
11179
  if (resolveEditable(candidateColumn, candidateRow) !== false) {
10725
11180
  return {
10726
11181
  id: this._idFor(candidateRow),
@@ -10963,6 +11418,53 @@ function sortHierarchyRows(rows, rowId, parentId, getValue, direction) {
10963
11418
  }
10964
11419
  return result;
10965
11420
  }
11421
+ function reorderTableRows(rows, rowId, id, targetId, position2 = "before", parentId = null) {
11422
+ if (!Array.isArray(rows) || Object.is(id, targetId)) return null;
11423
+ const from = rows.findIndex((row) => Object.is(readRowValue(rowId, row), id));
11424
+ const target = rows.findIndex((row) => Object.is(readRowValue(rowId, row), targetId));
11425
+ if (from < 0 || target < 0) return null;
11426
+ if (parentId && !sameHierarchyParent(
11427
+ readRowValue(parentId, rows[from]),
11428
+ readRowValue(parentId, rows[target])
11429
+ )) return null;
11430
+ if (parentId) {
11431
+ const forest = hierarchyForest(rows, rowId, parentId);
11432
+ const movingNode = forest.nodes[from];
11433
+ const targetNode = forest.nodes[target];
11434
+ if (!movingNode || !targetNode || movingNode.parent !== targetNode.parent) return null;
11435
+ const branchNodes = (root) => {
11436
+ const result = [];
11437
+ const stack = [root];
11438
+ while (stack.length) {
11439
+ const node = (
11440
+ /** @type {TableHierarchyNode} */
11441
+ stack.pop()
11442
+ );
11443
+ result.push(node);
11444
+ for (let index = node.children.length - 1; index >= 0; index -= 1) stack.push(node.children[index]);
11445
+ }
11446
+ return result;
11447
+ };
11448
+ const moving2 = new Set(branchNodes(movingNode).map((node) => node.index));
11449
+ const targetBranch = new Set(branchNodes(targetNode).map((node) => node.index));
11450
+ const block = rows.filter((_row, index) => moving2.has(index));
11451
+ const remaining = rows.filter((_row, index) => !moving2.has(index));
11452
+ const remainingSourceIndexes = rows.map((_row, index) => index).filter((index) => !moving2.has(index));
11453
+ let insertion = remainingSourceIndexes.findIndex((index) => index === target);
11454
+ if (position2 === "after") {
11455
+ const targetPositions = remainingSourceIndexes.map((sourceIndex, index) => targetBranch.has(sourceIndex) ? index : -1).filter((index) => index >= 0);
11456
+ insertion = Math.max(...targetPositions) + 1;
11457
+ }
11458
+ if (insertion < 0) return null;
11459
+ remaining.splice(insertion, 0, ...block);
11460
+ return remaining;
11461
+ }
11462
+ const next = [...rows];
11463
+ const [moving] = next.splice(from, 1);
11464
+ const adjustedTarget = next.findIndex((row) => Object.is(readRowValue(rowId, row), targetId));
11465
+ next.splice(adjustedTarget + (position2 === "after" ? 1 : 0), 0, moving);
11466
+ return next;
11467
+ }
10966
11468
  function resolveEditable(column, row) {
10967
11469
  if (!column || typeof column !== "object") return false;
10968
11470
  if (typeof column.editor === "function") {
@@ -11043,7 +11545,8 @@ function hierarchyForest(rows, rowId, parentId) {
11043
11545
  for (const node of nodes) if (!byId.has(node.id)) byId.set(node.id, node);
11044
11546
  const parentOf = /* @__PURE__ */ new Map();
11045
11547
  for (const node of nodes) {
11046
- const parent = byId.get(readRowValue(parentId, node.row)) ?? null;
11548
+ const parentValue = readRowValue(parentId, node.row);
11549
+ const parent = parentValue == null ? null : byId.get(parentValue) ?? null;
11047
11550
  parentOf.set(node, parent === node ? null : parent);
11048
11551
  }
11049
11552
  for (const node of nodes) {
@@ -11072,6 +11575,9 @@ function hierarchyBranchIds(rows, rowId, parentId) {
11072
11575
  function readRowValue(accessor, row) {
11073
11576
  return typeof accessor === "function" ? accessor(row) : row?.[accessor];
11074
11577
  }
11578
+ function sameHierarchyParent(left, right) {
11579
+ return left == null && right == null || Object.is(left, right);
11580
+ }
11075
11581
  function normalizeHierarchy(option, columns) {
11076
11582
  if (!option || typeof option !== "object") return null;
11077
11583
  const parentId = option.parentId;
@@ -11091,6 +11597,23 @@ function normalizeEditMode(mode) {
11091
11597
  function isInsideEditor(target) {
11092
11598
  return Boolean(target?.closest?.(".zx-table__editor"));
11093
11599
  }
11600
+ function isInteractiveCellContent(target) {
11601
+ const cell = target?.closest?.("td[data-column]");
11602
+ const interactive = target?.closest?.([
11603
+ "a[href]",
11604
+ "button",
11605
+ "input",
11606
+ "select",
11607
+ "textarea",
11608
+ "summary",
11609
+ '[contenteditable="true"]',
11610
+ '[role="button"]',
11611
+ '[role="link"]',
11612
+ '[tabindex]:not([tabindex="-1"])',
11613
+ "[data-zx-table-no-edit]"
11614
+ ].join(","));
11615
+ return Boolean(cell && interactive && interactive !== cell && cell.contains(interactive));
11616
+ }
11094
11617
  function deferToEditor(target) {
11095
11618
  if (target.closest("[popover]")) return true;
11096
11619
  return target.getAttribute?.("aria-expanded") === "true";
@@ -11125,6 +11648,106 @@ function growStep(growing) {
11125
11648
  return Number.isFinite(step) && step > 0 ? step : null;
11126
11649
  }
11127
11650
 
11651
+ // src/components/app-icon/app-icon.js
11652
+ var AppIcon = class extends Component {
11653
+ static cssName = "app-icon";
11654
+ /** @type {Readonly<AppIconOptions>} */
11655
+ static defaults = {
11656
+ icon: null,
11657
+ color: null,
11658
+ size: 36,
11659
+ iconSize: "52%",
11660
+ label: null,
11661
+ badge: null,
11662
+ selected: false,
11663
+ glass: "subtle",
11664
+ class: []
11665
+ };
11666
+ /** @returns {HTMLElement} */
11667
+ render() {
11668
+ this._createdRoot = this.el === null;
11669
+ const root = (
11670
+ /** @type {HTMLElement} */
11671
+ this.el ?? h("span")
11672
+ );
11673
+ this.el = root;
11674
+ this._snapshot = this._createdRoot ? null : snapshotTarget(root);
11675
+ this._state = { ...this.options };
11676
+ this._appliedClasses = [];
11677
+ this._sync();
11678
+ return root;
11679
+ }
11680
+ /** Updates identity or presentation without replacing the root. @param {Partial<AppIconOptions>} values @returns {this} */
11681
+ set(values = {}) {
11682
+ if (!values || typeof values !== "object") return this;
11683
+ Object.assign(this._state, values);
11684
+ this._sync();
11685
+ return this;
11686
+ }
11687
+ /** Restores an enhanced target. @returns {void} */
11688
+ destroy() {
11689
+ const root = this.el;
11690
+ const snapshot3 = this._snapshot;
11691
+ super.destroy();
11692
+ if (!this._createdRoot && root) restoreTarget(root, snapshot3);
11693
+ }
11694
+ /** @returns {void} */
11695
+ _sync() {
11696
+ const state = this._state;
11697
+ this.el.classList.remove(...this._appliedClasses);
11698
+ this._appliedClasses = [state.class].flat().flatMap((value) => String(value ?? "").split(/\s+/)).filter(Boolean);
11699
+ this.el.classList.add(...this._appliedClasses);
11700
+ const size = cssLength(state.size, "36px");
11701
+ const glyphSize = cssLength(state.iconSize, "52%");
11702
+ this.el.style.setProperty("--zx-app-icon-size", size);
11703
+ this.el.style.setProperty("--zx-app-icon-glyph-size", glyphSize);
11704
+ if (isCssColor(state.color)) this.el.style.setProperty("--zx-app-icon-color", String(state.color));
11705
+ else this.el.style.removeProperty("--zx-app-icon-color");
11706
+ this.el.dataset.glass = normalizeGlass(state.glass);
11707
+ this.el.toggleAttribute("data-selected", Boolean(state.selected));
11708
+ const label = state.label == null ? "" : String(state.label).trim();
11709
+ if (label) {
11710
+ this.el.setAttribute("role", "img");
11711
+ this.el.setAttribute("aria-label", label);
11712
+ this.el.removeAttribute("aria-hidden");
11713
+ } else {
11714
+ this.el.removeAttribute("role");
11715
+ this.el.removeAttribute("aria-label");
11716
+ this.el.setAttribute("aria-hidden", "true");
11717
+ }
11718
+ const visual = resolveVisual(state.icon);
11719
+ const badge = state.badge == null || state.badge === "" ? null : h("span", {
11720
+ class: "zx-app-icon__badge",
11721
+ ariaHidden: "true"
11722
+ }, String(state.badge));
11723
+ this.el.replaceChildren(
11724
+ h("span", { class: "zx-app-icon__shine", ariaHidden: "true" }),
11725
+ h("span", { class: "zx-app-icon__glyph", ariaHidden: "true" }, visual),
11726
+ badge
11727
+ );
11728
+ }
11729
+ };
11730
+ function appIcon(options = {}) {
11731
+ return new AppIcon(null, options).toElement();
11732
+ }
11733
+ function resolveVisual(value) {
11734
+ const resolved = typeof value === "function" ? value() : value;
11735
+ if (typeof resolved === "string") return icon(resolved, { size: "1em" });
11736
+ return resolved && typeof resolved === "object" && typeof resolved.nodeType === "number" ? (
11737
+ /** @type {Node} */
11738
+ resolved
11739
+ ) : null;
11740
+ }
11741
+ function cssLength(value, fallback) {
11742
+ if (typeof value === "number" && Number.isFinite(value)) return `${Math.max(1, value)}px`;
11743
+ const text3 = String(value ?? "").trim();
11744
+ return text3 && !/[;{}]/.test(text3) ? text3 : fallback;
11745
+ }
11746
+ function normalizeGlass(value) {
11747
+ if (value === false || value === "none") return "none";
11748
+ return value === "strong" ? "strong" : "subtle";
11749
+ }
11750
+
11128
11751
  // src/components/launcher/launcher.js
11129
11752
  var Launcher = class extends Component {
11130
11753
  static cssName = "launcher";
@@ -12967,7 +13590,7 @@ function moduleIcon(name, options = {}) {
12967
13590
  function moduleChip(name, options = {}) {
12968
13591
  const {
12969
13592
  size = 24,
12970
- iconSize = "1em",
13593
+ iconSize = "52%",
12971
13594
  label = null,
12972
13595
  title = false,
12973
13596
  standard = false,
@@ -12975,25 +13598,28 @@ function moduleChip(name, options = {}) {
12975
13598
  } = options;
12976
13599
  const key = normalizeModuleName(name);
12977
13600
  const info = moduleInfo(name);
12978
- const background = color || info.color;
12979
- const chip = h("span", {
12980
- class: ["zx-module-icon", options.class].flat().filter(Boolean),
12981
- dataset: { module: key },
12982
- style: {
12983
- "--zx-module-color": background,
12984
- "--zx-module-glyph": moduleGlyphColor(background),
12985
- "--zx-module-icon-size": typeof size === "number" ? `${size}px` : String(size)
12986
- },
12987
- title: title ? info.label : null
12988
- }, moduleIcon(name, { size: iconSize, standard }));
12989
- if (label === null) {
12990
- chip.setAttribute("aria-hidden", "true");
12991
- } else {
12992
- chip.setAttribute("role", "img");
12993
- chip.setAttribute("aria-label", label);
12994
- }
13601
+ const background = isCssColor(color) ? color : info.color;
13602
+ const chip = appIcon({
13603
+ // AppIcon owns glyph sizing. Keep the provider glyph at 1em so the percentage is not applied
13604
+ // once to the wrapper and a second time to a Font Awesome <i>.
13605
+ icon: options.icon ?? moduleIcon(name, { size: "1em", standard }),
13606
+ size,
13607
+ iconSize,
13608
+ color: background,
13609
+ label,
13610
+ glass: options.glass ?? "subtle",
13611
+ class: ["zx-module-icon", options.class].flat().filter(Boolean)
13612
+ });
13613
+ chip.dataset.module = key;
13614
+ chip.style.setProperty("--zx-module-color", background);
13615
+ chip.style.setProperty("--zx-module-glyph", moduleGlyphColor(background));
13616
+ chip.style.setProperty("--zx-app-icon-glyph", moduleGlyphColor(background));
13617
+ if (title) chip.title = info.label;
12995
13618
  return chip;
12996
13619
  }
13620
+ function zeyosAppIcon(name, options = {}) {
13621
+ return moduleChip(name, options);
13622
+ }
12997
13623
 
12998
13624
  // src/zeyos/launcher.js
12999
13625
  var ZEYOS_LAUNCHER_APPLICATIONS = Object.freeze([
@@ -13347,6 +13973,7 @@ function buildZeyosEntitySelectConfig(client, resource, opts = {}) {
13347
13973
  moduleKey = () => resource,
13348
13974
  groupKey = null,
13349
13975
  none = true,
13976
+ recent = [],
13350
13977
  create = false,
13351
13978
  renderIcon = defaultEntityIcon,
13352
13979
  renderItem,
@@ -13366,6 +13993,7 @@ function buildZeyosEntitySelectConfig(client, resource, opts = {}) {
13366
13993
  groupKey,
13367
13994
  clearable: none === false ? Boolean(rest.clearable) : true
13368
13995
  });
13996
+ base.recent = Array.isArray(recent) ? recent : [];
13369
13997
  const renderModuleIcon = (item, selected = false) => {
13370
13998
  if (renderIcon === null) return null;
13371
13999
  const module = readRecord(item, moduleKey) ?? resource;
@@ -13416,7 +14044,7 @@ function zeyosEntitySelect(client, resource, opts = {}) {
13416
14044
  const requestedValue = opts.value ?? null;
13417
14045
  const select = (
13418
14046
  /** @type {Select & {ready: Promise<Select>}} */
13419
- new Select(null, config2)
14047
+ Select.entity(null, config2)
13420
14048
  );
13421
14049
  select.ready = hydrateInitialValue(select, client, resource, requestedValue);
13422
14050
  return select;
@@ -13456,9 +14084,7 @@ function readRecord(item, reader) {
13456
14084
  return null;
13457
14085
  }
13458
14086
  function defaultEntityIcon(_item, context) {
13459
- const chip = moduleChip(String(context.module ?? "default"), { size: 20 });
13460
- chip.replaceChildren(icon("square", { size: 10 }));
13461
- return chip;
14087
+ return moduleChip(String(context.module ?? "default"), { size: 20 });
13462
14088
  }
13463
14089
  function defaultProjection(valueKey, labelKey, searchFields) {
13464
14090
  const keys = [valueKey, labelKey, ...searchFields].filter((key) => typeof key === "string" && key);
@@ -13868,6 +14494,7 @@ export {
13868
14494
  unixSecondsToDate,
13869
14495
  useZeyosIcons,
13870
14496
  zeyosActiveIdentifier,
14497
+ zeyosAppIcon,
13871
14498
  zeyosEntitySelect,
13872
14499
  zeyosForm,
13873
14500
  zeyosLauncher,