@zeyos/zx 3.0.0 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/CHANGELOG.md +95 -0
  2. package/DESIGN-SYSTEM.md +87 -71
  3. package/README.md +16 -37
  4. package/dist/types/components/app-icon/app-icon.d.ts +88 -0
  5. package/dist/types/components/app-sidebar/app-sidebar.d.ts +106 -32
  6. package/dist/types/components/card/card.d.ts +128 -0
  7. package/dist/types/components/chart/chart.d.ts +1 -0
  8. package/dist/types/components/filter/filter-model.d.ts +87 -0
  9. package/dist/types/components/filter/filter.d.ts +331 -0
  10. package/dist/types/components/launcher/launcher.d.ts +65 -6
  11. package/dist/types/components/select/select.d.ts +97 -4
  12. package/dist/types/components/table/table.d.ts +107 -3
  13. package/dist/types/components/tag-picker/tag-picker.d.ts +16 -0
  14. package/dist/types/core/dom.d.ts +9 -1
  15. package/dist/types/core/util.d.ts +9 -0
  16. package/dist/types/index.d.ts +6 -2
  17. package/dist/types/{components/app-rail → internal}/app-rail.d.ts +34 -19
  18. package/dist/types/zeyos/icons.d.ts +24 -2
  19. package/dist/types/zeyos/index.d.ts +3 -2
  20. package/dist/types/zeyos/launcher.d.ts +147 -0
  21. package/dist/types/zeyos/select.d.ts +41 -0
  22. package/dist/zx-zeyos.esm.js +2018 -113
  23. package/dist/zx-zeyos.esm.min.js +1 -1
  24. package/dist/zx.css +815 -16
  25. package/dist/zx.esm.js +2661 -338
  26. package/dist/zx.esm.min.js +3 -3
  27. package/dist/zx.global.js +2661 -338
  28. package/dist/zx.global.min.js +3 -3
  29. package/dist/zx.min.css +1 -1
  30. package/docs/api.json +602 -110
  31. package/docs/llms.md +181 -123
  32. package/docs/llms.txt +24 -21
  33. package/package.json +8 -17
  34. package/src/components/app-icon/app-icon.css +105 -0
  35. package/src/components/app-icon/app-icon.js +134 -0
  36. package/src/components/app-sidebar/app-sidebar.css +12 -0
  37. package/src/components/app-sidebar/app-sidebar.js +310 -110
  38. package/src/components/card/card.css +149 -0
  39. package/src/components/card/card.js +285 -0
  40. package/src/components/chart/chart.js +8 -3
  41. package/src/components/filter/filter-model.js +226 -0
  42. package/src/components/filter/filter.css +147 -0
  43. package/src/components/filter/filter.js +554 -0
  44. package/src/components/grid/grid.css +1 -1
  45. package/src/components/grid/grid.js +14 -4
  46. package/src/components/launcher/launcher.css +113 -3
  47. package/src/components/launcher/launcher.js +234 -26
  48. package/src/components/message/message.css +36 -3
  49. package/src/components/select/field-adapter.js +9 -4
  50. package/src/components/select/select.css +161 -10
  51. package/src/components/select/select.js +361 -48
  52. package/src/components/table/table.css +85 -0
  53. package/src/components/table/table.js +426 -31
  54. package/src/components/tag-picker/tag-picker.css +19 -0
  55. package/src/components/tag-picker/tag-picker.js +42 -5
  56. package/src/core/dom.js +21 -1
  57. package/src/core/util.js +20 -0
  58. package/src/index.js +9 -2
  59. package/src/{components/app-rail → internal}/app-rail.js +80 -30
  60. package/src/zeyos/icons.js +38 -20
  61. package/src/zeyos/index.js +8 -2
  62. package/src/zeyos/launcher.js +338 -0
  63. package/src/zeyos/modules.js +1 -0
  64. package/src/zeyos/select.js +116 -1
  65. package/styles/tokens/global.css +1 -1
  66. package/styles/tokens/modules.css +1 -0
  67. package/styles/tokens/semantic.css +9 -0
  68. package/styles/tokens/themes.css +2 -2
  69. package/styles/zx.css +3 -0
  70. package/MIGRATION.md +0 -749
  71. package/dist/types/compat/base.d.ts +0 -156
  72. package/dist/types/compat/globals.d.ts +0 -20
  73. package/dist/types/compat/index.d.ts +0 -128
  74. package/dist/types/compat/map/basic.d.ts +0 -175
  75. package/dist/types/compat/map/data.d.ts +0 -103
  76. package/dist/types/compat/map/date-time.d.ts +0 -80
  77. package/dist/types/compat/map/factory.d.ts +0 -9
  78. package/dist/types/compat/map/form.d.ts +0 -98
  79. package/dist/types/compat/map/helpers.d.ts +0 -12
  80. package/dist/types/compat/map/http.d.ts +0 -36
  81. package/dist/types/compat/map/layout.d.ts +0 -106
  82. package/dist/types/compat/map/options.d.ts +0 -80
  83. package/dist/types/compat/map/select.d.ts +0 -95
  84. package/dist/types/compat/map/stubs.d.ts +0 -32
  85. package/dist/types/compat/map/utilities.d.ts +0 -37
  86. package/dist/types/compat-entry.d.ts +0 -5
  87. package/dist/zx-compat.esm.js +0 -17848
  88. package/dist/zx-compat.esm.min.js +0 -1
  89. package/dist/zx-compat.global.js +0 -29008
  90. package/dist/zx-compat.global.min.js +0 -3
  91. package/src/compat/base.js +0 -291
  92. package/src/compat/globals.js +0 -167
  93. package/src/compat/index.js +0 -84
  94. package/src/compat/map/basic.js +0 -464
  95. package/src/compat/map/data.js +0 -254
  96. package/src/compat/map/date-time.js +0 -177
  97. package/src/compat/map/factory.js +0 -45
  98. package/src/compat/map/form.js +0 -330
  99. package/src/compat/map/helpers.js +0 -46
  100. package/src/compat/map/http.js +0 -117
  101. package/src/compat/map/layout.js +0 -256
  102. package/src/compat/map/options.js +0 -267
  103. package/src/compat/map/select.js +0 -198
  104. package/src/compat/map/stubs.js +0 -26
  105. package/src/compat/map/utilities.js +0 -113
  106. package/src/compat-entry.js +0 -11
@@ -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
  }
