@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.global.js CHANGED
@@ -5159,18 +5159,34 @@ var zx = (() => {
5159
5159
  }
5160
5160
 
5161
5161
  // src/components/multi-value-editor/multi-value-editor.js
5162
+ var REORDER = Object.freeze({
5163
+ both: { drag: true, buttons: true },
5164
+ buttons: { drag: false, buttons: true },
5165
+ drag: { drag: true, buttons: false },
5166
+ none: { drag: false, buttons: false }
5167
+ });
5168
+ var PARTS = Object.freeze({
5169
+ value: ".zx-multi-value-editor__value",
5170
+ handle: ".zx-multi-value-editor__handle",
5171
+ up: '[data-action="up"]',
5172
+ down: '[data-action="down"]'
5173
+ });
5162
5174
  var MultiValueEditor = class extends Component {
5163
5175
  static cssName = "multi-value-editor";
5164
5176
  /** @type {MultiValueEditorOptions} */
5165
- static defaults = { values: [], options: null, addLabel: "Add value" };
5177
+ static defaults = { values: [], options: null, addLabel: "Add value", reorder: "both" };
5166
5178
  /** @returns {HTMLElement} */
5167
5179
  render() {
5168
5180
  const root = (
5169
5181
  /** @type {HTMLElement} */
5170
5182
  this.el ?? h("div")
5171
5183
  );
5184
+ this.el = root;
5172
5185
  this._values = Array.isArray(this.options.values) ? this.options.values.map(String) : [];
5173
5186
  this._ownedNodes = [];
5187
+ this._initialState = root.getAttribute("data-state");
5188
+ this._dragIndex = -1;
5189
+ this._armed = null;
5174
5190
  this._rows = h("div", { class: "zx-multi-value-editor__rows" });
5175
5191
  this._add = h("button", {
5176
5192
  class: "zx-btn zx-multi-value-editor__add",
@@ -5181,6 +5197,17 @@ var zx = (() => {
5181
5197
  root.append(this._rows, this._add);
5182
5198
  this.listen(root, "click", (event) => this._handleClick(event));
5183
5199
  this.listen(this._rows, "change", (event) => this._handleChange(event));
5200
+ this.listen(this._rows, "keydown", (event) => this._handleKeydown(event));
5201
+ if (this._reorder().drag) {
5202
+ this.listen(this._rows, "pointerdown", (event) => this._handlePointerDown(event));
5203
+ this.listen(this._rows, "dragstart", (event) => this._handleDragStart(event));
5204
+ this.listen(this._rows, "dragover", (event) => this._handleDragOver(event));
5205
+ this.listen(this._rows, "dragleave", (event) => this._handleDragLeave(event));
5206
+ this.listen(this._rows, "drop", (event) => this._handleDrop(event));
5207
+ this.listen(this._rows, "dragend", () => this._finishDrag());
5208
+ this.listen(document, "pointerup", () => this._disarm());
5209
+ this.listen(document, "pointercancel", () => this._disarm());
5210
+ }
5184
5211
  this._renderRows();
5185
5212
  return root;
5186
5213
  }
@@ -5203,16 +5230,27 @@ var zx = (() => {
5203
5230
  if (!silent) this.emit("change", { values: this.getValues() });
5204
5231
  return this;
5205
5232
  }
5206
- /** @param {number} [focusIndex=-1] @returns {void} */
5207
- _renderRows(focusIndex = -1) {
5233
+ /** @returns {{drag: boolean, buttons: boolean}} */
5234
+ _reorder() {
5235
+ return REORDER[this.options.reorder] ?? REORDER.both;
5236
+ }
5237
+ /**
5238
+ * @param {number} [focusIndex=-1] Row to focus after the render, or -1 for none.
5239
+ * @param {keyof PARTS} [focusPart='value'] Which control of that row takes focus.
5240
+ * @returns {void}
5241
+ */
5242
+ _renderRows(focusIndex = -1, focusPart = "value") {
5208
5243
  const choices = normalizeOptions2(this.options.options);
5209
5244
  const useSelect = this.options.options !== null;
5245
+ const { drag, buttons } = this._reorder();
5246
+ const last = this._values.length - 1;
5210
5247
  const rows = this._values.map((value, index) => {
5248
+ const entries = !useSelect || choices.some(([option]) => option === value) ? choices : [[value, value], ...choices];
5211
5249
  const control = useSelect ? h("select", {
5212
5250
  class: "zx-multi-value-editor__value",
5213
5251
  ariaLabel: `Value ${index + 1}`,
5214
5252
  dataset: { index }
5215
- }, choices.map(([optionValue, label]) => h("option", {
5253
+ }, entries.map(([optionValue, label]) => h("option", {
5216
5254
  value: optionValue,
5217
5255
  selected: optionValue === value
5218
5256
  }, label))) : h("input", {
@@ -5225,40 +5263,52 @@ var zx = (() => {
5225
5263
  return h(
5226
5264
  "div",
5227
5265
  { class: "zx-multi-value-editor__row", dataset: { row: index } },
5266
+ drag ? h("button", {
5267
+ class: "zx-multi-value-editor__handle",
5268
+ type: "button",
5269
+ ariaLabel: `Reorder value ${index + 1}. Use the arrow keys to move it.`,
5270
+ dataset: { action: "grab", index }
5271
+ }, icon("drag", { size: 12 })) : null,
5228
5272
  control,
5229
5273
  h(
5230
5274
  "div",
5231
5275
  { class: "zx-multi-value-editor__controls" },
5232
- h("button", {
5276
+ buttons ? h("button", {
5233
5277
  class: "zx-btn",
5234
5278
  type: "button",
5235
- disabled: index === 0,
5279
+ // `aria-disabled` rather than `disabled`: the buttons re-render on every move, and a
5280
+ // control that disables itself under the reader's finger drops focus to the document.
5281
+ ariaDisabled: String(index === 0),
5236
5282
  ariaLabel: `Move value ${index + 1} up`,
5237
5283
  dataset: { action: "up", index }
5238
- }, "\u2191"),
5239
- h("button", {
5284
+ }, icon("chevron-up", { size: 12 })) : null,
5285
+ buttons ? h("button", {
5240
5286
  class: "zx-btn",
5241
5287
  type: "button",
5242
- disabled: index === this._values.length - 1,
5288
+ ariaDisabled: String(index === last),
5243
5289
  ariaLabel: `Move value ${index + 1} down`,
5244
5290
  dataset: { action: "down", index }
5245
- }, "\u2193"),
5291
+ }, icon("chevron-down", { size: 12 })) : null,
5246
5292
  h("button", {
5247
5293
  class: "zx-btn",
5248
5294
  type: "button",
5249
5295
  ariaLabel: `Remove value ${index + 1}`,
5250
5296
  dataset: { action: "remove", index }
5251
- }, "\xD7")
5297
+ }, icon("x", { size: 12 }))
5252
5298
  )
5253
5299
  );
5254
5300
  });
5255
5301
  this._rows.replaceChildren(...rows);
5256
- if (focusIndex >= 0) rows[focusIndex]?.querySelector(".zx-multi-value-editor__value")?.focus();
5302
+ if (focusIndex >= 0) {
5303
+ const row2 = rows[Math.min(focusIndex, rows.length - 1)];
5304
+ row2?.querySelector(PARTS[focusPart] ?? PARTS.value)?.focus();
5305
+ }
5257
5306
  }
5258
5307
  /** @param {MouseEvent} event @returns {void} */
5259
5308
  _handleClick(event) {
5260
5309
  const button2 = event.target instanceof Element ? event.target.closest("[data-action]") : null;
5261
5310
  if (!button2 || !this.el.contains(button2)) return;
5311
+ if (button2.getAttribute("aria-disabled") === "true") return;
5262
5312
  const action = button2.dataset.action;
5263
5313
  const index = Number(button2.dataset.index);
5264
5314
  if (action === "add") {
@@ -5271,28 +5321,156 @@ var zx = (() => {
5271
5321
  this._renderRows(Math.min(index, this._values.length - 1));
5272
5322
  this._emitChange();
5273
5323
  } else if ((action === "up" || action === "down") && Number.isInteger(index)) {
5274
- const target = index + (action === "up" ? -1 : 1);
5275
- if (target < 0 || target >= this._values.length) return;
5276
- [this._values[index], this._values[target]] = [this._values[target], this._values[index]];
5277
- this._renderRows(target);
5278
- this._emitChange();
5324
+ this._move(index, index + (action === "up" ? -1 : 1), action);
5279
5325
  }
5280
5326
  }
5281
5327
  /** @param {Event} event @returns {void} */
5282
5328
  _handleChange(event) {
5283
5329
  const control = event.target instanceof Element ? event.target.closest("[data-index]") : null;
5284
- if (!control) return;
5330
+ if (!control || !("value" in control)) return;
5285
5331
  const index = Number(control.dataset.index);
5286
5332
  if (!Number.isInteger(index) || index < 0 || index >= this._values.length) return;
5287
5333
  this._values[index] = control.value;
5288
5334
  this._emitChange();
5289
5335
  }
5336
+ /**
5337
+ * The handle is the keyboard equivalent of dragging it, so `reorder: 'drag'` is still operable
5338
+ * without a pointer.
5339
+ * @param {KeyboardEvent} event
5340
+ * @returns {void}
5341
+ */
5342
+ _handleKeydown(event) {
5343
+ const handle = event.target instanceof Element ? event.target.closest(".zx-multi-value-editor__handle") : null;
5344
+ if (!handle) return;
5345
+ const index = Number(handle.dataset.index);
5346
+ const target = event.key === "ArrowUp" ? index - 1 : event.key === "ArrowDown" ? index + 1 : event.key === "Home" ? 0 : event.key === "End" ? this._values.length - 1 : -1;
5347
+ if (target < 0 || target >= this._values.length) return;
5348
+ event.preventDefault();
5349
+ this._move(index, target, "handle");
5350
+ }
5351
+ /**
5352
+ * Arms the row for dragging. Only the handle does this, so text selection inside a row's input
5353
+ * keeps working and a stray drag cannot reorder anything.
5354
+ * @param {PointerEvent} event
5355
+ * @returns {void}
5356
+ */
5357
+ _handlePointerDown(event) {
5358
+ const handle = event.target instanceof Element ? event.target.closest(".zx-multi-value-editor__handle") : null;
5359
+ this._disarm();
5360
+ if (!handle || handle.disabled) return;
5361
+ const row2 = (
5362
+ /** @type {HTMLElement|null} */
5363
+ handle.closest(".zx-multi-value-editor__row")
5364
+ );
5365
+ if (!row2) return;
5366
+ row2.draggable = true;
5367
+ this._armed = row2;
5368
+ }
5369
+ /** @returns {void} */
5370
+ _disarm() {
5371
+ if (this._armed) this._armed.draggable = false;
5372
+ this._armed = null;
5373
+ }
5374
+ /** @param {DragEvent} event @returns {void} */
5375
+ _handleDragStart(event) {
5376
+ const row2 = event.target instanceof Element ? event.target.closest(".zx-multi-value-editor__row") : null;
5377
+ if (!row2 || row2 !== this._armed) {
5378
+ event.preventDefault();
5379
+ return;
5380
+ }
5381
+ this._dragIndex = Number(row2.dataset.row);
5382
+ row2.dataset.dragging = "true";
5383
+ this.el.setAttribute("data-state", "dragging");
5384
+ event.dataTransfer?.setData("text/plain", String(this._dragIndex));
5385
+ if (event.dataTransfer) event.dataTransfer.effectAllowed = "move";
5386
+ }
5387
+ /** @param {DragEvent} event @returns {void} */
5388
+ _handleDragOver(event) {
5389
+ if (this._dragIndex < 0) return;
5390
+ event.preventDefault();
5391
+ if (event.dataTransfer) event.dataTransfer.dropEffect = "move";
5392
+ this._showDrop(this._dropTarget(event.clientY));
5393
+ }
5394
+ /** @param {DragEvent} event @returns {void} */
5395
+ _handleDragLeave(event) {
5396
+ const to = event.relatedTarget;
5397
+ if (to instanceof Node && this._rows.contains(to)) return;
5398
+ this._clearDrop();
5399
+ }
5400
+ /** @param {DragEvent} event @returns {void} */
5401
+ _handleDrop(event) {
5402
+ if (this._dragIndex < 0) return;
5403
+ event.preventDefault();
5404
+ const from = this._dragIndex;
5405
+ const slot = this._dropTarget(event.clientY);
5406
+ this._finishDrag();
5407
+ this._move(from, slot > from ? slot - 1 : slot, "handle");
5408
+ }
5409
+ /** @returns {void} */
5410
+ _finishDrag() {
5411
+ this._dragIndex = -1;
5412
+ this._disarm();
5413
+ this._clearDrop();
5414
+ for (const row2 of this._rows.children) delete /** @type {HTMLElement} */
5415
+ row2.dataset.dragging;
5416
+ if (this._initialState === null) this.el.removeAttribute("data-state");
5417
+ else this.el.setAttribute("data-state", this._initialState);
5418
+ }
5419
+ /**
5420
+ * Insertion slot (0…length) for a pointer at `clientY`.
5421
+ * @param {number} clientY Pointer position.
5422
+ * @returns {number}
5423
+ */
5424
+ _dropTarget(clientY) {
5425
+ const rows = [...this._rows.children];
5426
+ for (const [index, row2] of rows.entries()) {
5427
+ const box = row2.getBoundingClientRect();
5428
+ if (clientY < box.top + box.height / 2) return index;
5429
+ }
5430
+ return rows.length;
5431
+ }
5432
+ /** @param {number} slot Insertion slot to mark. @returns {void} */
5433
+ _showDrop(slot) {
5434
+ const rows = [...this._rows.children];
5435
+ const inert = slot === this._dragIndex || slot === this._dragIndex + 1;
5436
+ for (const [index, row2] of rows.entries()) {
5437
+ const cell = (
5438
+ /** @type {HTMLElement} */
5439
+ row2
5440
+ );
5441
+ if (!inert && index === slot) cell.dataset.drop = "before";
5442
+ else if (!inert && slot === rows.length && index === rows.length - 1) cell.dataset.drop = "after";
5443
+ else delete cell.dataset.drop;
5444
+ }
5445
+ }
5446
+ /** @returns {void} */
5447
+ _clearDrop() {
5448
+ for (const row2 of this._rows.children) delete /** @type {HTMLElement} */
5449
+ row2.dataset.drop;
5450
+ }
5451
+ /**
5452
+ * @param {number} from Index to move.
5453
+ * @param {number} to Destination index.
5454
+ * @param {keyof PARTS} part Control of the moved row that takes focus.
5455
+ * @returns {boolean} Whether anything moved.
5456
+ */
5457
+ _move(from, to, part) {
5458
+ const size2 = this._values.length;
5459
+ if (from === to || from < 0 || to < 0 || from >= size2 || to >= size2) return false;
5460
+ const [value] = this._values.splice(from, 1);
5461
+ this._values.splice(to, 0, value);
5462
+ this._renderRows(to, part);
5463
+ this._emitChange();
5464
+ return true;
5465
+ }
5290
5466
  /** @returns {void} */
5291
5467
  _emitChange() {
5292
5468
  this.emit("change", { values: this.getValues() });
5293
5469
  }
5294
5470
  /** @returns {void} */
5295
5471
  destroy() {
5472
+ if (this._initialState === null) this.el?.removeAttribute("data-state");
5473
+ else this.el?.setAttribute("data-state", this._initialState);
5296
5474
  for (const node of this._ownedNodes ?? []) node.remove();
5297
5475
  super.destroy();
5298
5476
  }
@@ -6362,6 +6540,7 @@ var zx = (() => {
6362
6540
  /** @type {SelectOptions} */
6363
6541
  static defaults = {
6364
6542
  items: [],
6543
+ fixedItems: [],
6365
6544
  valueKey: "ID",
6366
6545
  labelKey: "name",
6367
6546
  renderItem: null,
@@ -6381,6 +6560,8 @@ var zx = (() => {
6381
6560
  render() {
6382
6561
  this._items = [];
6383
6562
  this._visibleItems = [];
6563
+ this._fixedItems = [];
6564
+ this._visibleFixed = [];
6384
6565
  this._selected = null;
6385
6566
  this._optionEntries = [];
6386
6567
  this._activeIndex = -1;
@@ -6464,6 +6645,8 @@ var zx = (() => {
6464
6645
  root.replaceChildren(control, panel);
6465
6646
  this._items = Array.isArray(this.options.items) ? this.options.items.slice() : [];
6466
6647
  this._visibleItems = this._items.slice();
6648
+ this._fixedItems = Array.isArray(this.options.fixedItems) ? this.options.fixedItems.slice() : [];
6649
+ this._visibleFixed = this._fixedItems.slice();
6467
6650
  this._disabled = Boolean(this.options.disabled);
6468
6651
  this._handleTypeahead = typeahead(
6469
6652
  () => this._optionEntries.filter((entry) => !entry.clear).map((entry) => entry.element),
@@ -6540,7 +6723,22 @@ var zx = (() => {
6540
6723
  const selectedValue = this.value;
6541
6724
  this._items = Array.isArray(items) ? items.slice() : [];
6542
6725
  this._visibleItems = this._items.slice();
6543
- this._selected = selectedValue == null ? null : this._items.find((item) => Object.is(this._itemValue(item), selectedValue)) ?? null;
6726
+ this._selected = selectedValue == null ? null : this._findItem(selectedValue);
6727
+ this._syncInputValue();
6728
+ this._renderOptions();
6729
+ this._position?.update();
6730
+ return this;
6731
+ }
6732
+ /**
6733
+ * Replaces the pinned choices and preserves selection by ID when possible.
6734
+ * @param {SelectItem[]} items New fixed items.
6735
+ * @returns {this}
6736
+ */
6737
+ setFixedItems(items) {
6738
+ const selectedValue = this.value;
6739
+ this._fixedItems = Array.isArray(items) ? items.slice() : [];
6740
+ this._visibleFixed = this._fixedItems.slice();
6741
+ this._selected = selectedValue == null ? null : this._findItem(selectedValue);
6544
6742
  this._syncInputValue();
6545
6743
  this._renderOptions();
6546
6744
  this._position?.update();
@@ -6594,6 +6792,7 @@ var zx = (() => {
6594
6792
  this.refs.input.removeAttribute("aria-activedescendant");
6595
6793
  this._activeIndex = -1;
6596
6794
  this._visibleItems = this._items.slice();
6795
+ this._visibleFixed = this._fixedItems.slice();
6597
6796
  this._syncInputValue();
6598
6797
  this.emit("close", {});
6599
6798
  return this;
@@ -6649,6 +6848,38 @@ var zx = (() => {
6649
6848
  select.set(requestedValue, { silent: true });
6650
6849
  return select;
6651
6850
  }
6851
+ /**
6852
+ * Creates the record-permission picker: the fixed Private and Public choices above the groups a
6853
+ * record can be shared with. The value is the tri-state ZeyOS stores — `false` for private,
6854
+ * `true` for public, or a group ID — so the control binds straight to the record field.
6855
+ *
6856
+ * @param {Element|string|null} target Component target.
6857
+ * @param {SelectOptions & {groups?: SelectItem[]|((query: string) => Promise<SelectItem[]>|SelectItem[])}} [options={}]
6858
+ * Select overrides other than the fixed choices; `groups` holds the shareable groups, either as
6859
+ * an array (filtered locally) or as a loader function (queried remotely).
6860
+ * @returns {Select}
6861
+ */
6862
+ static permission(target, options = {}) {
6863
+ const { groups = [], value = true, valueKey = "ID", labelKey = "name", ...rest } = options;
6864
+ const loader = typeof groups === "function" ? groups : null;
6865
+ const shareable = loader ? [] : groups;
6866
+ const choices = [{ ID: false, name: "Private" }, { ID: true, name: "Public" }];
6867
+ const select = new _Select(target, {
6868
+ filter: loader ?? "local",
6869
+ ...rest,
6870
+ value: null,
6871
+ items: shareable,
6872
+ fixedItems: [],
6873
+ // The two choices carry their own ID and label, so a caller's group readers stay untouched.
6874
+ valueKey: (item) => choices.includes(item) ? item.ID : readItem(item, valueKey),
6875
+ labelKey: (item) => choices.includes(item) ? item.name : String(readItem(item, labelKey) ?? "")
6876
+ });
6877
+ choices[0].name = select._message("permission.private", "Private");
6878
+ choices[1].name = select._message("permission.public", "Public");
6879
+ select.setFixedItems(choices);
6880
+ select.set(value, { silent: true });
6881
+ return select;
6882
+ }
6652
6883
  /** Cleans up the panel, pending async work, and enhanced target contents. @returns {void} */
6653
6884
  destroy() {
6654
6885
  this._cancelQuery();
@@ -6719,8 +6950,8 @@ var zx = (() => {
6719
6950
  );
6720
6951
  if (!this._open) this.open();
6721
6952
  if (this.options.filter === "local") {
6722
- const keys = this.options.searchKeys ?? [this.options.labelKey];
6723
- this._visibleItems = matchItems(this._items, query, keys);
6953
+ this._visibleFixed = this._matchFixed(query);
6954
+ this._visibleItems = matchItems(this._items, query, this._searchKeys());
6724
6955
  this._renderOptions();
6725
6956
  this._setActive(this._optionEntries.length > 0 ? 0 : -1);
6726
6957
  this._position?.update();
@@ -6775,6 +7006,10 @@ var zx = (() => {
6775
7006
  if (this.options.clearable) {
6776
7007
  this._appendOption(null, true, this.options.placeholder || this._message("select.none", "No selection"));
6777
7008
  }
7009
+ for (const item of this._visibleFixed) this._appendOption(item, false);
7010
+ if (this._visibleFixed.length > 0 && this._visibleItems.length > 0 && !this.options.groupKey) {
7011
+ list.append(h("div", { class: "zx-select__separator", role: "presentation" }));
7012
+ }
6778
7013
  for (const item of this._visibleItems) {
6779
7014
  if (this.options.groupKey) {
6780
7015
  const group = this._read(item, this.options.groupKey);
@@ -6788,7 +7023,7 @@ var zx = (() => {
6788
7023
  }
6789
7024
  this._appendOption(item, false);
6790
7025
  }
6791
- if (this._visibleItems.length === 0 && !this._loading) {
7026
+ if (this._visibleItems.length === 0 && this._visibleFixed.length === 0 && !this._loading) {
6792
7027
  list.append(h("div", {
6793
7028
  class: "zx-select__empty",
6794
7029
  role: "status"
@@ -6828,6 +7063,7 @@ var zx = (() => {
6828
7063
  if (this._queryTimer !== null) clearTimeout(this._queryTimer);
6829
7064
  this._requestSequence += 1;
6830
7065
  const sequence = this._requestSequence;
7066
+ this._visibleFixed = this._matchFixed(query);
6831
7067
  if (query.length < Math.max(0, Number(this.options.minQuery) || 0)) {
6832
7068
  this._setLoading(false);
6833
7069
  this._visibleItems = [];
@@ -6912,13 +7148,14 @@ var zx = (() => {
6912
7148
  }
6913
7149
  /** @param {unknown} candidate @returns {SelectItem|null} */
6914
7150
  _findItem(candidate) {
6915
- const direct = this._items.find((item) => item === candidate);
7151
+ const pool = this._fixedItems.length === 0 ? this._items : [...this._fixedItems, ...this._items];
7152
+ const direct = pool.find((item) => item === candidate);
6916
7153
  if (direct !== void 0) return direct;
6917
7154
  const value = candidate !== null && typeof candidate === "object" ? this._itemValue(
6918
7155
  /** @type {SelectItem} */
6919
7156
  candidate
6920
7157
  ) : candidate;
6921
- return this._items.find((item) => Object.is(this._itemValue(item), value)) ?? null;
7158
+ return pool.find((item) => Object.is(this._itemValue(item), value)) ?? null;
6922
7159
  }
6923
7160
  /** @param {SelectItem|null} item @returns {unknown} */
6924
7161
  _itemValue(item) {
@@ -6931,9 +7168,20 @@ var zx = (() => {
6931
7168
  }
6932
7169
  /** @param {SelectItem|null} item @param {SelectValueReader|SelectLabelReader} reader @returns {unknown} */
6933
7170
  _read(item, reader) {
6934
- if (typeof reader === "function") return reader(item);
6935
- if (item === null || typeof item !== "object") return item;
6936
- return item[reader];
7171
+ return readItem(item, reader);
7172
+ }
7173
+ /** @returns {Array<SelectValueReader|SelectLabelReader>} */
7174
+ _searchKeys() {
7175
+ return this.options.searchKeys ?? [this.options.labelKey];
7176
+ }
7177
+ /**
7178
+ * Narrows the pinned choices. They are always matched here and never handed to a filter
7179
+ * function: a fixed choice belongs to the control, not to whatever the remote source returns.
7180
+ * @param {string} query Search text.
7181
+ * @returns {SelectItem[]}
7182
+ */
7183
+ _matchFixed(query) {
7184
+ return matchItems(this._fixedItems, query, this._searchKeys());
6937
7185
  }
6938
7186
  /** @returns {number} */
6939
7187
  _selectedEntryIndex() {
@@ -6956,6 +7204,11 @@ var zx = (() => {
6956
7204
  return message === key ? fallback : message;
6957
7205
  }
6958
7206
  };
7207
+ function readItem(item, reader) {
7208
+ if (typeof reader === "function") return reader(item);
7209
+ if (item === null || typeof item !== "object") return item;
7210
+ return item[reader];
7211
+ }
6959
7212
  function isPrintable(event) {
6960
7213
  return event.key.length === 1 && !event.ctrlKey && !event.metaKey && !event.altKey;
6961
7214
  }
@@ -10469,6 +10722,10 @@ var zx = (() => {
10469
10722
  if (!tab || !tablist.contains(tab)) return;
10470
10723
  const record = this._recordForTab(tab);
10471
10724
  if (!record || record.definition.disabled) return;
10725
+ if (event.target.closest?.(".zx-tabbox__close")) {
10726
+ this._closeTab(record);
10727
+ return;
10728
+ }
10472
10729
  this._setFocused(record);
10473
10730
  this.openTab(record.definition.name);
10474
10731
  });
@@ -10518,8 +10775,9 @@ var zx = (() => {
10518
10775
  children.push(title, badge2);
10519
10776
  if (normalized.closable) {
10520
10777
  children.push(h("span", {
10521
- class: "zx-tabbox__close",
10522
- ariaHidden: "true"
10778
+ class: "zx-tabbox__close zx-icon-btn",
10779
+ ariaHidden: "true",
10780
+ title: "Close"
10523
10781
  }, icon("x", { size: 14 })));
10524
10782
  }
10525
10783
  const tab = (
@@ -10693,13 +10951,23 @@ var zx = (() => {
10693
10951
  }
10694
10952
  if (event.key === "Delete" && current.definition.closable) {
10695
10953
  event.preventDefault();
10696
- const index = this._tabs.indexOf(current);
10697
- if (!this._removeTab(current.definition.name, true)) return;
10698
- const remaining = this._tabs[Math.min(index, this._tabs.length - 1)] ?? this._tabs[index - 1];
10699
- const focusTarget = remaining && !remaining.definition.disabled ? remaining : this._active;
10700
- if (focusTarget) this._setFocused(focusTarget);
10954
+ this._closeTab(current);
10701
10955
  }
10702
10956
  }
10957
+ /**
10958
+ * Closes a tab on a user gesture, keeping roving focus inside the tablist.
10959
+ * @param {TabRecord} record Tab to close.
10960
+ * @returns {void}
10961
+ * @fires Tabbox#close
10962
+ */
10963
+ _closeTab(record) {
10964
+ const index = this._tabs.indexOf(record);
10965
+ const held = record.tab.contains(document.activeElement);
10966
+ if (!this._removeTab(record.definition.name, true)) return;
10967
+ const remaining = this._tabs[Math.min(index, this._tabs.length - 1)] ?? this._tabs[index - 1];
10968
+ const focusTarget = remaining && !remaining.definition.disabled ? remaining : this._active;
10969
+ if (focusTarget) this._setFocused(focusTarget, held);
10970
+ }
10703
10971
  /** @param {string} name @param {boolean} emitClose @returns {boolean} */
10704
10972
  _removeTab(name, emitClose) {
10705
10973
  const record = this._find(name);