@zeyos/zx 3.0.0 → 4.0.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 (88) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/DESIGN-SYSTEM.md +28 -68
  3. package/README.md +16 -37
  4. package/dist/types/components/app-sidebar/app-sidebar.d.ts +106 -32
  5. package/dist/types/components/card/card.d.ts +128 -0
  6. package/dist/types/components/chart/chart.d.ts +1 -0
  7. package/dist/types/components/launcher/launcher.d.ts +65 -6
  8. package/dist/types/components/select/select.d.ts +51 -0
  9. package/dist/types/components/table/table.d.ts +4 -3
  10. package/dist/types/core/dom.d.ts +8 -0
  11. package/dist/types/index.d.ts +2 -1
  12. package/dist/types/{components/app-rail → internal}/app-rail.d.ts +34 -19
  13. package/dist/types/zeyos/icons.d.ts +5 -0
  14. package/dist/types/zeyos/index.d.ts +2 -1
  15. package/dist/types/zeyos/launcher.d.ts +147 -0
  16. package/dist/types/zeyos/select.d.ts +39 -0
  17. package/dist/zx-zeyos.esm.js +1324 -46
  18. package/dist/zx-zeyos.esm.min.js +1 -1
  19. package/dist/zx.css +400 -12
  20. package/dist/zx.esm.js +1059 -189
  21. package/dist/zx.esm.min.js +3 -3
  22. package/dist/zx.global.js +1059 -189
  23. package/dist/zx.global.min.js +3 -3
  24. package/dist/zx.min.css +1 -1
  25. package/docs/api.json +258 -145
  26. package/docs/llms.md +93 -112
  27. package/docs/llms.txt +16 -15
  28. package/package.json +8 -17
  29. package/src/components/app-sidebar/app-sidebar.css +12 -0
  30. package/src/components/app-sidebar/app-sidebar.js +310 -110
  31. package/src/components/card/card.css +149 -0
  32. package/src/components/card/card.js +285 -0
  33. package/src/components/chart/chart.js +8 -3
  34. package/src/components/grid/grid.js +10 -3
  35. package/src/components/launcher/launcher.css +113 -3
  36. package/src/components/launcher/launcher.js +234 -26
  37. package/src/components/select/field-adapter.js +9 -4
  38. package/src/components/select/select.css +144 -10
  39. package/src/components/select/select.js +259 -27
  40. package/src/components/table/table.js +8 -7
  41. package/src/core/dom.js +20 -0
  42. package/src/index.js +2 -1
  43. package/src/{components/app-rail → internal}/app-rail.js +80 -30
  44. package/src/zeyos/icons.js +8 -3
  45. package/src/zeyos/index.js +7 -1
  46. package/src/zeyos/launcher.js +338 -0
  47. package/src/zeyos/modules.js +1 -0
  48. package/src/zeyos/select.js +115 -1
  49. package/styles/tokens/global.css +1 -1
  50. package/styles/tokens/modules.css +1 -0
  51. package/styles/zx.css +1 -0
  52. package/MIGRATION.md +0 -749
  53. package/dist/types/compat/base.d.ts +0 -156
  54. package/dist/types/compat/globals.d.ts +0 -20
  55. package/dist/types/compat/index.d.ts +0 -128
  56. package/dist/types/compat/map/basic.d.ts +0 -175
  57. package/dist/types/compat/map/data.d.ts +0 -103
  58. package/dist/types/compat/map/date-time.d.ts +0 -80
  59. package/dist/types/compat/map/factory.d.ts +0 -9
  60. package/dist/types/compat/map/form.d.ts +0 -98
  61. package/dist/types/compat/map/helpers.d.ts +0 -12
  62. package/dist/types/compat/map/http.d.ts +0 -36
  63. package/dist/types/compat/map/layout.d.ts +0 -106
  64. package/dist/types/compat/map/options.d.ts +0 -80
  65. package/dist/types/compat/map/select.d.ts +0 -95
  66. package/dist/types/compat/map/stubs.d.ts +0 -32
  67. package/dist/types/compat/map/utilities.d.ts +0 -37
  68. package/dist/types/compat-entry.d.ts +0 -5
  69. package/dist/zx-compat.esm.js +0 -17848
  70. package/dist/zx-compat.esm.min.js +0 -1
  71. package/dist/zx-compat.global.js +0 -29008
  72. package/dist/zx-compat.global.min.js +0 -3
  73. package/src/compat/base.js +0 -291
  74. package/src/compat/globals.js +0 -167
  75. package/src/compat/index.js +0 -84
  76. package/src/compat/map/basic.js +0 -464
  77. package/src/compat/map/data.js +0 -254
  78. package/src/compat/map/date-time.js +0 -177
  79. package/src/compat/map/factory.js +0 -45
  80. package/src/compat/map/form.js +0 -330
  81. package/src/compat/map/helpers.js +0 -46
  82. package/src/compat/map/http.js +0 -117
  83. package/src/compat/map/layout.js +0 -256
  84. package/src/compat/map/options.js +0 -267
  85. package/src/compat/map/select.js +0 -198
  86. package/src/compat/map/stubs.js +0 -26
  87. package/src/compat/map/utilities.js +0 -113
  88. package/src/compat-entry.js +0 -11