@@ -524,6 +538,18 @@ function cloneValue(value) {
524
538
 
525
539
  // src/core/dom.js
526
540
  var activeRefs = null;
541
+ function safeHref(value) {
542
+ if (typeof value !== "string") return null;
543
+ const href = value.trim();
544
+ if (!href) return null;
545
+ try {
546
+ const protocol = new URL(href, globalThis.location?.href ?? "https://zx.invalid/").protocol;
547
+ if (["javascript:", "data:", "vbscript:"].includes(protocol.toLowerCase())) return null;
548
+ } catch {
549
+ return null;
550
+ }
551
+ return href;
552
+ }
527
553
  function h(tag, props, ...children) {
528
554
  const element = document.createElement(tag);
529
555
  let properties = props;
@@ -6533,17 +6559,33 @@ var PRIORITY_KEYS = [
6533
6559
  "priority.high",
6534
6560
  "priority.highest"
6535
6561
  ];
6536
- var PRIORITY_FALLBACKS = ["Lowest", "Low", "Normal", "High", "Highest"];
6562
+ var PRIORITY_FALLBACKS = ["Lowest", "Low", "Medium", "High", "Highest"];
6563
+ var DEFAULT_STATUSES = Object.freeze([
6564
+ ["backlog", "status.backlog", "Backlog", "dotted", "muted"],
6565
+ ["todo", "status.todo", "Todo", "ring", "neutral"],
6566
+ ["in-progress", "status.inProgress", "In Progress", "progress", "warning"],
6567
+ ["in-review", "status.inReview", "In Review", "review", "info"],
6568
+ ["ready", "status.ready", "Ready to Merge", "ready", "accent"],
6569
+ ["waiting", "status.waiting", "Waiting for feedback", "waiting", "warning"],
6570
+ ["done", "status.done", "Done", "done", "info"],
6571
+ ["canceled", "status.canceled", "Canceled", "canceled", "muted"],
6572
+ ["duplicate", "status.duplicate", "Duplicate", "duplicate", "muted"]
6573
+ ]);
6537
6574
  var Select = class _Select extends Component {
6538
6575
  static cssName = "select";
6539
6576
  /** @type {SelectOptions} */
6540
6577
  static defaults = {
6541
6578
  items: [],
6542
6579
  fixedItems: [],
6580
+ fixedLabel: null,
6543
6581
  valueKey: "ID",
6544
6582
  labelKey: "name",
6545
6583
  renderItem: null,
6546
6584
  renderValue: null,
6585
+ renderValueAdornment: null,
6586
+ renderNone: null,
6587
+ noneLabel: null,
6588
+ actions: [],
6547
6589
  value: null,
6548
6590
  disabled: false,
6549
6591
  placeholder: "",
@@ -6561,6 +6603,7 @@ var Select = class _Select extends Component {
6561
6603
  this._visibleItems = [];
6562
6604
  this._fixedItems = [];
6563
6605
  this._visibleFixed = [];
6606
+ this._actions = [];
6564
6607
  this._selected = null;
6565
6608
  this._optionEntries = [];
6566
6609
  this._activeIndex = -1;
@@ -6572,8 +6615,7 @@ var Select = class _Select extends Component {
6572
6615
  this._position = null;
6573
6616
  this._createdRoot = false;
6574
6617
  this._root = null;
6575
- this._originalChildren = [];
6576
- this._originalAttributes = /* @__PURE__ */ new Map();
6618
+ this._snapshot = null;
6577
6619
  this._createdRoot = this.el === null;
6578
6620
  const root = (
6579
6621
  /** @type {HTMLElement} */
@@ -6581,10 +6623,7 @@ var Select = class _Select extends Component {
6581
6623
  );
6582
6624
  this.el = root;
6583
6625
  this._root = root;
6584
- if (!this._createdRoot) this._originalChildren = Array.from(root.childNodes);
6585
- this._rememberAttribute(root, "data-state");
6586
- this._rememberAttribute(root, "data-loading");
6587
- this._rememberAttribute(root, "aria-disabled");
6626
+ this._snapshot = this._createdRoot ? null : snapshotTarget(root);
6588
6627
  const listId = uid("zx-select-list");
6589
6628
  const input = h("input", {
6590
6629
  ref: "input",
@@ -6599,6 +6638,12 @@ var Select = class _Select extends Component {
6599
6638
  placeholder: String(this.options.placeholder ?? ""),
6600
6639
  readOnly: !this.options.filter
6601
6640
  });
6641
+ const valueAdornment = h("span", {
6642
+ ref: "valueAdornment",
6643
+ class: "zx-select__value-adornment",
6644
+ ariaHidden: "true",
6645
+ hidden: true
6646
+ });
6602
6647
  const clear = h("button", {
6603
6648
  ref: "clear",
6604
6649
  class: "zx-icon-btn zx-select__clear",
@@ -6624,6 +6669,7 @@ var Select = class _Select extends Component {
6624
6669
  const control = h(
6625
6670
  "div",
6626
6671
  { ref: "control", class: "zx-select__control" },
6672
+ valueAdornment,
6627
6673
  input,
6628
6674
  clear,
6629
6675
  loading,
@@ -6646,9 +6692,10 @@ var Select = class _Select extends Component {
6646
6692
  this._visibleItems = this._items.slice();
6647
6693
  this._fixedItems = Array.isArray(this.options.fixedItems) ? this.options.fixedItems.slice() : [];
6648
6694
  this._visibleFixed = this._fixedItems.slice();
6695
+ this._actions = normalizeSelectActions(this.options.actions);
6649
6696
  this._disabled = Boolean(this.options.disabled);
6650
6697
  this._handleTypeahead = typeahead(
6651
- () => this._optionEntries.filter((entry) => !entry.clear).map((entry) => entry.element),
6698
+ () => this._optionEntries.filter((entry) => !entry.clear && !entry.action).map((entry) => entry.element),
6652
6699
  (_option, index) => {
6653
6700
  if (!this._open) this.open();
6654
6701
  this._setActive(this._itemEntryIndex(index));
@@ -6743,6 +6790,13 @@ var Select = class _Select extends Component {
6743
6790
  this._position?.update();
6744
6791
  return this;
6745
6792
  }
6793
+ /** Replaces command rows without changing the selected value. @param {SelectAction[]} actions @returns {this} */
6794
+ setActions(actions) {
6795
+ this._actions = normalizeSelectActions(actions);
6796
+ this._renderOptions();
6797
+ this._position?.update();
6798
+ return this;
6799
+ }
6746
6800
  /**
6747
6801
  * Restores the configured initial value.
6748
6802
  * @param {{silent?: boolean}} [options={}] Reset behavior.
@@ -6827,24 +6881,109 @@ var Select = class _Select extends Component {
6827
6881
  value: null,
6828
6882
  items: [],
6829
6883
  renderItem(item) {
6884
+ return priorityContent(item, true);
6885
+ },
6886
+ renderValueAdornment(item) {
6887
+ return priorityIndicator(item.level);
6888
+ }
6889
+ });
6890
+ select.setItems(PRIORITY_KEYS.map((key, level) => {
6891
+ let name = level === 2 ? select.msg("priority.medium") : key;
6892
+ if (name === "priority.medium") name = select._message(key, PRIORITY_FALLBACKS[level]);
6893
+ else if (level !== 2) name = select._message(key, PRIORITY_FALLBACKS[level]);
6894
+ return { ID: level, name, level };
6895
+ }));
6896
+ select.set(requestedValue, { silent: true });
6897
+ return select;
6898
+ }
6899
+ /**
6900
+ * Creates a status picker with shape- and color-distinct workflow indicators.
6901
+ * @param {Element|string|null} target Component target.
6902
+ * @param {SelectOptions & {items?: Array<Record<string, any>>}} [options={}] Select overrides and status items.
6903
+ * @returns {Select}
6904
+ */
6905
+ static status(target, options = {}) {
6906
+ const { items = null, value = null, ...rest } = options;
6907
+ const select = new _Select(target, {
6908
+ ...rest,
6909
+ value: null,
6910
+ items: [],
6911
+ renderItem(item) {
6912
+ return statusContent(item);
6913
+ },
6914
+ renderValueAdornment(item) {
6915
+ return statusIndicator(item);
6916
+ }
6917
+ });
6918
+ const statuses = Array.isArray(items) ? items.map(normalizeStatusItem).filter(Boolean) : DEFAULT_STATUSES.map(([ID, key, fallback, indicator, tone], index) => ({
6919
+ ID,
6920
+ name: select._message(key, fallback),
6921
+ indicator,
6922
+ tone,
6923
+ shortcut: String(index + 1)
6924
+ }));
6925
+ select.setItems(statuses);
6926
+ select.set(value, { silent: true });
6927
+ return select;
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
+ );
6830
6977
  return h(
6831
6978
  "span",
6832
- { class: "zx-select__priority" },
6833
- h("span", {
6834
- class: "zx-select__priority-icon",
6835
- dataset: { level: item.level },
6836
- ariaHidden: "true"
6837
- }),
6838
- h("span", {}, String(item.name))
6979
+ { class: "zx-select__entity" },
6980
+ decoration ? h("span", { class: "zx-select__entity-icon", ariaHidden: "true" }, decoration) : null,
6981
+ copy
6839
6982
  );
6840
- }
6983
+ }),
6984
+ renderValueAdornment: renderValueAdornment ?? ((item) => visual(item, true))
6841
6985
  });
6842
- select.setItems(PRIORITY_KEYS.map((key, level) => ({
6843
- ID: level,
6844
- name: select._message(key, PRIORITY_FALLBACKS[level]),
6845
- level
6846
- })));
6847
- select.set(requestedValue, { silent: true });
6986
+ select.el.dataset.preset = "entity";
6848
6987
  return select;
6849
6988
  }
6850
6989
  /**
@@ -6859,12 +6998,21 @@ var Select = class _Select extends Component {
6859
6998
  * @returns {Select}
6860
6999
  */
6861
7000
  static permission(target, options = {}) {
6862
- const { groups = [], value = true, valueKey = "ID", labelKey = "name", ...rest } = options;
7001
+ const {
7002
+ groups = [],
7003
+ value = true,
7004
+ valueKey = "ID",
7005
+ labelKey = "name",
7006
+ filter = "local",
7007
+ ...rest
7008
+ } = options;
6863
7009
  const loader = typeof groups === "function" ? groups : null;
6864
7010
  const shareable = loader ? [] : groups;
6865
7011
  const choices = [{ ID: false, name: "Private" }, { ID: true, name: "Public" }];
6866
7012
  const select = new _Select(target, {
6867
- filter: loader ?? "local",
7013
+ // An async group loader is the data source and cannot be disconnected by an incidental
7014
+ // `filter` override. Array-backed groups still honor local/disabled filtering.
7015
+ filter: loader ?? filter,
6868
7016
  ...rest,
6869
7017
  value: null,
6870
7018
  items: shareable,
@@ -6888,18 +7036,24 @@ var Select = class _Select extends Component {
6888
7036
  /** @type {HTMLElement} */
6889
7037
  this.el
6890
7038
  );
6891
- if (!this._createdRoot && root) {
6892
- root.replaceChildren(...this._originalChildren);
6893
- for (const [name, value] of this._originalAttributes) {
6894
- if (value === null) root.removeAttribute(name);
6895
- else root.setAttribute(name, value);
6896
- }
6897
- }
7039
+ if (!this._createdRoot && root) restoreTarget(root, this._snapshot);
6898
7040
  super.destroy();
6899
7041
  }
6900
7042
  /** @param {KeyboardEvent} event @returns {void} */
6901
7043
  _onKeydown(event) {
6902
7044
  if (this._disabled) return;
7045
+ if (this._open && isPrintable(event)) {
7046
+ const shortcut = this._optionEntries.findIndex((entry) => !entry.clear && !entry.action && String(
7047
+ /** @type {Record<string, any>|null} */
7048
+ entry.item?.shortcut ?? ""
7049
+ ) === event.key);
7050
+ if (shortcut >= 0) {
7051
+ event.preventDefault();
7052
+ this._setActive(shortcut);
7053
+ this._chooseEntry(this._optionEntries[shortcut]);
7054
+ return;
7055
+ }
7056
+ }
6903
7057
  if (event.key === "ArrowDown") {
6904
7058
  event.preventDefault();
6905
7059
  if (!this._open) this.open();
@@ -6947,6 +7101,7 @@ var Select = class _Select extends Component {
6947
7101
  /** @type {HTMLInputElement} */
6948
7102
  this.refs.input.value
6949
7103
  );
7104
+ this._syncValueAdornment();
6950
7105
  if (!this._open) this.open();
6951
7106
  if (this.options.filter === "local") {
6952
7107
  this._visibleFixed = this._matchFixed(query);
@@ -6970,6 +7125,22 @@ var Select = class _Select extends Component {
6970
7125
  /** @param {SelectOptionEntry|undefined} entry @returns {void} */
6971
7126
  _chooseEntry(entry) {
6972
7127
  if (!entry) return;
7128
+ if (entry.action) {
7129
+ const detail = {
7130
+ id: entry.action.id,
7131
+ action: entry.action,
7132
+ query: (
7133
+ /** @type {HTMLInputElement} */
7134
+ this.refs.input.value
7135
+ ),
7136
+ select: this
7137
+ };
7138
+ const action = this.emit("action", detail);
7139
+ if (!action.defaultPrevented) entry.action.invoke?.(detail);
7140
+ this.close();
7141
+ this.focus();
7142
+ return;
7143
+ }
6973
7144
  this.set(entry.clear ? null : entry.item);
6974
7145
  this.close();
6975
7146
  this.focus();
@@ -7002,14 +7173,19 @@ var Select = class _Select extends Component {
7002
7173
  list.replaceChildren();
7003
7174
  this._optionEntries = [];
7004
7175
  let lastGroup = /* @__PURE__ */ Symbol("initial-group");
7176
+ const fixed = this._uniqueItems(this._visibleFixed);
7177
+ const items = this._uniqueItems(this._visibleItems, fixed);
7005
7178
  if (this.options.clearable) {
7006
- this._appendOption(null, true, this.options.placeholder || this._message("select.none", "No selection"));
7179
+ this._appendOption(null, true, this.options.noneLabel ?? this._message("select.none", "No selection"));
7007
7180
  }
7008
- for (const item of this._visibleFixed) this._appendOption(item, false);
7009
- 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) {
7010
7186
  list.append(h("div", { class: "zx-select__separator", role: "presentation" }));
7011
7187
  }
7012
- for (const item of this._visibleItems) {
7188
+ for (const item of items) {
7013
7189
  if (this.options.groupKey) {
7014
7190
  const group = this._read(item, this.options.groupKey);
7015
7191
  if (!Object.is(group, lastGroup)) {
@@ -7022,7 +7198,23 @@ var Select = class _Select extends Component {
7022
7198
  }
7023
7199
  this._appendOption(item, false);
7024
7200
  }
7025
- if (this._visibleItems.length === 0 && this._visibleFixed.length === 0 && !this._loading) {
7201
+ if (this._actions.length > 0) {
7202
+ if (fixed.length > 0 || items.length > 0 || this.options.clearable) {
7203
+ list.append(h("div", { class: "zx-select__separator", role: "presentation" }));
7204
+ }
7205
+ let actionGroup = /* @__PURE__ */ Symbol("initial-action-group");
7206
+ for (const action of this._actions) {
7207
+ if (action.group && action.group !== actionGroup) {
7208
+ list.append(h("div", {
7209
+ class: "zx-select__group",
7210
+ role: "presentation"
7211
+ }, action.group));
7212
+ actionGroup = action.group;
7213
+ }
7214
+ this._appendAction(action);
7215
+ }
7216
+ }
7217
+ if (items.length === 0 && fixed.length === 0 && this._actions.length === 0 && !this._loading) {
7026
7218
  list.append(h("div", {
7027
7219
  class: "zx-select__empty",
7028
7220
  role: "status"
@@ -7034,7 +7226,7 @@ var Select = class _Select extends Component {
7034
7226
  _appendOption(item, clear, label) {
7035
7227
  const selected = clear ? this._selected === null : this._selected !== null && Object.is(this._itemValue(item), this.value);
7036
7228
  const index = this._optionEntries.length;
7037
- const content = clear ? String(label ?? "") : this._renderItem(item);
7229
+ const content = clear && typeof this.options.renderNone === "function" ? normalizeRenderedContent(this.options.renderNone(String(label ?? ""))) : clear ? String(label ?? "") : this._renderItem(item);
7038
7230
  const option = h("div", {
7039
7231
  class: "zx-select__option",
7040
7232
  id: uid("zx-select-option"),
@@ -7044,16 +7236,37 @@ var Select = class _Select extends Component {
7044
7236
  }, h("span", { class: "zx-select__option-content" }, content));
7045
7237
  if (selected) option.append(icon("check", { size: 15 }));
7046
7238
  this.refs.list.append(option);
7047
- this._optionEntries.push({ item, clear, element: option });
7239
+ this._optionEntries.push({ item, clear, action: null, element: option });
7240
+ }
7241
+ /** @param {SelectAction} action @returns {void} */
7242
+ _appendAction(action) {
7243
+ const index = this._optionEntries.length;
7244
+ const visual = selectActionIcon(action.icon);
7245
+ const option = h("div", {
7246
+ class: "zx-select__option zx-select__action",
7247
+ id: uid("zx-select-option"),
7248
+ role: "option",
7249
+ ariaSelected: "false",
7250
+ dataset: { index, kind: "action" }
7251
+ }, h(
7252
+ "span",
7253
+ { class: "zx-select__option-content zx-select__action-content" },
7254
+ visual ? h("span", { class: "zx-select__action-icon", ariaHidden: "true" }, visual) : null,
7255
+ h(
7256
+ "span",
7257
+ { class: "zx-select__action-copy" },
7258
+ h("span", { class: "zx-select__action-label" }, action.label),
7259
+ action.description ? h("span", { class: "zx-select__action-description" }, action.description) : null
7260
+ )
7261
+ ));
7262
+ this.refs.list.append(option);
7263
+ this._optionEntries.push({ item: null, clear: false, action, element: option });
7048
7264
  }
7049
7265
  /** @param {SelectItem|null} item @returns {Node|string} */
7050
7266
  _renderItem(item) {
7051
7267
  if (typeof this.options.renderItem === "function") {
7052
7268
  const rendered = this.options.renderItem(item);
7053
- return rendered && typeof rendered === "object" && "nodeType" in rendered ? (
7054
- /** @type {Node} */
7055
- rendered
7056
- ) : String(rendered ?? "");
7269
+ return normalizeRenderedContent(rendered);
7057
7270
  }
7058
7271
  return this._itemLabel(item);
7059
7272
  }
@@ -7124,9 +7337,29 @@ var Select = class _Select extends Component {
7124
7337
  this.refs.input
7125
7338
  );
7126
7339
  input.value = this._selected === null ? "" : typeof this.options.renderValue === "function" ? String(this.options.renderValue(this._selected) ?? "") : this._itemLabel(this._selected);
7340
+ this._syncValueAdornment();
7127
7341
  this.refs.clear.hidden = !this.options.clearable || this._selected === null;
7128
7342
  }
7129
7343
  /** @returns {void} */
7344
+ _syncValueAdornment() {
7345
+ const host = this.refs.valueAdornment;
7346
+ if (!host) return;
7347
+ const selectedLabel = this._selected === null ? "" : typeof this.options.renderValue === "function" ? String(this.options.renderValue(this._selected) ?? "") : this._itemLabel(this._selected);
7348
+ const query = String(
7349
+ /** @type {HTMLInputElement} */
7350
+ this.refs.input?.value ?? ""
7351
+ );
7352
+ if (this._selected === null || typeof this.options.renderValueAdornment !== "function" || Boolean(this.options.filter && query !== selectedLabel)) {
7353
+ host.replaceChildren();
7354
+ host.hidden = true;
7355
+ return;
7356
+ }
7357
+ const rendered = this.options.renderValueAdornment(this._selected);
7358
+ const content = normalizeRenderedContent(rendered);
7359
+ host.replaceChildren(content instanceof Node ? content : document.createTextNode(content));
7360
+ host.hidden = rendered == null || typeof content === "string" && content === "";
7361
+ }
7362
+ /** @returns {void} */
7130
7363
  _syncDisabled() {
7131
7364
  const input = (
7132
7365
  /** @type {HTMLInputElement} */
@@ -7182,6 +7415,16 @@ var Select = class _Select extends Component {
7182
7415
  _matchFixed(query) {
7183
7416
  return matchItems(this._fixedItems, query, this._searchKeys());
7184
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
+ }
7185
7428
  /** @returns {number} */
7186
7429
  _selectedEntryIndex() {
7187
7430
  if (this._selected === null) return this.options.clearable ? 0 : -1;
@@ -7193,16 +7436,104 @@ var Select = class _Select extends Component {
7193
7436
  _itemEntryIndex(itemIndex) {
7194
7437
  return itemIndex + (this.options.clearable ? 1 : 0);
7195
7438
  }
7196
- /** @param {string} name @returns {void} */
7197
- _rememberAttribute(element, name) {
7198
- this._originalAttributes.set(name, element.getAttribute(name));
7199
- }
7200
7439
  /** @param {string} key @param {string} fallback @returns {string} */
7201
7440
  _message(key, fallback) {
7202
7441
  const message = this.msg(key);
7203
7442
  return message === key ? fallback : message;
7204
7443
  }
7205
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
+ }
7461
+ function normalizeRenderedContent(rendered) {
7462
+ return rendered && typeof rendered === "object" && "nodeType" in rendered ? (
7463
+ /** @type {Node} */
7464
+ rendered
7465
+ ) : String(rendered ?? "");
7466
+ }
7467
+ function normalizeSelectActions(actions) {
7468
+ if (!Array.isArray(actions)) return [];
7469
+ return actions.filter((action) => action && typeof action === "object" && action.id != null && action.label != null).map((action) => ({
7470
+ ...action,
7471
+ label: String(action.label),
7472
+ description: action.description == null ? void 0 : String(action.description),
7473
+ group: action.group == null ? void 0 : String(action.group)
7474
+ }));
7475
+ }
7476
+ function selectActionIcon(visual) {
7477
+ if (typeof visual === "string") return icon(visual);
7478
+ if (visual instanceof Node) return visual.cloneNode(true);
7479
+ return null;
7480
+ }
7481
+ function priorityContent(item, label) {
7482
+ return h(
7483
+ "span",
7484
+ { class: "zx-select__priority" },
7485
+ priorityIndicator(item.level),
7486
+ label ? h("span", {}, String(item.name)) : null
7487
+ );
7488
+ }
7489
+ function priorityIndicator(level) {
7490
+ const normalized = Math.max(0, Math.min(4, Math.trunc(Number(level) || 0)));
7491
+ return h("span", {
7492
+ class: "zx-select__priority-icon",
7493
+ dataset: { level: normalized },
7494
+ ariaHidden: "true"
7495
+ }, Array.from({ length: 5 }, (_, index) => h("span", {
7496
+ dataset: { filled: String(index <= normalized) }
7497
+ })));
7498
+ }
7499
+ function normalizeStatusItem(input) {
7500
+ if (!input || typeof input !== "object" || input.ID == null || input.name == null) return null;
7501
+ const indicators = ["dotted", "ring", "progress", "review", "ready", "waiting", "done", "canceled", "duplicate"];
7502
+ const tones = ["muted", "neutral", "warning", "info", "accent", "success", "danger"];
7503
+ return {
7504
+ ...input,
7505
+ name: String(input.name),
7506
+ indicator: indicators.includes(String(input.indicator)) ? String(input.indicator) : "ring",
7507
+ tone: tones.includes(String(input.tone)) ? String(input.tone) : "muted",
7508
+ description: input.description == null ? "" : String(input.description),
7509
+ shortcut: input.shortcut == null ? "" : String(input.shortcut)
7510
+ };
7511
+ }
7512
+ function statusContent(item) {
7513
+ return h(
7514
+ "span",
7515
+ { class: "zx-select__status" },
7516
+ statusIndicator(item),
7517
+ h(
7518
+ "span",
7519
+ { class: "zx-select__status-copy" },
7520
+ h("span", { class: "zx-select__status-label" }, String(item.name)),
7521
+ item.description ? h("span", { class: "zx-select__status-description" }, item.description) : null
7522
+ ),
7523
+ item.shortcut ? h("kbd", { class: "zx-select__status-shortcut" }, item.shortcut) : null
7524
+ );
7525
+ }
7526
+ function statusIndicator(item) {
7527
+ const indicator = String(item.indicator ?? "ring");
7528
+ const marker = h("span", {
7529
+ class: "zx-select__status-indicator",
7530
+ dataset: { indicator, tone: String(item.tone ?? "muted") },
7531
+ ariaHidden: "true"
7532
+ });
7533
+ if (indicator === "done") marker.append(icon("check", { size: 8 }));
7534
+ if (indicator === "canceled") marker.append(icon("x", { size: 7 }));
7535
+ return marker;
7536
+ }
7206
7537
  function readItem(item, reader) {
7207
7538
  if (typeof reader === "function") return reader(item);
7208
7539
  if (item === null || typeof item !== "object") return item;
@@ -7215,10 +7546,11 @@ function isPrintable(event) {
7215
7546
  // src/components/select/field-adapter.js
7216
7547
  function registerSelectFieldAdapter() {
7217
7548
  Field.register("zxselect", (field, options) => {
7218
- const select = field.own(new Select(null, {
7219
- ...options.props ?? {},
7220
- value: options.value
7221
- }));
7549
+ const props = options.props ?? {};
7550
+ const preset = props.preset;
7551
+ const selectOptions = { ...props, value: options.value };
7552
+ delete selectOptions.preset;
7553
+ const select = field.own(preset === "priority" ? Select.priority(null, selectOptions) : preset === "status" ? Select.status(null, selectOptions) : new Select(null, selectOptions));
7222
7554
  field.listen(select.el, "change", (event) => event.stopImmediatePropagation());
7223
7555
  select.on("change", (event) => field.emit("change", { value: event.detail.value }));
7224
7556
  return {
@@ -7602,6 +7934,8 @@ var TagPicker = class extends Component {
7602
7934
  values: [],
7603
7935
  valueKey: "ID",
7604
7936
  labelKey: "name",
7937
+ iconKey: "icon",
7938
+ colorKey: "color",
7605
7939
  searchKeys: null,
7606
7940
  filter: "local",
7607
7941
  minQuery: 0,
@@ -7942,12 +8276,13 @@ var TagPicker = class extends Component {
7942
8276
  _renderTags() {
7943
8277
  const tags = this._selected.map((item) => {
7944
8278
  const label = this._labelOf(item);
7945
- const content = this.options.renderTag ? this.options.renderTag(item) : label;
7946
- const tag = h(
7947
- "span",
7948
- { class: "zx-tag-picker__tag" },
7949
- h("span", { class: "zx-tag-picker__tag-label" }, content)
7950
- );
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);
7951
8286
  if (!this._readonly && !this._disabled) {
7952
8287
  tag.append(h("button", {
7953
8288
  class: "zx-icon-btn zx-tag-picker__remove",
@@ -7985,7 +8320,7 @@ var TagPicker = class extends Component {
7985
8320
  h(
7986
8321
  "span",
7987
8322
  { class: "zx-tag-picker__option-label" },
7988
- 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))]
7989
8324
  )
7990
8325
  );
7991
8326
  if (!selected && this.isFull()) option.setAttribute("aria-disabled", "true");
@@ -8306,6 +8641,34 @@ var TagPicker = class extends Component {
8306
8641
  if (item !== null && typeof item === "object") return String(item[key] ?? "");
8307
8642
  return String(item ?? "");
8308
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
+ }
8309
8672
  /** @returns {void} */
8310
8673
  _emitChange() {
8311
8674
  this.emit("change", { values: this.getValues(), items: this.getItems() });
@@ -9256,7 +9619,11 @@ var Table = class extends Component {
9256
9619
  rowClass: null,
9257
9620
  zebra: true,
9258
9621
  editMode: false,
9259
- hierarchy: false
9622
+ editTrigger: "double",
9623
+ hierarchy: false,
9624
+ rowReorder: false,
9625
+ columnVisibility: false,
9626
+ hiddenColumns: []
9260
9627
  };
9261
9628
  /**
9262
9629
  * Creates a table around a target or creates a new root when target is null.
@@ -9268,12 +9635,14 @@ var Table = class extends Component {
9268
9635
  }
9269
9636
  /** @returns {HTMLElement} Table root. */
9270
9637
  render() {
9638
+ const created = this.el === null;
9271
9639
  const root = (
9272
9640
  /** @type {HTMLElement} */
9273
9641
  this.el ?? h("div")
9274
9642
  );
9275
9643
  this.el = root;
9276
- this._originalChildren = this.el ? Array.from(this.el.childNodes) : null;
9644
+ this._createdRoot = created;
9645
+ this._original = created ? null : snapshotTarget(root);
9277
9646
  this._columns = Array.isArray(this.options.columns) ? this.options.columns.map((column) => ({ ...column })) : [];
9278
9647
  this._data = Array.isArray(this.options.data) ? [...this.options.data] : [];
9279
9648
  this._selected = /* @__PURE__ */ new Set();
@@ -9283,6 +9652,7 @@ var Table = class extends Component {
9283
9652
  this._rowElements = /* @__PURE__ */ new Map();
9284
9653
  this._restored = false;
9285
9654
  this._editMode = normalizeEditMode(this.options.editMode);
9655
+ this._editTrigger = this.options.editTrigger === "single" ? "single" : "double";
9286
9656
  this._edit = null;
9287
9657
  this._editMessage = null;
9288
9658
  this._stacked = false;
@@ -9294,6 +9664,10 @@ var Table = class extends Component {
9294
9664
  this._rovingCell = null;
9295
9665
  this._rovingMatched = false;
9296
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;
9297
9671
  this._expanded = initialExpanded(this._hierarchy?.expanded);
9298
9672
  if (this._sort && this.options.sortMode === "local") this._sortData();
9299
9673
  if (this._hierarchy?.expanded === true) {
@@ -9315,11 +9689,17 @@ var Table = class extends Component {
9315
9689
  zebra: String(Boolean(this.options.zebra))
9316
9690
  }
9317
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;
9318
9697
  if (this.options.height != null) {
9319
9698
  this._scroll.style.blockSize = typeof this.options.height === "number" ? `${this.options.height}px` : String(this.options.height);
9320
9699
  }
9700
+ this._columnControls = this.options.columnVisibility ? this._createColumnControls() : null;
9321
9701
  if (this._editMode === false) {
9322
- root.replaceChildren(this._scroll);
9702
+ root.replaceChildren(...[this._columnControls, this._scroll, this._rowMoveMessage].filter(Boolean));
9323
9703
  } else {
9324
9704
  root.dataset.editMode = this._editMode;
9325
9705
  this._editMessage = h("div", {
@@ -9328,7 +9708,7 @@ var Table = class extends Component {
9328
9708
  role: "status",
9329
9709
  ariaLive: "polite"
9330
9710
  });
9331
- root.replaceChildren(this._scroll, this._editMessage);
9711
+ root.replaceChildren(...[this._columnControls, this._scroll, this._editMessage, this._rowMoveMessage].filter(Boolean));
9332
9712
  }
9333
9713
  this._renderColumns();
9334
9714
  this._renderHeader();
@@ -9336,6 +9716,34 @@ var Table = class extends Component {
9336
9716
  this._watchWidth();
9337
9717
  this.listen(this._tbody, "click", (event) => this._handleBodyClick(event));
9338
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
+ }
9339
9747
  if (this._editMode !== false) {
9340
9748
  this.listen(this._tbody, "keydown", (event) => this._handleEditKeydown(
9341
9749
  /** @type {KeyboardEvent} */
@@ -9348,8 +9756,8 @@ var Table = class extends Component {
9348
9756
  }
9349
9757
  /**
9350
9758
  * Toggles a loading state. While loading, the body is dimmed, an indeterminate progress bar is
9351
- * shown, and `aria-busy` is set — a lightweight take on the busy/skeleton pattern used by
9352
- * enterprise tables (SAP UI5, Salesforce SLDS). Call `setLoading(false)` (or `setData`) when done.
9759
+ * shown, and `aria-busy` is set — a lightweight enterprise-table busy and skeleton pattern.
9760
+ * Call `setLoading(false)` (or `setData`) when done.
9353
9761
  * @param {boolean} [loading=true] Whether the table is loading.
9354
9762
  * @returns {this}
9355
9763
  */
@@ -9451,6 +9859,81 @@ var Table = class extends Component {
9451
9859
  getData() {
9452
9860
  return [...this._data];
9453
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
+ }
9454
9937
  /**
9455
9938
  * Removes all rows.
9456
9939
  * @returns {this}
@@ -9619,14 +10102,17 @@ var Table = class extends Component {
9619
10102
  */
9620
10103
  startEdit(rowId, columnId) {
9621
10104
  if (this._editMode === false) return this;
9622
- const column = this._columns.find((candidate) => candidate.id === columnId);
9623
- if (!column) return this;
10105
+ const requested = this._columns.find((candidate) => candidate.id === columnId);
10106
+ if (!requested) return this;
9624
10107
  const current = this._data.find((candidate) => Object.is(this._idFor(candidate), rowId));
9625
- 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;
9626
10112
  if (this._edit) {
9627
- if (Object.is(this._edit.id, rowId) && this._edit.cells.has(columnId)) {
9628
- this._edit.columnId = columnId;
9629
- 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);
9630
10116
  return this;
9631
10117
  }
9632
10118
  this.commitEdit();
@@ -9637,7 +10123,7 @@ var Table = class extends Component {
9637
10123
  const row = this._data[index];
9638
10124
  const tr = this._rowElements.get(rowId)?.[0];
9639
10125
  if (!tr) return this;
9640
- 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];
9641
10127
  const cells = /* @__PURE__ */ new Map();
9642
10128
  for (const target of targets) {
9643
10129
  const td = cellIn(tr, target.id);
@@ -9646,10 +10132,10 @@ var Table = class extends Component {
9646
10132
  if (entry) cells.set(target.id, entry);
9647
10133
  }
9648
10134
  if (cells.size === 0) return this;
9649
- this._edit = { id: rowId, columnId, row, cells, tr };
10135
+ this._edit = { id: rowId, columnId: activeColumnId, row, cells, tr };
9650
10136
  tr.dataset.editing = "true";
9651
- this.emit("editstart", { row, id: rowId, column, columnId, value: row?.[columnId] });
9652
- this._focusEditor(columnId);
10137
+ this.emit("editstart", { row, id: rowId, column, columnId: activeColumnId, value: row?.[activeColumnId] });
10138
+ this._focusEditor(activeColumnId);
9653
10139
  return this;
9654
10140
  }
9655
10141
  /**
@@ -9898,23 +10384,203 @@ var Table = class extends Component {
9898
10384
  this._teardownEdit();
9899
10385
  this._width?.destroy();
9900
10386
  this._width = null;
9901
- if (this.el) delete this.el.dataset.editMode;
9902
- if (!this._restored && this._originalChildren) {
9903
- this.el.replaceChildren(...this._originalChildren);
10387
+ if (!this._restored && !this._createdRoot) {
10388
+ restoreTarget(this.el, this._original);
9904
10389
  this._restored = true;
9905
10390
  }
9906
10391
  super.destroy();
9907
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
+ }
9908
10572
  /** @returns {void} */
9909
10573
  _renderColumns() {
9910
10574
  const fragment2 = document.createDocumentFragment();
10575
+ if (this.options.rowReorder) fragment2.append(h("col", { class: "zx-table__reorder-column" }));
9911
10576
  if (this.options.selectable === "multi") {
9912
10577
  fragment2.append(h("col", { class: "zx-table__selection-column" }));
9913
10578
  }
9914
- const fractions = this._columns.map((column) => parseFraction(column.width));
10579
+ const columns = this._visibleColumns();
10580
+ const fractions = columns.map((column) => parseFraction(column.width));
9915
10581
  const total = fractions.reduce((sum, value) => sum + value, 0);
9916
10582
  this._table.dataset.layout = total > 0 ? "fixed" : "auto";
9917
- this._columns.forEach((column, index) => {
10583
+ columns.forEach((column, index) => {
9918
10584
  const col = h("col");
9919
10585
  if (fractions[index] > 0) col.style.width = `${fractions[index] / total * 100}%`;
9920
10586
  else if (column.width && column.width !== "auto") col.style.width = String(column.width);
@@ -9926,6 +10592,11 @@ var Table = class extends Component {
9926
10592
  _renderHeader() {
9927
10593
  const row = h("tr", { class: "zx-table__header-row" });
9928
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
+ }));
9929
10600
  if (this.options.selectable === "multi") {
9930
10601
  this._selectAll = h("input", {
9931
10602
  class: "zx-table__checkbox zx-table__select-all",
@@ -9951,7 +10622,7 @@ var Table = class extends Component {
9951
10622
  this._selectAll = null;
9952
10623
  }
9953
10624
  this._headers = /* @__PURE__ */ new Map();
9954
- for (const column of this._columns) {
10625
+ for (const column of this._visibleColumns()) {
9955
10626
  const th = h("th", { scope: "col" });
9956
10627
  const align = column.align ?? (isNumericColumn(column) ? "end" : null);
9957
10628
  if (align) th.dataset.align = align;
@@ -9991,7 +10662,7 @@ var Table = class extends Component {
9991
10662
  { class: "zx-table__empty-row" },
9992
10663
  h("td", {
9993
10664
  class: "zx-table__empty",
9994
- 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)
9995
10666
  }, this._emptyContent())
9996
10667
  ));
9997
10668
  } else {
@@ -10000,6 +10671,7 @@ var Table = class extends Component {
10000
10671
  });
10001
10672
  }
10002
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)));
10003
10675
  if (this._growStep) this._table.setAttribute("aria-rowcount", String(projected.length + 1));
10004
10676
  this._syncSelectAll();
10005
10677
  this._syncGrowing();
@@ -10043,6 +10715,19 @@ var Table = class extends Component {
10043
10715
  const elements = this._rowElements.get(id) ?? [];
10044
10716
  elements.push(tr);
10045
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
+ }
10046
10731
  if (this.options.selectable === "multi") {
10047
10732
  const checkbox = h("input", {
10048
10733
  class: "zx-table__checkbox zx-table__row-checkbox",
@@ -10052,7 +10737,7 @@ var Table = class extends Component {
10052
10737
  });
10053
10738
  tr.append(h("td", { class: "zx-table__selection-cell" }, checkbox));
10054
10739
  }
10055
- this._columns.forEach((column) => {
10740
+ this._visibleColumns().forEach((column) => {
10056
10741
  const cell = (
10057
10742
  /** @type {HTMLTableCellElement} */
10058
10743
  h("td")
@@ -10088,7 +10773,7 @@ var Table = class extends Component {
10088
10773
  const content = h("span", { class: "zx-table__cell-content" });
10089
10774
  if (value && typeof value === "object" && typeof value.nodeType === "number") content.append(value);
10090
10775
  else if (value != null) content.append(document.createTextNode(String(value)));
10091
- if (!this._hierarchy || column.id !== this._hierarchy.column || !tree) {
10776
+ if (!this._hierarchy || column.id !== this._hierarchyColumn() || !tree) {
10092
10777
  cell.replaceChildren(...content.childNodes);
10093
10778
  return;
10094
10779
  }
@@ -10119,6 +10804,7 @@ var Table = class extends Component {
10119
10804
  if (!rowElement || !this._tbody.contains(rowElement)) return;
10120
10805
  const meta = this._rowMeta.get(rowElement);
10121
10806
  if (!meta) return;
10807
+ if (target.closest(".zx-table__row-handle")) return;
10122
10808
  if (this._edit) {
10123
10809
  this.commitEdit();
10124
10810
  if (this._edit) return;
@@ -10138,10 +10824,18 @@ var Table = class extends Component {
10138
10824
  );
10139
10825
  return;
10140
10826
  }
10141
- this.emit("rowclick", { ...meta, event });
10142
- if (this.options.selectable === "single" && !this._selected.has(meta.id)) {
10143
- this._selected = /* @__PURE__ */ new Set([meta.id]);
10144
- this._selectionAnchorId = meta.id;
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
+ }
10835
+ this.emit("rowclick", { ...meta, event });
10836
+ if (this.options.selectable === "single" && !this._selected.has(meta.id)) {
10837
+ this._selected = /* @__PURE__ */ new Set([meta.id]);
10838
+ this._selectionAnchorId = meta.id;
10145
10839
  this._syncSelection();
10146
10840
  this._emitSelectionChange();
10147
10841
  }
@@ -10158,7 +10852,8 @@ var Table = class extends Component {
10158
10852
  if (!rowElement || !this._tbody.contains(rowElement)) return;
10159
10853
  const meta = this._rowMeta.get(rowElement);
10160
10854
  if (!meta) return;
10161
- if (this._editMode !== false) {
10855
+ if (isInteractiveCellContent(target)) return;
10856
+ if (this._editMode !== false && this._editTrigger === "double") {
10162
10857
  const cell = target?.closest('td[data-editable="true"]');
10163
10858
  if (cell && this._tbody.contains(cell)) {
10164
10859
  this.startEdit(meta.id, String(cell.dataset.column));
@@ -10460,13 +11155,14 @@ var Table = class extends Component {
10460
11155
  * @returns {{id: unknown, columnId: string, index: number}|null} Next cell, or null at the end.
10461
11156
  */
10462
11157
  _nextEditableCell(rowIndex, columnId, direction) {
10463
- const columnCount = this._columns.length;
11158
+ const columns = this._visibleColumns();
11159
+ const columnCount = columns.length;
10464
11160
  if (rowIndex < 0 || columnCount === 0) return null;
10465
11161
  const visible = this._visibleData();
10466
11162
  const startingId = this._idFor(this._data[rowIndex]);
10467
11163
  let row = visible.findIndex((candidate) => Object.is(this._idFor(candidate), startingId));
10468
11164
  if (row < 0) return null;
10469
- let column = this._columns.findIndex((candidate) => candidate.id === columnId);
11165
+ let column = columns.findIndex((candidate) => candidate.id === columnId);
10470
11166
  const limit = visible.length * columnCount + columnCount;
10471
11167
  for (let step = 0; step < limit; step += 1) {
10472
11168
  column += direction;
@@ -10479,7 +11175,7 @@ var Table = class extends Component {
10479
11175
  }
10480
11176
  if (row < 0 || row >= visible.length) return null;
10481
11177
  const candidateRow = visible[row];
10482
- const candidateColumn = this._columns[column];
11178
+ const candidateColumn = columns[column];
10483
11179
  if (resolveEditable(candidateColumn, candidateRow) !== false) {
10484
11180
  return {
10485
11181
  id: this._idFor(candidateRow),
@@ -10722,6 +11418,53 @@ function sortHierarchyRows(rows, rowId, parentId, getValue, direction) {
10722
11418
  }
10723
11419
  return result;
10724
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
+ }
10725
11468
  function resolveEditable(column, row) {
10726
11469
  if (!column || typeof column !== "object") return false;
10727
11470
  if (typeof column.editor === "function") {
@@ -10802,7 +11545,8 @@ function hierarchyForest(rows, rowId, parentId) {
10802
11545
  for (const node of nodes) if (!byId.has(node.id)) byId.set(node.id, node);
10803
11546
  const parentOf = /* @__PURE__ */ new Map();
10804
11547
  for (const node of nodes) {
10805
- 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;
10806
11550
  parentOf.set(node, parent === node ? null : parent);
10807
11551
  }
10808
11552
  for (const node of nodes) {
@@ -10831,6 +11575,9 @@ function hierarchyBranchIds(rows, rowId, parentId) {
10831
11575
  function readRowValue(accessor, row) {
10832
11576
  return typeof accessor === "function" ? accessor(row) : row?.[accessor];
10833
11577
  }
11578
+ function sameHierarchyParent(left, right) {
11579
+ return left == null && right == null || Object.is(left, right);
11580
+ }
10834
11581
  function normalizeHierarchy(option, columns) {
10835
11582
  if (!option || typeof option !== "object") return null;
10836
11583
  const parentId = option.parentId;
@@ -10850,6 +11597,23 @@ function normalizeEditMode(mode) {
10850
11597
  function isInsideEditor(target) {
10851
11598
  return Boolean(target?.closest?.(".zx-table__editor"));
10852
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
+ }
10853
11617
  function deferToEditor(target) {
10854
11618
  if (target.closest("[popover]")) return true;
10855
11619
  return target.getAttribute?.("aria-expanded") === "true";
@@ -10884,6 +11648,730 @@ function growStep(growing) {
10884
11648
  return Number.isFinite(step) && step > 0 ? step : null;
10885
11649
  }
10886
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
+
11751
+ // src/components/launcher/launcher.js
11752
+ var Launcher = class extends Component {
11753
+ static cssName = "launcher";
11754
+ /** @type {Readonly<LauncherOptions>} */
11755
+ static defaults = {
11756
+ items: [],
11757
+ sources: [],
11758
+ query: "",
11759
+ debounce: 250,
11760
+ minQuery: 0,
11761
+ maxResults: 100,
11762
+ placeholder: "Search applications and records",
11763
+ label: "Launcher",
11764
+ closeLabel: "Close launcher",
11765
+ resultsLabel: "Launcher results",
11766
+ emptyText: "No results",
11767
+ loadingText: "Searching\u2026",
11768
+ shortcut: "mod+k",
11769
+ hints: { move: "Move", open: "Open", close: "Close" }
11770
+ };
11771
+ /**
11772
+ * Creates an owned launcher dialog or enhances an existing `<dialog>` target.
11773
+ * @param {HTMLDialogElement|string|null} [target=null] Dialog target, selector, or null.
11774
+ * @param {LauncherOptions} [options={}] Launcher options.
11775
+ */
11776
+ constructor(target = null, options = {}) {
11777
+ super(target, options);
11778
+ }
11779
+ /** @returns {HTMLDialogElement} */
11780
+ render() {
11781
+ if (this.el !== null && !(this.el instanceof HTMLDialogElement)) {
11782
+ throw new TypeError("Launcher target must be a <dialog> element");
11783
+ }
11784
+ this._createdRoot = this.el === null;
11785
+ const dialog = (
11786
+ /** @type {HTMLDialogElement} */
11787
+ this.el ?? h("dialog")
11788
+ );
11789
+ this.el = dialog;
11790
+ this._original = this._createdRoot ? null : {
11791
+ attributes: Array.from(dialog.attributes, (attribute) => [attribute.name, attribute.value]),
11792
+ children: Array.from(dialog.childNodes)
11793
+ };
11794
+ this._items = normalizeItems(this.options.items);
11795
+ this._sources = normalizeSources(this.options.sources);
11796
+ this._sourceItems = /* @__PURE__ */ new Map();
11797
+ this._query = String(this.options.query ?? "");
11798
+ this._renderedQuery = null;
11799
+ this._results = [];
11800
+ this._active = -1;
11801
+ this._request = null;
11802
+ this._requestSequence = 0;
11803
+ this._queryTimer = null;
11804
+ this._busy = false;
11805
+ this._destroyed = false;
11806
+ const listId = uid("zx-launcher-results");
11807
+ const input = h("input", {
11808
+ ref: "input",
11809
+ class: "zx-launcher__input",
11810
+ type: "search",
11811
+ role: "combobox",
11812
+ autocomplete: "off",
11813
+ spellcheck: false,
11814
+ ariaAutocomplete: "list",
11815
+ ariaControls: listId,
11816
+ ariaExpanded: "false",
11817
+ ariaLabel: this.options.label,
11818
+ placeholder: this.options.placeholder,
11819
+ value: this._query
11820
+ });
11821
+ const close = h("button", {
11822
+ ref: "close",
11823
+ class: "zx-icon-btn zx-launcher__close",
11824
+ type: "button",
11825
+ ariaLabel: this.options.closeLabel
11826
+ }, icon("x"));
11827
+ const status = h("div", {
11828
+ ref: "status",
11829
+ class: "zx-launcher__status",
11830
+ role: "status",
11831
+ ariaLive: "polite"
11832
+ });
11833
+ const results = h("div", {
11834
+ ref: "results",
11835
+ class: "zx-launcher__results",
11836
+ id: listId,
11837
+ role: "listbox",
11838
+ ariaLabel: this.options.resultsLabel
11839
+ });
11840
+ const shortcut = this.options.shortcut === "mod+k" ? h("kbd", {
11841
+ class: "zx-launcher__shortcut",
11842
+ ariaHidden: "true"
11843
+ }, launcherShortcutLabel()) : null;
11844
+ const footer = launcherHints(this.options.hints);
11845
+ dialog.setAttribute("aria-label", String(this.options.label));
11846
+ dialog.replaceChildren(
11847
+ h(
11848
+ "div",
11849
+ { class: "zx-launcher__surface" },
11850
+ h("div", {
11851
+ class: "zx-launcher__search",
11852
+ dataset: { shortcut: shortcut ? "true" : "false" }
11853
+ }, icon("search"), input, shortcut, close),
11854
+ status,
11855
+ results,
11856
+ footer
11857
+ )
11858
+ );
11859
+ dialog.dataset.state = "closed";
11860
+ if (this._createdRoot) document.body.append(dialog);
11861
+ this.listen(input, "input", () => {
11862
+ this._query = /** @type {HTMLInputElement} */
11863
+ input.value;
11864
+ this.emit("query", { query: this._query.trim() });
11865
+ this._refresh();
11866
+ });
11867
+ this.listen(input, "keydown", (event) => this._onKeydown(
11868
+ /** @type {KeyboardEvent} */
11869
+ event
11870
+ ));
11871
+ this.listen(close, "click", () => this.close());
11872
+ this.listen(results, "click", (event) => {
11873
+ const option = (
11874
+ /** @type {Element|null} */
11875
+ event.target?.closest?.("[data-launcher-index]")
11876
+ );
11877
+ if (!option || !results.contains(option)) return;
11878
+ this._activate(Number(option.getAttribute("data-launcher-index")), event);
11879
+ });
11880
+ this.listen(results, "pointermove", (event) => {
11881
+ const option = (
11882
+ /** @type {Element|null} */
11883
+ event.target?.closest?.("[data-launcher-index]")
11884
+ );
11885
+ if (!option || !results.contains(option)) return;
11886
+ const index = Number(option.getAttribute("data-launcher-index"));
11887
+ if (!Number.isInteger(index) || this._results[index]?.disabled || index === this._active) return;
11888
+ this._active = index;
11889
+ this._syncActive();
11890
+ });
11891
+ this.listen(dialog, "cancel", (event) => {
11892
+ event.preventDefault();
11893
+ this.close();
11894
+ });
11895
+ this.listen(dialog, "close", () => {
11896
+ dialog.dataset.state = "closed";
11897
+ input.setAttribute("aria-expanded", "false");
11898
+ this._abortRequest();
11899
+ this.emit("close");
11900
+ });
11901
+ this.listen(document, "keydown", (event) => this._onDocumentKeydown(
11902
+ /** @type {KeyboardEvent} */
11903
+ event
11904
+ ));
11905
+ this._renderResults();
11906
+ return dialog;
11907
+ }
11908
+ /** Opens the launcher and focuses/selects its query. @returns {this} @fires Launcher#open */
11909
+ open() {
11910
+ if (this.isOpen() || this._destroyed) return this;
11911
+ this.el.showModal();
11912
+ this.el.dataset.state = "open";
11913
+ this.refs.input.setAttribute("aria-expanded", "true");
11914
+ this._refresh();
11915
+ queueMicrotask(() => {
11916
+ if (!this.isOpen()) return;
11917
+ this.refs.input.focus();
11918
+ this.refs.input.select();
11919
+ });
11920
+ this.emit("open");
11921
+ return this;
11922
+ }
11923
+ /** Closes the launcher. @returns {this} */
11924
+ close() {
11925
+ if (this.isOpen()) this.el.close();
11926
+ return this;
11927
+ }
11928
+ /** Toggles the launcher. @returns {this} */
11929
+ toggle() {
11930
+ return this.isOpen() ? this.close() : this.open();
11931
+ }
11932
+ /** Reports whether the launcher is open. @returns {boolean} */
11933
+ isOpen() {
11934
+ return Boolean(this.el.open);
11935
+ }
11936
+ /** Focuses and selects the query field. @returns {this} */
11937
+ focus() {
11938
+ this.refs.input.focus();
11939
+ this.refs.input.select();
11940
+ return this;
11941
+ }
11942
+ /** Replaces the query and refreshes local and remote results. @param {string} query @returns {this} */
11943
+ setQuery(query) {
11944
+ this._query = String(query ?? "");
11945
+ this.refs.input.value = this._query;
11946
+ this.emit("query", { query: this._query.trim() });
11947
+ this._refresh();
11948
+ return this;
11949
+ }
11950
+ /** Returns the untrimmed query. @returns {string} */
11951
+ getQuery() {
11952
+ return this._query;
11953
+ }
11954
+ /** Replaces local catalogue items. @param {LauncherItem[]} items @returns {this} */
11955
+ setItems(items) {
11956
+ this._items = normalizeItems(items);
11957
+ this._renderResults();
11958
+ return this;
11959
+ }
11960
+ /** Marks one local item as the current destination. @param {string|number|null} id Item id. @returns {this} */
11961
+ setCurrent(id) {
11962
+ const key = id == null ? null : String(id);
11963
+ this._items = this._items.map((item) => ({ ...item, current: key !== null && String(item.id) === key }));
11964
+ this._renderResults();
11965
+ return this;
11966
+ }
11967
+ /** Replaces asynchronous sources and aborts work from the old set. @param {LauncherSource[]} sources @returns {this} */
11968
+ setSources(sources) {
11969
+ this._abortRequest();
11970
+ this._sources = normalizeSources(sources);
11971
+ this._sourceItems.clear();
11972
+ this._refresh();
11973
+ return this;
11974
+ }
11975
+ /** Aborts sources and removes the owned dialog. @returns {void} */
11976
+ destroy() {
11977
+ if (this._destroyed) return;
11978
+ this._destroyed = true;
11979
+ const original = this._original;
11980
+ this._abortRequest();
11981
+ if (this.isOpen()) this.el.close();
11982
+ super.destroy();
11983
+ if (!this._createdRoot && original) {
11984
+ for (const attribute of Array.from(this.el.attributes)) this.el.removeAttribute(attribute.name);
11985
+ for (const [name, value] of original.attributes) this.el.setAttribute(name, value);
11986
+ this.el.replaceChildren(...original.children);
11987
+ }
11988
+ }
11989
+ /** @returns {void} */
11990
+ _refresh() {
11991
+ this._sourceItems.clear();
11992
+ this._renderResults();
11993
+ this._abortRequest();
11994
+ if (this._sources.length === 0) return;
11995
+ const delay = Math.max(0, Number(this.options.debounce) || 0);
11996
+ this._queryTimer = setTimeout(() => this._loadSources(), delay);
11997
+ }
11998
+ /** @returns {Promise<void>} */
11999
+ async _loadSources() {
12000
+ this._queryTimer = null;
12001
+ const query = this._query.trim();
12002
+ const sources = this._sources.filter((source) => visibleForQuery(source.when, query) && query.length >= Math.max(Number(this.options.minQuery) || 0, Number(source.minQuery) || 0));
12003
+ if (sources.length === 0) return;
12004
+ const request = new AbortController();
12005
+ const sequence = ++this._requestSequence;
12006
+ this._request = request;
12007
+ this._setBusy(true);
12008
+ await Promise.all(sources.map(async (source) => {
12009
+ try {
12010
+ const loaded = await source.load(query, { signal: request.signal });
12011
+ if (request.signal.aborted || sequence !== this._requestSequence) return;
12012
+ this._sourceItems.set(source.id, normalizeItems(loaded).map((item, itemOrder) => ({
12013
+ ...item,
12014
+ _source: source.id,
12015
+ _sourceOrder: source.order === "source",
12016
+ group: item.group ?? source.label ?? "",
12017
+ itemOrder: item.itemOrder ?? (source.order === "source" ? itemOrder : void 0)
12018
+ })));
12019
+ this._renderResults();
12020
+ } catch (error) {
12021
+ if (request.signal.aborted || sequence !== this._requestSequence) return;
12022
+ this.emit("error", { source, error });
12023
+ }
12024
+ }));
12025
+ if (request.signal.aborted || sequence !== this._requestSequence) return;
12026
+ this._request = null;
12027
+ this._setBusy(false);
12028
+ }
12029
+ /** @param {boolean} busy @returns {void} */
12030
+ _setBusy(busy) {
12031
+ this._busy = busy;
12032
+ this.el.setAttribute("aria-busy", String(busy));
12033
+ this._renderStatus();
12034
+ }
12035
+ /** @returns {void} */
12036
+ _abortRequest() {
12037
+ if (this._queryTimer !== null) clearTimeout(this._queryTimer);
12038
+ this._queryTimer = null;
12039
+ this._request?.abort();
12040
+ this._request = null;
12041
+ this._requestSequence += 1;
12042
+ if (this._busy) this._setBusy(false);
12043
+ }
12044
+ /** @returns {void} */
12045
+ _renderResults() {
12046
+ const queryKey = this._query;
12047
+ const activeId = this._renderedQuery === queryKey && this._active >= 0 && this._results[this._active] ? String(this._results[this._active].id) : null;
12048
+ const query = this._query.trim();
12049
+ const local = this._items.filter((item) => visibleForQuery(item.when, query));
12050
+ const rankedLocal = rankLauncherItems(local, query, this.options.maxResults);
12051
+ const remote = this._sources.flatMap((source) => {
12052
+ const loaded = this._sourceItems.get(String(source.id)) ?? [];
12053
+ return source.order === "source" ? loaded.filter((item) => visibleForQuery(item.when, query)) : rankLauncherItems(
12054
+ loaded.filter((item) => visibleForQuery(item.when, query)),
12055
+ query,
12056
+ this.options.maxResults
12057
+ );
12058
+ });
12059
+ const seen = /* @__PURE__ */ new Set();
12060
+ const combined = [...rankedLocal.map((item) => ({ ...item, _source: null })), ...remote].filter((item) => {
12061
+ const key = String(item.id);
12062
+ if (seen.has(key)) return false;
12063
+ seen.add(key);
12064
+ return true;
12065
+ }).slice(0, Math.max(0, Number(this.options.maxResults) || 0));
12066
+ const groups = /* @__PURE__ */ new Map();
12067
+ for (const item of combined) {
12068
+ const group = String(item.group ?? "");
12069
+ if (!groups.has(group)) groups.set(group, []);
12070
+ groups.get(group).push(item);
12071
+ }
12072
+ this._results = [...groups.values()].flat();
12073
+ this._renderedQuery = queryKey;
12074
+ const preserved = activeId === null ? -1 : this._results.findIndex((item) => !item.disabled && String(item.id) === activeId);
12075
+ this._active = preserved >= 0 ? preserved : this._results.findIndex((item) => !item.disabled);
12076
+ const nodes = [];
12077
+ let index = 0;
12078
+ for (const [group, items] of groups) {
12079
+ const options = items.map((item) => this._option(item, index++));
12080
+ if (group) nodes.push(h("div", {
12081
+ class: "zx-launcher__group",
12082
+ role: "group",
12083
+ ariaLabel: group,
12084
+ dataset: { layout: items.every((item) => item.kind === "application") ? "grid" : "list" }
12085
+ }, h("div", { class: "zx-launcher__group-label", ariaHidden: "true" }, group), options));
12086
+ else nodes.push(...options);
12087
+ }
12088
+ this.refs.results.replaceChildren(...nodes);
12089
+ this._syncActive();
12090
+ this._renderStatus();
12091
+ }
12092
+ /** @param {RankedLauncherItem} item @param {number} index @returns {HTMLElement} */
12093
+ _option(item, index) {
12094
+ const href = safeHref(item.href);
12095
+ const tag = href ? "a" : "button";
12096
+ const children = [];
12097
+ const visual = launcherItemIcon(item);
12098
+ if (visual) children.push(h("span", {
12099
+ class: "zx-launcher__icon",
12100
+ ariaHidden: "true"
12101
+ }, visual));
12102
+ children.push(h(
12103
+ "span",
12104
+ { class: "zx-launcher__copy" },
12105
+ h("span", { class: "zx-launcher__label" }, item.label),
12106
+ item.description ? h("span", { class: "zx-launcher__description" }, item.description) : null
12107
+ ));
12108
+ if (item.badge != null) children.push(h("span", { class: "zx-launcher__badge" }, String(item.badge)));
12109
+ return (
12110
+ /** @type {HTMLElement} */
12111
+ h(tag, {
12112
+ class: "zx-launcher__option",
12113
+ type: tag === "button" ? "button" : null,
12114
+ href,
12115
+ target: item.target ?? null,
12116
+ rel: tag === "a" ? "noopener" : null,
12117
+ role: "option",
12118
+ id: uid("zx-launcher-option"),
12119
+ tabIndex: -1,
12120
+ ariaSelected: "false",
12121
+ ariaDisabled: item.disabled ? "true" : null,
12122
+ ariaCurrent: item.current ? "page" : null,
12123
+ dataset: {
12124
+ launcherIndex: index,
12125
+ pinned: item.pinned !== void 0 && item.pinned !== false ? "true" : null,
12126
+ kind: normalizeLauncherKind(item.kind)
12127
+ }
12128
+ }, children)
12129
+ );
12130
+ }
12131
+ /** @returns {void} */
12132
+ _renderStatus() {
12133
+ if (this._busy) this.refs.status.textContent = String(this.options.loadingText);
12134
+ else if (this._results.length === 0) this.refs.status.textContent = String(this.options.emptyText);
12135
+ else this.refs.status.textContent = "";
12136
+ this.refs.status.hidden = !this.refs.status.textContent;
12137
+ }
12138
+ /** @returns {void} */
12139
+ _syncActive() {
12140
+ const options = [...this.refs.results.querySelectorAll("[data-launcher-index]")];
12141
+ for (const option of options) {
12142
+ const selected = Number(option.getAttribute("data-launcher-index")) === this._active;
12143
+ option.setAttribute("aria-selected", String(selected));
12144
+ }
12145
+ const active = options.find((option) => Number(option.getAttribute("data-launcher-index")) === this._active);
12146
+ if (active) this.refs.input.setAttribute("aria-activedescendant", active.id);
12147
+ else this.refs.input.removeAttribute("aria-activedescendant");
12148
+ }
12149
+ /** @param {KeyboardEvent} event @returns {void} */
12150
+ _onKeydown(event) {
12151
+ if (event.key === "Escape") {
12152
+ this.close();
12153
+ return;
12154
+ }
12155
+ if (event.key === "Enter") {
12156
+ if (this._active < 0) return;
12157
+ event.preventDefault();
12158
+ const option = this.refs.results.querySelector(`[data-launcher-index="${this._active}"]`);
12159
+ option?.click();
12160
+ return;
12161
+ }
12162
+ const enabled = this._results.map((item, index) => item.disabled ? -1 : index).filter((index) => index >= 0);
12163
+ if (enabled.length === 0) return;
12164
+ if (/^Arrow(?:Left|Right|Up|Down)$/.test(event.key)) {
12165
+ const next = this._spatialIndex(event.key, enabled);
12166
+ if (next !== null) {
12167
+ event.preventDefault();
12168
+ this._active = next;
12169
+ this._syncActive();
12170
+ this.refs.results.querySelector(`[data-launcher-index="${this._active}"]`)?.scrollIntoView({ block: "nearest", inline: "nearest" });
12171
+ return;
12172
+ }
12173
+ if (event.key === "ArrowLeft" || event.key === "ArrowRight") return;
12174
+ }
12175
+ let position2 = enabled.indexOf(this._active);
12176
+ if (event.key === "Home" || event.key === "PageUp") position2 = 0;
12177
+ else if (event.key === "End" || event.key === "PageDown") position2 = enabled.length - 1;
12178
+ else if (event.key === "ArrowDown") position2 = (position2 + 1 + enabled.length) % enabled.length;
12179
+ else if (event.key === "ArrowUp") position2 = (position2 - 1 + enabled.length) % enabled.length;
12180
+ else return;
12181
+ event.preventDefault();
12182
+ this._active = enabled[position2];
12183
+ this._syncActive();
12184
+ this.refs.results.querySelector(`[data-launcher-index="${this._active}"]`)?.scrollIntoView({ block: "nearest" });
12185
+ }
12186
+ /** @param {string} direction @param {number[]} enabled @returns {number|null} */
12187
+ _spatialIndex(direction, enabled) {
12188
+ const current = this.refs.results.querySelector(`[data-launcher-index="${this._active}"]`);
12189
+ if (!(current instanceof HTMLElement)) return null;
12190
+ if ((direction === "ArrowLeft" || direction === "ArrowRight") && this._results[this._active]?.kind !== "application") return null;
12191
+ const candidates = [...this.refs.results.querySelectorAll("[data-launcher-index]")].filter((option) => enabled.includes(Number(option.getAttribute("data-launcher-index"))) && (direction !== "ArrowLeft" && direction !== "ArrowRight" || option.getAttribute("data-kind") === "application"));
12192
+ const next = findSpatialLauncherOption(candidates, current, direction);
12193
+ return next ? Number(next.getAttribute("data-launcher-index")) : null;
12194
+ }
12195
+ /** @param {number} index @param {Event} event @returns {void} */
12196
+ _activate(index, event) {
12197
+ const item = this._results[index];
12198
+ if (!item || item.disabled) {
12199
+ event.preventDefault();
12200
+ return;
12201
+ }
12202
+ const selected = this.emit("select", {
12203
+ item,
12204
+ value: item.value ?? item.id,
12205
+ source: item._source ?? null,
12206
+ query: this._query.trim()
12207
+ });
12208
+ if (selected.defaultPrevented) {
12209
+ event.preventDefault();
12210
+ return;
12211
+ }
12212
+ if (!item.href) {
12213
+ event.preventDefault();
12214
+ item.invoke?.();
12215
+ }
12216
+ this.close();
12217
+ }
12218
+ /** @param {KeyboardEvent} event @returns {void} */
12219
+ _onDocumentKeydown(event) {
12220
+ if (this.options.shortcut !== "mod+k" || event.isComposing || event.key.toLowerCase() !== "k" || Number(event.metaKey) + Number(event.ctrlKey) !== 1 || event.altKey || event.shiftKey) return;
12221
+ const active = document.activeElement;
12222
+ if (active instanceof HTMLInputElement || active instanceof HTMLTextAreaElement || active instanceof HTMLSelectElement || active instanceof HTMLElement && active.isContentEditable) return;
12223
+ const modal = document.querySelector("dialog:modal");
12224
+ if (modal && modal !== this.el) return;
12225
+ event.preventDefault();
12226
+ this.open();
12227
+ }
12228
+ };
12229
+ function launcherShortcutLabel() {
12230
+ const platform = String(
12231
+ /** @type {any} */
12232
+ navigator.userAgentData?.platform ?? navigator.platform ?? ""
12233
+ );
12234
+ return /mac|iphone|ipad/i.test(platform) ? "\u2318 K" : "Ctrl K";
12235
+ }
12236
+ function launcherHints(hints) {
12237
+ if (hints === false) return null;
12238
+ const labels = hints && typeof hints === "object" ? hints : {};
12239
+ return h(
12240
+ "footer",
12241
+ { class: "zx-launcher__hints", ariaHidden: "true" },
12242
+ h("span", {}, h("kbd", {}, "\u2191\u2193\u2190\u2192"), labels.move ?? "Move"),
12243
+ h("span", {}, h("kbd", {}, "\u21B5"), labels.open ?? "Open"),
12244
+ h("span", {}, h("kbd", {}, "Esc"), labels.close ?? "Close")
12245
+ );
12246
+ }
12247
+ function launcherItemIcon(item) {
12248
+ let visual = item.icon;
12249
+ if (typeof visual === "function") visual = visual(item);
12250
+ if (typeof visual === "string") return icon(visual);
12251
+ if (visual instanceof Node) return visual.cloneNode(true);
12252
+ if (visual && typeof visual === "object" && typeof /** @type {any} */
12253
+ visual.toElement === "function") {
12254
+ const element = (
12255
+ /** @type {any} */
12256
+ visual.toElement()
12257
+ );
12258
+ return element instanceof Node ? element.cloneNode(true) : null;
12259
+ }
12260
+ return null;
12261
+ }
12262
+ function normalizeLauncherKind(kind) {
12263
+ return kind === "application" || kind === "record" ? kind : "action";
12264
+ }
12265
+ function visibleForQuery(when, query) {
12266
+ if (when === "empty") return query === "";
12267
+ if (when === "query") return query !== "";
12268
+ return true;
12269
+ }
12270
+ function findSpatialLauncherOption(options, current, direction) {
12271
+ const origin = current.getBoundingClientRect();
12272
+ if (origin.width === 0 && origin.height === 0) return null;
12273
+ const originX = origin.left + origin.width / 2;
12274
+ const originY = origin.top + origin.height / 2;
12275
+ let best = null;
12276
+ let bestScore = Infinity;
12277
+ for (const option of options) {
12278
+ if (option === current) continue;
12279
+ const rect = option.getBoundingClientRect();
12280
+ const dx = rect.left + rect.width / 2 - originX;
12281
+ const dy = rect.top + rect.height / 2 - originY;
12282
+ let valid = false;
12283
+ let score = Infinity;
12284
+ if (direction === "ArrowLeft") {
12285
+ valid = dx < -2 && Math.abs(dy) < Math.max(origin.height, rect.height) / 2;
12286
+ score = Math.abs(dx) + Math.abs(dy) * 4;
12287
+ } else if (direction === "ArrowRight") {
12288
+ valid = dx > 2 && Math.abs(dy) < Math.max(origin.height, rect.height) / 2;
12289
+ score = Math.abs(dx) + Math.abs(dy) * 4;
12290
+ } else if (direction === "ArrowUp") {
12291
+ valid = dy < -2;
12292
+ score = Math.abs(dy) * 10 + Math.abs(dx);
12293
+ } else if (direction === "ArrowDown") {
12294
+ valid = dy > 2;
12295
+ score = Math.abs(dy) * 10 + Math.abs(dx);
12296
+ }
12297
+ if (valid && score < bestScore) {
12298
+ best = option;
12299
+ bestScore = score;
12300
+ }
12301
+ }
12302
+ return best;
12303
+ }
12304
+ function rankLauncherItems(items, query, limit = 100) {
12305
+ const needle = searchable(query).trim();
12306
+ const ranked = items.map((item, order) => ({ item, order, score: launcherScore(item, needle) })).filter(({ score }) => score < Infinity);
12307
+ ranked.sort((a, b) => {
12308
+ const aGroup = finiteOrder(a.item.groupOrder);
12309
+ const bGroup = finiteOrder(b.item.groupOrder);
12310
+ if (aGroup !== bGroup) return aGroup - bGroup;
12311
+ if (needle === "") {
12312
+ const aPinned = pinOrder(a.item.pinned, a.order);
12313
+ const bPinned = pinOrder(b.item.pinned, b.order);
12314
+ if (aPinned !== bPinned) return aPinned - bPinned;
12315
+ }
12316
+ return a.score - b.score || finiteOrder(a.item.itemOrder) - finiteOrder(b.item.itemOrder) || compareLauncherText(a.item.label, b.item.label) || a.order - b.order;
12317
+ });
12318
+ const maximum = Math.max(0, Number(limit) || 0);
12319
+ return ranked.slice(0, maximum || 0).map(({ item, score }) => ({ ...item, _score: score }));
12320
+ }
12321
+ function launcherScore(item, needle) {
12322
+ if (!needle) return item.pinned ? -1 : 0;
12323
+ const label = searchable(item.label);
12324
+ const identifier = searchable(item.id);
12325
+ const keywords = searchable(Array.isArray(item.keywords) ? item.keywords.join(" ") : item.keywords ?? "");
12326
+ if (label === needle) return 0;
12327
+ if (label.startsWith(needle)) return 10;
12328
+ if (label.split(/[^\p{L}\p{N}]+/u).some((word) => word.startsWith(needle))) return 20;
12329
+ if (label.includes(needle) || keywords.includes(needle)) return 30;
12330
+ if (identifier.includes(needle)) return 40;
12331
+ const acronym = label.split(/[^\p{L}\p{N}]+/u).filter(Boolean).map((word) => word[0]).join("");
12332
+ if (acronym.startsWith(needle)) return 50;
12333
+ return Infinity;
12334
+ }
12335
+ function pinOrder(pinned, order) {
12336
+ if (typeof pinned === "number" && Number.isFinite(pinned)) return pinned;
12337
+ if (pinned) return order;
12338
+ return Number.MAX_SAFE_INTEGER;
12339
+ }
12340
+ function finiteOrder(value) {
12341
+ return typeof value === "number" && Number.isFinite(value) ? value : Number.MAX_SAFE_INTEGER;
12342
+ }
12343
+ function searchable(value) {
12344
+ return String(value ?? "").normalize("NFKD").replace(/\p{M}/gu, "").toLowerCase();
12345
+ }
12346
+ function compareLauncherText(left, right) {
12347
+ const a = searchable(left);
12348
+ const b = searchable(right);
12349
+ if (a < b) return -1;
12350
+ if (a > b) return 1;
12351
+ const rawA = String(left ?? "").normalize("NFC");
12352
+ const rawB = String(right ?? "").normalize("NFC");
12353
+ return rawA < rawB ? -1 : rawA > rawB ? 1 : 0;
12354
+ }
12355
+ function normalizeItems(items) {
12356
+ if (!Array.isArray(items)) return [];
12357
+ return items.filter((item) => item && typeof item === "object" && item.id != null && item.label != null).map((item) => normalizeLauncherItem(item));
12358
+ }
12359
+ function normalizeLauncherItem(item) {
12360
+ const normalized = { ...item, label: String(item.label) };
12361
+ if (item.href == null) return normalized;
12362
+ const href = safeHref(item.href);
12363
+ if (href !== null) normalized.href = href;
12364
+ else {
12365
+ delete normalized.href;
12366
+ if (typeof normalized.invoke !== "function") normalized.disabled = true;
12367
+ }
12368
+ return normalized;
12369
+ }
12370
+ function normalizeSources(sources) {
12371
+ if (!Array.isArray(sources)) return [];
12372
+ return sources.filter((source) => source && typeof source === "object" && source.id != null && typeof source.load === "function").map((source) => ({ ...source, id: String(source.id) }));
12373
+ }
12374
+
10887
12375
  // src/components/message/message.js
10888
12376
  var MessageQueue = class {
10889
12377
  #limit;
@@ -11993,6 +13481,7 @@ var zeyosModules = Object.freeze({
11993
13481
  weblets: { label: "Weblets", icon: "zeyos-weblets", color: "#4f46e5", fa: "window-restore" }
11994
13482
  });
11995
13483
  var moduleAliases = Object.freeze({
13484
+ main: "zeyos",
11996
13485
  addresses: "contacts",
11997
13486
  clocking: "actionsteps",
11998
13487
  comments: "notes",
@@ -12099,26 +13588,338 @@ function moduleIcon(name, options = {}) {
12099
13588
  return icon(standard ? `fa:${info.fa}` : `kit:${info.icon}`, rest);
12100
13589
  }
12101
13590
  function moduleChip(name, options = {}) {
12102
- const { size = 24, iconSize = "1em", label = null, title = false, standard = false } = options;
13591
+ const {
13592
+ size = 24,
13593
+ iconSize = "52%",
13594
+ label = null,
13595
+ title = false,
13596
+ standard = false,
13597
+ color = null
13598
+ } = options;
12103
13599
  const key = normalizeModuleName(name);
12104
13600
  const info = moduleInfo(name);
12105
- const chip = h("span", {
12106
- class: ["zx-module-icon", options.class].flat().filter(Boolean),
12107
- dataset: { module: key },
12108
- style: {
12109
- "--zx-module-color": info.color,
12110
- "--zx-module-glyph": moduleGlyphColor(info.color),
12111
- "--zx-module-icon-size": typeof size === "number" ? `${size}px` : String(size)
12112
- },
12113
- title: title ? info.label : null
12114
- }, moduleIcon(name, { size: iconSize, standard }));
12115
- if (label === null) {
12116
- chip.setAttribute("aria-hidden", "true");
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;
13618
+ return chip;
13619
+ }
13620
+ function zeyosAppIcon(name, options = {}) {
13621
+ return moduleChip(name, options);
13622
+ }
13623
+
13624
+ // src/zeyos/launcher.js
13625
+ var ZEYOS_LAUNCHER_APPLICATIONS = Object.freeze([
13626
+ "accounts",
13627
+ "actionsteps",
13628
+ "admin",
13629
+ "auth",
13630
+ "billing",
13631
+ "calendar",
13632
+ "campaigns",
13633
+ "collection",
13634
+ "contacts",
13635
+ "contracts",
13636
+ "control",
13637
+ "coupons",
13638
+ "dev",
13639
+ "documents",
13640
+ "inventory",
13641
+ "mailinglists",
13642
+ "main",
13643
+ "messages",
13644
+ "links",
13645
+ "notes",
13646
+ "objects",
13647
+ "opportunities",
13648
+ "payments",
13649
+ "pricelists",
13650
+ "procurement",
13651
+ "production",
13652
+ "projects",
13653
+ "tasks",
13654
+ "tickets"
13655
+ ]);
13656
+ function buildZeyosLauncherConfig(state = {}, options = {}) {
13657
+ const {
13658
+ standardIcons = false,
13659
+ items: extraItems = [],
13660
+ sources: extraSources = [],
13661
+ ...launcherOptions
13662
+ } = options;
13663
+ const labels = launcherLabels(state.labels);
13664
+ const iconOptions = {
13665
+ standardIcons,
13666
+ renderApplicationIcon: state.renderApplicationIcon,
13667
+ renderWebletIcon: state.renderWebletIcon
13668
+ };
13669
+ const applications = normalizeZeyosLauncherApplications(state, iconOptions);
13670
+ const recent = typeof state.loadRecent === "function" ? [] : normalizeZeyosLauncherRecords(state.recent ?? [], {
13671
+ query: "",
13672
+ labels,
13673
+ resolveRecord: state.resolveRecord,
13674
+ renderRecordIcon: state.renderRecordIcon,
13675
+ standardIcons
13676
+ });
13677
+ const sources = [];
13678
+ if (typeof state.loadRecent === "function") {
13679
+ sources.push({
13680
+ id: "zeyos-recent",
13681
+ minQuery: 0,
13682
+ when: "empty",
13683
+ order: "source",
13684
+ load: async (_query, context) => normalizeZeyosLauncherRecords(
13685
+ await state.loadRecent(context),
13686
+ {
13687
+ query: "",
13688
+ labels,
13689
+ resolveRecord: state.resolveRecord,
13690
+ renderRecordIcon: state.renderRecordIcon,
13691
+ standardIcons
13692
+ }
13693
+ )
13694
+ });
13695
+ }
13696
+ if (typeof state.search === "function") {
13697
+ sources.push({
13698
+ id: "zeyos-records",
13699
+ minQuery: 1,
13700
+ when: "query",
13701
+ order: "source",
13702
+ load: async (query, context) => normalizeZeyosLauncherRecords(
13703
+ await state.search(query, context),
13704
+ {
13705
+ query,
13706
+ labels,
13707
+ resolveRecord: state.resolveRecord,
13708
+ renderRecordIcon: state.renderRecordIcon,
13709
+ standardIcons
13710
+ }
13711
+ )
13712
+ });
13713
+ }
13714
+ sources.push(...extraSources);
13715
+ return {
13716
+ ...launcherOptions,
13717
+ items: [...applications, ...recent, ...extraItems],
13718
+ sources,
13719
+ hints: options.hints ?? {
13720
+ move: labels.move,
13721
+ open: labels.open,
13722
+ close: labels.close
13723
+ }
13724
+ };
13725
+ }
13726
+ function zeyosLauncher(state = {}, options = {}) {
13727
+ return new Launcher(null, buildZeyosLauncherConfig(state, options));
13728
+ }
13729
+ function normalizeZeyosLauncherApplications(state = {}, options = {}) {
13730
+ const labels = launcherLabels(state.labels);
13731
+ const pinned = Array.isArray(state.pinned) ? state.pinned.map(String) : [];
13732
+ const seen = /* @__PURE__ */ new Set();
13733
+ const items = [];
13734
+ const add = (raw2, fallbackId = null, fallbackLabel = null) => {
13735
+ const app = normalizeApplication(raw2, fallbackId, fallbackLabel);
13736
+ if (!app || seen.has(app.identifier)) return;
13737
+ if (app.type === "module" && typeof state.canUseModule === "function" && !state.canUseModule(app.module)) return;
13738
+ seen.add(app.identifier);
13739
+ const resolved = mergeDestination(app.raw, state.resolveApplication);
13740
+ const pin = pinned.indexOf(app.identifier);
13741
+ const icon2 = () => {
13742
+ const custom = typeof options.renderApplicationIcon === "function" ? options.renderApplicationIcon(app.raw, app) : app.type === "weblet" && typeof options.renderWebletIcon === "function" ? options.renderWebletIcon(app.raw, app) : null;
13743
+ return custom ?? moduleChip(app.module, {
13744
+ size: 36,
13745
+ standard: Boolean(options.standardIcons),
13746
+ color: app.color
13747
+ });
13748
+ };
13749
+ items.push({
13750
+ id: app.identifier,
13751
+ label: app.label,
13752
+ keywords: [app.identifier, app.module],
13753
+ group: labels.applications,
13754
+ groupOrder: 0,
13755
+ kind: "application",
13756
+ icon: icon2,
13757
+ pinned: pin >= 0 ? pin : void 0,
13758
+ current: app.identifier === state.activeIdentifier,
13759
+ disabled: !resolved.href && typeof resolved.invoke !== "function",
13760
+ ...resolved,
13761
+ _zeyos: app
13762
+ });
13763
+ };
13764
+ for (const raw2 of Array.isArray(state.menuApps) ? state.menuApps : []) add(raw2);
13765
+ if (Array.isArray(state.modules)) {
13766
+ for (const module of state.modules) {
13767
+ if (typeof module === "string") add([module, moduleInfo(module).label]);
13768
+ else add(module, module?.id ?? module?.module, module?.label);
13769
+ }
12117
13770
  } else {
12118
- chip.setAttribute("role", "img");
12119
- chip.setAttribute("aria-label", label);
13771
+ for (const [module, label] of Object.entries(state.modules ?? {})) add([module, label]);
13772
+ }
13773
+ return items;
13774
+ }
13775
+ function normalizeZeyosLauncherRecords(payload, options = {}) {
13776
+ const query = String(options.query ?? "");
13777
+ const labels = launcherLabels(options.labels);
13778
+ const records = flattenRecords(payload, query);
13779
+ const groupOrders = /* @__PURE__ */ new Map();
13780
+ return records.flatMap(({ raw: raw2, group }, itemOrder) => {
13781
+ const record = normalizeRecord(raw2, group, query);
13782
+ if (!record) return [];
13783
+ const groupName = query === "" ? labels.recent : record.group === "" ? labels.results : labels.entities[record.group] ?? record.group;
13784
+ if (!groupOrders.has(groupName)) groupOrders.set(groupName, groupOrders.size + 1);
13785
+ const resolved = mergeDestination(record.raw, options.resolveRecord);
13786
+ return [{
13787
+ id: `record:${record.entity}:${record.fork ?? ""}:${record.ID}`,
13788
+ label: record.name,
13789
+ description: record.sec,
13790
+ keywords: [record.entity, record.ID, record.fork ?? ""],
13791
+ group: groupName,
13792
+ groupOrder: groupOrders.get(groupName),
13793
+ itemOrder,
13794
+ kind: "record",
13795
+ when: query === "" ? "empty" : "query",
13796
+ icon: () => {
13797
+ const custom = typeof options.renderRecordIcon === "function" ? options.renderRecordIcon(record.raw, record) : null;
13798
+ return custom ?? moduleChip(record.entity, {
13799
+ size: 28,
13800
+ standard: Boolean(options.standardIcons)
13801
+ });
13802
+ },
13803
+ disabled: !resolved.href && typeof resolved.invoke !== "function",
13804
+ ...resolved,
13805
+ _zeyos: record
13806
+ }];
13807
+ });
13808
+ }
13809
+ function zeyosActiveIdentifier(state = {}) {
13810
+ if (state.weblet != null && state.weblet !== "") return `weblet#${state.weblet}`;
13811
+ if (state.fork != null && state.fork !== "") return `fork#${state.fork}`;
13812
+ return String(state.module ?? "");
13813
+ }
13814
+ function normalizeApplication(raw2, fallbackId, fallbackLabel) {
13815
+ if (Array.isArray(raw2)) {
13816
+ if (raw2.length < 2) return null;
13817
+ if (raw2.length === 2) {
13818
+ const module = String(raw2[0]);
13819
+ return { raw: raw2, identifier: module, label: String(raw2[1]), module, type: "module", color: null };
13820
+ }
13821
+ if (raw2.length === 4) {
13822
+ const module = String(raw2[2] || "default");
13823
+ return {
13824
+ raw: raw2,
13825
+ identifier: `fork#${raw2[0]}`,
13826
+ label: String(raw2[1]),
13827
+ module,
13828
+ type: "fork",
13829
+ color: normalizeServerColor(raw2[3])
13830
+ };
13831
+ }
13832
+ return {
13833
+ raw: raw2,
13834
+ identifier: `weblet#${raw2[0]}`,
13835
+ label: String(raw2[1]),
13836
+ module: "weblets",
13837
+ type: "weblet",
13838
+ color: normalizeServerColor(raw2[8])
13839
+ };
12120
13840
  }
12121
- return chip;
13841
+ if (!raw2 || typeof raw2 !== "object") return null;
13842
+ const identifier = String(raw2.identifier ?? raw2.id ?? fallbackId ?? "");
13843
+ if (!identifier) return null;
13844
+ const type = identifier.startsWith("fork#") ? "fork" : identifier.startsWith("weblet#") ? "weblet" : "module";
13845
+ return {
13846
+ raw: raw2,
13847
+ identifier,
13848
+ label: String(raw2.label ?? fallbackLabel ?? moduleInfo(identifier).label),
13849
+ module: String(raw2.module ?? (type === "module" ? identifier : type === "fork" ? "default" : "weblets")),
13850
+ type,
13851
+ color: normalizeServerColor(raw2.color)
13852
+ };
13853
+ }
13854
+ function flattenRecords(payload, query) {
13855
+ if (Array.isArray(payload)) return payload.map((raw2) => ({ raw: raw2, group: raw2?.group ?? "" }));
13856
+ if (!payload || typeof payload !== "object") return [];
13857
+ const result = [];
13858
+ for (const [group, rows] of Object.entries(payload)) {
13859
+ if (!Array.isArray(rows)) continue;
13860
+ for (const row of rows) {
13861
+ if (!Array.isArray(row)) {
13862
+ result.push({ raw: row, group });
13863
+ continue;
13864
+ }
13865
+ const raw2 = group === "" && row.length >= 5 ? { ID: row[0], fork: row[1], name: row[2], sec: row[3], entity: row[4], group } : { ID: row[0], fork: row[1], name: row[2], sec: row[3], entity: group, group };
13866
+ result.push({ raw: raw2, group });
13867
+ }
13868
+ }
13869
+ return result;
13870
+ }
13871
+ function normalizeRecord(raw2, group, query) {
13872
+ if (Array.isArray(raw2)) {
13873
+ if (query === "" && raw2.length >= 4) {
13874
+ return {
13875
+ raw: raw2,
13876
+ entity: String(raw2[0]),
13877
+ ID: raw2[1],
13878
+ fork: raw2[2] ?? null,
13879
+ name: String(raw2[3]),
13880
+ sec: String(raw2[4] ?? ""),
13881
+ group: ""
13882
+ };
13883
+ }
13884
+ return null;
13885
+ }
13886
+ if (!raw2 || typeof raw2 !== "object" || raw2.ID == null || raw2.name == null) return null;
13887
+ return {
13888
+ raw: raw2,
13889
+ entity: String(raw2.entity ?? group ?? "records"),
13890
+ ID: raw2.ID,
13891
+ fork: raw2.fork ?? null,
13892
+ name: String(raw2.name),
13893
+ sec: String(raw2.sec ?? ""),
13894
+ group: String(raw2.group ?? group ?? "")
13895
+ };
13896
+ }
13897
+ function mergeDestination(raw2, resolver) {
13898
+ const direct = raw2 && typeof raw2 === "object" && !Array.isArray(raw2) ? raw2 : {};
13899
+ const resolved = typeof resolver === "function" ? resolver(raw2) : null;
13900
+ const destination = resolved && typeof resolved === "object" ? resolved : {};
13901
+ return Object.fromEntries(["href", "target", "invoke", "value"].flatMap((key) => {
13902
+ const value = destination[key] ?? direct[key];
13903
+ return value === void 0 ? [] : [[key, value]];
13904
+ }));
13905
+ }
13906
+ function normalizeServerColor(value) {
13907
+ const source = String(value ?? "").trim();
13908
+ if (!source) return null;
13909
+ const color = source.startsWith("#") ? source : `#${source}`;
13910
+ return /^#[\da-f]{3}(?:[\da-f]{3})?$/i.test(color) ? color : null;
13911
+ }
13912
+ function launcherLabels(input) {
13913
+ const labels = input && typeof input === "object" ? input : {};
13914
+ return {
13915
+ applications: String(labels.applications ?? "Applications"),
13916
+ recent: String(labels.recent ?? "Recent"),
13917
+ results: String(labels.results ?? "Top results"),
13918
+ entities: labels.entities && typeof labels.entities === "object" ? labels.entities : {},
13919
+ move: String(labels.move ?? "Move"),
13920
+ open: String(labels.open ?? "Open"),
13921
+ close: String(labels.close ?? "Close")
13922
+ };
12122
13923
  }
12123
13924
 
12124
13925
  // src/zeyos/select.js
@@ -12162,6 +13963,92 @@ function zeyosSelect(client, resource, opts = {}) {
12162
13963
  select.ready = hydrateInitialValue(select, client, resource, requestedValue);
12163
13964
  return select;
12164
13965
  }
13966
+ function buildZeyosEntitySelectConfig(client, resource, opts = {}) {
13967
+ const {
13968
+ fields,
13969
+ labelKey = inferLabelKey(client, resource),
13970
+ valueKey = "ID",
13971
+ searchFields = inferSearchFields(client, resource),
13972
+ subtitleKey = null,
13973
+ moduleKey = () => resource,
13974
+ groupKey = null,
13975
+ none = true,
13976
+ recent = [],
13977
+ create = false,
13978
+ renderIcon = defaultEntityIcon,
13979
+ renderItem,
13980
+ renderValueAdornment,
13981
+ ...rest
13982
+ } = opts;
13983
+ const projection = fields ?? defaultProjection(valueKey, labelKey, [
13984
+ ...searchFields,
13985
+ ...[subtitleKey, moduleKey, groupKey].filter((reader) => typeof reader === "string")
13986
+ ]);
13987
+ const base = buildZeyosSelectConfig(client, resource, {
13988
+ ...rest,
13989
+ fields: projection,
13990
+ labelKey,
13991
+ valueKey,
13992
+ searchFields,
13993
+ groupKey,
13994
+ clearable: none === false ? Boolean(rest.clearable) : true
13995
+ });
13996
+ base.recent = Array.isArray(recent) ? recent : [];
13997
+ const renderModuleIcon = (item, selected = false) => {
13998
+ if (renderIcon === null) return null;
13999
+ const module = readRecord(item, moduleKey) ?? resource;
14000
+ return renderIcon(item, { resource, module: String(module), selected });
14001
+ };
14002
+ base.renderItem = renderItem ?? ((item) => {
14003
+ const visual = renderModuleIcon(item);
14004
+ return h(
14005
+ "span",
14006
+ { class: "zx-select__entity" },
14007
+ visual ? h("span", {
14008
+ class: "zx-select__entity-icon",
14009
+ ariaHidden: "true"
14010
+ }, visual) : null,
14011
+ h(
14012
+ "span",
14013
+ { class: "zx-select__entity-copy" },
14014
+ h("span", { class: "zx-select__entity-label" }, String(readRecord(item, labelKey) ?? "")),
14015
+ subtitleKey && readRecord(item, subtitleKey) != null ? h("span", { class: "zx-select__entity-subtitle" }, String(readRecord(item, subtitleKey))) : null
14016
+ )
14017
+ );
14018
+ });
14019
+ base.renderValueAdornment = renderValueAdornment ?? ((item) => renderModuleIcon(item, true));
14020
+ if (none !== false) {
14021
+ base.noneLabel = typeof none === "string" ? none : rest.noneLabel ?? "No selection";
14022
+ base.renderNone = rest.renderNone ?? ((label) => h(
14023
+ "span",
14024
+ { class: "zx-select__entity-none" },
14025
+ h("span", { class: "zx-select__entity-none-icon", ariaHidden: "true" }),
14026
+ h("span", {}, label)
14027
+ ));
14028
+ }
14029
+ if (create) {
14030
+ const descriptor = create === true ? {} : create;
14031
+ const invoke = typeof descriptor.oninvoke === "function" ? descriptor.oninvoke : null;
14032
+ base.actions = [...Array.isArray(base.actions) ? base.actions : [], {
14033
+ id: descriptor.id ?? "create",
14034
+ label: descriptor.label ?? "Create new\u2026",
14035
+ group: descriptor.group ?? "New",
14036
+ icon: descriptor.icon ?? "plus",
14037
+ invoke: (detail) => invoke?.({ ...detail, resource })
14038
+ }];
14039
+ }
14040
+ return base;
14041
+ }
14042
+ function zeyosEntitySelect(client, resource, opts = {}) {
14043
+ const config2 = buildZeyosEntitySelectConfig(client, resource, opts);
14044
+ const requestedValue = opts.value ?? null;
14045
+ const select = (
14046
+ /** @type {Select & {ready: Promise<Select>}} */
14047
+ Select.entity(null, config2)
14048
+ );
14049
+ select.ready = hydrateInitialValue(select, client, resource, requestedValue);
14050
+ return select;
14051
+ }
12165
14052
  function normalizeZeyosListResult(client, result) {
12166
14053
  if (typeof client?.normalizeListResult === "function") {
12167
14054
  return normalizeLocalListResult(client.normalizeListResult(result));
@@ -12178,12 +14065,12 @@ function normalizeLocalListResult(result) {
12178
14065
  async function hydrateInitialValue(select, client, resource, value) {
12179
14066
  if (value == null) return select;
12180
14067
  const record = value && typeof value === "object" ? value : await connect(client).get(resource, value);
12181
- const normalized = normalizeRecord(record);
14068
+ const normalized = normalizeRecord2(record);
12182
14069
  select.setItems([normalized]);
12183
14070
  select.set(normalized, { silent: true });
12184
14071
  return select;
12185
14072
  }
12186
- function normalizeRecord(result) {
14073
+ function normalizeRecord2(result) {
12187
14074
  if (result && typeof result === "object" && !Array.isArray(result)) {
12188
14075
  const nested = result.data;
12189
14076
  if (nested && typeof nested === "object" && !Array.isArray(nested)) return nested;
@@ -12191,6 +14078,14 @@ function normalizeRecord(result) {
12191
14078
  }
12192
14079
  throw new TypeError("ZeyOS get operation did not return a record");
12193
14080
  }
14081
+ function readRecord(item, reader) {
14082
+ if (typeof reader === "function") return reader(item);
14083
+ if (item && typeof item === "object" && typeof reader === "string") return item[reader];
14084
+ return null;
14085
+ }
14086
+ function defaultEntityIcon(_item, context) {
14087
+ return moduleChip(String(context.module ?? "default"), { size: 20 });
14088
+ }
12194
14089
  function defaultProjection(valueKey, labelKey, searchFields) {
12195
14090
  const keys = [valueKey, labelKey, ...searchFields].filter((key) => typeof key === "string" && key);
12196
14091
  return keys.length ? [...new Set(keys)] : void 0;
@@ -12270,7 +14165,7 @@ function zeyosForm(client, resource, opts = {}) {
12270
14165
  await Promise.all(config2.entityFields.map(async ({ id, resource: referencedResource2 }) => {
12271
14166
  const value = record[id];
12272
14167
  if (value == null) return;
12273
- const selected = value && typeof value === "object" ? value : normalizeRecord2(await facade.get(referencedResource2, value));
14168
+ const selected = value && typeof value === "object" ? value : normalizeRecord3(await facade.get(referencedResource2, value));
12274
14169
  const select = Select.from(form.getField(id)?.getInput());
12275
14170
  if (select instanceof Select) select.setItems([selected]);
12276
14171
  }));
@@ -12279,7 +14174,7 @@ function zeyosForm(client, resource, opts = {}) {
12279
14174
  };
12280
14175
  const load = async (id) => {
12281
14176
  if (id == null || id === "") throw new TypeError("zeyosForm.load() requires an id");
12282
- const record = normalizeRecord2(await facade.get(resource, id));
14177
+ const record = normalizeRecord3(await facade.get(resource, id));
12283
14178
  currentId = record.ID ?? id;
12284
14179
  await applyRecord(record);
12285
14180
  return record;
@@ -12298,7 +14193,7 @@ function zeyosForm(client, resource, opts = {}) {
12298
14193
  throw error;
12299
14194
  }
12300
14195
  const result = currentId == null ? await facade.create(resource, payload) : await facade.update(resource, { ID: currentId, ...payload });
12301
- const record = normalizeRecord2(result);
14196
+ const record = normalizeRecord3(result);
12302
14197
  currentId = record.ID ?? currentId;
12303
14198
  if (opts.onSaved) {
12304
14199
  try {
@@ -12376,7 +14271,7 @@ function normalizeColumns(value) {
12376
14271
  [1, 2, 3].includes(number) ? number : 2
12377
14272
  );
12378
14273
  }
12379
- function normalizeRecord2(result) {
14274
+ function normalizeRecord3(result) {
12380
14275
  if (result && typeof result === "object" && !Array.isArray(result)) {
12381
14276
  const nested = result.data;
12382
14277
  if (nested && typeof nested === "object" && !Array.isArray(nested)) return nested;
@@ -12571,7 +14466,11 @@ function copyFilters(filters) {
12571
14466
  }
12572
14467
  export {
12573
14468
  ZEYOS_ICON_KIT,
14469
+ ZEYOS_LAUNCHER_APPLICATIONS,
12574
14470
  buildListQuery,
14471
+ buildZeyosEntitySelectConfig,
14472
+ buildZeyosLauncherConfig,
14473
+ buildZeyosSelectConfig,
12575
14474
  connect,
12576
14475
  dataFilterStateToFilters,
12577
14476
  dateToUnixSeconds,
@@ -12586,13 +14485,19 @@ export {
12586
14485
  moduleInfo,
12587
14486
  moduleKeys,
12588
14487
  normalizeModuleName,
14488
+ normalizeZeyosLauncherApplications,
14489
+ normalizeZeyosLauncherRecords,
12589
14490
  registerFieldType,
12590
14491
  registerModules,
12591
14492
  resolveFields,
12592
14493
  tableSortToQuery,
12593
14494
  unixSecondsToDate,
12594
14495
  useZeyosIcons,
14496
+ zeyosActiveIdentifier,
14497
+ zeyosAppIcon,
14498
+ zeyosEntitySelect,
12595
14499
  zeyosForm,
14500
+ zeyosLauncher,
12596
14501
  zeyosModules,
12597
14502
  zeyosSelect,
12598
14503
  zeyosTable