@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
@@ -5118,18 +5118,34 @@ function registerFieldUploadAdapter() {
5118
5118
  }
5119
5119
 
5120
5120
  // src/components/multi-value-editor/multi-value-editor.js
5121
+ var REORDER = Object.freeze({
5122
+ both: { drag: true, buttons: true },
5123
+ buttons: { drag: false, buttons: true },
5124
+ drag: { drag: true, buttons: false },
5125
+ none: { drag: false, buttons: false }
5126
+ });
5127
+ var PARTS = Object.freeze({
5128
+ value: ".zx-multi-value-editor__value",
5129
+ handle: ".zx-multi-value-editor__handle",
5130
+ up: '[data-action="up"]',
5131
+ down: '[data-action="down"]'
5132
+ });
5121
5133
  var MultiValueEditor = class extends Component {
5122
5134
  static cssName = "multi-value-editor";
5123
5135
  /** @type {MultiValueEditorOptions} */
5124
- static defaults = { values: [], options: null, addLabel: "Add value" };
5136
+ static defaults = { values: [], options: null, addLabel: "Add value", reorder: "both" };
5125
5137
  /** @returns {HTMLElement} */
5126
5138
  render() {
5127
5139
  const root = (
5128
5140
  /** @type {HTMLElement} */
5129
5141
  this.el ?? h("div")
5130
5142
  );
5143
+ this.el = root;
5131
5144
  this._values = Array.isArray(this.options.values) ? this.options.values.map(String) : [];
5132
5145
  this._ownedNodes = [];
5146
+ this._initialState = root.getAttribute("data-state");
5147
+ this._dragIndex = -1;
5148
+ this._armed = null;
5133
5149
  this._rows = h("div", { class: "zx-multi-value-editor__rows" });
5134
5150
  this._add = h("button", {
5135
5151
  class: "zx-btn zx-multi-value-editor__add",
@@ -5140,6 +5156,17 @@ var MultiValueEditor = class extends Component {
5140
5156
  root.append(this._rows, this._add);
5141
5157
  this.listen(root, "click", (event) => this._handleClick(event));
5142
5158
  this.listen(this._rows, "change", (event) => this._handleChange(event));
5159
+ this.listen(this._rows, "keydown", (event) => this._handleKeydown(event));
5160
+ if (this._reorder().drag) {
5161
+ this.listen(this._rows, "pointerdown", (event) => this._handlePointerDown(event));
5162
+ this.listen(this._rows, "dragstart", (event) => this._handleDragStart(event));
5163
+ this.listen(this._rows, "dragover", (event) => this._handleDragOver(event));
5164
+ this.listen(this._rows, "dragleave", (event) => this._handleDragLeave(event));
5165
+ this.listen(this._rows, "drop", (event) => this._handleDrop(event));
5166
+ this.listen(this._rows, "dragend", () => this._finishDrag());
5167
+ this.listen(document, "pointerup", () => this._disarm());
5168
+ this.listen(document, "pointercancel", () => this._disarm());
5169
+ }
5143
5170
  this._renderRows();
5144
5171
  return root;
5145
5172
  }
@@ -5162,16 +5189,27 @@ var MultiValueEditor = class extends Component {
5162
5189
  if (!silent) this.emit("change", { values: this.getValues() });
5163
5190
  return this;
5164
5191
  }
5165
- /** @param {number} [focusIndex=-1] @returns {void} */
5166
- _renderRows(focusIndex = -1) {
5192
+ /** @returns {{drag: boolean, buttons: boolean}} */
5193
+ _reorder() {
5194
+ return REORDER[this.options.reorder] ?? REORDER.both;
5195
+ }
5196
+ /**
5197
+ * @param {number} [focusIndex=-1] Row to focus after the render, or -1 for none.
5198
+ * @param {keyof PARTS} [focusPart='value'] Which control of that row takes focus.
5199
+ * @returns {void}
5200
+ */
5201
+ _renderRows(focusIndex = -1, focusPart = "value") {
5167
5202
  const choices = normalizeOptions2(this.options.options);
5168
5203
  const useSelect = this.options.options !== null;
5204
+ const { drag, buttons } = this._reorder();
5205
+ const last = this._values.length - 1;
5169
5206
  const rows = this._values.map((value, index) => {
5207
+ const entries = !useSelect || choices.some(([option]) => option === value) ? choices : [[value, value], ...choices];
5170
5208
  const control = useSelect ? h("select", {
5171
5209
  class: "zx-multi-value-editor__value",
5172
5210
  ariaLabel: `Value ${index + 1}`,
5173
5211
  dataset: { index }
5174
- }, choices.map(([optionValue, label]) => h("option", {
5212
+ }, entries.map(([optionValue, label]) => h("option", {
5175
5213
  value: optionValue,
5176
5214
  selected: optionValue === value
5177
5215
  }, label))) : h("input", {
@@ -5184,40 +5222,52 @@ var MultiValueEditor = class extends Component {
5184
5222
  return h(
5185
5223
  "div",
5186
5224
  { class: "zx-multi-value-editor__row", dataset: { row: index } },
5225
+ drag ? h("button", {
5226
+ class: "zx-multi-value-editor__handle",
5227
+ type: "button",
5228
+ ariaLabel: `Reorder value ${index + 1}. Use the arrow keys to move it.`,
5229
+ dataset: { action: "grab", index }
5230
+ }, icon("drag", { size: 12 })) : null,
5187
5231
  control,
5188
5232
  h(
5189
5233
  "div",
5190
5234
  { class: "zx-multi-value-editor__controls" },
5191
- h("button", {
5235
+ buttons ? h("button", {
5192
5236
  class: "zx-btn",
5193
5237
  type: "button",
5194
- disabled: index === 0,
5238
+ // `aria-disabled` rather than `disabled`: the buttons re-render on every move, and a
5239
+ // control that disables itself under the reader's finger drops focus to the document.
5240
+ ariaDisabled: String(index === 0),
5195
5241
  ariaLabel: `Move value ${index + 1} up`,
5196
5242
  dataset: { action: "up", index }
5197
- }, "\u2191"),
5198
- h("button", {
5243
+ }, icon("chevron-up", { size: 12 })) : null,
5244
+ buttons ? h("button", {
5199
5245
  class: "zx-btn",
5200
5246
  type: "button",
5201
- disabled: index === this._values.length - 1,
5247
+ ariaDisabled: String(index === last),
5202
5248
  ariaLabel: `Move value ${index + 1} down`,
5203
5249
  dataset: { action: "down", index }
5204
- }, "\u2193"),
5250
+ }, icon("chevron-down", { size: 12 })) : null,
5205
5251
  h("button", {
5206
5252
  class: "zx-btn",
5207
5253
  type: "button",
5208
5254
  ariaLabel: `Remove value ${index + 1}`,
5209
5255
  dataset: { action: "remove", index }
5210
- }, "\xD7")
5256
+ }, icon("x", { size: 12 }))
5211
5257
  )
5212
5258
  );
5213
5259
  });
5214
5260
  this._rows.replaceChildren(...rows);
5215
- if (focusIndex >= 0) rows[focusIndex]?.querySelector(".zx-multi-value-editor__value")?.focus();
5261
+ if (focusIndex >= 0) {
5262
+ const row = rows[Math.min(focusIndex, rows.length - 1)];
5263
+ row?.querySelector(PARTS[focusPart] ?? PARTS.value)?.focus();
5264
+ }
5216
5265
  }
5217
5266
  /** @param {MouseEvent} event @returns {void} */
5218
5267
  _handleClick(event) {
5219
5268
  const button2 = event.target instanceof Element ? event.target.closest("[data-action]") : null;
5220
5269
  if (!button2 || !this.el.contains(button2)) return;
5270
+ if (button2.getAttribute("aria-disabled") === "true") return;
5221
5271
  const action = button2.dataset.action;
5222
5272
  const index = Number(button2.dataset.index);
5223
5273
  if (action === "add") {
@@ -5230,28 +5280,156 @@ var MultiValueEditor = class extends Component {
5230
5280
  this._renderRows(Math.min(index, this._values.length - 1));
5231
5281
  this._emitChange();
5232
5282
  } else if ((action === "up" || action === "down") && Number.isInteger(index)) {
5233
- const target = index + (action === "up" ? -1 : 1);
5234
- if (target < 0 || target >= this._values.length) return;
5235
- [this._values[index], this._values[target]] = [this._values[target], this._values[index]];
5236
- this._renderRows(target);
5237
- this._emitChange();
5283
+ this._move(index, index + (action === "up" ? -1 : 1), action);
5238
5284
  }
5239
5285
  }
5240
5286
  /** @param {Event} event @returns {void} */
5241
5287
  _handleChange(event) {
5242
5288
  const control = event.target instanceof Element ? event.target.closest("[data-index]") : null;
5243
- if (!control) return;
5289
+ if (!control || !("value" in control)) return;
5244
5290
  const index = Number(control.dataset.index);
5245
5291
  if (!Number.isInteger(index) || index < 0 || index >= this._values.length) return;
5246
5292
  this._values[index] = control.value;
5247
5293
  this._emitChange();
5248
5294
  }
5295
+ /**
5296
+ * The handle is the keyboard equivalent of dragging it, so `reorder: 'drag'` is still operable
5297
+ * without a pointer.
5298
+ * @param {KeyboardEvent} event
5299
+ * @returns {void}
5300
+ */
5301
+ _handleKeydown(event) {
5302
+ const handle = event.target instanceof Element ? event.target.closest(".zx-multi-value-editor__handle") : null;
5303
+ if (!handle) return;
5304
+ const index = Number(handle.dataset.index);
5305
+ const target = event.key === "ArrowUp" ? index - 1 : event.key === "ArrowDown" ? index + 1 : event.key === "Home" ? 0 : event.key === "End" ? this._values.length - 1 : -1;
5306
+ if (target < 0 || target >= this._values.length) return;
5307
+ event.preventDefault();
5308
+ this._move(index, target, "handle");
5309
+ }
5310
+ /**
5311
+ * Arms the row for dragging. Only the handle does this, so text selection inside a row's input
5312
+ * keeps working and a stray drag cannot reorder anything.
5313
+ * @param {PointerEvent} event
5314
+ * @returns {void}
5315
+ */
5316
+ _handlePointerDown(event) {
5317
+ const handle = event.target instanceof Element ? event.target.closest(".zx-multi-value-editor__handle") : null;
5318
+ this._disarm();
5319
+ if (!handle || handle.disabled) return;
5320
+ const row = (
5321
+ /** @type {HTMLElement|null} */
5322
+ handle.closest(".zx-multi-value-editor__row")
5323
+ );
5324
+ if (!row) return;
5325
+ row.draggable = true;
5326
+ this._armed = row;
5327
+ }
5328
+ /** @returns {void} */
5329
+ _disarm() {
5330
+ if (this._armed) this._armed.draggable = false;
5331
+ this._armed = null;
5332
+ }
5333
+ /** @param {DragEvent} event @returns {void} */
5334
+ _handleDragStart(event) {
5335
+ const row = event.target instanceof Element ? event.target.closest(".zx-multi-value-editor__row") : null;
5336
+ if (!row || row !== this._armed) {
5337
+ event.preventDefault();
5338
+ return;
5339
+ }
5340
+ this._dragIndex = Number(row.dataset.row);
5341
+ row.dataset.dragging = "true";
5342
+ this.el.setAttribute("data-state", "dragging");
5343
+ event.dataTransfer?.setData("text/plain", String(this._dragIndex));
5344
+ if (event.dataTransfer) event.dataTransfer.effectAllowed = "move";
5345
+ }
5346
+ /** @param {DragEvent} event @returns {void} */
5347
+ _handleDragOver(event) {
5348
+ if (this._dragIndex < 0) return;
5349
+ event.preventDefault();
5350
+ if (event.dataTransfer) event.dataTransfer.dropEffect = "move";
5351
+ this._showDrop(this._dropTarget(event.clientY));
5352
+ }
5353
+ /** @param {DragEvent} event @returns {void} */
5354
+ _handleDragLeave(event) {
5355
+ const to = event.relatedTarget;
5356
+ if (to instanceof Node && this._rows.contains(to)) return;
5357
+ this._clearDrop();
5358
+ }
5359
+ /** @param {DragEvent} event @returns {void} */
5360
+ _handleDrop(event) {
5361
+ if (this._dragIndex < 0) return;
5362
+ event.preventDefault();
5363
+ const from = this._dragIndex;
5364
+ const slot = this._dropTarget(event.clientY);
5365
+ this._finishDrag();
5366
+ this._move(from, slot > from ? slot - 1 : slot, "handle");
5367
+ }
5368
+ /** @returns {void} */
5369
+ _finishDrag() {
5370
+ this._dragIndex = -1;
5371
+ this._disarm();
5372
+ this._clearDrop();
5373
+ for (const row of this._rows.children) delete /** @type {HTMLElement} */
5374
+ row.dataset.dragging;
5375
+ if (this._initialState === null) this.el.removeAttribute("data-state");
5376
+ else this.el.setAttribute("data-state", this._initialState);
5377
+ }
5378
+ /**
5379
+ * Insertion slot (0…length) for a pointer at `clientY`.
5380
+ * @param {number} clientY Pointer position.
5381
+ * @returns {number}
5382
+ */
5383
+ _dropTarget(clientY) {
5384
+ const rows = [...this._rows.children];
5385
+ for (const [index, row] of rows.entries()) {
5386
+ const box = row.getBoundingClientRect();
5387
+ if (clientY < box.top + box.height / 2) return index;
5388
+ }
5389
+ return rows.length;
5390
+ }
5391
+ /** @param {number} slot Insertion slot to mark. @returns {void} */
5392
+ _showDrop(slot) {
5393
+ const rows = [...this._rows.children];
5394
+ const inert = slot === this._dragIndex || slot === this._dragIndex + 1;
5395
+ for (const [index, row] of rows.entries()) {
5396
+ const cell = (
5397
+ /** @type {HTMLElement} */
5398
+ row
5399
+ );
5400
+ if (!inert && index === slot) cell.dataset.drop = "before";
5401
+ else if (!inert && slot === rows.length && index === rows.length - 1) cell.dataset.drop = "after";
5402
+ else delete cell.dataset.drop;
5403
+ }
5404
+ }
5405
+ /** @returns {void} */
5406
+ _clearDrop() {
5407
+ for (const row of this._rows.children) delete /** @type {HTMLElement} */
5408
+ row.dataset.drop;
5409
+ }
5410
+ /**
5411
+ * @param {number} from Index to move.
5412
+ * @param {number} to Destination index.
5413
+ * @param {keyof PARTS} part Control of the moved row that takes focus.
5414
+ * @returns {boolean} Whether anything moved.
5415
+ */
5416
+ _move(from, to, part) {
5417
+ const size = this._values.length;
5418
+ if (from === to || from < 0 || to < 0 || from >= size || to >= size) return false;
5419
+ const [value] = this._values.splice(from, 1);
5420
+ this._values.splice(to, 0, value);
5421
+ this._renderRows(to, part);
5422
+ this._emitChange();
5423
+ return true;
5424
+ }
5249
5425
  /** @returns {void} */
5250
5426
  _emitChange() {
5251
5427
  this.emit("change", { values: this.getValues() });
5252
5428
  }
5253
5429
  /** @returns {void} */
5254
5430
  destroy() {
5431
+ if (this._initialState === null) this.el?.removeAttribute("data-state");
5432
+ else this.el?.setAttribute("data-state", this._initialState);
5255
5433
  for (const node of this._ownedNodes ?? []) node.remove();
5256
5434
  super.destroy();
5257
5435
  }
@@ -6276,6 +6454,7 @@ var Select = class _Select extends Component {
6276
6454
  /** @type {SelectOptions} */
6277
6455
  static defaults = {
6278
6456
  items: [],
6457
+ fixedItems: [],
6279
6458
  valueKey: "ID",
6280
6459
  labelKey: "name",
6281
6460
  renderItem: null,
@@ -6295,6 +6474,8 @@ var Select = class _Select extends Component {
6295
6474
  render() {
6296
6475
  this._items = [];
6297
6476
  this._visibleItems = [];
6477
+ this._fixedItems = [];
6478
+ this._visibleFixed = [];
6298
6479
  this._selected = null;
6299
6480
  this._optionEntries = [];
6300
6481
  this._activeIndex = -1;
@@ -6378,6 +6559,8 @@ var Select = class _Select extends Component {
6378
6559
  root.replaceChildren(control, panel);
6379
6560
  this._items = Array.isArray(this.options.items) ? this.options.items.slice() : [];
6380
6561
  this._visibleItems = this._items.slice();
6562
+ this._fixedItems = Array.isArray(this.options.fixedItems) ? this.options.fixedItems.slice() : [];
6563
+ this._visibleFixed = this._fixedItems.slice();
6381
6564
  this._disabled = Boolean(this.options.disabled);
6382
6565
  this._handleTypeahead = typeahead(
6383
6566
  () => this._optionEntries.filter((entry) => !entry.clear).map((entry) => entry.element),
@@ -6454,7 +6637,22 @@ var Select = class _Select extends Component {
6454
6637
  const selectedValue = this.value;
6455
6638
  this._items = Array.isArray(items) ? items.slice() : [];
6456
6639
  this._visibleItems = this._items.slice();
6457
- this._selected = selectedValue == null ? null : this._items.find((item) => Object.is(this._itemValue(item), selectedValue)) ?? null;
6640
+ this._selected = selectedValue == null ? null : this._findItem(selectedValue);
6641
+ this._syncInputValue();
6642
+ this._renderOptions();
6643
+ this._position?.update();
6644
+ return this;
6645
+ }
6646
+ /**
6647
+ * Replaces the pinned choices and preserves selection by ID when possible.
6648
+ * @param {SelectItem[]} items New fixed items.
6649
+ * @returns {this}
6650
+ */
6651
+ setFixedItems(items) {
6652
+ const selectedValue = this.value;
6653
+ this._fixedItems = Array.isArray(items) ? items.slice() : [];
6654
+ this._visibleFixed = this._fixedItems.slice();
6655
+ this._selected = selectedValue == null ? null : this._findItem(selectedValue);
6458
6656
  this._syncInputValue();
6459
6657
  this._renderOptions();
6460
6658
  this._position?.update();
@@ -6508,6 +6706,7 @@ var Select = class _Select extends Component {
6508
6706
  this.refs.input.removeAttribute("aria-activedescendant");
6509
6707
  this._activeIndex = -1;
6510
6708
  this._visibleItems = this._items.slice();
6709
+ this._visibleFixed = this._fixedItems.slice();
6511
6710
  this._syncInputValue();
6512
6711
  this.emit("close", {});
6513
6712
  return this;
@@ -6563,6 +6762,38 @@ var Select = class _Select extends Component {
6563
6762
  select.set(requestedValue, { silent: true });
6564
6763
  return select;
6565
6764
  }
6765
+ /**
6766
+ * Creates the record-permission picker: the fixed Private and Public choices above the groups a
6767
+ * record can be shared with. The value is the tri-state ZeyOS stores — `false` for private,
6768
+ * `true` for public, or a group ID — so the control binds straight to the record field.
6769
+ *
6770
+ * @param {Element|string|null} target Component target.
6771
+ * @param {SelectOptions & {groups?: SelectItem[]|((query: string) => Promise<SelectItem[]>|SelectItem[])}} [options={}]
6772
+ * Select overrides other than the fixed choices; `groups` holds the shareable groups, either as
6773
+ * an array (filtered locally) or as a loader function (queried remotely).
6774
+ * @returns {Select}
6775
+ */
6776
+ static permission(target, options = {}) {
6777
+ const { groups = [], value = true, valueKey = "ID", labelKey = "name", ...rest } = options;
6778
+ const loader = typeof groups === "function" ? groups : null;
6779
+ const shareable = loader ? [] : groups;
6780
+ const choices = [{ ID: false, name: "Private" }, { ID: true, name: "Public" }];
6781
+ const select = new _Select(target, {
6782
+ filter: loader ?? "local",
6783
+ ...rest,
6784
+ value: null,
6785
+ items: shareable,
6786
+ fixedItems: [],
6787
+ // The two choices carry their own ID and label, so a caller's group readers stay untouched.
6788
+ valueKey: (item) => choices.includes(item) ? item.ID : readItem(item, valueKey),
6789
+ labelKey: (item) => choices.includes(item) ? item.name : String(readItem(item, labelKey) ?? "")
6790
+ });
6791
+ choices[0].name = select._message("permission.private", "Private");
6792
+ choices[1].name = select._message("permission.public", "Public");
6793
+ select.setFixedItems(choices);
6794
+ select.set(value, { silent: true });
6795
+ return select;
6796
+ }
6566
6797
  /** Cleans up the panel, pending async work, and enhanced target contents. @returns {void} */
6567
6798
  destroy() {
6568
6799
  this._cancelQuery();
@@ -6633,8 +6864,8 @@ var Select = class _Select extends Component {
6633
6864
  );
6634
6865
  if (!this._open) this.open();
6635
6866
  if (this.options.filter === "local") {
6636
- const keys = this.options.searchKeys ?? [this.options.labelKey];
6637
- this._visibleItems = matchItems(this._items, query, keys);
6867
+ this._visibleFixed = this._matchFixed(query);
6868
+ this._visibleItems = matchItems(this._items, query, this._searchKeys());
6638
6869
  this._renderOptions();
6639
6870
  this._setActive(this._optionEntries.length > 0 ? 0 : -1);
6640
6871
  this._position?.update();
@@ -6689,6 +6920,10 @@ var Select = class _Select extends Component {
6689
6920
  if (this.options.clearable) {
6690
6921
  this._appendOption(null, true, this.options.placeholder || this._message("select.none", "No selection"));
6691
6922
  }
6923
+ for (const item of this._visibleFixed) this._appendOption(item, false);
6924
+ if (this._visibleFixed.length > 0 && this._visibleItems.length > 0 && !this.options.groupKey) {
6925
+ list.append(h("div", { class: "zx-select__separator", role: "presentation" }));
6926
+ }
6692
6927
  for (const item of this._visibleItems) {
6693
6928
  if (this.options.groupKey) {
6694
6929
  const group = this._read(item, this.options.groupKey);
@@ -6702,7 +6937,7 @@ var Select = class _Select extends Component {
6702
6937
  }
6703
6938
  this._appendOption(item, false);
6704
6939
  }
6705
- if (this._visibleItems.length === 0 && !this._loading) {
6940
+ if (this._visibleItems.length === 0 && this._visibleFixed.length === 0 && !this._loading) {
6706
6941
  list.append(h("div", {
6707
6942
  class: "zx-select__empty",
6708
6943
  role: "status"
@@ -6742,6 +6977,7 @@ var Select = class _Select extends Component {
6742
6977
  if (this._queryTimer !== null) clearTimeout(this._queryTimer);
6743
6978
  this._requestSequence += 1;
6744
6979
  const sequence = this._requestSequence;
6980
+ this._visibleFixed = this._matchFixed(query);
6745
6981
  if (query.length < Math.max(0, Number(this.options.minQuery) || 0)) {
6746
6982
  this._setLoading(false);
6747
6983
  this._visibleItems = [];
@@ -6826,13 +7062,14 @@ var Select = class _Select extends Component {
6826
7062
  }
6827
7063
  /** @param {unknown} candidate @returns {SelectItem|null} */
6828
7064
  _findItem(candidate) {
6829
- const direct = this._items.find((item) => item === candidate);
7065
+ const pool = this._fixedItems.length === 0 ? this._items : [...this._fixedItems, ...this._items];
7066
+ const direct = pool.find((item) => item === candidate);
6830
7067
  if (direct !== void 0) return direct;
6831
7068
  const value = candidate !== null && typeof candidate === "object" ? this._itemValue(
6832
7069
  /** @type {SelectItem} */
6833
7070
  candidate
6834
7071
  ) : candidate;
6835
- return this._items.find((item) => Object.is(this._itemValue(item), value)) ?? null;
7072
+ return pool.find((item) => Object.is(this._itemValue(item), value)) ?? null;
6836
7073
  }
6837
7074
  /** @param {SelectItem|null} item @returns {unknown} */
6838
7075
  _itemValue(item) {
@@ -6845,9 +7082,20 @@ var Select = class _Select extends Component {
6845
7082
  }
6846
7083
  /** @param {SelectItem|null} item @param {SelectValueReader|SelectLabelReader} reader @returns {unknown} */
6847
7084
  _read(item, reader) {
6848
- if (typeof reader === "function") return reader(item);
6849
- if (item === null || typeof item !== "object") return item;
6850
- return item[reader];
7085
+ return readItem(item, reader);
7086
+ }
7087
+ /** @returns {Array<SelectValueReader|SelectLabelReader>} */
7088
+ _searchKeys() {
7089
+ return this.options.searchKeys ?? [this.options.labelKey];
7090
+ }
7091
+ /**
7092
+ * Narrows the pinned choices. They are always matched here and never handed to a filter
7093
+ * function: a fixed choice belongs to the control, not to whatever the remote source returns.
7094
+ * @param {string} query Search text.
7095
+ * @returns {SelectItem[]}
7096
+ */
7097
+ _matchFixed(query) {
7098
+ return matchItems(this._fixedItems, query, this._searchKeys());
6851
7099
  }
6852
7100
  /** @returns {number} */
6853
7101
  _selectedEntryIndex() {
@@ -6870,6 +7118,11 @@ var Select = class _Select extends Component {
6870
7118
  return message === key ? fallback : message;
6871
7119
  }
6872
7120
  };
7121
+ function readItem(item, reader) {
7122
+ if (typeof reader === "function") return reader(item);
7123
+ if (item === null || typeof item !== "object") return item;
7124
+ return item[reader];
7125
+ }
6873
7126
  function isPrintable(event) {
6874
7127
  return event.key.length === 1 && !event.ctrlKey && !event.metaKey && !event.altKey;
6875
7128
  }
@@ -9996,6 +10249,10 @@ var Tabbox = class extends Component {
9996
10249
  if (!tab || !tablist.contains(tab)) return;
9997
10250
  const record = this._recordForTab(tab);
9998
10251
  if (!record || record.definition.disabled) return;
10252
+ if (event.target.closest?.(".zx-tabbox__close")) {
10253
+ this._closeTab(record);
10254
+ return;
10255
+ }
9999
10256
  this._setFocused(record);
10000
10257
  this.openTab(record.definition.name);
10001
10258
  });
@@ -10045,8 +10302,9 @@ var Tabbox = class extends Component {
10045
10302
  children.push(title, badge);
10046
10303
  if (normalized.closable) {
10047
10304
  children.push(h("span", {
10048
- class: "zx-tabbox__close",
10049
- ariaHidden: "true"
10305
+ class: "zx-tabbox__close zx-icon-btn",
10306
+ ariaHidden: "true",
10307
+ title: "Close"
10050
10308
  }, icon("x", { size: 14 })));
10051
10309
  }
10052
10310
  const tab = (
@@ -10220,13 +10478,23 @@ var Tabbox = class extends Component {
10220
10478
  }
10221
10479
  if (event.key === "Delete" && current.definition.closable) {
10222
10480
  event.preventDefault();
10223
- const index = this._tabs.indexOf(current);
10224
- if (!this._removeTab(current.definition.name, true)) return;
10225
- const remaining = this._tabs[Math.min(index, this._tabs.length - 1)] ?? this._tabs[index - 1];
10226
- const focusTarget = remaining && !remaining.definition.disabled ? remaining : this._active;
10227
- if (focusTarget) this._setFocused(focusTarget);
10481
+ this._closeTab(current);
10228
10482
  }
10229
10483
  }
10484
+ /**
10485
+ * Closes a tab on a user gesture, keeping roving focus inside the tablist.
10486
+ * @param {TabRecord} record Tab to close.
10487
+ * @returns {void}
10488
+ * @fires Tabbox#close
10489
+ */
10490
+ _closeTab(record) {
10491
+ const index = this._tabs.indexOf(record);
10492
+ const held = record.tab.contains(document.activeElement);
10493
+ if (!this._removeTab(record.definition.name, true)) return;
10494
+ const remaining = this._tabs[Math.min(index, this._tabs.length - 1)] ?? this._tabs[index - 1];
10495
+ const focusTarget = remaining && !remaining.definition.disabled ? remaining : this._active;
10496
+ if (focusTarget) this._setFocused(focusTarget, held);
10497
+ }
10230
10498
  /** @param {string} name @param {boolean} emitClose @returns {boolean} */
10231
10499
  _removeTab(name, emitClose) {
10232
10500
  const record = this._find(name);