@@ -524,6 +524,18 @@ function cloneValue(value) {
524
524
 
525
525
  // src/core/dom.js
526
526
  var activeRefs = null;
527
+ function safeHref(value) {
528
+ if (typeof value !== "string") return null;
529
+ const href = value.trim();
530
+ if (!href) return null;
531
+ try {
532
+ const protocol = new URL(href, globalThis.location?.href ?? "https://zx.invalid/").protocol;
533
+ if (["javascript:", "data:", "vbscript:"].includes(protocol.toLowerCase())) return null;
534
+ } catch {
535
+ return null;
536
+ }
537
+ return href;
538
+ }
527
539
  function h(tag, props, ...children) {
528
540
  const element = document.createElement(tag);
529
541
  let properties = props;
@@ -6533,7 +6545,18 @@ var PRIORITY_KEYS = [
6533
6545
  "priority.high",
6534
6546
  "priority.highest"
6535
6547
  ];
6536
- var PRIORITY_FALLBACKS = ["Lowest", "Low", "Normal", "High", "Highest"];
6548
+ var PRIORITY_FALLBACKS = ["Lowest", "Low", "Medium", "High", "Highest"];
6549
+ var DEFAULT_STATUSES = Object.freeze([
6550
+ ["backlog", "status.backlog", "Backlog", "dotted", "muted"],
6551
+ ["todo", "status.todo", "Todo", "ring", "neutral"],
6552
+ ["in-progress", "status.inProgress", "In Progress", "progress", "warning"],
6553
+ ["in-review", "status.inReview", "In Review", "review", "info"],
6554
+ ["ready", "status.ready", "Ready to Merge", "ready", "accent"],
6555
+ ["waiting", "status.waiting", "Waiting for feedback", "waiting", "warning"],
6556
+ ["done", "status.done", "Done", "done", "info"],
6557
+ ["canceled", "status.canceled", "Canceled", "canceled", "muted"],
6558
+ ["duplicate", "status.duplicate", "Duplicate", "duplicate", "muted"]
6559
+ ]);
6537
6560
  var Select = class _Select extends Component {
6538
6561
  static cssName = "select";
6539
6562
  /** @type {SelectOptions} */
@@ -6544,6 +6567,10 @@ var Select = class _Select extends Component {
6544
6567
  labelKey: "name",
6545
6568
  renderItem: null,
6546
6569
  renderValue: null,
6570
+ renderValueAdornment: null,
6571
+ renderNone: null,
6572
+ noneLabel: null,
6573
+ actions: [],
6547
6574
  value: null,
6548
6575
  disabled: false,
6549
6576
  placeholder: "",
@@ -6561,6 +6588,7 @@ var Select = class _Select extends Component {
6561
6588
  this._visibleItems = [];
6562
6589
  this._fixedItems = [];
6563
6590
  this._visibleFixed = [];
6591
+ this._actions = [];
6564
6592
  this._selected = null;
6565
6593
  this._optionEntries = [];
6566
6594
  this._activeIndex = -1;
@@ -6599,6 +6627,12 @@ var Select = class _Select extends Component {
6599
6627
  placeholder: String(this.options.placeholder ?? ""),
6600
6628
  readOnly: !this.options.filter
6601
6629
  });
6630
+ const valueAdornment = h("span", {
6631
+ ref: "valueAdornment",
6632
+ class: "zx-select__value-adornment",
6633
+ ariaHidden: "true",
6634
+ hidden: true
6635
+ });
6602
6636
  const clear = h("button", {
6603
6637
  ref: "clear",
6604
6638
  class: "zx-icon-btn zx-select__clear",
@@ -6624,6 +6658,7 @@ var Select = class _Select extends Component {
6624
6658
  const control = h(
6625
6659
  "div",
6626
6660
  { ref: "control", class: "zx-select__control" },
6661
+ valueAdornment,
6627
6662
  input,
6628
6663
  clear,
6629
6664
  loading,
@@ -6646,9 +6681,10 @@ var Select = class _Select extends Component {
6646
6681
  this._visibleItems = this._items.slice();
6647
6682
  this._fixedItems = Array.isArray(this.options.fixedItems) ? this.options.fixedItems.slice() : [];
6648
6683
  this._visibleFixed = this._fixedItems.slice();
6684
+ this._actions = normalizeSelectActions(this.options.actions);
6649
6685
  this._disabled = Boolean(this.options.disabled);
6650
6686
  this._handleTypeahead = typeahead(
6651
- () => this._optionEntries.filter((entry) => !entry.clear).map((entry) => entry.element),
6687
+ () => this._optionEntries.filter((entry) => !entry.clear && !entry.action).map((entry) => entry.element),
6652
6688
  (_option, index) => {
6653
6689
  if (!this._open) this.open();
6654
6690
  this._setActive(this._itemEntryIndex(index));
@@ -6743,6 +6779,13 @@ var Select = class _Select extends Component {
6743
6779
  this._position?.update();
6744
6780
  return this;
6745
6781
  }
6782
+ /** Replaces command rows without changing the selected value. @param {SelectAction[]} actions @returns {this} */
6783
+ setActions(actions) {
6784
+ this._actions = normalizeSelectActions(actions);
6785
+ this._renderOptions();
6786
+ this._position?.update();
6787
+ return this;
6788
+ }
6746
6789
  /**
6747
6790
  * Restores the configured initial value.
6748
6791
  * @param {{silent?: boolean}} [options={}] Reset behavior.
@@ -6827,26 +6870,51 @@ var Select = class _Select extends Component {
6827
6870
  value: null,
6828
6871
  items: [],
6829
6872
  renderItem(item) {
6830
- return h(
6831
- "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))
6839
- );
6873
+ return priorityContent(item, true);
6874
+ },
6875
+ renderValueAdornment(item) {
6876
+ return priorityIndicator(item.level);
6840
6877
  }
6841
6878
  });
6842
- select.setItems(PRIORITY_KEYS.map((key, level) => ({
6843
- ID: level,
6844
- name: select._message(key, PRIORITY_FALLBACKS[level]),
6845
- level
6846
- })));
6879
+ select.setItems(PRIORITY_KEYS.map((key, level) => {
6880
+ let name = level === 2 ? select.msg("priority.medium") : key;
6881
+ if (name === "priority.medium") name = select._message(key, PRIORITY_FALLBACKS[level]);
6882
+ else if (level !== 2) name = select._message(key, PRIORITY_FALLBACKS[level]);
6883
+ return { ID: level, name, level };
6884
+ }));
6847
6885
  select.set(requestedValue, { silent: true });
6848
6886
  return select;
6849
6887
  }
6888
+ /**
6889
+ * Creates a status picker with shape- and color-distinct workflow indicators.
6890
+ * @param {Element|string|null} target Component target.
6891
+ * @param {SelectOptions & {items?: Array<Record<string, any>>}} [options={}] Select overrides and status items.
6892
+ * @returns {Select}
6893
+ */
6894
+ static status(target, options = {}) {
6895
+ const { items = null, value = null, ...rest } = options;
6896
+ const select = new _Select(target, {
6897
+ ...rest,
6898
+ value: null,
6899
+ items: [],
6900
+ renderItem(item) {
6901
+ return statusContent(item);
6902
+ },
6903
+ renderValueAdornment(item) {
6904
+ return statusIndicator(item);
6905
+ }
6906
+ });
6907
+ const statuses = Array.isArray(items) ? items.map(normalizeStatusItem).filter(Boolean) : DEFAULT_STATUSES.map(([ID, key, fallback, indicator, tone], index) => ({
6908
+ ID,
6909
+ name: select._message(key, fallback),
6910
+ indicator,
6911
+ tone,
6912
+ shortcut: String(index + 1)
6913
+ }));
6914
+ select.setItems(statuses);
6915
+ select.set(value, { silent: true });
6916
+ return select;
6917
+ }
6850
6918
  /**
6851
6919
  * Creates the record-permission picker: the fixed Private and Public choices above the groups a
6852
6920
  * record can be shared with. The value is the tri-state ZeyOS stores — `false` for private,
@@ -6859,12 +6927,21 @@ var Select = class _Select extends Component {
6859
6927
  * @returns {Select}
6860
6928
  */
6861
6929
  static permission(target, options = {}) {
6862
- const { groups = [], value = true, valueKey = "ID", labelKey = "name", ...rest } = options;
6930
+ const {
6931
+ groups = [],
6932
+ value = true,
6933
+ valueKey = "ID",
6934
+ labelKey = "name",
6935
+ filter = "local",
6936
+ ...rest
6937
+ } = options;
6863
6938
  const loader = typeof groups === "function" ? groups : null;
6864
6939
  const shareable = loader ? [] : groups;
6865
6940
  const choices = [{ ID: false, name: "Private" }, { ID: true, name: "Public" }];
6866
6941
  const select = new _Select(target, {
6867
- filter: loader ?? "local",
6942
+ // An async group loader is the data source and cannot be disconnected by an incidental
6943
+ // `filter` override. Array-backed groups still honor local/disabled filtering.
6944
+ filter: loader ?? filter,
6868
6945
  ...rest,
6869
6946
  value: null,
6870
6947
  items: shareable,
@@ -6900,6 +6977,18 @@ var Select = class _Select extends Component {
6900
6977
  /** @param {KeyboardEvent} event @returns {void} */
6901
6978
  _onKeydown(event) {
6902
6979
  if (this._disabled) return;
6980
+ if (this._open && isPrintable(event)) {
6981
+ const shortcut = this._optionEntries.findIndex((entry) => !entry.clear && !entry.action && String(
6982
+ /** @type {Record<string, any>|null} */
6983
+ entry.item?.shortcut ?? ""
6984
+ ) === event.key);
6985
+ if (shortcut >= 0) {
6986
+ event.preventDefault();
6987
+ this._setActive(shortcut);
6988
+ this._chooseEntry(this._optionEntries[shortcut]);
6989
+ return;
6990
+ }
6991
+ }
6903
6992
  if (event.key === "ArrowDown") {
6904
6993
  event.preventDefault();
6905
6994
  if (!this._open) this.open();
@@ -6947,6 +7036,7 @@ var Select = class _Select extends Component {
6947
7036
  /** @type {HTMLInputElement} */
6948
7037
  this.refs.input.value
6949
7038
  );
7039
+ this._syncValueAdornment();
6950
7040
  if (!this._open) this.open();
6951
7041
  if (this.options.filter === "local") {
6952
7042
  this._visibleFixed = this._matchFixed(query);
@@ -6970,6 +7060,22 @@ var Select = class _Select extends Component {
6970
7060
  /** @param {SelectOptionEntry|undefined} entry @returns {void} */
6971
7061
  _chooseEntry(entry) {
6972
7062
  if (!entry) return;
7063
+ if (entry.action) {
7064
+ const detail = {
7065
+ id: entry.action.id,
7066
+ action: entry.action,
7067
+ query: (
7068
+ /** @type {HTMLInputElement} */
7069
+ this.refs.input.value
7070
+ ),
7071
+ select: this
7072
+ };
7073
+ const action = this.emit("action", detail);
7074
+ if (!action.defaultPrevented) entry.action.invoke?.(detail);
7075
+ this.close();
7076
+ this.focus();
7077
+ return;
7078
+ }
6973
7079
  this.set(entry.clear ? null : entry.item);
6974
7080
  this.close();
6975
7081
  this.focus();
@@ -7003,7 +7109,7 @@ var Select = class _Select extends Component {
7003
7109
  this._optionEntries = [];
7004
7110
  let lastGroup = /* @__PURE__ */ Symbol("initial-group");
7005
7111
  if (this.options.clearable) {
7006
- this._appendOption(null, true, this.options.placeholder || this._message("select.none", "No selection"));
7112
+ this._appendOption(null, true, this.options.noneLabel ?? this._message("select.none", "No selection"));
7007
7113
  }
7008
7114
  for (const item of this._visibleFixed) this._appendOption(item, false);
7009
7115
  if (this._visibleFixed.length > 0 && this._visibleItems.length > 0 && !this.options.groupKey) {
@@ -7022,7 +7128,23 @@ var Select = class _Select extends Component {
7022
7128
  }
7023
7129
  this._appendOption(item, false);
7024
7130
  }
7025
- if (this._visibleItems.length === 0 && this._visibleFixed.length === 0 && !this._loading) {
7131
+ if (this._actions.length > 0) {
7132
+ if (this._visibleFixed.length > 0 || this._visibleItems.length > 0 || this.options.clearable) {
7133
+ list.append(h("div", { class: "zx-select__separator", role: "presentation" }));
7134
+ }
7135
+ let actionGroup = /* @__PURE__ */ Symbol("initial-action-group");
7136
+ for (const action of this._actions) {
7137
+ if (action.group && action.group !== actionGroup) {
7138
+ list.append(h("div", {
7139
+ class: "zx-select__group",
7140
+ role: "presentation"
7141
+ }, action.group));
7142
+ actionGroup = action.group;
7143
+ }
7144
+ this._appendAction(action);
7145
+ }
7146
+ }
7147
+ if (this._visibleItems.length === 0 && this._visibleFixed.length === 0 && this._actions.length === 0 && !this._loading) {
7026
7148
  list.append(h("div", {
7027
7149
  class: "zx-select__empty",
7028
7150
  role: "status"
@@ -7034,7 +7156,7 @@ var Select = class _Select extends Component {
7034
7156
  _appendOption(item, clear, label) {
7035
7157
  const selected = clear ? this._selected === null : this._selected !== null && Object.is(this._itemValue(item), this.value);
7036
7158
  const index = this._optionEntries.length;
7037
- const content = clear ? String(label ?? "") : this._renderItem(item);
7159
+ const content = clear && typeof this.options.renderNone === "function" ? normalizeRenderedContent(this.options.renderNone(String(label ?? ""))) : clear ? String(label ?? "") : this._renderItem(item);
7038
7160
  const option = h("div", {
7039
7161
  class: "zx-select__option",
7040
7162
  id: uid("zx-select-option"),
@@ -7044,16 +7166,37 @@ var Select = class _Select extends Component {
7044
7166
  }, h("span", { class: "zx-select__option-content" }, content));
7045
7167
  if (selected) option.append(icon("check", { size: 15 }));
7046
7168
  this.refs.list.append(option);
7047
- this._optionEntries.push({ item, clear, element: option });
7169
+ this._optionEntries.push({ item, clear, action: null, element: option });
7170
+ }
7171
+ /** @param {SelectAction} action @returns {void} */
7172
+ _appendAction(action) {
7173
+ const index = this._optionEntries.length;
7174
+ const visual = selectActionIcon(action.icon);
7175
+ const option = h("div", {
7176
+ class: "zx-select__option zx-select__action",
7177
+ id: uid("zx-select-option"),
7178
+ role: "option",
7179
+ ariaSelected: "false",
7180
+ dataset: { index, kind: "action" }
7181
+ }, h(
7182
+ "span",
7183
+ { class: "zx-select__option-content zx-select__action-content" },
7184
+ visual ? h("span", { class: "zx-select__action-icon", ariaHidden: "true" }, visual) : null,
7185
+ h(
7186
+ "span",
7187
+ { class: "zx-select__action-copy" },
7188
+ h("span", { class: "zx-select__action-label" }, action.label),
7189
+ action.description ? h("span", { class: "zx-select__action-description" }, action.description) : null
7190
+ )
7191
+ ));
7192
+ this.refs.list.append(option);
7193
+ this._optionEntries.push({ item: null, clear: false, action, element: option });
7048
7194
  }
7049
7195
  /** @param {SelectItem|null} item @returns {Node|string} */
7050
7196
  _renderItem(item) {
7051
7197
  if (typeof this.options.renderItem === "function") {
7052
7198
  const rendered = this.options.renderItem(item);
7053
- return rendered && typeof rendered === "object" && "nodeType" in rendered ? (
7054
- /** @type {Node} */
7055
- rendered
7056
- ) : String(rendered ?? "");
7199
+ return normalizeRenderedContent(rendered);
7057
7200
  }
7058
7201
  return this._itemLabel(item);
7059
7202
  }
@@ -7124,9 +7267,29 @@ var Select = class _Select extends Component {
7124
7267
  this.refs.input
7125
7268
  );
7126
7269
  input.value = this._selected === null ? "" : typeof this.options.renderValue === "function" ? String(this.options.renderValue(this._selected) ?? "") : this._itemLabel(this._selected);
7270
+ this._syncValueAdornment();
7127
7271
  this.refs.clear.hidden = !this.options.clearable || this._selected === null;
7128
7272
  }
7129
7273
  /** @returns {void} */
7274
+ _syncValueAdornment() {
7275
+ const host = this.refs.valueAdornment;
7276
+ if (!host) return;
7277
+ const selectedLabel = this._selected === null ? "" : typeof this.options.renderValue === "function" ? String(this.options.renderValue(this._selected) ?? "") : this._itemLabel(this._selected);
7278
+ const query = String(
7279
+ /** @type {HTMLInputElement} */
7280
+ this.refs.input?.value ?? ""
7281
+ );
7282
+ if (this._selected === null || typeof this.options.renderValueAdornment !== "function" || Boolean(this.options.filter && query !== selectedLabel)) {
7283
+ host.replaceChildren();
7284
+ host.hidden = true;
7285
+ return;
7286
+ }
7287
+ const rendered = this.options.renderValueAdornment(this._selected);
7288
+ const content = normalizeRenderedContent(rendered);
7289
+ host.replaceChildren(content instanceof Node ? content : document.createTextNode(content));
7290
+ host.hidden = rendered == null || typeof content === "string" && content === "";
7291
+ }
7292
+ /** @returns {void} */
7130
7293
  _syncDisabled() {
7131
7294
  const input = (
7132
7295
  /** @type {HTMLInputElement} */
@@ -7203,6 +7366,82 @@ var Select = class _Select extends Component {
7203
7366
  return message === key ? fallback : message;
7204
7367
  }
7205
7368
  };
7369
+ function normalizeRenderedContent(rendered) {
7370
+ return rendered && typeof rendered === "object" && "nodeType" in rendered ? (
7371
+ /** @type {Node} */
7372
+ rendered
7373
+ ) : String(rendered ?? "");
7374
+ }
7375
+ function normalizeSelectActions(actions) {
7376
+ if (!Array.isArray(actions)) return [];
7377
+ return actions.filter((action) => action && typeof action === "object" && action.id != null && action.label != null).map((action) => ({
7378
+ ...action,
7379
+ label: String(action.label),
7380
+ description: action.description == null ? void 0 : String(action.description),
7381
+ group: action.group == null ? void 0 : String(action.group)
7382
+ }));
7383
+ }
7384
+ function selectActionIcon(visual) {
7385
+ if (typeof visual === "string") return icon(visual);
7386
+ if (visual instanceof Node) return visual.cloneNode(true);
7387
+ return null;
7388
+ }
7389
+ function priorityContent(item, label) {
7390
+ return h(
7391
+ "span",
7392
+ { class: "zx-select__priority" },
7393
+ priorityIndicator(item.level),
7394
+ label ? h("span", {}, String(item.name)) : null
7395
+ );
7396
+ }
7397
+ function priorityIndicator(level) {
7398
+ const normalized = Math.max(0, Math.min(4, Math.trunc(Number(level) || 0)));
7399
+ return h("span", {
7400
+ class: "zx-select__priority-icon",
7401
+ dataset: { level: normalized },
7402
+ ariaHidden: "true"
7403
+ }, Array.from({ length: 5 }, (_, index) => h("span", {
7404
+ dataset: { filled: String(index <= normalized) }
7405
+ })));
7406
+ }
7407
+ function normalizeStatusItem(input) {
7408
+ if (!input || typeof input !== "object" || input.ID == null || input.name == null) return null;
7409
+ const indicators = ["dotted", "ring", "progress", "review", "ready", "waiting", "done", "canceled", "duplicate"];
7410
+ const tones = ["muted", "neutral", "warning", "info", "accent", "success", "danger"];
7411
+ return {
7412
+ ...input,
7413
+ name: String(input.name),
7414
+ indicator: indicators.includes(String(input.indicator)) ? String(input.indicator) : "ring",
7415
+ tone: tones.includes(String(input.tone)) ? String(input.tone) : "muted",
7416
+ description: input.description == null ? "" : String(input.description),
7417
+ shortcut: input.shortcut == null ? "" : String(input.shortcut)
7418
+ };
7419
+ }
7420
+ function statusContent(item) {
7421
+ return h(
7422
+ "span",
7423
+ { class: "zx-select__status" },
7424
+ statusIndicator(item),
7425
+ h(
7426
+ "span",
7427
+ { class: "zx-select__status-copy" },
7428
+ h("span", { class: "zx-select__status-label" }, String(item.name)),
7429
+ item.description ? h("span", { class: "zx-select__status-description" }, item.description) : null
7430
+ ),
7431
+ item.shortcut ? h("kbd", { class: "zx-select__status-shortcut" }, item.shortcut) : null
7432
+ );
7433
+ }
7434
+ function statusIndicator(item) {
7435
+ const indicator = String(item.indicator ?? "ring");
7436
+ const marker = h("span", {
7437
+ class: "zx-select__status-indicator",
7438
+ dataset: { indicator, tone: String(item.tone ?? "muted") },
7439
+ ariaHidden: "true"
7440
+ });
7441
+ if (indicator === "done") marker.append(icon("check", { size: 8 }));
7442
+ if (indicator === "canceled") marker.append(icon("x", { size: 7 }));
7443
+ return marker;
7444
+ }
7206
7445
  function readItem(item, reader) {
7207
7446
  if (typeof reader === "function") return reader(item);
7208
7447
  if (item === null || typeof item !== "object") return item;
@@ -7215,10 +7454,11 @@ function isPrintable(event) {
7215
7454
  // src/components/select/field-adapter.js
7216
7455
  function registerSelectFieldAdapter() {
7217
7456
  Field.register("zxselect", (field, options) => {
7218
- const select = field.own(new Select(null, {
7219
- ...options.props ?? {},
7220
- value: options.value
7221
- }));
7457
+ const props = options.props ?? {};
7458
+ const preset = props.preset;
7459
+ const selectOptions = { ...props, value: options.value };
7460
+ delete selectOptions.preset;
7461
+ const select = field.own(preset === "priority" ? Select.priority(null, selectOptions) : preset === "status" ? Select.status(null, selectOptions) : new Select(null, selectOptions));
7222
7462
  field.listen(select.el, "change", (event) => event.stopImmediatePropagation());
7223
7463
  select.on("change", (event) => field.emit("change", { value: event.detail.value }));
7224
7464
  return {
@@ -9268,12 +9508,14 @@ var Table = class extends Component {
9268
9508
  }
9269
9509
  /** @returns {HTMLElement} Table root. */
9270
9510
  render() {
9511
+ const created = this.el === null;
9271
9512
  const root = (
9272
9513
  /** @type {HTMLElement} */
9273
9514
  this.el ?? h("div")
9274
9515
  );
9275
9516
  this.el = root;
9276
- this._originalChildren = this.el ? Array.from(this.el.childNodes) : null;
9517
+ this._createdRoot = created;
9518
+ this._original = created ? null : snapshotTarget(root);
9277
9519
  this._columns = Array.isArray(this.options.columns) ? this.options.columns.map((column) => ({ ...column })) : [];
9278
9520
  this._data = Array.isArray(this.options.data) ? [...this.options.data] : [];
9279
9521
  this._selected = /* @__PURE__ */ new Set();
@@ -9348,8 +9590,8 @@ var Table = class extends Component {
9348
9590
  }
9349
9591
  /**
9350
9592
  * 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.
9593
+ * shown, and `aria-busy` is set — a lightweight enterprise-table busy and skeleton pattern.
9594
+ * Call `setLoading(false)` (or `setData`) when done.
9353
9595
  * @param {boolean} [loading=true] Whether the table is loading.
9354
9596
  * @returns {this}
9355
9597
  */
@@ -9898,9 +10140,8 @@ var Table = class extends Component {
9898
10140
  this._teardownEdit();
9899
10141
  this._width?.destroy();
9900
10142
  this._width = null;
9901
- if (this.el) delete this.el.dataset.editMode;
9902
- if (!this._restored && this._originalChildren) {
9903
- this.el.replaceChildren(...this._originalChildren);
10143
+ if (!this._restored && !this._createdRoot) {
10144
+ restoreTarget(this.el, this._original);
9904
10145
  this._restored = true;
9905
10146
  }
9906
10147
  super.destroy();
@@ -10884,6 +11125,630 @@ function growStep(growing) {
10884
11125
  return Number.isFinite(step) && step > 0 ? step : null;
10885
11126
  }
10886
11127
 
11128
+ // src/components/launcher/launcher.js
11129
+ var Launcher = class extends Component {
11130
+ static cssName = "launcher";
11131
+ /** @type {Readonly<LauncherOptions>} */
11132
+ static defaults = {
11133
+ items: [],
11134
+ sources: [],
11135
+ query: "",
11136
+ debounce: 250,
11137
+ minQuery: 0,
11138
+ maxResults: 100,
11139
+ placeholder: "Search applications and records",
11140
+ label: "Launcher",
11141
+ closeLabel: "Close launcher",
11142
+ resultsLabel: "Launcher results",
11143
+ emptyText: "No results",
11144
+ loadingText: "Searching\u2026",
11145
+ shortcut: "mod+k",
11146
+ hints: { move: "Move", open: "Open", close: "Close" }
11147
+ };
11148
+ /**
11149
+ * Creates an owned launcher dialog or enhances an existing `<dialog>` target.
11150
+ * @param {HTMLDialogElement|string|null} [target=null] Dialog target, selector, or null.
11151
+ * @param {LauncherOptions} [options={}] Launcher options.
11152
+ */
11153
+ constructor(target = null, options = {}) {
11154
+ super(target, options);
11155
+ }
11156
+ /** @returns {HTMLDialogElement} */
11157
+ render() {
11158
+ if (this.el !== null && !(this.el instanceof HTMLDialogElement)) {
11159
+ throw new TypeError("Launcher target must be a <dialog> element");
11160
+ }
11161
+ this._createdRoot = this.el === null;
11162
+ const dialog = (
11163
+ /** @type {HTMLDialogElement} */
11164
+ this.el ?? h("dialog")
11165
+ );
11166
+ this.el = dialog;
11167
+ this._original = this._createdRoot ? null : {
11168
+ attributes: Array.from(dialog.attributes, (attribute) => [attribute.name, attribute.value]),
11169
+ children: Array.from(dialog.childNodes)
11170
+ };
11171
+ this._items = normalizeItems(this.options.items);
11172
+ this._sources = normalizeSources(this.options.sources);
11173
+ this._sourceItems = /* @__PURE__ */ new Map();
11174
+ this._query = String(this.options.query ?? "");
11175
+ this._renderedQuery = null;
11176
+ this._results = [];
11177
+ this._active = -1;
11178
+ this._request = null;
11179
+ this._requestSequence = 0;
11180
+ this._queryTimer = null;
11181
+ this._busy = false;
11182
+ this._destroyed = false;
11183
+ const listId = uid("zx-launcher-results");
11184
+ const input = h("input", {
11185
+ ref: "input",
11186
+ class: "zx-launcher__input",
11187
+ type: "search",
11188
+ role: "combobox",
11189
+ autocomplete: "off",
11190
+ spellcheck: false,
11191
+ ariaAutocomplete: "list",
11192
+ ariaControls: listId,
11193
+ ariaExpanded: "false",
11194
+ ariaLabel: this.options.label,
11195
+ placeholder: this.options.placeholder,
11196
+ value: this._query
11197
+ });
11198
+ const close = h("button", {
11199
+ ref: "close",
11200
+ class: "zx-icon-btn zx-launcher__close",
11201
+ type: "button",
11202
+ ariaLabel: this.options.closeLabel
11203
+ }, icon("x"));
11204
+ const status = h("div", {
11205
+ ref: "status",
11206
+ class: "zx-launcher__status",
11207
+ role: "status",
11208
+ ariaLive: "polite"
11209
+ });
11210
+ const results = h("div", {
11211
+ ref: "results",
11212
+ class: "zx-launcher__results",
11213
+ id: listId,
11214
+ role: "listbox",
11215
+ ariaLabel: this.options.resultsLabel
11216
+ });
11217
+ const shortcut = this.options.shortcut === "mod+k" ? h("kbd", {
11218
+ class: "zx-launcher__shortcut",
11219
+ ariaHidden: "true"
11220
+ }, launcherShortcutLabel()) : null;
11221
+ const footer = launcherHints(this.options.hints);
11222
+ dialog.setAttribute("aria-label", String(this.options.label));
11223
+ dialog.replaceChildren(
11224
+ h(
11225
+ "div",
11226
+ { class: "zx-launcher__surface" },
11227
+ h("div", {
11228
+ class: "zx-launcher__search",
11229
+ dataset: { shortcut: shortcut ? "true" : "false" }
11230
+ }, icon("search"), input, shortcut, close),
11231
+ status,
11232
+ results,
11233
+ footer
11234
+ )
11235
+ );
11236
+ dialog.dataset.state = "closed";
11237
+ if (this._createdRoot) document.body.append(dialog);
11238
+ this.listen(input, "input", () => {
11239
+ this._query = /** @type {HTMLInputElement} */
11240
+ input.value;
11241
+ this.emit("query", { query: this._query.trim() });
11242
+ this._refresh();
11243
+ });
11244
+ this.listen(input, "keydown", (event) => this._onKeydown(
11245
+ /** @type {KeyboardEvent} */
11246
+ event
11247
+ ));
11248
+ this.listen(close, "click", () => this.close());
11249
+ this.listen(results, "click", (event) => {
11250
+ const option = (
11251
+ /** @type {Element|null} */
11252
+ event.target?.closest?.("[data-launcher-index]")
11253
+ );
11254
+ if (!option || !results.contains(option)) return;
11255
+ this._activate(Number(option.getAttribute("data-launcher-index")), event);
11256
+ });
11257
+ this.listen(results, "pointermove", (event) => {
11258
+ const option = (
11259
+ /** @type {Element|null} */
11260
+ event.target?.closest?.("[data-launcher-index]")
11261
+ );
11262
+ if (!option || !results.contains(option)) return;
11263
+ const index = Number(option.getAttribute("data-launcher-index"));
11264
+ if (!Number.isInteger(index) || this._results[index]?.disabled || index === this._active) return;
11265
+ this._active = index;
11266
+ this._syncActive();
11267
+ });
11268
+ this.listen(dialog, "cancel", (event) => {
11269
+ event.preventDefault();
11270
+ this.close();
11271
+ });
11272
+ this.listen(dialog, "close", () => {
11273
+ dialog.dataset.state = "closed";
11274
+ input.setAttribute("aria-expanded", "false");
11275
+ this._abortRequest();
11276
+ this.emit("close");
11277
+ });
11278
+ this.listen(document, "keydown", (event) => this._onDocumentKeydown(
11279
+ /** @type {KeyboardEvent} */
11280
+ event
11281
+ ));
11282
+ this._renderResults();
11283
+ return dialog;
11284
+ }
11285
+ /** Opens the launcher and focuses/selects its query. @returns {this} @fires Launcher#open */
11286
+ open() {
11287
+ if (this.isOpen() || this._destroyed) return this;
11288
+ this.el.showModal();
11289
+ this.el.dataset.state = "open";
11290
+ this.refs.input.setAttribute("aria-expanded", "true");
11291
+ this._refresh();
11292
+ queueMicrotask(() => {
11293
+ if (!this.isOpen()) return;
11294
+ this.refs.input.focus();
11295
+ this.refs.input.select();
11296
+ });
11297
+ this.emit("open");
11298
+ return this;
11299
+ }
11300
+ /** Closes the launcher. @returns {this} */
11301
+ close() {
11302
+ if (this.isOpen()) this.el.close();
11303
+ return this;
11304
+ }
11305
+ /** Toggles the launcher. @returns {this} */
11306
+ toggle() {
11307
+ return this.isOpen() ? this.close() : this.open();
11308
+ }
11309
+ /** Reports whether the launcher is open. @returns {boolean} */
11310
+ isOpen() {
11311
+ return Boolean(this.el.open);
11312
+ }
11313
+ /** Focuses and selects the query field. @returns {this} */
11314
+ focus() {
11315
+ this.refs.input.focus();
11316
+ this.refs.input.select();
11317
+ return this;
11318
+ }
11319
+ /** Replaces the query and refreshes local and remote results. @param {string} query @returns {this} */
11320
+ setQuery(query) {
11321
+ this._query = String(query ?? "");
11322
+ this.refs.input.value = this._query;
11323
+ this.emit("query", { query: this._query.trim() });
11324
+ this._refresh();
11325
+ return this;
11326
+ }
11327
+ /** Returns the untrimmed query. @returns {string} */
11328
+ getQuery() {
11329
+ return this._query;
11330
+ }
11331
+ /** Replaces local catalogue items. @param {LauncherItem[]} items @returns {this} */
11332
+ setItems(items) {
11333
+ this._items = normalizeItems(items);
11334
+ this._renderResults();
11335
+ return this;
11336
+ }
11337
+ /** Marks one local item as the current destination. @param {string|number|null} id Item id. @returns {this} */
11338
+ setCurrent(id) {
11339
+ const key = id == null ? null : String(id);
11340
+ this._items = this._items.map((item) => ({ ...item, current: key !== null && String(item.id) === key }));
11341
+ this._renderResults();
11342
+ return this;
11343
+ }
11344
+ /** Replaces asynchronous sources and aborts work from the old set. @param {LauncherSource[]} sources @returns {this} */
11345
+ setSources(sources) {
11346
+ this._abortRequest();
11347
+ this._sources = normalizeSources(sources);
11348
+ this._sourceItems.clear();
11349
+ this._refresh();
11350
+ return this;
11351
+ }
11352
+ /** Aborts sources and removes the owned dialog. @returns {void} */
11353
+ destroy() {
11354
+ if (this._destroyed) return;
11355
+ this._destroyed = true;
11356
+ const original = this._original;
11357
+ this._abortRequest();
11358
+ if (this.isOpen()) this.el.close();
11359
+ super.destroy();
11360
+ if (!this._createdRoot && original) {
11361
+ for (const attribute of Array.from(this.el.attributes)) this.el.removeAttribute(attribute.name);
11362
+ for (const [name, value] of original.attributes) this.el.setAttribute(name, value);
11363
+ this.el.replaceChildren(...original.children);
11364
+ }
11365
+ }
11366
+ /** @returns {void} */
11367
+ _refresh() {
11368
+ this._sourceItems.clear();
11369
+ this._renderResults();
11370
+ this._abortRequest();
11371
+ if (this._sources.length === 0) return;
11372
+ const delay = Math.max(0, Number(this.options.debounce) || 0);
11373
+ this._queryTimer = setTimeout(() => this._loadSources(), delay);
11374
+ }
11375
+ /** @returns {Promise<void>} */
11376
+ async _loadSources() {
11377
+ this._queryTimer = null;
11378
+ const query = this._query.trim();
11379
+ const sources = this._sources.filter((source) => visibleForQuery(source.when, query) && query.length >= Math.max(Number(this.options.minQuery) || 0, Number(source.minQuery) || 0));
11380
+ if (sources.length === 0) return;
11381
+ const request = new AbortController();
11382
+ const sequence = ++this._requestSequence;
11383
+ this._request = request;
11384
+ this._setBusy(true);
11385
+ await Promise.all(sources.map(async (source) => {
11386
+ try {
11387
+ const loaded = await source.load(query, { signal: request.signal });
11388
+ if (request.signal.aborted || sequence !== this._requestSequence) return;
11389
+ this._sourceItems.set(source.id, normalizeItems(loaded).map((item, itemOrder) => ({
11390
+ ...item,
11391
+ _source: source.id,
11392
+ _sourceOrder: source.order === "source",
11393
+ group: item.group ?? source.label ?? "",
11394
+ itemOrder: item.itemOrder ?? (source.order === "source" ? itemOrder : void 0)
11395
+ })));
11396
+ this._renderResults();
11397
+ } catch (error) {
11398
+ if (request.signal.aborted || sequence !== this._requestSequence) return;
11399
+ this.emit("error", { source, error });
11400
+ }
11401
+ }));
11402
+ if (request.signal.aborted || sequence !== this._requestSequence) return;
11403
+ this._request = null;
11404
+ this._setBusy(false);
11405
+ }
11406
+ /** @param {boolean} busy @returns {void} */
11407
+ _setBusy(busy) {
11408
+ this._busy = busy;
11409
+ this.el.setAttribute("aria-busy", String(busy));
11410
+ this._renderStatus();
11411
+ }
11412
+ /** @returns {void} */
11413
+ _abortRequest() {
11414
+ if (this._queryTimer !== null) clearTimeout(this._queryTimer);
11415
+ this._queryTimer = null;
11416
+ this._request?.abort();
11417
+ this._request = null;
11418
+ this._requestSequence += 1;
11419
+ if (this._busy) this._setBusy(false);
11420
+ }
11421
+ /** @returns {void} */
11422
+ _renderResults() {
11423
+ const queryKey = this._query;
11424
+ const activeId = this._renderedQuery === queryKey && this._active >= 0 && this._results[this._active] ? String(this._results[this._active].id) : null;
11425
+ const query = this._query.trim();
11426
+ const local = this._items.filter((item) => visibleForQuery(item.when, query));
11427
+ const rankedLocal = rankLauncherItems(local, query, this.options.maxResults);
11428
+ const remote = this._sources.flatMap((source) => {
11429
+ const loaded = this._sourceItems.get(String(source.id)) ?? [];
11430
+ return source.order === "source" ? loaded.filter((item) => visibleForQuery(item.when, query)) : rankLauncherItems(
11431
+ loaded.filter((item) => visibleForQuery(item.when, query)),
11432
+ query,
11433
+ this.options.maxResults
11434
+ );
11435
+ });
11436
+ const seen = /* @__PURE__ */ new Set();
11437
+ const combined = [...rankedLocal.map((item) => ({ ...item, _source: null })), ...remote].filter((item) => {
11438
+ const key = String(item.id);
11439
+ if (seen.has(key)) return false;
11440
+ seen.add(key);
11441
+ return true;
11442
+ }).slice(0, Math.max(0, Number(this.options.maxResults) || 0));
11443
+ const groups = /* @__PURE__ */ new Map();
11444
+ for (const item of combined) {
11445
+ const group = String(item.group ?? "");
11446
+ if (!groups.has(group)) groups.set(group, []);
11447
+ groups.get(group).push(item);
11448
+ }
11449
+ this._results = [...groups.values()].flat();
11450
+ this._renderedQuery = queryKey;
11451
+ const preserved = activeId === null ? -1 : this._results.findIndex((item) => !item.disabled && String(item.id) === activeId);
11452
+ this._active = preserved >= 0 ? preserved : this._results.findIndex((item) => !item.disabled);
11453
+ const nodes = [];
11454
+ let index = 0;
11455
+ for (const [group, items] of groups) {
11456
+ const options = items.map((item) => this._option(item, index++));
11457
+ if (group) nodes.push(h("div", {
11458
+ class: "zx-launcher__group",
11459
+ role: "group",
11460
+ ariaLabel: group,
11461
+ dataset: { layout: items.every((item) => item.kind === "application") ? "grid" : "list" }
11462
+ }, h("div", { class: "zx-launcher__group-label", ariaHidden: "true" }, group), options));
11463
+ else nodes.push(...options);
11464
+ }
11465
+ this.refs.results.replaceChildren(...nodes);
11466
+ this._syncActive();
11467
+ this._renderStatus();
11468
+ }
11469
+ /** @param {RankedLauncherItem} item @param {number} index @returns {HTMLElement} */
11470
+ _option(item, index) {
11471
+ const href = safeHref(item.href);
11472
+ const tag = href ? "a" : "button";
11473
+ const children = [];
11474
+ const visual = launcherItemIcon(item);
11475
+ if (visual) children.push(h("span", {
11476
+ class: "zx-launcher__icon",
11477
+ ariaHidden: "true"
11478
+ }, visual));
11479
+ children.push(h(
11480
+ "span",
11481
+ { class: "zx-launcher__copy" },
11482
+ h("span", { class: "zx-launcher__label" }, item.label),
11483
+ item.description ? h("span", { class: "zx-launcher__description" }, item.description) : null
11484
+ ));
11485
+ if (item.badge != null) children.push(h("span", { class: "zx-launcher__badge" }, String(item.badge)));
11486
+ return (
11487
+ /** @type {HTMLElement} */
11488
+ h(tag, {
11489
+ class: "zx-launcher__option",
11490
+ type: tag === "button" ? "button" : null,
11491
+ href,
11492
+ target: item.target ?? null,
11493
+ rel: tag === "a" ? "noopener" : null,
11494
+ role: "option",
11495
+ id: uid("zx-launcher-option"),
11496
+ tabIndex: -1,
11497
+ ariaSelected: "false",
11498
+ ariaDisabled: item.disabled ? "true" : null,
11499
+ ariaCurrent: item.current ? "page" : null,
11500
+ dataset: {
11501
+ launcherIndex: index,
11502
+ pinned: item.pinned !== void 0 && item.pinned !== false ? "true" : null,
11503
+ kind: normalizeLauncherKind(item.kind)
11504
+ }
11505
+ }, children)
11506
+ );
11507
+ }
11508
+ /** @returns {void} */
11509
+ _renderStatus() {
11510
+ if (this._busy) this.refs.status.textContent = String(this.options.loadingText);
11511
+ else if (this._results.length === 0) this.refs.status.textContent = String(this.options.emptyText);
11512
+ else this.refs.status.textContent = "";
11513
+ this.refs.status.hidden = !this.refs.status.textContent;
11514
+ }
11515
+ /** @returns {void} */
11516
+ _syncActive() {
11517
+ const options = [...this.refs.results.querySelectorAll("[data-launcher-index]")];
11518
+ for (const option of options) {
11519
+ const selected = Number(option.getAttribute("data-launcher-index")) === this._active;
11520
+ option.setAttribute("aria-selected", String(selected));
11521
+ }
11522
+ const active = options.find((option) => Number(option.getAttribute("data-launcher-index")) === this._active);
11523
+ if (active) this.refs.input.setAttribute("aria-activedescendant", active.id);
11524
+ else this.refs.input.removeAttribute("aria-activedescendant");
11525
+ }
11526
+ /** @param {KeyboardEvent} event @returns {void} */
11527
+ _onKeydown(event) {
11528
+ if (event.key === "Escape") {
11529
+ this.close();
11530
+ return;
11531
+ }
11532
+ if (event.key === "Enter") {
11533
+ if (this._active < 0) return;
11534
+ event.preventDefault();
11535
+ const option = this.refs.results.querySelector(`[data-launcher-index="${this._active}"]`);
11536
+ option?.click();
11537
+ return;
11538
+ }
11539
+ const enabled = this._results.map((item, index) => item.disabled ? -1 : index).filter((index) => index >= 0);
11540
+ if (enabled.length === 0) return;
11541
+ if (/^Arrow(?:Left|Right|Up|Down)$/.test(event.key)) {
11542
+ const next = this._spatialIndex(event.key, enabled);
11543
+ if (next !== null) {
11544
+ event.preventDefault();
11545
+ this._active = next;
11546
+ this._syncActive();
11547
+ this.refs.results.querySelector(`[data-launcher-index="${this._active}"]`)?.scrollIntoView({ block: "nearest", inline: "nearest" });
11548
+ return;
11549
+ }
11550
+ if (event.key === "ArrowLeft" || event.key === "ArrowRight") return;
11551
+ }
11552
+ let position2 = enabled.indexOf(this._active);
11553
+ if (event.key === "Home" || event.key === "PageUp") position2 = 0;
11554
+ else if (event.key === "End" || event.key === "PageDown") position2 = enabled.length - 1;
11555
+ else if (event.key === "ArrowDown") position2 = (position2 + 1 + enabled.length) % enabled.length;
11556
+ else if (event.key === "ArrowUp") position2 = (position2 - 1 + enabled.length) % enabled.length;
11557
+ else return;
11558
+ event.preventDefault();
11559
+ this._active = enabled[position2];
11560
+ this._syncActive();
11561
+ this.refs.results.querySelector(`[data-launcher-index="${this._active}"]`)?.scrollIntoView({ block: "nearest" });
11562
+ }
11563
+ /** @param {string} direction @param {number[]} enabled @returns {number|null} */
11564
+ _spatialIndex(direction, enabled) {
11565
+ const current = this.refs.results.querySelector(`[data-launcher-index="${this._active}"]`);
11566
+ if (!(current instanceof HTMLElement)) return null;
11567
+ if ((direction === "ArrowLeft" || direction === "ArrowRight") && this._results[this._active]?.kind !== "application") return null;
11568
+ 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"));
11569
+ const next = findSpatialLauncherOption(candidates, current, direction);
11570
+ return next ? Number(next.getAttribute("data-launcher-index")) : null;
11571
+ }
11572
+ /** @param {number} index @param {Event} event @returns {void} */
11573
+ _activate(index, event) {
11574
+ const item = this._results[index];
11575
+ if (!item || item.disabled) {
11576
+ event.preventDefault();
11577
+ return;
11578
+ }
11579
+ const selected = this.emit("select", {
11580
+ item,
11581
+ value: item.value ?? item.id,
11582
+ source: item._source ?? null,
11583
+ query: this._query.trim()
11584
+ });
11585
+ if (selected.defaultPrevented) {
11586
+ event.preventDefault();
11587
+ return;
11588
+ }
11589
+ if (!item.href) {
11590
+ event.preventDefault();
11591
+ item.invoke?.();
11592
+ }
11593
+ this.close();
11594
+ }
11595
+ /** @param {KeyboardEvent} event @returns {void} */
11596
+ _onDocumentKeydown(event) {
11597
+ if (this.options.shortcut !== "mod+k" || event.isComposing || event.key.toLowerCase() !== "k" || Number(event.metaKey) + Number(event.ctrlKey) !== 1 || event.altKey || event.shiftKey) return;
11598
+ const active = document.activeElement;
11599
+ if (active instanceof HTMLInputElement || active instanceof HTMLTextAreaElement || active instanceof HTMLSelectElement || active instanceof HTMLElement && active.isContentEditable) return;
11600
+ const modal = document.querySelector("dialog:modal");
11601
+ if (modal && modal !== this.el) return;
11602
+ event.preventDefault();
11603
+ this.open();
11604
+ }
11605
+ };
11606
+ function launcherShortcutLabel() {
11607
+ const platform = String(
11608
+ /** @type {any} */
11609
+ navigator.userAgentData?.platform ?? navigator.platform ?? ""
11610
+ );
11611
+ return /mac|iphone|ipad/i.test(platform) ? "\u2318 K" : "Ctrl K";
11612
+ }
11613
+ function launcherHints(hints) {
11614
+ if (hints === false) return null;
11615
+ const labels = hints && typeof hints === "object" ? hints : {};
11616
+ return h(
11617
+ "footer",
11618
+ { class: "zx-launcher__hints", ariaHidden: "true" },
11619
+ h("span", {}, h("kbd", {}, "\u2191\u2193\u2190\u2192"), labels.move ?? "Move"),
11620
+ h("span", {}, h("kbd", {}, "\u21B5"), labels.open ?? "Open"),
11621
+ h("span", {}, h("kbd", {}, "Esc"), labels.close ?? "Close")
11622
+ );
11623
+ }
11624
+ function launcherItemIcon(item) {
11625
+ let visual = item.icon;
11626
+ if (typeof visual === "function") visual = visual(item);
11627
+ if (typeof visual === "string") return icon(visual);
11628
+ if (visual instanceof Node) return visual.cloneNode(true);
11629
+ if (visual && typeof visual === "object" && typeof /** @type {any} */
11630
+ visual.toElement === "function") {
11631
+ const element = (
11632
+ /** @type {any} */
11633
+ visual.toElement()
11634
+ );
11635
+ return element instanceof Node ? element.cloneNode(true) : null;
11636
+ }
11637
+ return null;
11638
+ }
11639
+ function normalizeLauncherKind(kind) {
11640
+ return kind === "application" || kind === "record" ? kind : "action";
11641
+ }
11642
+ function visibleForQuery(when, query) {
11643
+ if (when === "empty") return query === "";
11644
+ if (when === "query") return query !== "";
11645
+ return true;
11646
+ }
11647
+ function findSpatialLauncherOption(options, current, direction) {
11648
+ const origin = current.getBoundingClientRect();
11649
+ if (origin.width === 0 && origin.height === 0) return null;
11650
+ const originX = origin.left + origin.width / 2;
11651
+ const originY = origin.top + origin.height / 2;
11652
+ let best = null;
11653
+ let bestScore = Infinity;
11654
+ for (const option of options) {
11655
+ if (option === current) continue;
11656
+ const rect = option.getBoundingClientRect();
11657
+ const dx = rect.left + rect.width / 2 - originX;
11658
+ const dy = rect.top + rect.height / 2 - originY;
11659
+ let valid = false;
11660
+ let score = Infinity;
11661
+ if (direction === "ArrowLeft") {
11662
+ valid = dx < -2 && Math.abs(dy) < Math.max(origin.height, rect.height) / 2;
11663
+ score = Math.abs(dx) + Math.abs(dy) * 4;
11664
+ } else if (direction === "ArrowRight") {
11665
+ valid = dx > 2 && Math.abs(dy) < Math.max(origin.height, rect.height) / 2;
11666
+ score = Math.abs(dx) + Math.abs(dy) * 4;
11667
+ } else if (direction === "ArrowUp") {
11668
+ valid = dy < -2;
11669
+ score = Math.abs(dy) * 10 + Math.abs(dx);
11670
+ } else if (direction === "ArrowDown") {
11671
+ valid = dy > 2;
11672
+ score = Math.abs(dy) * 10 + Math.abs(dx);
11673
+ }
11674
+ if (valid && score < bestScore) {
11675
+ best = option;
11676
+ bestScore = score;
11677
+ }
11678
+ }
11679
+ return best;
11680
+ }
11681
+ function rankLauncherItems(items, query, limit = 100) {
11682
+ const needle = searchable(query).trim();
11683
+ const ranked = items.map((item, order) => ({ item, order, score: launcherScore(item, needle) })).filter(({ score }) => score < Infinity);
11684
+ ranked.sort((a, b) => {
11685
+ const aGroup = finiteOrder(a.item.groupOrder);
11686
+ const bGroup = finiteOrder(b.item.groupOrder);
11687
+ if (aGroup !== bGroup) return aGroup - bGroup;
11688
+ if (needle === "") {
11689
+ const aPinned = pinOrder(a.item.pinned, a.order);
11690
+ const bPinned = pinOrder(b.item.pinned, b.order);
11691
+ if (aPinned !== bPinned) return aPinned - bPinned;
11692
+ }
11693
+ return a.score - b.score || finiteOrder(a.item.itemOrder) - finiteOrder(b.item.itemOrder) || compareLauncherText(a.item.label, b.item.label) || a.order - b.order;
11694
+ });
11695
+ const maximum = Math.max(0, Number(limit) || 0);
11696
+ return ranked.slice(0, maximum || 0).map(({ item, score }) => ({ ...item, _score: score }));
11697
+ }
11698
+ function launcherScore(item, needle) {
11699
+ if (!needle) return item.pinned ? -1 : 0;
11700
+ const label = searchable(item.label);
11701
+ const identifier = searchable(item.id);
11702
+ const keywords = searchable(Array.isArray(item.keywords) ? item.keywords.join(" ") : item.keywords ?? "");
11703
+ if (label === needle) return 0;
11704
+ if (label.startsWith(needle)) return 10;
11705
+ if (label.split(/[^\p{L}\p{N}]+/u).some((word) => word.startsWith(needle))) return 20;
11706
+ if (label.includes(needle) || keywords.includes(needle)) return 30;
11707
+ if (identifier.includes(needle)) return 40;
11708
+ const acronym = label.split(/[^\p{L}\p{N}]+/u).filter(Boolean).map((word) => word[0]).join("");
11709
+ if (acronym.startsWith(needle)) return 50;
11710
+ return Infinity;
11711
+ }
11712
+ function pinOrder(pinned, order) {
11713
+ if (typeof pinned === "number" && Number.isFinite(pinned)) return pinned;
11714
+ if (pinned) return order;
11715
+ return Number.MAX_SAFE_INTEGER;
11716
+ }
11717
+ function finiteOrder(value) {
11718
+ return typeof value === "number" && Number.isFinite(value) ? value : Number.MAX_SAFE_INTEGER;
11719
+ }
11720
+ function searchable(value) {
11721
+ return String(value ?? "").normalize("NFKD").replace(/\p{M}/gu, "").toLowerCase();
11722
+ }
11723
+ function compareLauncherText(left, right) {
11724
+ const a = searchable(left);
11725
+ const b = searchable(right);
11726
+ if (a < b) return -1;
11727
+ if (a > b) return 1;
11728
+ const rawA = String(left ?? "").normalize("NFC");
11729
+ const rawB = String(right ?? "").normalize("NFC");
11730
+ return rawA < rawB ? -1 : rawA > rawB ? 1 : 0;
11731
+ }
11732
+ function normalizeItems(items) {
11733
+ if (!Array.isArray(items)) return [];
11734
+ return items.filter((item) => item && typeof item === "object" && item.id != null && item.label != null).map((item) => normalizeLauncherItem(item));
11735
+ }
11736
+ function normalizeLauncherItem(item) {
11737
+ const normalized = { ...item, label: String(item.label) };
11738
+ if (item.href == null) return normalized;
11739
+ const href = safeHref(item.href);
11740
+ if (href !== null) normalized.href = href;
11741
+ else {
11742
+ delete normalized.href;
11743
+ if (typeof normalized.invoke !== "function") normalized.disabled = true;
11744
+ }
11745
+ return normalized;
11746
+ }
11747
+ function normalizeSources(sources) {
11748
+ if (!Array.isArray(sources)) return [];
11749
+ return sources.filter((source) => source && typeof source === "object" && source.id != null && typeof source.load === "function").map((source) => ({ ...source, id: String(source.id) }));
11750
+ }
11751
+
10887
11752
  // src/components/message/message.js
10888
11753
  var MessageQueue = class {
10889
11754
  #limit;
@@ -11993,6 +12858,7 @@ var zeyosModules = Object.freeze({
11993
12858
  weblets: { label: "Weblets", icon: "zeyos-weblets", color: "#4f46e5", fa: "window-restore" }
11994
12859
  });
11995
12860
  var moduleAliases = Object.freeze({
12861
+ main: "zeyos",
11996
12862
  addresses: "contacts",
11997
12863
  clocking: "actionsteps",
11998
12864
  comments: "notes",
@@ -12099,15 +12965,23 @@ function moduleIcon(name, options = {}) {
12099
12965
  return icon(standard ? `fa:${info.fa}` : `kit:${info.icon}`, rest);
12100
12966
  }
12101
12967
  function moduleChip(name, options = {}) {
12102
- const { size = 24, iconSize = "1em", label = null, title = false, standard = false } = options;
12968
+ const {
12969
+ size = 24,
12970
+ iconSize = "1em",
12971
+ label = null,
12972
+ title = false,
12973
+ standard = false,
12974
+ color = null
12975
+ } = options;
12103
12976
  const key = normalizeModuleName(name);
12104
12977
  const info = moduleInfo(name);
12978
+ const background = color || info.color;
12105
12979
  const chip = h("span", {
12106
12980
  class: ["zx-module-icon", options.class].flat().filter(Boolean),
12107
12981
  dataset: { module: key },
12108
12982
  style: {
12109
- "--zx-module-color": info.color,
12110
- "--zx-module-glyph": moduleGlyphColor(info.color),
12983
+ "--zx-module-color": background,
12984
+ "--zx-module-glyph": moduleGlyphColor(background),
12111
12985
  "--zx-module-icon-size": typeof size === "number" ? `${size}px` : String(size)
12112
12986
  },
12113
12987
  title: title ? info.label : null
@@ -12121,6 +12995,307 @@ function moduleChip(name, options = {}) {
12121
12995
  return chip;
12122
12996
  }
12123
12997
 
12998
+ // src/zeyos/launcher.js
12999
+ var ZEYOS_LAUNCHER_APPLICATIONS = Object.freeze([
13000
+ "accounts",
13001
+ "actionsteps",
13002
+ "admin",
13003
+ "auth",
13004
+ "billing",
13005
+ "calendar",
13006
+ "campaigns",
13007
+ "collection",
13008
+ "contacts",
13009
+ "contracts",
13010
+ "control",
13011
+ "coupons",
13012
+ "dev",
13013
+ "documents",
13014
+ "inventory",
13015
+ "mailinglists",
13016
+ "main",
13017
+ "messages",
13018
+ "links",
13019
+ "notes",
13020
+ "objects",
13021
+ "opportunities",
13022
+ "payments",
13023
+ "pricelists",
13024
+ "procurement",
13025
+ "production",
13026
+ "projects",
13027
+ "tasks",
13028
+ "tickets"
13029
+ ]);
13030
+ function buildZeyosLauncherConfig(state = {}, options = {}) {
13031
+ const {
13032
+ standardIcons = false,
13033
+ items: extraItems = [],
13034
+ sources: extraSources = [],
13035
+ ...launcherOptions
13036
+ } = options;
13037
+ const labels = launcherLabels(state.labels);
13038
+ const iconOptions = {
13039
+ standardIcons,
13040
+ renderApplicationIcon: state.renderApplicationIcon,
13041
+ renderWebletIcon: state.renderWebletIcon
13042
+ };
13043
+ const applications = normalizeZeyosLauncherApplications(state, iconOptions);
13044
+ const recent = typeof state.loadRecent === "function" ? [] : normalizeZeyosLauncherRecords(state.recent ?? [], {
13045
+ query: "",
13046
+ labels,
13047
+ resolveRecord: state.resolveRecord,
13048
+ renderRecordIcon: state.renderRecordIcon,
13049
+ standardIcons
13050
+ });
13051
+ const sources = [];
13052
+ if (typeof state.loadRecent === "function") {
13053
+ sources.push({
13054
+ id: "zeyos-recent",
13055
+ minQuery: 0,
13056
+ when: "empty",
13057
+ order: "source",
13058
+ load: async (_query, context) => normalizeZeyosLauncherRecords(
13059
+ await state.loadRecent(context),
13060
+ {
13061
+ query: "",
13062
+ labels,
13063
+ resolveRecord: state.resolveRecord,
13064
+ renderRecordIcon: state.renderRecordIcon,
13065
+ standardIcons
13066
+ }
13067
+ )
13068
+ });
13069
+ }
13070
+ if (typeof state.search === "function") {
13071
+ sources.push({
13072
+ id: "zeyos-records",
13073
+ minQuery: 1,
13074
+ when: "query",
13075
+ order: "source",
13076
+ load: async (query, context) => normalizeZeyosLauncherRecords(
13077
+ await state.search(query, context),
13078
+ {
13079
+ query,
13080
+ labels,
13081
+ resolveRecord: state.resolveRecord,
13082
+ renderRecordIcon: state.renderRecordIcon,
13083
+ standardIcons
13084
+ }
13085
+ )
13086
+ });
13087
+ }
13088
+ sources.push(...extraSources);
13089
+ return {
13090
+ ...launcherOptions,
13091
+ items: [...applications, ...recent, ...extraItems],
13092
+ sources,
13093
+ hints: options.hints ?? {
13094
+ move: labels.move,
13095
+ open: labels.open,
13096
+ close: labels.close
13097
+ }
13098
+ };
13099
+ }
13100
+ function zeyosLauncher(state = {}, options = {}) {
13101
+ return new Launcher(null, buildZeyosLauncherConfig(state, options));
13102
+ }
13103
+ function normalizeZeyosLauncherApplications(state = {}, options = {}) {
13104
+ const labels = launcherLabels(state.labels);
13105
+ const pinned = Array.isArray(state.pinned) ? state.pinned.map(String) : [];
13106
+ const seen = /* @__PURE__ */ new Set();
13107
+ const items = [];
13108
+ const add = (raw2, fallbackId = null, fallbackLabel = null) => {
13109
+ const app = normalizeApplication(raw2, fallbackId, fallbackLabel);
13110
+ if (!app || seen.has(app.identifier)) return;
13111
+ if (app.type === "module" && typeof state.canUseModule === "function" && !state.canUseModule(app.module)) return;
13112
+ seen.add(app.identifier);
13113
+ const resolved = mergeDestination(app.raw, state.resolveApplication);
13114
+ const pin = pinned.indexOf(app.identifier);
13115
+ const icon2 = () => {
13116
+ const custom = typeof options.renderApplicationIcon === "function" ? options.renderApplicationIcon(app.raw, app) : app.type === "weblet" && typeof options.renderWebletIcon === "function" ? options.renderWebletIcon(app.raw, app) : null;
13117
+ return custom ?? moduleChip(app.module, {
13118
+ size: 36,
13119
+ standard: Boolean(options.standardIcons),
13120
+ color: app.color
13121
+ });
13122
+ };
13123
+ items.push({
13124
+ id: app.identifier,
13125
+ label: app.label,
13126
+ keywords: [app.identifier, app.module],
13127
+ group: labels.applications,
13128
+ groupOrder: 0,
13129
+ kind: "application",
13130
+ icon: icon2,
13131
+ pinned: pin >= 0 ? pin : void 0,
13132
+ current: app.identifier === state.activeIdentifier,
13133
+ disabled: !resolved.href && typeof resolved.invoke !== "function",
13134
+ ...resolved,
13135
+ _zeyos: app
13136
+ });
13137
+ };
13138
+ for (const raw2 of Array.isArray(state.menuApps) ? state.menuApps : []) add(raw2);
13139
+ if (Array.isArray(state.modules)) {
13140
+ for (const module of state.modules) {
13141
+ if (typeof module === "string") add([module, moduleInfo(module).label]);
13142
+ else add(module, module?.id ?? module?.module, module?.label);
13143
+ }
13144
+ } else {
13145
+ for (const [module, label] of Object.entries(state.modules ?? {})) add([module, label]);
13146
+ }
13147
+ return items;
13148
+ }
13149
+ function normalizeZeyosLauncherRecords(payload, options = {}) {
13150
+ const query = String(options.query ?? "");
13151
+ const labels = launcherLabels(options.labels);
13152
+ const records = flattenRecords(payload, query);
13153
+ const groupOrders = /* @__PURE__ */ new Map();
13154
+ return records.flatMap(({ raw: raw2, group }, itemOrder) => {
13155
+ const record = normalizeRecord(raw2, group, query);
13156
+ if (!record) return [];
13157
+ const groupName = query === "" ? labels.recent : record.group === "" ? labels.results : labels.entities[record.group] ?? record.group;
13158
+ if (!groupOrders.has(groupName)) groupOrders.set(groupName, groupOrders.size + 1);
13159
+ const resolved = mergeDestination(record.raw, options.resolveRecord);
13160
+ return [{
13161
+ id: `record:${record.entity}:${record.fork ?? ""}:${record.ID}`,
13162
+ label: record.name,
13163
+ description: record.sec,
13164
+ keywords: [record.entity, record.ID, record.fork ?? ""],
13165
+ group: groupName,
13166
+ groupOrder: groupOrders.get(groupName),
13167
+ itemOrder,
13168
+ kind: "record",
13169
+ when: query === "" ? "empty" : "query",
13170
+ icon: () => {
13171
+ const custom = typeof options.renderRecordIcon === "function" ? options.renderRecordIcon(record.raw, record) : null;
13172
+ return custom ?? moduleChip(record.entity, {
13173
+ size: 28,
13174
+ standard: Boolean(options.standardIcons)
13175
+ });
13176
+ },
13177
+ disabled: !resolved.href && typeof resolved.invoke !== "function",
13178
+ ...resolved,
13179
+ _zeyos: record
13180
+ }];
13181
+ });
13182
+ }
13183
+ function zeyosActiveIdentifier(state = {}) {
13184
+ if (state.weblet != null && state.weblet !== "") return `weblet#${state.weblet}`;
13185
+ if (state.fork != null && state.fork !== "") return `fork#${state.fork}`;
13186
+ return String(state.module ?? "");
13187
+ }
13188
+ function normalizeApplication(raw2, fallbackId, fallbackLabel) {
13189
+ if (Array.isArray(raw2)) {
13190
+ if (raw2.length < 2) return null;
13191
+ if (raw2.length === 2) {
13192
+ const module = String(raw2[0]);
13193
+ return { raw: raw2, identifier: module, label: String(raw2[1]), module, type: "module", color: null };
13194
+ }
13195
+ if (raw2.length === 4) {
13196
+ const module = String(raw2[2] || "default");
13197
+ return {
13198
+ raw: raw2,
13199
+ identifier: `fork#${raw2[0]}`,
13200
+ label: String(raw2[1]),
13201
+ module,
13202
+ type: "fork",
13203
+ color: normalizeServerColor(raw2[3])
13204
+ };
13205
+ }
13206
+ return {
13207
+ raw: raw2,
13208
+ identifier: `weblet#${raw2[0]}`,
13209
+ label: String(raw2[1]),
13210
+ module: "weblets",
13211
+ type: "weblet",
13212
+ color: normalizeServerColor(raw2[8])
13213
+ };
13214
+ }
13215
+ if (!raw2 || typeof raw2 !== "object") return null;
13216
+ const identifier = String(raw2.identifier ?? raw2.id ?? fallbackId ?? "");
13217
+ if (!identifier) return null;
13218
+ const type = identifier.startsWith("fork#") ? "fork" : identifier.startsWith("weblet#") ? "weblet" : "module";
13219
+ return {
13220
+ raw: raw2,
13221
+ identifier,
13222
+ label: String(raw2.label ?? fallbackLabel ?? moduleInfo(identifier).label),
13223
+ module: String(raw2.module ?? (type === "module" ? identifier : type === "fork" ? "default" : "weblets")),
13224
+ type,
13225
+ color: normalizeServerColor(raw2.color)
13226
+ };
13227
+ }
13228
+ function flattenRecords(payload, query) {
13229
+ if (Array.isArray(payload)) return payload.map((raw2) => ({ raw: raw2, group: raw2?.group ?? "" }));
13230
+ if (!payload || typeof payload !== "object") return [];
13231
+ const result = [];
13232
+ for (const [group, rows] of Object.entries(payload)) {
13233
+ if (!Array.isArray(rows)) continue;
13234
+ for (const row of rows) {
13235
+ if (!Array.isArray(row)) {
13236
+ result.push({ raw: row, group });
13237
+ continue;
13238
+ }
13239
+ 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 };
13240
+ result.push({ raw: raw2, group });
13241
+ }
13242
+ }
13243
+ return result;
13244
+ }
13245
+ function normalizeRecord(raw2, group, query) {
13246
+ if (Array.isArray(raw2)) {
13247
+ if (query === "" && raw2.length >= 4) {
13248
+ return {
13249
+ raw: raw2,
13250
+ entity: String(raw2[0]),
13251
+ ID: raw2[1],
13252
+ fork: raw2[2] ?? null,
13253
+ name: String(raw2[3]),
13254
+ sec: String(raw2[4] ?? ""),
13255
+ group: ""
13256
+ };
13257
+ }
13258
+ return null;
13259
+ }
13260
+ if (!raw2 || typeof raw2 !== "object" || raw2.ID == null || raw2.name == null) return null;
13261
+ return {
13262
+ raw: raw2,
13263
+ entity: String(raw2.entity ?? group ?? "records"),
13264
+ ID: raw2.ID,
13265
+ fork: raw2.fork ?? null,
13266
+ name: String(raw2.name),
13267
+ sec: String(raw2.sec ?? ""),
13268
+ group: String(raw2.group ?? group ?? "")
13269
+ };
13270
+ }
13271
+ function mergeDestination(raw2, resolver) {
13272
+ const direct = raw2 && typeof raw2 === "object" && !Array.isArray(raw2) ? raw2 : {};
13273
+ const resolved = typeof resolver === "function" ? resolver(raw2) : null;
13274
+ const destination = resolved && typeof resolved === "object" ? resolved : {};
13275
+ return Object.fromEntries(["href", "target", "invoke", "value"].flatMap((key) => {
13276
+ const value = destination[key] ?? direct[key];
13277
+ return value === void 0 ? [] : [[key, value]];
13278
+ }));
13279
+ }
13280
+ function normalizeServerColor(value) {
13281
+ const source = String(value ?? "").trim();
13282
+ if (!source) return null;
13283
+ const color = source.startsWith("#") ? source : `#${source}`;
13284
+ return /^#[\da-f]{3}(?:[\da-f]{3})?$/i.test(color) ? color : null;
13285
+ }
13286
+ function launcherLabels(input) {
13287
+ const labels = input && typeof input === "object" ? input : {};
13288
+ return {
13289
+ applications: String(labels.applications ?? "Applications"),
13290
+ recent: String(labels.recent ?? "Recent"),
13291
+ results: String(labels.results ?? "Top results"),
13292
+ entities: labels.entities && typeof labels.entities === "object" ? labels.entities : {},
13293
+ move: String(labels.move ?? "Move"),
13294
+ open: String(labels.open ?? "Open"),
13295
+ close: String(labels.close ?? "Close")
13296
+ };
13297
+ }
13298
+
12124
13299
  // src/zeyos/select.js
12125
13300
  function buildZeyosSelectConfig(client, resource, opts = {}) {
12126
13301
  const {
@@ -12162,6 +13337,90 @@ function zeyosSelect(client, resource, opts = {}) {
12162
13337
  select.ready = hydrateInitialValue(select, client, resource, requestedValue);
12163
13338
  return select;
12164
13339
  }
13340
+ function buildZeyosEntitySelectConfig(client, resource, opts = {}) {
13341
+ const {
13342
+ fields,
13343
+ labelKey = inferLabelKey(client, resource),
13344
+ valueKey = "ID",
13345
+ searchFields = inferSearchFields(client, resource),
13346
+ subtitleKey = null,
13347
+ moduleKey = () => resource,
13348
+ groupKey = null,
13349
+ none = true,
13350
+ create = false,
13351
+ renderIcon = defaultEntityIcon,
13352
+ renderItem,
13353
+ renderValueAdornment,
13354
+ ...rest
13355
+ } = opts;
13356
+ const projection = fields ?? defaultProjection(valueKey, labelKey, [
13357
+ ...searchFields,
13358
+ ...[subtitleKey, moduleKey, groupKey].filter((reader) => typeof reader === "string")
13359
+ ]);
13360
+ const base = buildZeyosSelectConfig(client, resource, {
13361
+ ...rest,
13362
+ fields: projection,
13363
+ labelKey,
13364
+ valueKey,
13365
+ searchFields,
13366
+ groupKey,
13367
+ clearable: none === false ? Boolean(rest.clearable) : true
13368
+ });
13369
+ const renderModuleIcon = (item, selected = false) => {
13370
+ if (renderIcon === null) return null;
13371
+ const module = readRecord(item, moduleKey) ?? resource;
13372
+ return renderIcon(item, { resource, module: String(module), selected });
13373
+ };
13374
+ base.renderItem = renderItem ?? ((item) => {
13375
+ const visual = renderModuleIcon(item);
13376
+ return h(
13377
+ "span",
13378
+ { class: "zx-select__entity" },
13379
+ visual ? h("span", {
13380
+ class: "zx-select__entity-icon",
13381
+ ariaHidden: "true"
13382
+ }, visual) : null,
13383
+ h(
13384
+ "span",
13385
+ { class: "zx-select__entity-copy" },
13386
+ h("span", { class: "zx-select__entity-label" }, String(readRecord(item, labelKey) ?? "")),
13387
+ subtitleKey && readRecord(item, subtitleKey) != null ? h("span", { class: "zx-select__entity-subtitle" }, String(readRecord(item, subtitleKey))) : null
13388
+ )
13389
+ );
13390
+ });
13391
+ base.renderValueAdornment = renderValueAdornment ?? ((item) => renderModuleIcon(item, true));
13392
+ if (none !== false) {
13393
+ base.noneLabel = typeof none === "string" ? none : rest.noneLabel ?? "No selection";
13394
+ base.renderNone = rest.renderNone ?? ((label) => h(
13395
+ "span",
13396
+ { class: "zx-select__entity-none" },
13397
+ h("span", { class: "zx-select__entity-none-icon", ariaHidden: "true" }),
13398
+ h("span", {}, label)
13399
+ ));
13400
+ }
13401
+ if (create) {
13402
+ const descriptor = create === true ? {} : create;
13403
+ const invoke = typeof descriptor.oninvoke === "function" ? descriptor.oninvoke : null;
13404
+ base.actions = [...Array.isArray(base.actions) ? base.actions : [], {
13405
+ id: descriptor.id ?? "create",
13406
+ label: descriptor.label ?? "Create new\u2026",
13407
+ group: descriptor.group ?? "New",
13408
+ icon: descriptor.icon ?? "plus",
13409
+ invoke: (detail) => invoke?.({ ...detail, resource })
13410
+ }];
13411
+ }
13412
+ return base;
13413
+ }
13414
+ function zeyosEntitySelect(client, resource, opts = {}) {
13415
+ const config2 = buildZeyosEntitySelectConfig(client, resource, opts);
13416
+ const requestedValue = opts.value ?? null;
13417
+ const select = (
13418
+ /** @type {Select & {ready: Promise<Select>}} */
13419
+ new Select(null, config2)
13420
+ );
13421
+ select.ready = hydrateInitialValue(select, client, resource, requestedValue);
13422
+ return select;
13423
+ }
12165
13424
  function normalizeZeyosListResult(client, result) {
12166
13425
  if (typeof client?.normalizeListResult === "function") {
12167
13426
  return normalizeLocalListResult(client.normalizeListResult(result));
@@ -12178,12 +13437,12 @@ function normalizeLocalListResult(result) {
12178
13437
  async function hydrateInitialValue(select, client, resource, value) {
12179
13438
  if (value == null) return select;
12180
13439
  const record = value && typeof value === "object" ? value : await connect(client).get(resource, value);
12181
- const normalized = normalizeRecord(record);
13440
+ const normalized = normalizeRecord2(record);
12182
13441
  select.setItems([normalized]);
12183
13442
  select.set(normalized, { silent: true });
12184
13443
  return select;
12185
13444
  }
12186
- function normalizeRecord(result) {
13445
+ function normalizeRecord2(result) {
12187
13446
  if (result && typeof result === "object" && !Array.isArray(result)) {
12188
13447
  const nested = result.data;
12189
13448
  if (nested && typeof nested === "object" && !Array.isArray(nested)) return nested;
@@ -12191,6 +13450,16 @@ function normalizeRecord(result) {
12191
13450
  }
12192
13451
  throw new TypeError("ZeyOS get operation did not return a record");
12193
13452
  }
13453
+ function readRecord(item, reader) {
13454
+ if (typeof reader === "function") return reader(item);
13455
+ if (item && typeof item === "object" && typeof reader === "string") return item[reader];
13456
+ return null;
13457
+ }
13458
+ function defaultEntityIcon(_item, context) {
13459
+ const chip = moduleChip(String(context.module ?? "default"), { size: 20 });
13460
+ chip.replaceChildren(icon("square", { size: 10 }));
13461
+ return chip;
13462
+ }
12194
13463
  function defaultProjection(valueKey, labelKey, searchFields) {
12195
13464
  const keys = [valueKey, labelKey, ...searchFields].filter((key) => typeof key === "string" && key);
12196
13465
  return keys.length ? [...new Set(keys)] : void 0;
@@ -12270,7 +13539,7 @@ function zeyosForm(client, resource, opts = {}) {
12270
13539
  await Promise.all(config2.entityFields.map(async ({ id, resource: referencedResource2 }) => {
12271
13540
  const value = record[id];
12272
13541
  if (value == null) return;
12273
- const selected = value && typeof value === "object" ? value : normalizeRecord2(await facade.get(referencedResource2, value));
13542
+ const selected = value && typeof value === "object" ? value : normalizeRecord3(await facade.get(referencedResource2, value));
12274
13543
  const select = Select.from(form.getField(id)?.getInput());
12275
13544
  if (select instanceof Select) select.setItems([selected]);
12276
13545
  }));
@@ -12279,7 +13548,7 @@ function zeyosForm(client, resource, opts = {}) {
12279
13548
  };
12280
13549
  const load = async (id) => {
12281
13550
  if (id == null || id === "") throw new TypeError("zeyosForm.load() requires an id");
12282
- const record = normalizeRecord2(await facade.get(resource, id));
13551
+ const record = normalizeRecord3(await facade.get(resource, id));
12283
13552
  currentId = record.ID ?? id;
12284
13553
  await applyRecord(record);
12285
13554
  return record;
@@ -12298,7 +13567,7 @@ function zeyosForm(client, resource, opts = {}) {
12298
13567
  throw error;
12299
13568
  }
12300
13569
  const result = currentId == null ? await facade.create(resource, payload) : await facade.update(resource, { ID: currentId, ...payload });
12301
- const record = normalizeRecord2(result);
13570
+ const record = normalizeRecord3(result);
12302
13571
  currentId = record.ID ?? currentId;
12303
13572
  if (opts.onSaved) {
12304
13573
  try {
@@ -12376,7 +13645,7 @@ function normalizeColumns(value) {
12376
13645
  [1, 2, 3].includes(number) ? number : 2
12377
13646
  );
12378
13647
  }
12379
- function normalizeRecord2(result) {
13648
+ function normalizeRecord3(result) {
12380
13649
  if (result && typeof result === "object" && !Array.isArray(result)) {
12381
13650
  const nested = result.data;
12382
13651
  if (nested && typeof nested === "object" && !Array.isArray(nested)) return nested;
@@ -12571,7 +13840,11 @@ function copyFilters(filters) {
12571
13840
  }
12572
13841
  export {
12573
13842
  ZEYOS_ICON_KIT,
13843
+ ZEYOS_LAUNCHER_APPLICATIONS,
12574
13844
  buildListQuery,
13845
+ buildZeyosEntitySelectConfig,
13846
+ buildZeyosLauncherConfig,
13847
+ buildZeyosSelectConfig,
12575
13848
  connect,
12576
13849
  dataFilterStateToFilters,
12577
13850
  dateToUnixSeconds,
@@ -12586,13 +13859,18 @@ export {
12586
13859
  moduleInfo,
12587
13860
  moduleKeys,
12588
13861
  normalizeModuleName,
13862
+ normalizeZeyosLauncherApplications,
13863
+ normalizeZeyosLauncherRecords,
12589
13864
  registerFieldType,
12590
13865
  registerModules,
12591
13866
  resolveFields,
12592
13867
  tableSortToQuery,
12593
13868
  unixSecondsToDate,
12594
13869
  useZeyosIcons,
13870
+ zeyosActiveIdentifier,
13871
+ zeyosEntitySelect,
12595
13872
  zeyosForm,
13873
+ zeyosLauncher,
12596
13874
  zeyosModules,
12597
13875
  zeyosSelect,
12598
13876
  zeyosTable