@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
@@ -5147,18 +5147,34 @@
5147
5147
  }
5148
5148
 
5149
5149
  // src/components/multi-value-editor/multi-value-editor.js
5150
+ var REORDER = Object.freeze({
5151
+ both: { drag: true, buttons: true },
5152
+ buttons: { drag: false, buttons: true },
5153
+ drag: { drag: true, buttons: false },
5154
+ none: { drag: false, buttons: false }
5155
+ });
5156
+ var PARTS = Object.freeze({
5157
+ value: ".zx-multi-value-editor__value",
5158
+ handle: ".zx-multi-value-editor__handle",
5159
+ up: '[data-action="up"]',
5160
+ down: '[data-action="down"]'
5161
+ });
5150
5162
  var MultiValueEditor = class extends Component {
5151
5163
  static cssName = "multi-value-editor";
5152
5164
  /** @type {MultiValueEditorOptions} */
5153
- static defaults = { values: [], options: null, addLabel: "Add value" };
5165
+ static defaults = { values: [], options: null, addLabel: "Add value", reorder: "both" };
5154
5166
  /** @returns {HTMLElement} */
5155
5167
  render() {
5156
5168
  const root = (
5157
5169
  /** @type {HTMLElement} */
5158
5170
  this.el ?? h("div")
5159
5171
  );
5172
+ this.el = root;
5160
5173
  this._values = Array.isArray(this.options.values) ? this.options.values.map(String) : [];
5161
5174
  this._ownedNodes = [];
5175
+ this._initialState = root.getAttribute("data-state");
5176
+ this._dragIndex = -1;
5177
+ this._armed = null;
5162
5178
  this._rows = h("div", { class: "zx-multi-value-editor__rows" });
5163
5179
  this._add = h("button", {
5164
5180
  class: "zx-btn zx-multi-value-editor__add",
@@ -5169,6 +5185,17 @@
5169
5185
  root.append(this._rows, this._add);
5170
5186
  this.listen(root, "click", (event) => this._handleClick(event));
5171
5187
  this.listen(this._rows, "change", (event) => this._handleChange(event));
5188
+ this.listen(this._rows, "keydown", (event) => this._handleKeydown(event));
5189
+ if (this._reorder().drag) {
5190
+ this.listen(this._rows, "pointerdown", (event) => this._handlePointerDown(event));
5191
+ this.listen(this._rows, "dragstart", (event) => this._handleDragStart(event));
5192
+ this.listen(this._rows, "dragover", (event) => this._handleDragOver(event));
5193
+ this.listen(this._rows, "dragleave", (event) => this._handleDragLeave(event));
5194
+ this.listen(this._rows, "drop", (event) => this._handleDrop(event));
5195
+ this.listen(this._rows, "dragend", () => this._finishDrag());
5196
+ this.listen(document, "pointerup", () => this._disarm());
5197
+ this.listen(document, "pointercancel", () => this._disarm());
5198
+ }
5172
5199
  this._renderRows();
5173
5200
  return root;
5174
5201
  }
@@ -5191,16 +5218,27 @@
5191
5218
  if (!silent) this.emit("change", { values: this.getValues() });
5192
5219
  return this;
5193
5220
  }
5194
- /** @param {number} [focusIndex=-1] @returns {void} */
5195
- _renderRows(focusIndex = -1) {
5221
+ /** @returns {{drag: boolean, buttons: boolean}} */
5222
+ _reorder() {
5223
+ return REORDER[this.options.reorder] ?? REORDER.both;
5224
+ }
5225
+ /**
5226
+ * @param {number} [focusIndex=-1] Row to focus after the render, or -1 for none.
5227
+ * @param {keyof PARTS} [focusPart='value'] Which control of that row takes focus.
5228
+ * @returns {void}
5229
+ */
5230
+ _renderRows(focusIndex = -1, focusPart = "value") {
5196
5231
  const choices = normalizeOptions2(this.options.options);
5197
5232
  const useSelect = this.options.options !== null;
5233
+ const { drag, buttons } = this._reorder();
5234
+ const last = this._values.length - 1;
5198
5235
  const rows = this._values.map((value, index) => {
5236
+ const entries = !useSelect || choices.some(([option]) => option === value) ? choices : [[value, value], ...choices];
5199
5237
  const control = useSelect ? h("select", {
5200
5238
  class: "zx-multi-value-editor__value",
5201
5239
  ariaLabel: `Value ${index + 1}`,
5202
5240
  dataset: { index }
5203
- }, choices.map(([optionValue, label]) => h("option", {
5241
+ }, entries.map(([optionValue, label]) => h("option", {
5204
5242
  value: optionValue,
5205
5243
  selected: optionValue === value
5206
5244
  }, label))) : h("input", {
@@ -5213,40 +5251,52 @@
5213
5251
  return h(
5214
5252
  "div",
5215
5253
  { class: "zx-multi-value-editor__row", dataset: { row: index } },
5254
+ drag ? h("button", {
5255
+ class: "zx-multi-value-editor__handle",
5256
+ type: "button",
5257
+ ariaLabel: `Reorder value ${index + 1}. Use the arrow keys to move it.`,
5258
+ dataset: { action: "grab", index }
5259
+ }, icon("drag", { size: 12 })) : null,
5216
5260
  control,
5217
5261
  h(
5218
5262
  "div",
5219
5263
  { class: "zx-multi-value-editor__controls" },
5220
- h("button", {
5264
+ buttons ? h("button", {
5221
5265
  class: "zx-btn",
5222
5266
  type: "button",
5223
- disabled: index === 0,
5267
+ // `aria-disabled` rather than `disabled`: the buttons re-render on every move, and a
5268
+ // control that disables itself under the reader's finger drops focus to the document.
5269
+ ariaDisabled: String(index === 0),
5224
5270
  ariaLabel: `Move value ${index + 1} up`,
5225
5271
  dataset: { action: "up", index }
5226
- }, "\u2191"),
5227
- h("button", {
5272
+ }, icon("chevron-up", { size: 12 })) : null,
5273
+ buttons ? h("button", {
5228
5274
  class: "zx-btn",
5229
5275
  type: "button",
5230
- disabled: index === this._values.length - 1,
5276
+ ariaDisabled: String(index === last),
5231
5277
  ariaLabel: `Move value ${index + 1} down`,
5232
5278
  dataset: { action: "down", index }
5233
- }, "\u2193"),
5279
+ }, icon("chevron-down", { size: 12 })) : null,
5234
5280
  h("button", {
5235
5281
  class: "zx-btn",
5236
5282
  type: "button",
5237
5283
  ariaLabel: `Remove value ${index + 1}`,
5238
5284
  dataset: { action: "remove", index }
5239
- }, "\xD7")
5285
+ }, icon("x", { size: 12 }))
5240
5286
  )
5241
5287
  );
5242
5288
  });
5243
5289
  this._rows.replaceChildren(...rows);
5244
- if (focusIndex >= 0) rows[focusIndex]?.querySelector(".zx-multi-value-editor__value")?.focus();
5290
+ if (focusIndex >= 0) {
5291
+ const row2 = rows[Math.min(focusIndex, rows.length - 1)];
5292
+ row2?.querySelector(PARTS[focusPart] ?? PARTS.value)?.focus();
5293
+ }
5245
5294
  }
5246
5295
  /** @param {MouseEvent} event @returns {void} */
5247
5296
  _handleClick(event) {
5248
5297
  const button2 = event.target instanceof Element ? event.target.closest("[data-action]") : null;
5249
5298
  if (!button2 || !this.el.contains(button2)) return;
5299
+ if (button2.getAttribute("aria-disabled") === "true") return;
5250
5300
  const action = button2.dataset.action;
5251
5301
  const index = Number(button2.dataset.index);
5252
5302
  if (action === "add") {
@@ -5259,28 +5309,156 @@
5259
5309
  this._renderRows(Math.min(index, this._values.length - 1));
5260
5310
  this._emitChange();
5261
5311
  } else if ((action === "up" || action === "down") && Number.isInteger(index)) {
5262
- const target = index + (action === "up" ? -1 : 1);
5263
- if (target < 0 || target >= this._values.length) return;
5264
- [this._values[index], this._values[target]] = [this._values[target], this._values[index]];
5265
- this._renderRows(target);
5266
- this._emitChange();
5312
+ this._move(index, index + (action === "up" ? -1 : 1), action);
5267
5313
  }
5268
5314
  }
5269
5315
  /** @param {Event} event @returns {void} */
5270
5316
  _handleChange(event) {
5271
5317
  const control = event.target instanceof Element ? event.target.closest("[data-index]") : null;
5272
- if (!control) return;
5318
+ if (!control || !("value" in control)) return;
5273
5319
  const index = Number(control.dataset.index);
5274
5320
  if (!Number.isInteger(index) || index < 0 || index >= this._values.length) return;
5275
5321
  this._values[index] = control.value;
5276
5322
  this._emitChange();
5277
5323
  }
5324
+ /**
5325
+ * The handle is the keyboard equivalent of dragging it, so `reorder: 'drag'` is still operable
5326
+ * without a pointer.
5327
+ * @param {KeyboardEvent} event
5328
+ * @returns {void}
5329
+ */
5330
+ _handleKeydown(event) {
5331
+ const handle = event.target instanceof Element ? event.target.closest(".zx-multi-value-editor__handle") : null;
5332
+ if (!handle) return;
5333
+ const index = Number(handle.dataset.index);
5334
+ const target = event.key === "ArrowUp" ? index - 1 : event.key === "ArrowDown" ? index + 1 : event.key === "Home" ? 0 : event.key === "End" ? this._values.length - 1 : -1;
5335
+ if (target < 0 || target >= this._values.length) return;
5336
+ event.preventDefault();
5337
+ this._move(index, target, "handle");
5338
+ }
5339
+ /**
5340
+ * Arms the row for dragging. Only the handle does this, so text selection inside a row's input
5341
+ * keeps working and a stray drag cannot reorder anything.
5342
+ * @param {PointerEvent} event
5343
+ * @returns {void}
5344
+ */
5345
+ _handlePointerDown(event) {
5346
+ const handle = event.target instanceof Element ? event.target.closest(".zx-multi-value-editor__handle") : null;
5347
+ this._disarm();
5348
+ if (!handle || handle.disabled) return;
5349
+ const row2 = (
5350
+ /** @type {HTMLElement|null} */
5351
+ handle.closest(".zx-multi-value-editor__row")
5352
+ );
5353
+ if (!row2) return;
5354
+ row2.draggable = true;
5355
+ this._armed = row2;
5356
+ }
5357
+ /** @returns {void} */
5358
+ _disarm() {
5359
+ if (this._armed) this._armed.draggable = false;
5360
+ this._armed = null;
5361
+ }
5362
+ /** @param {DragEvent} event @returns {void} */
5363
+ _handleDragStart(event) {
5364
+ const row2 = event.target instanceof Element ? event.target.closest(".zx-multi-value-editor__row") : null;
5365
+ if (!row2 || row2 !== this._armed) {
5366
+ event.preventDefault();
5367
+ return;
5368
+ }
5369
+ this._dragIndex = Number(row2.dataset.row);
5370
+ row2.dataset.dragging = "true";
5371
+ this.el.setAttribute("data-state", "dragging");
5372
+ event.dataTransfer?.setData("text/plain", String(this._dragIndex));
5373
+ if (event.dataTransfer) event.dataTransfer.effectAllowed = "move";
5374
+ }
5375
+ /** @param {DragEvent} event @returns {void} */
5376
+ _handleDragOver(event) {
5377
+ if (this._dragIndex < 0) return;
5378
+ event.preventDefault();
5379
+ if (event.dataTransfer) event.dataTransfer.dropEffect = "move";
5380
+ this._showDrop(this._dropTarget(event.clientY));
5381
+ }
5382
+ /** @param {DragEvent} event @returns {void} */
5383
+ _handleDragLeave(event) {
5384
+ const to = event.relatedTarget;
5385
+ if (to instanceof Node && this._rows.contains(to)) return;
5386
+ this._clearDrop();
5387
+ }
5388
+ /** @param {DragEvent} event @returns {void} */
5389
+ _handleDrop(event) {
5390
+ if (this._dragIndex < 0) return;
5391
+ event.preventDefault();
5392
+ const from = this._dragIndex;
5393
+ const slot = this._dropTarget(event.clientY);
5394
+ this._finishDrag();
5395
+ this._move(from, slot > from ? slot - 1 : slot, "handle");
5396
+ }
5397
+ /** @returns {void} */
5398
+ _finishDrag() {
5399
+ this._dragIndex = -1;
5400
+ this._disarm();
5401
+ this._clearDrop();
5402
+ for (const row2 of this._rows.children) delete /** @type {HTMLElement} */
5403
+ row2.dataset.dragging;
5404
+ if (this._initialState === null) this.el.removeAttribute("data-state");
5405
+ else this.el.setAttribute("data-state", this._initialState);
5406
+ }
5407
+ /**
5408
+ * Insertion slot (0…length) for a pointer at `clientY`.
5409
+ * @param {number} clientY Pointer position.
5410
+ * @returns {number}
5411
+ */
5412
+ _dropTarget(clientY) {
5413
+ const rows = [...this._rows.children];
5414
+ for (const [index, row2] of rows.entries()) {
5415
+ const box = row2.getBoundingClientRect();
5416
+ if (clientY < box.top + box.height / 2) return index;
5417
+ }
5418
+ return rows.length;
5419
+ }
5420
+ /** @param {number} slot Insertion slot to mark. @returns {void} */
5421
+ _showDrop(slot) {
5422
+ const rows = [...this._rows.children];
5423
+ const inert = slot === this._dragIndex || slot === this._dragIndex + 1;
5424
+ for (const [index, row2] of rows.entries()) {
5425
+ const cell = (
5426
+ /** @type {HTMLElement} */
5427
+ row2
5428
+ );
5429
+ if (!inert && index === slot) cell.dataset.drop = "before";
5430
+ else if (!inert && slot === rows.length && index === rows.length - 1) cell.dataset.drop = "after";
5431
+ else delete cell.dataset.drop;
5432
+ }
5433
+ }
5434
+ /** @returns {void} */
5435
+ _clearDrop() {
5436
+ for (const row2 of this._rows.children) delete /** @type {HTMLElement} */
5437
+ row2.dataset.drop;
5438
+ }
5439
+ /**
5440
+ * @param {number} from Index to move.
5441
+ * @param {number} to Destination index.
5442
+ * @param {keyof PARTS} part Control of the moved row that takes focus.
5443
+ * @returns {boolean} Whether anything moved.
5444
+ */
5445
+ _move(from, to, part) {
5446
+ const size2 = this._values.length;
5447
+ if (from === to || from < 0 || to < 0 || from >= size2 || to >= size2) return false;
5448
+ const [value] = this._values.splice(from, 1);
5449
+ this._values.splice(to, 0, value);
5450
+ this._renderRows(to, part);
5451
+ this._emitChange();
5452
+ return true;
5453
+ }
5278
5454
  /** @returns {void} */
5279
5455
  _emitChange() {
5280
5456
  this.emit("change", { values: this.getValues() });
5281
5457
  }
5282
5458
  /** @returns {void} */
5283
5459
  destroy() {
5460
+ if (this._initialState === null) this.el?.removeAttribute("data-state");
5461
+ else this.el?.setAttribute("data-state", this._initialState);
5284
5462
  for (const node of this._ownedNodes ?? []) node.remove();
5285
5463
  super.destroy();
5286
5464
  }
@@ -6350,6 +6528,7 @@
6350
6528
  /** @type {SelectOptions} */
6351
6529
  static defaults = {
6352
6530
  items: [],
6531
+ fixedItems: [],
6353
6532
  valueKey: "ID",
6354
6533
  labelKey: "name",
6355
6534
  renderItem: null,
@@ -6369,6 +6548,8 @@
6369
6548
  render() {
6370
6549
  this._items = [];
6371
6550
  this._visibleItems = [];
6551
+ this._fixedItems = [];
6552
+ this._visibleFixed = [];
6372
6553
  this._selected = null;
6373
6554
  this._optionEntries = [];
6374
6555
  this._activeIndex = -1;
@@ -6452,6 +6633,8 @@
6452
6633
  root.replaceChildren(control, panel);
6453
6634
  this._items = Array.isArray(this.options.items) ? this.options.items.slice() : [];
6454
6635
  this._visibleItems = this._items.slice();
6636
+ this._fixedItems = Array.isArray(this.options.fixedItems) ? this.options.fixedItems.slice() : [];
6637
+ this._visibleFixed = this._fixedItems.slice();
6455
6638
  this._disabled = Boolean(this.options.disabled);
6456
6639
  this._handleTypeahead = typeahead(
6457
6640
  () => this._optionEntries.filter((entry) => !entry.clear).map((entry) => entry.element),
@@ -6528,7 +6711,22 @@
6528
6711
  const selectedValue = this.value;
6529
6712
  this._items = Array.isArray(items) ? items.slice() : [];
6530
6713
  this._visibleItems = this._items.slice();
6531
- this._selected = selectedValue == null ? null : this._items.find((item) => Object.is(this._itemValue(item), selectedValue)) ?? null;
6714
+ this._selected = selectedValue == null ? null : this._findItem(selectedValue);
6715
+ this._syncInputValue();
6716
+ this._renderOptions();
6717
+ this._position?.update();
6718
+ return this;
6719
+ }
6720
+ /**
6721
+ * Replaces the pinned choices and preserves selection by ID when possible.
6722
+ * @param {SelectItem[]} items New fixed items.
6723
+ * @returns {this}
6724
+ */
6725
+ setFixedItems(items) {
6726
+ const selectedValue = this.value;
6727
+ this._fixedItems = Array.isArray(items) ? items.slice() : [];
6728
+ this._visibleFixed = this._fixedItems.slice();
6729
+ this._selected = selectedValue == null ? null : this._findItem(selectedValue);
6532
6730
  this._syncInputValue();
6533
6731
  this._renderOptions();
6534
6732
  this._position?.update();
@@ -6582,6 +6780,7 @@
6582
6780
  this.refs.input.removeAttribute("aria-activedescendant");
6583
6781
  this._activeIndex = -1;
6584
6782
  this._visibleItems = this._items.slice();
6783
+ this._visibleFixed = this._fixedItems.slice();
6585
6784
  this._syncInputValue();
6586
6785
  this.emit("close", {});
6587
6786
  return this;
@@ -6637,6 +6836,38 @@
6637
6836
  select.set(requestedValue, { silent: true });
6638
6837
  return select;
6639
6838
  }
6839
+ /**
6840
+ * Creates the record-permission picker: the fixed Private and Public choices above the groups a
6841
+ * record can be shared with. The value is the tri-state ZeyOS stores — `false` for private,
6842
+ * `true` for public, or a group ID — so the control binds straight to the record field.
6843
+ *
6844
+ * @param {Element|string|null} target Component target.
6845
+ * @param {SelectOptions & {groups?: SelectItem[]|((query: string) => Promise<SelectItem[]>|SelectItem[])}} [options={}]
6846
+ * Select overrides other than the fixed choices; `groups` holds the shareable groups, either as
6847
+ * an array (filtered locally) or as a loader function (queried remotely).
6848
+ * @returns {Select}
6849
+ */
6850
+ static permission(target, options = {}) {
6851
+ const { groups = [], value = true, valueKey = "ID", labelKey = "name", ...rest } = options;
6852
+ const loader = typeof groups === "function" ? groups : null;
6853
+ const shareable = loader ? [] : groups;
6854
+ const choices = [{ ID: false, name: "Private" }, { ID: true, name: "Public" }];
6855
+ const select = new _Select(target, {
6856
+ filter: loader ?? "local",
6857
+ ...rest,
6858
+ value: null,
6859
+ items: shareable,
6860
+ fixedItems: [],
6861
+ // The two choices carry their own ID and label, so a caller's group readers stay untouched.
6862
+ valueKey: (item) => choices.includes(item) ? item.ID : readItem(item, valueKey),
6863
+ labelKey: (item) => choices.includes(item) ? item.name : String(readItem(item, labelKey) ?? "")
6864
+ });
6865
+ choices[0].name = select._message("permission.private", "Private");
6866
+ choices[1].name = select._message("permission.public", "Public");
6867
+ select.setFixedItems(choices);
6868
+ select.set(value, { silent: true });
6869
+ return select;
6870
+ }
6640
6871
  /** Cleans up the panel, pending async work, and enhanced target contents. @returns {void} */
6641
6872
  destroy() {
6642
6873
  this._cancelQuery();
@@ -6707,8 +6938,8 @@
6707
6938
  );
6708
6939
  if (!this._open) this.open();
6709
6940
  if (this.options.filter === "local") {
6710
- const keys = this.options.searchKeys ?? [this.options.labelKey];
6711
- this._visibleItems = matchItems(this._items, query, keys);
6941
+ this._visibleFixed = this._matchFixed(query);
6942
+ this._visibleItems = matchItems(this._items, query, this._searchKeys());
6712
6943
  this._renderOptions();
6713
6944
  this._setActive(this._optionEntries.length > 0 ? 0 : -1);
6714
6945
  this._position?.update();
@@ -6763,6 +6994,10 @@
6763
6994
  if (this.options.clearable) {
6764
6995
  this._appendOption(null, true, this.options.placeholder || this._message("select.none", "No selection"));
6765
6996
  }
6997
+ for (const item of this._visibleFixed) this._appendOption(item, false);
6998
+ if (this._visibleFixed.length > 0 && this._visibleItems.length > 0 && !this.options.groupKey) {
6999
+ list.append(h("div", { class: "zx-select__separator", role: "presentation" }));
7000
+ }
6766
7001
  for (const item of this._visibleItems) {
6767
7002
  if (this.options.groupKey) {
6768
7003
  const group = this._read(item, this.options.groupKey);
@@ -6776,7 +7011,7 @@
6776
7011
  }
6777
7012
  this._appendOption(item, false);
6778
7013
  }
6779
- if (this._visibleItems.length === 0 && !this._loading) {
7014
+ if (this._visibleItems.length === 0 && this._visibleFixed.length === 0 && !this._loading) {
6780
7015
  list.append(h("div", {
6781
7016
  class: "zx-select__empty",
6782
7017
  role: "status"
@@ -6816,6 +7051,7 @@
6816
7051
  if (this._queryTimer !== null) clearTimeout(this._queryTimer);
6817
7052
  this._requestSequence += 1;
6818
7053
  const sequence = this._requestSequence;
7054
+ this._visibleFixed = this._matchFixed(query);
6819
7055
  if (query.length < Math.max(0, Number(this.options.minQuery) || 0)) {
6820
7056
  this._setLoading(false);
6821
7057
  this._visibleItems = [];
@@ -6900,13 +7136,14 @@
6900
7136
  }
6901
7137
  /** @param {unknown} candidate @returns {SelectItem|null} */
6902
7138
  _findItem(candidate) {
6903
- const direct = this._items.find((item) => item === candidate);
7139
+ const pool = this._fixedItems.length === 0 ? this._items : [...this._fixedItems, ...this._items];
7140
+ const direct = pool.find((item) => item === candidate);
6904
7141
  if (direct !== void 0) return direct;
6905
7142
  const value = candidate !== null && typeof candidate === "object" ? this._itemValue(
6906
7143
  /** @type {SelectItem} */
6907
7144
  candidate
6908
7145
  ) : candidate;
6909
- return this._items.find((item) => Object.is(this._itemValue(item), value)) ?? null;
7146
+ return pool.find((item) => Object.is(this._itemValue(item), value)) ?? null;
6910
7147
  }
6911
7148
  /** @param {SelectItem|null} item @returns {unknown} */
6912
7149
  _itemValue(item) {
@@ -6919,9 +7156,20 @@
6919
7156
  }
6920
7157
  /** @param {SelectItem|null} item @param {SelectValueReader|SelectLabelReader} reader @returns {unknown} */
6921
7158
  _read(item, reader) {
6922
- if (typeof reader === "function") return reader(item);
6923
- if (item === null || typeof item !== "object") return item;
6924
- return item[reader];
7159
+ return readItem(item, reader);
7160
+ }
7161
+ /** @returns {Array<SelectValueReader|SelectLabelReader>} */
7162
+ _searchKeys() {
7163
+ return this.options.searchKeys ?? [this.options.labelKey];
7164
+ }
7165
+ /**
7166
+ * Narrows the pinned choices. They are always matched here and never handed to a filter
7167
+ * function: a fixed choice belongs to the control, not to whatever the remote source returns.
7168
+ * @param {string} query Search text.
7169
+ * @returns {SelectItem[]}
7170
+ */
7171
+ _matchFixed(query) {
7172
+ return matchItems(this._fixedItems, query, this._searchKeys());
6925
7173
  }
6926
7174
  /** @returns {number} */
6927
7175
  _selectedEntryIndex() {
@@ -6944,6 +7192,11 @@
6944
7192
  return message === key ? fallback : message;
6945
7193
  }
6946
7194
  };
7195
+ function readItem(item, reader) {
7196
+ if (typeof reader === "function") return reader(item);
7197
+ if (item === null || typeof item !== "object") return item;
7198
+ return item[reader];
7199
+ }
6947
7200
  function isPrintable(event) {
6948
7201
  return event.key.length === 1 && !event.ctrlKey && !event.metaKey && !event.altKey;
6949
7202
  }
@@ -10457,6 +10710,10 @@
10457
10710
  if (!tab || !tablist.contains(tab)) return;
10458
10711
  const record = this._recordForTab(tab);
10459
10712
  if (!record || record.definition.disabled) return;
10713
+ if (event.target.closest?.(".zx-tabbox__close")) {
10714
+ this._closeTab(record);
10715
+ return;
10716
+ }
10460
10717
  this._setFocused(record);
10461
10718
  this.openTab(record.definition.name);
10462
10719
  });
@@ -10506,8 +10763,9 @@
10506
10763
  children.push(title, badge2);
10507
10764
  if (normalized.closable) {
10508
10765
  children.push(h("span", {
10509
- class: "zx-tabbox__close",
10510
- ariaHidden: "true"
10766
+ class: "zx-tabbox__close zx-icon-btn",
10767
+ ariaHidden: "true",
10768
+ title: "Close"
10511
10769
  }, icon("x", { size: 14 })));
10512
10770
  }
10513
10771
  const tab = (
@@ -10681,13 +10939,23 @@
10681
10939
  }
10682
10940
  if (event.key === "Delete" && current.definition.closable) {
10683
10941
  event.preventDefault();
10684
- const index = this._tabs.indexOf(current);
10685
- if (!this._removeTab(current.definition.name, true)) return;
10686
- const remaining = this._tabs[Math.min(index, this._tabs.length - 1)] ?? this._tabs[index - 1];
10687
- const focusTarget = remaining && !remaining.definition.disabled ? remaining : this._active;
10688
- if (focusTarget) this._setFocused(focusTarget);
10942
+ this._closeTab(current);
10689
10943
  }
10690
10944
  }
10945
+ /**
10946
+ * Closes a tab on a user gesture, keeping roving focus inside the tablist.
10947
+ * @param {TabRecord} record Tab to close.
10948
+ * @returns {void}
10949
+ * @fires Tabbox#close
10950
+ */
10951
+ _closeTab(record) {
10952
+ const index = this._tabs.indexOf(record);
10953
+ const held = record.tab.contains(document.activeElement);
10954
+ if (!this._removeTab(record.definition.name, true)) return;
10955
+ const remaining = this._tabs[Math.min(index, this._tabs.length - 1)] ?? this._tabs[index - 1];
10956
+ const focusTarget = remaining && !remaining.definition.disabled ? remaining : this._active;
10957
+ if (focusTarget) this._setFocused(focusTarget, held);
10958
+ }
10691
10959
  /** @param {string} name @param {boolean} emitClose @returns {boolean} */
10692
10960
  _removeTab(name, emitClose) {
10693
10961
  const record = this._find(name);