@zeyos/zx 2.2.1 → 2.3.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 (39) hide show
  1. package/CHANGELOG.md +110 -0
  2. package/README.md +28 -2
  3. package/dist/types/components/multi-value-editor/multi-value-editor.d.ts +67 -8
  4. package/dist/types/components/permission/permission.d.ts +3 -0
  5. package/dist/types/components/select/select.d.ts +36 -1
  6. package/dist/types/components/tabbox/tabbox.d.ts +12 -3
  7. package/dist/zx-compat.esm.js +302 -34
  8. package/dist/zx-compat.esm.min.js +1 -1
  9. package/dist/zx-compat.global.js +302 -34
  10. package/dist/zx-compat.global.min.js +3 -3
  11. package/dist/zx-zeyos.esm.js +280 -27
  12. package/dist/zx-zeyos.esm.min.js +1 -1
  13. package/dist/zx.css +159 -33
  14. package/dist/zx.esm.js +302 -34
  15. package/dist/zx.esm.min.js +3 -3
  16. package/dist/zx.global.js +302 -34
  17. package/dist/zx.global.min.js +3 -3
  18. package/dist/zx.min.css +1 -1
  19. package/docs/api.json +420 -17
  20. package/docs/llms.md +54 -12
  21. package/package.json +1 -1
  22. package/src/components/date-picker/date-range-picker.css +7 -0
  23. package/src/components/fieldset/fieldset.css +7 -0
  24. package/src/components/form/form.css +7 -0
  25. package/src/components/multi-value-editor/multi-value-editor.css +99 -15
  26. package/src/components/multi-value-editor/multi-value-editor.js +235 -18
  27. package/src/components/navigation-bar/navigation-bar.css +7 -0
  28. package/src/components/permission/permission.js +3 -0
  29. package/src/components/search/search.css +10 -2
  30. package/src/components/select/select.css +6 -0
  31. package/src/components/select/select.js +99 -11
  32. package/src/components/stepper/stepper.css +12 -2
  33. package/src/components/tabbox/tabbox.css +13 -3
  34. package/src/components/tabbox/tabbox.js +34 -10
  35. package/styles/tokens/dark.css +13 -10
  36. package/styles/tokens/global.css +12 -0
  37. package/styles/tokens/semantic.css +9 -5
  38. package/styles/tokens/themes.css +72 -0
  39. package/styles/zx.css +1 -0
package/dist/zx.esm.js CHANGED
@@ -4988,18 +4988,34 @@ function registerFieldUploadAdapter() {
4988
4988
  }
4989
4989
 
4990
4990
  // src/components/multi-value-editor/multi-value-editor.js
4991
+ var REORDER = Object.freeze({
4992
+ both: { drag: true, buttons: true },
4993
+ buttons: { drag: false, buttons: true },
4994
+ drag: { drag: true, buttons: false },
4995
+ none: { drag: false, buttons: false }
4996
+ });
4997
+ var PARTS = Object.freeze({
4998
+ value: ".zx-multi-value-editor__value",
4999
+ handle: ".zx-multi-value-editor__handle",
5000
+ up: '[data-action="up"]',
5001
+ down: '[data-action="down"]'
5002
+ });
4991
5003
  var MultiValueEditor = class extends Component {
4992
5004
  static cssName = "multi-value-editor";
4993
5005
  /** @type {MultiValueEditorOptions} */
4994
- static defaults = { values: [], options: null, addLabel: "Add value" };
5006
+ static defaults = { values: [], options: null, addLabel: "Add value", reorder: "both" };
4995
5007
  /** @returns {HTMLElement} */
4996
5008
  render() {
4997
5009
  const root = (
4998
5010
  /** @type {HTMLElement} */
4999
5011
  this.el ?? h("div")
5000
5012
  );
5013
+ this.el = root;
5001
5014
  this._values = Array.isArray(this.options.values) ? this.options.values.map(String) : [];
5002
5015
  this._ownedNodes = [];
5016
+ this._initialState = root.getAttribute("data-state");
5017
+ this._dragIndex = -1;
5018
+ this._armed = null;
5003
5019
  this._rows = h("div", { class: "zx-multi-value-editor__rows" });
5004
5020
  this._add = h("button", {
5005
5021
  class: "zx-btn zx-multi-value-editor__add",
@@ -5010,6 +5026,17 @@ var MultiValueEditor = class extends Component {
5010
5026
  root.append(this._rows, this._add);
5011
5027
  this.listen(root, "click", (event) => this._handleClick(event));
5012
5028
  this.listen(this._rows, "change", (event) => this._handleChange(event));
5029
+ this.listen(this._rows, "keydown", (event) => this._handleKeydown(event));
5030
+ if (this._reorder().drag) {
5031
+ this.listen(this._rows, "pointerdown", (event) => this._handlePointerDown(event));
5032
+ this.listen(this._rows, "dragstart", (event) => this._handleDragStart(event));
5033
+ this.listen(this._rows, "dragover", (event) => this._handleDragOver(event));
5034
+ this.listen(this._rows, "dragleave", (event) => this._handleDragLeave(event));
5035
+ this.listen(this._rows, "drop", (event) => this._handleDrop(event));
5036
+ this.listen(this._rows, "dragend", () => this._finishDrag());
5037
+ this.listen(document, "pointerup", () => this._disarm());
5038
+ this.listen(document, "pointercancel", () => this._disarm());
5039
+ }
5013
5040
  this._renderRows();
5014
5041
  return root;
5015
5042
  }
@@ -5032,16 +5059,27 @@ var MultiValueEditor = class extends Component {
5032
5059
  if (!silent) this.emit("change", { values: this.getValues() });
5033
5060
  return this;
5034
5061
  }
5035
- /** @param {number} [focusIndex=-1] @returns {void} */
5036
- _renderRows(focusIndex = -1) {
5062
+ /** @returns {{drag: boolean, buttons: boolean}} */
5063
+ _reorder() {
5064
+ return REORDER[this.options.reorder] ?? REORDER.both;
5065
+ }
5066
+ /**
5067
+ * @param {number} [focusIndex=-1] Row to focus after the render, or -1 for none.
5068
+ * @param {keyof PARTS} [focusPart='value'] Which control of that row takes focus.
5069
+ * @returns {void}
5070
+ */
5071
+ _renderRows(focusIndex = -1, focusPart = "value") {
5037
5072
  const choices = normalizeOptions2(this.options.options);
5038
5073
  const useSelect = this.options.options !== null;
5074
+ const { drag, buttons } = this._reorder();
5075
+ const last = this._values.length - 1;
5039
5076
  const rows = this._values.map((value, index) => {
5077
+ const entries = !useSelect || choices.some(([option]) => option === value) ? choices : [[value, value], ...choices];
5040
5078
  const control = useSelect ? h("select", {
5041
5079
  class: "zx-multi-value-editor__value",
5042
5080
  ariaLabel: `Value ${index + 1}`,
5043
5081
  dataset: { index }
5044
- }, choices.map(([optionValue, label]) => h("option", {
5082
+ }, entries.map(([optionValue, label]) => h("option", {
5045
5083
  value: optionValue,
5046
5084
  selected: optionValue === value
5047
5085
  }, label))) : h("input", {
@@ -5054,40 +5092,52 @@ var MultiValueEditor = class extends Component {
5054
5092
  return h(
5055
5093
  "div",
5056
5094
  { class: "zx-multi-value-editor__row", dataset: { row: index } },
5095
+ drag ? h("button", {
5096
+ class: "zx-multi-value-editor__handle",
5097
+ type: "button",
5098
+ ariaLabel: `Reorder value ${index + 1}. Use the arrow keys to move it.`,
5099
+ dataset: { action: "grab", index }
5100
+ }, icon("drag", { size: 12 })) : null,
5057
5101
  control,
5058
5102
  h(
5059
5103
  "div",
5060
5104
  { class: "zx-multi-value-editor__controls" },
5061
- h("button", {
5105
+ buttons ? h("button", {
5062
5106
  class: "zx-btn",
5063
5107
  type: "button",
5064
- disabled: index === 0,
5108
+ // `aria-disabled` rather than `disabled`: the buttons re-render on every move, and a
5109
+ // control that disables itself under the reader's finger drops focus to the document.
5110
+ ariaDisabled: String(index === 0),
5065
5111
  ariaLabel: `Move value ${index + 1} up`,
5066
5112
  dataset: { action: "up", index }
5067
- }, "\u2191"),
5068
- h("button", {
5113
+ }, icon("chevron-up", { size: 12 })) : null,
5114
+ buttons ? h("button", {
5069
5115
  class: "zx-btn",
5070
5116
  type: "button",
5071
- disabled: index === this._values.length - 1,
5117
+ ariaDisabled: String(index === last),
5072
5118
  ariaLabel: `Move value ${index + 1} down`,
5073
5119
  dataset: { action: "down", index }
5074
- }, "\u2193"),
5120
+ }, icon("chevron-down", { size: 12 })) : null,
5075
5121
  h("button", {
5076
5122
  class: "zx-btn",
5077
5123
  type: "button",
5078
5124
  ariaLabel: `Remove value ${index + 1}`,
5079
5125
  dataset: { action: "remove", index }
5080
- }, "\xD7")
5126
+ }, icon("x", { size: 12 }))
5081
5127
  )
5082
5128
  );
5083
5129
  });
5084
5130
  this._rows.replaceChildren(...rows);
5085
- if (focusIndex >= 0) rows[focusIndex]?.querySelector(".zx-multi-value-editor__value")?.focus();
5131
+ if (focusIndex >= 0) {
5132
+ const row2 = rows[Math.min(focusIndex, rows.length - 1)];
5133
+ row2?.querySelector(PARTS[focusPart] ?? PARTS.value)?.focus();
5134
+ }
5086
5135
  }
5087
5136
  /** @param {MouseEvent} event @returns {void} */
5088
5137
  _handleClick(event) {
5089
5138
  const button2 = event.target instanceof Element ? event.target.closest("[data-action]") : null;
5090
5139
  if (!button2 || !this.el.contains(button2)) return;
5140
+ if (button2.getAttribute("aria-disabled") === "true") return;
5091
5141
  const action = button2.dataset.action;
5092
5142
  const index = Number(button2.dataset.index);
5093
5143
  if (action === "add") {
@@ -5100,28 +5150,156 @@ var MultiValueEditor = class extends Component {
5100
5150
  this._renderRows(Math.min(index, this._values.length - 1));
5101
5151
  this._emitChange();
5102
5152
  } else if ((action === "up" || action === "down") && Number.isInteger(index)) {
5103
- const target = index + (action === "up" ? -1 : 1);
5104
- if (target < 0 || target >= this._values.length) return;
5105
- [this._values[index], this._values[target]] = [this._values[target], this._values[index]];
5106
- this._renderRows(target);
5107
- this._emitChange();
5153
+ this._move(index, index + (action === "up" ? -1 : 1), action);
5108
5154
  }
5109
5155
  }
5110
5156
  /** @param {Event} event @returns {void} */
5111
5157
  _handleChange(event) {
5112
5158
  const control = event.target instanceof Element ? event.target.closest("[data-index]") : null;
5113
- if (!control) return;
5159
+ if (!control || !("value" in control)) return;
5114
5160
  const index = Number(control.dataset.index);
5115
5161
  if (!Number.isInteger(index) || index < 0 || index >= this._values.length) return;
5116
5162
  this._values[index] = control.value;
5117
5163
  this._emitChange();
5118
5164
  }
5165
+ /**
5166
+ * The handle is the keyboard equivalent of dragging it, so `reorder: 'drag'` is still operable
5167
+ * without a pointer.
5168
+ * @param {KeyboardEvent} event
5169
+ * @returns {void}
5170
+ */
5171
+ _handleKeydown(event) {
5172
+ const handle = event.target instanceof Element ? event.target.closest(".zx-multi-value-editor__handle") : null;
5173
+ if (!handle) return;
5174
+ const index = Number(handle.dataset.index);
5175
+ const target = event.key === "ArrowUp" ? index - 1 : event.key === "ArrowDown" ? index + 1 : event.key === "Home" ? 0 : event.key === "End" ? this._values.length - 1 : -1;
5176
+ if (target < 0 || target >= this._values.length) return;
5177
+ event.preventDefault();
5178
+ this._move(index, target, "handle");
5179
+ }
5180
+ /**
5181
+ * Arms the row for dragging. Only the handle does this, so text selection inside a row's input
5182
+ * keeps working and a stray drag cannot reorder anything.
5183
+ * @param {PointerEvent} event
5184
+ * @returns {void}
5185
+ */
5186
+ _handlePointerDown(event) {
5187
+ const handle = event.target instanceof Element ? event.target.closest(".zx-multi-value-editor__handle") : null;
5188
+ this._disarm();
5189
+ if (!handle || handle.disabled) return;
5190
+ const row2 = (
5191
+ /** @type {HTMLElement|null} */
5192
+ handle.closest(".zx-multi-value-editor__row")
5193
+ );
5194
+ if (!row2) return;
5195
+ row2.draggable = true;
5196
+ this._armed = row2;
5197
+ }
5198
+ /** @returns {void} */
5199
+ _disarm() {
5200
+ if (this._armed) this._armed.draggable = false;
5201
+ this._armed = null;
5202
+ }
5203
+ /** @param {DragEvent} event @returns {void} */
5204
+ _handleDragStart(event) {
5205
+ const row2 = event.target instanceof Element ? event.target.closest(".zx-multi-value-editor__row") : null;
5206
+ if (!row2 || row2 !== this._armed) {
5207
+ event.preventDefault();
5208
+ return;
5209
+ }
5210
+ this._dragIndex = Number(row2.dataset.row);
5211
+ row2.dataset.dragging = "true";
5212
+ this.el.setAttribute("data-state", "dragging");
5213
+ event.dataTransfer?.setData("text/plain", String(this._dragIndex));
5214
+ if (event.dataTransfer) event.dataTransfer.effectAllowed = "move";
5215
+ }
5216
+ /** @param {DragEvent} event @returns {void} */
5217
+ _handleDragOver(event) {
5218
+ if (this._dragIndex < 0) return;
5219
+ event.preventDefault();
5220
+ if (event.dataTransfer) event.dataTransfer.dropEffect = "move";
5221
+ this._showDrop(this._dropTarget(event.clientY));
5222
+ }
5223
+ /** @param {DragEvent} event @returns {void} */
5224
+ _handleDragLeave(event) {
5225
+ const to = event.relatedTarget;
5226
+ if (to instanceof Node && this._rows.contains(to)) return;
5227
+ this._clearDrop();
5228
+ }
5229
+ /** @param {DragEvent} event @returns {void} */
5230
+ _handleDrop(event) {
5231
+ if (this._dragIndex < 0) return;
5232
+ event.preventDefault();
5233
+ const from = this._dragIndex;
5234
+ const slot = this._dropTarget(event.clientY);
5235
+ this._finishDrag();
5236
+ this._move(from, slot > from ? slot - 1 : slot, "handle");
5237
+ }
5238
+ /** @returns {void} */
5239
+ _finishDrag() {
5240
+ this._dragIndex = -1;
5241
+ this._disarm();
5242
+ this._clearDrop();
5243
+ for (const row2 of this._rows.children) delete /** @type {HTMLElement} */
5244
+ row2.dataset.dragging;
5245
+ if (this._initialState === null) this.el.removeAttribute("data-state");
5246
+ else this.el.setAttribute("data-state", this._initialState);
5247
+ }
5248
+ /**
5249
+ * Insertion slot (0…length) for a pointer at `clientY`.
5250
+ * @param {number} clientY Pointer position.
5251
+ * @returns {number}
5252
+ */
5253
+ _dropTarget(clientY) {
5254
+ const rows = [...this._rows.children];
5255
+ for (const [index, row2] of rows.entries()) {
5256
+ const box = row2.getBoundingClientRect();
5257
+ if (clientY < box.top + box.height / 2) return index;
5258
+ }
5259
+ return rows.length;
5260
+ }
5261
+ /** @param {number} slot Insertion slot to mark. @returns {void} */
5262
+ _showDrop(slot) {
5263
+ const rows = [...this._rows.children];
5264
+ const inert = slot === this._dragIndex || slot === this._dragIndex + 1;
5265
+ for (const [index, row2] of rows.entries()) {
5266
+ const cell = (
5267
+ /** @type {HTMLElement} */
5268
+ row2
5269
+ );
5270
+ if (!inert && index === slot) cell.dataset.drop = "before";
5271
+ else if (!inert && slot === rows.length && index === rows.length - 1) cell.dataset.drop = "after";
5272
+ else delete cell.dataset.drop;
5273
+ }
5274
+ }
5275
+ /** @returns {void} */
5276
+ _clearDrop() {
5277
+ for (const row2 of this._rows.children) delete /** @type {HTMLElement} */
5278
+ row2.dataset.drop;
5279
+ }
5280
+ /**
5281
+ * @param {number} from Index to move.
5282
+ * @param {number} to Destination index.
5283
+ * @param {keyof PARTS} part Control of the moved row that takes focus.
5284
+ * @returns {boolean} Whether anything moved.
5285
+ */
5286
+ _move(from, to, part) {
5287
+ const size2 = this._values.length;
5288
+ if (from === to || from < 0 || to < 0 || from >= size2 || to >= size2) return false;
5289
+ const [value] = this._values.splice(from, 1);
5290
+ this._values.splice(to, 0, value);
5291
+ this._renderRows(to, part);
5292
+ this._emitChange();
5293
+ return true;
5294
+ }
5119
5295
  /** @returns {void} */
5120
5296
  _emitChange() {
5121
5297
  this.emit("change", { values: this.getValues() });
5122
5298
  }
5123
5299
  /** @returns {void} */
5124
5300
  destroy() {
5301
+ if (this._initialState === null) this.el?.removeAttribute("data-state");
5302
+ else this.el?.setAttribute("data-state", this._initialState);
5125
5303
  for (const node of this._ownedNodes ?? []) node.remove();
5126
5304
  super.destroy();
5127
5305
  }
@@ -6191,6 +6369,7 @@ var Select = class _Select extends Component {
6191
6369
  /** @type {SelectOptions} */
6192
6370
  static defaults = {
6193
6371
  items: [],
6372
+ fixedItems: [],
6194
6373
  valueKey: "ID",
6195
6374
  labelKey: "name",
6196
6375
  renderItem: null,
@@ -6210,6 +6389,8 @@ var Select = class _Select extends Component {
6210
6389
  render() {
6211
6390
  this._items = [];
6212
6391
  this._visibleItems = [];
6392
+ this._fixedItems = [];
6393
+ this._visibleFixed = [];
6213
6394
  this._selected = null;
6214
6395
  this._optionEntries = [];
6215
6396
  this._activeIndex = -1;
@@ -6293,6 +6474,8 @@ var Select = class _Select extends Component {
6293
6474
  root.replaceChildren(control, panel);
6294
6475
  this._items = Array.isArray(this.options.items) ? this.options.items.slice() : [];
6295
6476
  this._visibleItems = this._items.slice();
6477
+ this._fixedItems = Array.isArray(this.options.fixedItems) ? this.options.fixedItems.slice() : [];
6478
+ this._visibleFixed = this._fixedItems.slice();
6296
6479
  this._disabled = Boolean(this.options.disabled);
6297
6480
  this._handleTypeahead = typeahead(
6298
6481
  () => this._optionEntries.filter((entry) => !entry.clear).map((entry) => entry.element),
@@ -6369,7 +6552,22 @@ var Select = class _Select extends Component {
6369
6552
  const selectedValue = this.value;
6370
6553
  this._items = Array.isArray(items) ? items.slice() : [];
6371
6554
  this._visibleItems = this._items.slice();
6372
- this._selected = selectedValue == null ? null : this._items.find((item) => Object.is(this._itemValue(item), selectedValue)) ?? null;
6555
+ this._selected = selectedValue == null ? null : this._findItem(selectedValue);
6556
+ this._syncInputValue();
6557
+ this._renderOptions();
6558
+ this._position?.update();
6559
+ return this;
6560
+ }
6561
+ /**
6562
+ * Replaces the pinned choices and preserves selection by ID when possible.
6563
+ * @param {SelectItem[]} items New fixed items.
6564
+ * @returns {this}
6565
+ */
6566
+ setFixedItems(items) {
6567
+ const selectedValue = this.value;
6568
+ this._fixedItems = Array.isArray(items) ? items.slice() : [];
6569
+ this._visibleFixed = this._fixedItems.slice();
6570
+ this._selected = selectedValue == null ? null : this._findItem(selectedValue);
6373
6571
  this._syncInputValue();
6374
6572
  this._renderOptions();
6375
6573
  this._position?.update();
@@ -6423,6 +6621,7 @@ var Select = class _Select extends Component {
6423
6621
  this.refs.input.removeAttribute("aria-activedescendant");
6424
6622
  this._activeIndex = -1;
6425
6623
  this._visibleItems = this._items.slice();
6624
+ this._visibleFixed = this._fixedItems.slice();
6426
6625
  this._syncInputValue();
6427
6626
  this.emit("close", {});
6428
6627
  return this;
@@ -6478,6 +6677,38 @@ var Select = class _Select extends Component {
6478
6677
  select.set(requestedValue, { silent: true });
6479
6678
  return select;
6480
6679
  }
6680
+ /**
6681
+ * Creates the record-permission picker: the fixed Private and Public choices above the groups a
6682
+ * record can be shared with. The value is the tri-state ZeyOS stores — `false` for private,
6683
+ * `true` for public, or a group ID — so the control binds straight to the record field.
6684
+ *
6685
+ * @param {Element|string|null} target Component target.
6686
+ * @param {SelectOptions & {groups?: SelectItem[]|((query: string) => Promise<SelectItem[]>|SelectItem[])}} [options={}]
6687
+ * Select overrides other than the fixed choices; `groups` holds the shareable groups, either as
6688
+ * an array (filtered locally) or as a loader function (queried remotely).
6689
+ * @returns {Select}
6690
+ */
6691
+ static permission(target, options = {}) {
6692
+ const { groups = [], value = true, valueKey = "ID", labelKey = "name", ...rest } = options;
6693
+ const loader = typeof groups === "function" ? groups : null;
6694
+ const shareable = loader ? [] : groups;
6695
+ const choices = [{ ID: false, name: "Private" }, { ID: true, name: "Public" }];
6696
+ const select = new _Select(target, {
6697
+ filter: loader ?? "local",
6698
+ ...rest,
6699
+ value: null,
6700
+ items: shareable,
6701
+ fixedItems: [],
6702
+ // The two choices carry their own ID and label, so a caller's group readers stay untouched.
6703
+ valueKey: (item) => choices.includes(item) ? item.ID : readItem(item, valueKey),
6704
+ labelKey: (item) => choices.includes(item) ? item.name : String(readItem(item, labelKey) ?? "")
6705
+ });
6706
+ choices[0].name = select._message("permission.private", "Private");
6707
+ choices[1].name = select._message("permission.public", "Public");
6708
+ select.setFixedItems(choices);
6709
+ select.set(value, { silent: true });
6710
+ return select;
6711
+ }
6481
6712
  /** Cleans up the panel, pending async work, and enhanced target contents. @returns {void} */
6482
6713
  destroy() {
6483
6714
  this._cancelQuery();
@@ -6548,8 +6779,8 @@ var Select = class _Select extends Component {
6548
6779
  );
6549
6780
  if (!this._open) this.open();
6550
6781
  if (this.options.filter === "local") {
6551
- const keys = this.options.searchKeys ?? [this.options.labelKey];
6552
- this._visibleItems = matchItems(this._items, query, keys);
6782
+ this._visibleFixed = this._matchFixed(query);
6783
+ this._visibleItems = matchItems(this._items, query, this._searchKeys());
6553
6784
  this._renderOptions();
6554
6785
  this._setActive(this._optionEntries.length > 0 ? 0 : -1);
6555
6786
  this._position?.update();
@@ -6604,6 +6835,10 @@ var Select = class _Select extends Component {
6604
6835
  if (this.options.clearable) {
6605
6836
  this._appendOption(null, true, this.options.placeholder || this._message("select.none", "No selection"));
6606
6837
  }
6838
+ for (const item of this._visibleFixed) this._appendOption(item, false);
6839
+ if (this._visibleFixed.length > 0 && this._visibleItems.length > 0 && !this.options.groupKey) {
6840
+ list.append(h("div", { class: "zx-select__separator", role: "presentation" }));
6841
+ }
6607
6842
  for (const item of this._visibleItems) {
6608
6843
  if (this.options.groupKey) {
6609
6844
  const group = this._read(item, this.options.groupKey);
@@ -6617,7 +6852,7 @@ var Select = class _Select extends Component {
6617
6852
  }
6618
6853
  this._appendOption(item, false);
6619
6854
  }
6620
- if (this._visibleItems.length === 0 && !this._loading) {
6855
+ if (this._visibleItems.length === 0 && this._visibleFixed.length === 0 && !this._loading) {
6621
6856
  list.append(h("div", {
6622
6857
  class: "zx-select__empty",
6623
6858
  role: "status"
@@ -6657,6 +6892,7 @@ var Select = class _Select extends Component {
6657
6892
  if (this._queryTimer !== null) clearTimeout(this._queryTimer);
6658
6893
  this._requestSequence += 1;
6659
6894
  const sequence = this._requestSequence;
6895
+ this._visibleFixed = this._matchFixed(query);
6660
6896
  if (query.length < Math.max(0, Number(this.options.minQuery) || 0)) {
6661
6897
  this._setLoading(false);
6662
6898
  this._visibleItems = [];
@@ -6741,13 +6977,14 @@ var Select = class _Select extends Component {
6741
6977
  }
6742
6978
  /** @param {unknown} candidate @returns {SelectItem|null} */
6743
6979
  _findItem(candidate) {
6744
- const direct = this._items.find((item) => item === candidate);
6980
+ const pool = this._fixedItems.length === 0 ? this._items : [...this._fixedItems, ...this._items];
6981
+ const direct = pool.find((item) => item === candidate);
6745
6982
  if (direct !== void 0) return direct;
6746
6983
  const value = candidate !== null && typeof candidate === "object" ? this._itemValue(
6747
6984
  /** @type {SelectItem} */
6748
6985
  candidate
6749
6986
  ) : candidate;
6750
- return this._items.find((item) => Object.is(this._itemValue(item), value)) ?? null;
6987
+ return pool.find((item) => Object.is(this._itemValue(item), value)) ?? null;
6751
6988
  }
6752
6989
  /** @param {SelectItem|null} item @returns {unknown} */
6753
6990
  _itemValue(item) {
@@ -6760,9 +6997,20 @@ var Select = class _Select extends Component {
6760
6997
  }
6761
6998
  /** @param {SelectItem|null} item @param {SelectValueReader|SelectLabelReader} reader @returns {unknown} */
6762
6999
  _read(item, reader) {
6763
- if (typeof reader === "function") return reader(item);
6764
- if (item === null || typeof item !== "object") return item;
6765
- return item[reader];
7000
+ return readItem(item, reader);
7001
+ }
7002
+ /** @returns {Array<SelectValueReader|SelectLabelReader>} */
7003
+ _searchKeys() {
7004
+ return this.options.searchKeys ?? [this.options.labelKey];
7005
+ }
7006
+ /**
7007
+ * Narrows the pinned choices. They are always matched here and never handed to a filter
7008
+ * function: a fixed choice belongs to the control, not to whatever the remote source returns.
7009
+ * @param {string} query Search text.
7010
+ * @returns {SelectItem[]}
7011
+ */
7012
+ _matchFixed(query) {
7013
+ return matchItems(this._fixedItems, query, this._searchKeys());
6766
7014
  }
6767
7015
  /** @returns {number} */
6768
7016
  _selectedEntryIndex() {
@@ -6785,6 +7033,11 @@ var Select = class _Select extends Component {
6785
7033
  return message === key ? fallback : message;
6786
7034
  }
6787
7035
  };
7036
+ function readItem(item, reader) {
7037
+ if (typeof reader === "function") return reader(item);
7038
+ if (item === null || typeof item !== "object") return item;
7039
+ return item[reader];
7040
+ }
6788
7041
  function isPrintable(event) {
6789
7042
  return event.key.length === 1 && !event.ctrlKey && !event.metaKey && !event.altKey;
6790
7043
  }
@@ -10298,6 +10551,10 @@ var Tabbox = class extends Component {
10298
10551
  if (!tab || !tablist.contains(tab)) return;
10299
10552
  const record = this._recordForTab(tab);
10300
10553
  if (!record || record.definition.disabled) return;
10554
+ if (event.target.closest?.(".zx-tabbox__close")) {
10555
+ this._closeTab(record);
10556
+ return;
10557
+ }
10301
10558
  this._setFocused(record);
10302
10559
  this.openTab(record.definition.name);
10303
10560
  });
@@ -10347,8 +10604,9 @@ var Tabbox = class extends Component {
10347
10604
  children.push(title, badge2);
10348
10605
  if (normalized.closable) {
10349
10606
  children.push(h("span", {
10350
- class: "zx-tabbox__close",
10351
- ariaHidden: "true"
10607
+ class: "zx-tabbox__close zx-icon-btn",
10608
+ ariaHidden: "true",
10609
+ title: "Close"
10352
10610
  }, icon("x", { size: 14 })));
10353
10611
  }
10354
10612
  const tab = (
@@ -10522,13 +10780,23 @@ var Tabbox = class extends Component {
10522
10780
  }
10523
10781
  if (event.key === "Delete" && current.definition.closable) {
10524
10782
  event.preventDefault();
10525
- const index = this._tabs.indexOf(current);
10526
- if (!this._removeTab(current.definition.name, true)) return;
10527
- const remaining = this._tabs[Math.min(index, this._tabs.length - 1)] ?? this._tabs[index - 1];
10528
- const focusTarget = remaining && !remaining.definition.disabled ? remaining : this._active;
10529
- if (focusTarget) this._setFocused(focusTarget);
10783
+ this._closeTab(current);
10530
10784
  }
10531
10785
  }
10786
+ /**
10787
+ * Closes a tab on a user gesture, keeping roving focus inside the tablist.
10788
+ * @param {TabRecord} record Tab to close.
10789
+ * @returns {void}
10790
+ * @fires Tabbox#close
10791
+ */
10792
+ _closeTab(record) {
10793
+ const index = this._tabs.indexOf(record);
10794
+ const held = record.tab.contains(document.activeElement);
10795
+ if (!this._removeTab(record.definition.name, true)) return;
10796
+ const remaining = this._tabs[Math.min(index, this._tabs.length - 1)] ?? this._tabs[index - 1];
10797
+ const focusTarget = remaining && !remaining.definition.disabled ? remaining : this._active;
10798
+ if (focusTarget) this._setFocused(focusTarget, held);
10799
+ }
10532
10800
  /** @param {string} name @param {boolean} emitClose @returns {boolean} */
10533
10801
  _removeTab(name, emitClose) {
10534
10802
  const record = this._find(name);