flexdesk 0.18.0 → 0.18.1

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 (44) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/css/flexdesk.css +33 -5
  3. package/css/tokens.css +7 -0
  4. package/dist/charts.js +78 -12
  5. package/dist/charts.js.map +2 -2
  6. package/dist/{chunk-5W4EEKSK.js → chunk-3BLIVOFD.js} +3 -3
  7. package/dist/{chunk-P74F6I6P.js → chunk-4X4SY3CP.js} +4 -4
  8. package/dist/{chunk-P74F6I6P.js.map → chunk-4X4SY3CP.js.map} +2 -2
  9. package/dist/{chunk-ZL3VKINP.js → chunk-HL3IIHU3.js} +494 -3
  10. package/dist/{chunk-ZL3VKINP.js.map → chunk-HL3IIHU3.js.map} +4 -4
  11. package/dist/{chunk-RKVYIXDC.js → chunk-NFZJ6P42.js} +19 -500
  12. package/dist/chunk-NFZJ6P42.js.map +7 -0
  13. package/dist/{chunk-U5GKJUOC.js → chunk-QRBOIUU3.js} +5 -6
  14. package/dist/chunk-QRBOIUU3.js.map +7 -0
  15. package/dist/flexdesk.css +33 -5
  16. package/dist/flow.js +1 -1
  17. package/dist/git.js +3 -3
  18. package/dist/tiles.js +4 -4
  19. package/dist/tokens.css +7 -0
  20. package/dist/widgets.js +226 -58
  21. package/dist/widgets.js.map +2 -2
  22. package/dist/wm.js +2 -2
  23. package/docs/components/charts.md +40 -0
  24. package/docs/components/chat-shell.md +8 -2
  25. package/docs/components/filter-builder.md +52 -6
  26. package/docs/components/forms-and-dialogs.md +8 -0
  27. package/docs/components/input-fields.md +24 -0
  28. package/docs/components/menus.md +4 -0
  29. package/docs/components/shell.md +4 -0
  30. package/docs/components/workspace-picker.md +46 -2
  31. package/package.json +1 -1
  32. package/src/charts/plot_config_editor.js +92 -6
  33. package/src/charts/plot_grid.js +6 -1
  34. package/src/ui/components/chat_shell.js +7 -1
  35. package/src/ui/components/context_menu.js +8 -2
  36. package/src/ui/components/filter_builder.js +53 -28
  37. package/src/ui/components/filter_model.js +86 -18
  38. package/src/ui/components/inline_renamer.js +10 -5
  39. package/src/ui/components/modal.js +25 -8
  40. package/src/ui/components/workspace_picker.js +125 -27
  41. package/src/ui/utils/tooltip_service.js +81 -14
  42. package/dist/chunk-RKVYIXDC.js.map +0 -7
  43. package/dist/chunk-U5GKJUOC.js.map +0 -7
  44. /package/dist/{chunk-5W4EEKSK.js.map → chunk-3BLIVOFD.js.map} +0 -0
package/dist/widgets.js CHANGED
@@ -23,7 +23,7 @@ import {
23
23
  toastInfo,
24
24
  toastSuccess,
25
25
  toastWarn
26
- } from "./chunk-5W4EEKSK.js";
26
+ } from "./chunk-3BLIVOFD.js";
27
27
  import {
28
28
  StateMachine
29
29
  } from "./chunk-3PHCPZHT.js";
@@ -54,15 +54,12 @@ import {
54
54
  import {
55
55
  ColorListField,
56
56
  SortableList,
57
- attachInputOptions,
58
- createCombobox,
59
- createOptionList,
60
57
  hideContextMenu,
61
58
  openConfirm,
62
59
  openForm,
63
60
  openModal,
64
61
  showContextMenu
65
- } from "./chunk-RKVYIXDC.js";
62
+ } from "./chunk-NFZJ6P42.js";
66
63
  import {
67
64
  ACTIONS,
68
65
  AutocompleteField,
@@ -109,7 +106,7 @@ import {
109
106
  resolvePaging,
110
107
  toTSV,
111
108
  tsvCell
112
- } from "./chunk-U5GKJUOC.js";
109
+ } from "./chunk-QRBOIUU3.js";
113
110
  import {
114
111
  ROW_CONTROL_SELECTOR,
115
112
  endsTextSelection,
@@ -122,10 +119,13 @@ import {
122
119
  DEFAULT_WINDOW_STORAGE_KEY,
123
120
  ManagedWindow,
124
121
  ToggleField,
122
+ attachInputOptions,
123
+ createCombobox,
124
+ createOptionList,
125
125
  fireEvent,
126
126
  isHexColor,
127
127
  normalizeHex
128
- } from "./chunk-ZL3VKINP.js";
128
+ } from "./chunk-HL3IIHU3.js";
129
129
  import {
130
130
  choiceSections,
131
131
  filterSections,
@@ -3142,6 +3142,7 @@ function mountChatShell(host, {
3142
3142
  closeReply("stopped");
3143
3143
  machine.cancel();
3144
3144
  if (sent) callCancel(request);
3145
+ else runEnding = null;
3145
3146
  syncWelcome();
3146
3147
  changed();
3147
3148
  return true;
@@ -3213,6 +3214,7 @@ function mountChatShell(host, {
3213
3214
  const modelBtn = el3("button", "twm-chat-shell__model-btn");
3214
3215
  modelBtn.type = "button";
3215
3216
  modelBtn.setAttribute("aria-haspopup", "menu");
3217
+ modelBtn.setAttribute("aria-expanded", "false");
3216
3218
  const modelDot = el3("span", "twm-chat-shell__status");
3217
3219
  const modelLabel = el3("span", "twm-chat-shell__model-label");
3218
3220
  modelBtn.append(modelDot, modelLabel, icon2("expand_more", "twm-chat-shell__model-chevron"));
@@ -3887,6 +3889,25 @@ function createFilterModel({
3887
3889
  if (value !== void 0) clause.value = value;
3888
3890
  return clause;
3889
3891
  }
3892
+ function valueFits(field, op, value) {
3893
+ const errors = [];
3894
+ validateClause({ field: field.key, op: op.id, value }, errors, "");
3895
+ if (errors.length) return false;
3896
+ const options = op.multi || field.type === "enum" ? filterOptions(field) : [];
3897
+ return !options.length || arr(value).every((v) => options.some((o) => o.value === v));
3898
+ }
3899
+ function moveClause(clause, key) {
3900
+ const next = newClause(key);
3901
+ if (!next) return null;
3902
+ const field = byKey.get(key);
3903
+ const op = operatorsFor(key).find((o) => o.id === clause.op);
3904
+ if (!op) return next;
3905
+ next.op = op.id;
3906
+ delete next.value;
3907
+ const value = op.arity && valueFits(field, op, clause.value) ? clause.value : defaultValue(field, op);
3908
+ if (value !== void 0) next.value = value;
3909
+ return next;
3910
+ }
3890
3911
  function compileClause(clause) {
3891
3912
  const field = byKey.get(clause?.field);
3892
3913
  if (!field) {
@@ -4066,26 +4087,30 @@ function createFilterModel({
4066
4087
  const [a, b] = arr(value);
4067
4088
  return `${shown(a)} and ${shown(b)}`;
4068
4089
  }
4069
- return shown(first(value));
4090
+ return Array.isArray(value) ? value.map(shown).join(", ") : shown(value);
4070
4091
  }
4071
- function describeNode(node2, depth, format) {
4092
+ function describeNode(node2, depth, opts) {
4072
4093
  if (!node2) return "";
4073
4094
  if (node2.kind === "clause") {
4074
4095
  const field = byKey.get(node2.field);
4075
- if (!field) return `\u26A0 unknown field \u201C${unknownName(String(node2.field ?? ""))}\u201D`;
4076
- const op = catalogue.get(node2.op);
4077
- if (!op) return `${field.label} ${node2.op ?? ""}`.trim();
4078
- const val = describeValue(field, op, node2.value, format);
4079
- return val ? `${field.label} ${op.label} ${val}` : `${field.label} ${op.label}`;
4096
+ const op = catalogue.get(node2.op) ?? { label: node2.op ?? "", arity: 1 };
4097
+ const subject = field ? field.label : `\u26A0 unknown field \u201C${unknownName(String(node2.field ?? ""))}\u201D`;
4098
+ const value = describeValue(field, op, node2.value, field ? opts.formatValue : null);
4099
+ return [subject, op.label, value].filter(Boolean).join(" ");
4080
4100
  }
4081
4101
  if (node2.kind !== "group") return `\u26A0 ${JSON.stringify(node2.node ?? node2)}`;
4082
- const parts = (Array.isArray(node2.children) ? node2.children : []).map((c) => describeNode(c, depth + 1, format)).filter(Boolean);
4102
+ const parts = (Array.isArray(node2.children) ? node2.children : []).map((c) => describeNode(c, depth + 1, opts)).filter(Boolean);
4083
4103
  if (!parts.length) return emptyMatches(node2) !== !!node2.negate ? "everything" : "nothing";
4084
4104
  const joined = parts.join(String(node2.op || "AND").toUpperCase() === "OR" ? " OR " : " AND ");
4105
+ const many = parts.length > 1;
4106
+ if (opts.groupBrackets) {
4107
+ const group = many ? `(${joined})` : joined;
4108
+ return node2.negate ? `NOT ${group}` : group;
4109
+ }
4085
4110
  if (node2.negate) return `NOT (${joined})`;
4086
- return depth > 0 && parts.length > 1 ? `(${joined})` : joined;
4111
+ return depth > 0 && many ? `(${joined})` : joined;
4087
4112
  }
4088
- const describeFilter = (expr, { formatValue = null } = {}) => describeNode(expr, 0, formatValue);
4113
+ const describeFilter = (expr, { formatValue = null, groupBrackets = false } = {}) => describeNode(expr, 0, { formatValue, groupBrackets });
4089
4114
  function unknownFields(expr) {
4090
4115
  const out = /* @__PURE__ */ new Set();
4091
4116
  const walk = (n) => {
@@ -4112,13 +4137,13 @@ function createFilterModel({
4112
4137
  }
4113
4138
  if (op.arity === 0) return;
4114
4139
  const say = (what) => errors.push(`${field.label} ${op.label}: ${what}`);
4115
- if (op.multi) {
4116
- if (!Array.isArray(node2.value) || node2.value.length === 0) say("pick at least one value");
4140
+ if (op.multi && (!Array.isArray(node2.value) || node2.value.length === 0)) {
4141
+ say("pick at least one value");
4117
4142
  return;
4118
4143
  }
4119
- const values = op.arity === 2 ? arr(node2.value).slice(0, 2) : [first(node2.value)];
4144
+ const values = op.multi ? node2.value : op.arity === 2 ? arr(node2.value).slice(0, 2) : [first(node2.value)];
4120
4145
  const needed = op.arity === 2 ? 2 : 1;
4121
- if (values.length < needed || values.some(isBlank)) {
4146
+ if (!op.multi && (values.length < needed || values.some(isBlank))) {
4122
4147
  say(needed === 2 ? "needs two values" : "needs a value");
4123
4148
  return;
4124
4149
  }
@@ -4154,6 +4179,24 @@ function createFilterModel({
4154
4179
  validateNode(expr, errors, "expr");
4155
4180
  return { ok: errors.length === 0, errors };
4156
4181
  }
4182
+ function partialRanges(expr) {
4183
+ const said = [];
4184
+ const walk = (node2) => {
4185
+ if (node2?.kind === "group") {
4186
+ (Array.isArray(node2.children) ? node2.children : []).forEach(walk);
4187
+ return;
4188
+ }
4189
+ if (node2?.kind !== "clause") return;
4190
+ const field = byKey.get(node2.field);
4191
+ const op = catalogue.get(node2.op);
4192
+ if (!field || op?.arity !== 2 || op.multi) return;
4193
+ const ends = arr(node2.value).slice(0, 2);
4194
+ const filled = ends.filter((v) => !isBlank(v)).length;
4195
+ if (filled === 1) said.push(`${field.label} ${op.label}: needs two values`);
4196
+ };
4197
+ walk(expr);
4198
+ return said;
4199
+ }
4157
4200
  function prune(node2) {
4158
4201
  if (!node2 || typeof node2 !== "object") return null;
4159
4202
  if (node2.kind === "clause") {
@@ -4177,6 +4220,7 @@ function createFilterModel({
4177
4220
  operatorById,
4178
4221
  defaultValue,
4179
4222
  newClause,
4223
+ moveClause,
4180
4224
  emptyMatches,
4181
4225
  unknownName,
4182
4226
  matcherFor,
@@ -4184,7 +4228,8 @@ function createFilterModel({
4184
4228
  describeFilter,
4185
4229
  validateFilter,
4186
4230
  unknownFields,
4187
- pruneFilter
4231
+ pruneFilter,
4232
+ partialRanges
4188
4233
  });
4189
4234
  }
4190
4235
 
@@ -4532,18 +4577,21 @@ function createFilterBuilder(host, { model, value = null, onChange = null } = {}
4532
4577
  function operatorChoices(clause) {
4533
4578
  const offered = model.operatorsFor(clause.field);
4534
4579
  const held = model.operatorById(clause.op);
4535
- const list = held && model.fieldByKey(clause.field) && !offered.includes(held) ? [...offered, held] : offered;
4580
+ const list = held && !offered.includes(held) ? [...offered, held] : offered;
4536
4581
  return list.map((op) => ({ value: op.id, label: op.label }));
4537
4582
  }
4538
4583
  function clauseEl(clause) {
4539
4584
  const row = el("div", "twm-filter-builder__row");
4585
+ const known = !!model.fieldByKey(clause.field);
4586
+ const unknownWords = `\u26A0 unknown field \u201C${model.unknownName(String(clause.field ?? ""))}\u201D`;
4540
4587
  const fieldSelect = selectBox(fieldChoices(clause.field), clause.field, {
4541
4588
  label: "Field",
4542
- className: `${SELECT_CLASS} twm-filter-builder__select--field`,
4543
- notFound: () => `\u26A0 unknown field \u201C${model.unknownName(String(clause.field ?? ""))}\u201D`
4589
+ className: `${SELECT_CLASS} twm-filter-builder__select--field${known ? "" : " twm-filter-builder__select--unknown"}`,
4590
+ notFound: () => unknownWords
4544
4591
  });
4592
+ if (!known) fieldSelect.box.title = unknownWords;
4545
4593
  fieldSelect.box.addEventListener("change", () => {
4546
- const next = model.newClause(fieldSelect.valueOf());
4594
+ const next = model.moveClause(clause, fieldSelect.valueOf());
4547
4595
  for (const key of ["op", "value"]) delete clause[key];
4548
4596
  Object.assign(clause, next);
4549
4597
  restructure(clause, "field");
@@ -4581,7 +4629,7 @@ function createFilterBuilder(host, { model, value = null, onChange = null } = {}
4581
4629
  function drawValue(clause, cellEl, cell, drawCell) {
4582
4630
  const field = model.fieldByKey(clause.field);
4583
4631
  const op = model.operatorById(clause.op);
4584
- if (!field || !op) {
4632
+ if (!field || !op || op.arity && !model.operatorsFor(clause.field).includes(op)) {
4585
4633
  if (clause.value !== void 0) {
4586
4634
  cellEl.appendChild(el("span", "twm-filter-builder__carried", JSON.stringify(clause.value)));
4587
4635
  }
@@ -4635,6 +4683,7 @@ function createFilterBuilder(host, { model, value = null, onChange = null } = {}
4635
4683
  if (numeric) {
4636
4684
  input.type = "number";
4637
4685
  input.step = "any";
4686
+ if (op.input === "days") input.min = "0";
4638
4687
  } else if (field.type === "date") {
4639
4688
  input.type = field.time ? "datetime-local" : "date";
4640
4689
  } else {
@@ -4814,6 +4863,7 @@ async function openFilterDialog({
4814
4863
  requireValid = true,
4815
4864
  prune = false,
4816
4865
  confirmDiscard = false,
4866
+ dismissOnBackdrop = false,
4817
4867
  submitLabel = "Apply",
4818
4868
  onSubmit = null,
4819
4869
  clearLabel = null,
@@ -4822,10 +4872,11 @@ async function openFilterDialog({
4822
4872
  height = 560
4823
4873
  } = {}) {
4824
4874
  if (!model) throw new Error("[FilterDialog] needs a filter model");
4825
- const start = asRoot(value);
4826
- if (seedField) {
4827
- const seeded = model.newClause(seedField);
4828
- if (seeded) start.children.push(seeded);
4875
+ let start = asRoot(value);
4876
+ const seeded = seedField ? model.newClause(seedField) : null;
4877
+ if (seeded) {
4878
+ if (start.negate) start = { kind: "group", op: "AND", children: [start, seeded] };
4879
+ else start.children.push(seeded);
4829
4880
  }
4830
4881
  const content = el("div", "twm-filter-dialog");
4831
4882
  if (head) content.appendChild(head);
@@ -4895,9 +4946,12 @@ async function openFilterDialog({
4895
4946
  const expr = builder.getValue();
4896
4947
  return prune ? model.pruneFilter(expr) : expr;
4897
4948
  }
4898
- const checkOf = (expr) => expr ? model.validateFilter(expr) : { ok: true, errors: [] };
4949
+ const refusals = (expr) => {
4950
+ if (!expr) return [];
4951
+ return requireValid ? model.validateFilter(expr).errors : model.partialRanges(expr);
4952
+ };
4899
4953
  const nameMissing = () => !!nameInput && name.required !== false && !nameInput.value.trim();
4900
- const canSubmit = () => !busy && !nameMissing() && (!requireValid || checkOf(current()).ok);
4954
+ const canSubmit = () => !busy && !nameMissing() && !refusals(current()).length;
4901
4955
  function refresh() {
4902
4956
  if (!controls) return;
4903
4957
  for (const { value: value2 } of modalActions) {
@@ -4914,11 +4968,11 @@ async function openFilterDialog({
4914
4968
  }
4915
4969
  function drawPreview() {
4916
4970
  const expr = current();
4917
- const { ok, errors } = checkOf(expr);
4971
+ const errors = refusals(expr);
4918
4972
  previewRun?.abort();
4919
4973
  previewRun = null;
4920
- const blocked = requireValid && !ok;
4921
- showErrors(blocked ? errors : []);
4974
+ const blocked = errors.length > 0;
4975
+ showErrors(errors);
4922
4976
  if (!preview) return;
4923
4977
  slot.hidden = blocked;
4924
4978
  if (blocked) {
@@ -4954,11 +5008,15 @@ async function openFilterDialog({
4954
5008
  showFailure("");
4955
5009
  busy = true;
4956
5010
  try {
4957
- if (action.confirm && !await openConfirm(action.confirm)) return;
4958
- refresh();
4959
5011
  const result = { action: action.value, expr: action.value === CLEAR ? null : current() };
4960
5012
  if (nameInput) result.name = nameInput.value.trim();
4961
5013
  if (picker) result.icon = picker.value();
5014
+ if (action.confirm) {
5015
+ const { message } = action.confirm;
5016
+ const asked = typeof message === "function" ? { ...action.confirm, message: message(result) } : action.confirm;
5017
+ if (!await openConfirm(asked)) return;
5018
+ }
5019
+ refresh();
4962
5020
  await action.run?.(result);
4963
5021
  close(result);
4964
5022
  } catch (err) {
@@ -4999,9 +5057,12 @@ async function openFilterDialog({
4999
5057
  height,
5000
5058
  maximizable: true,
5001
5059
  beforeClose: mayDiscard,
5060
+ dismissOnBackdrop,
5002
5061
  onMount: (_body, modalControls) => {
5003
5062
  controls = modalControls;
5004
5063
  refresh();
5064
+ nameInput?.focus();
5065
+ nameInput?.select();
5005
5066
  }
5006
5067
  });
5007
5068
  drawPreview();
@@ -9198,8 +9259,11 @@ function keyedChildren(container, build, update) {
9198
9259
  if (!next.has(key)) entry.node.remove();
9199
9260
  }
9200
9261
  drawn = next;
9262
+ const focused = document.activeElement;
9201
9263
  nodes.forEach((node2, index) => {
9202
- if (container.children[index] !== node2) {
9264
+ if (node2.contains(focused)) {
9265
+ while (container.children[index] !== node2) container.append(container.children[index]);
9266
+ } else if (container.children[index] !== node2) {
9203
9267
  container.insertBefore(node2, container.children[index] || null);
9204
9268
  }
9205
9269
  });
@@ -9375,6 +9439,7 @@ function createPickerSwitcher({
9375
9439
  let activeKey = null;
9376
9440
  let activeNode = null;
9377
9441
  let moved = false;
9442
+ const rovingKeys = {};
9378
9443
  let nextEntryId = 0;
9379
9444
  const kept = /* @__PURE__ */ new Map();
9380
9445
  const idOf = (item) => String(describe(item, { current: false }).id);
@@ -9444,6 +9509,7 @@ function createPickerSwitcher({
9444
9509
  const node2 = part("div", "twm-wspicker__row");
9445
9510
  node2.id = `${uid}-row-${++nextEntryId}`;
9446
9511
  node2.setAttribute("role", "option");
9512
+ node2.tabIndex = -1;
9447
9513
  const icon2 = glyph("", "twm-wspicker__row-icon");
9448
9514
  const text = part("span", "twm-wspicker__row-text");
9449
9515
  const name = part("span", "twm-wspicker__row-name");
@@ -9483,6 +9549,7 @@ function createPickerSwitcher({
9483
9549
  const disabled = !!model.disabled;
9484
9550
  paintRow(entry, { ...model, disabled, isCurrentRow: isCurrent(item) });
9485
9551
  entry.run = disabled ? null : () => pick(item);
9552
+ if (disabled) entry.node.tabIndex = -1;
9486
9553
  const removable = !!onRemove && model.removable !== false;
9487
9554
  entry.remove = removable ? () => removeItem(item) : null;
9488
9555
  show(entry.bin, removable);
@@ -9594,6 +9661,15 @@ function createPickerSwitcher({
9594
9661
  }
9595
9662
  for (const [key, entry] of drawnFoot) next.push({ key: `foot:${key}`, node: entry.node, run: entry.run });
9596
9663
  stops = next;
9664
+ syncTabStops();
9665
+ }
9666
+ function syncTabStops() {
9667
+ const currentKey = current == null ? null : `row:${idOf(current)}`;
9668
+ for (const prefix of ["row:", "action:"]) {
9669
+ const rows = stops.filter((s) => s.key.startsWith(prefix));
9670
+ const chosen = rows.find((s) => s.key === rovingKeys[prefix]) ?? rows.find((s) => s.key === currentKey) ?? rows[0];
9671
+ for (const stop of rows) stop.node.tabIndex = stop === chosen ? 0 : -1;
9672
+ }
9597
9673
  }
9598
9674
  function paintActive() {
9599
9675
  const stop = stops.find((s) => s.key === activeKey) ?? null;
@@ -9616,19 +9692,29 @@ function createPickerSwitcher({
9616
9692
  }
9617
9693
  if (!moved || !stops.some((s) => s.key === activeKey)) {
9618
9694
  reveal = moved;
9695
+ const at = document.activeElement;
9696
+ if (moved && isOpen && (at === document.body || panel.contains(at) && at !== input)) input.focus();
9619
9697
  moved = false;
9620
9698
  activeKey = stops.find((s) => s.primary)?.key ?? null;
9621
9699
  }
9622
9700
  paintActive();
9623
9701
  if (reveal) revealActive();
9624
9702
  }
9625
- function move(step) {
9626
- if (!stops.length) return;
9627
- const at = stops.findIndex((s) => s.key === activeKey);
9628
- const next = at === -1 ? step > 0 ? 0 : stops.length - 1 : (at + step + stops.length) % stops.length;
9629
- activeKey = stops[next].key;
9703
+ function focusStop(stop) {
9704
+ activeKey = stop.key;
9630
9705
  moved = true;
9706
+ stop.node.focus({ preventScroll: true });
9631
9707
  paintActive();
9708
+ }
9709
+ function move(step) {
9710
+ const walk = [input, ...stops.map((s) => s.node)];
9711
+ const from = document.activeElement === input && moved ? activeNode : document.activeElement;
9712
+ const next = walk[(walk.indexOf(from) + step + walk.length) % walk.length];
9713
+ if (next === input) {
9714
+ input.focus();
9715
+ return;
9716
+ }
9717
+ focusStop(stops.find((s) => s.node === next));
9632
9718
  revealActive();
9633
9719
  }
9634
9720
  function render() {
@@ -9652,7 +9738,7 @@ function createPickerSwitcher({
9652
9738
  if (remaining > 0) {
9653
9739
  setText2(moreLabel, labels.loadMore({ next: Math.min(pageSize, remaining), shown: items.length, total }));
9654
9740
  }
9655
- more.disabled = loading || loadingMore;
9741
+ more.setAttribute("aria-disabled", String(loading || loadingMore));
9656
9742
  collectStops(rowEntries, actionEntries, shown);
9657
9743
  settleActive();
9658
9744
  }
@@ -9745,7 +9831,11 @@ function createPickerSwitcher({
9745
9831
  }
9746
9832
  }
9747
9833
  renderEverything();
9748
- if (byKey && stayed) revealActive();
9834
+ if (stayed) {
9835
+ const stop = stops.find((s) => s.key === activeKey);
9836
+ if (stop) focusStop(stop);
9837
+ if (byKey) revealActive();
9838
+ }
9749
9839
  }
9750
9840
  more.addEventListener("click", () => loadMore());
9751
9841
  function setBusy(waiting) {
@@ -9758,6 +9848,7 @@ function createPickerSwitcher({
9758
9848
  const removedKey = `row:${idOf(item)}`;
9759
9849
  const at = stops.findIndex((s) => s.key === removedKey);
9760
9850
  const following = stops.slice(at + 1).find((s) => s.key.startsWith("row:")) ?? stops.slice(0, Math.max(at, 0)).reverse().find((s) => s.key.startsWith("row:"));
9851
+ const fromRow = stops[at]?.node === document.activeElement;
9761
9852
  setBusy(true);
9762
9853
  let outcome;
9763
9854
  try {
@@ -9781,6 +9872,11 @@ function createPickerSwitcher({
9781
9872
  moved = true;
9782
9873
  }
9783
9874
  ask(remote ? answered : "");
9875
+ if (fromRow && following) {
9876
+ focusStop(following);
9877
+ revealActive();
9878
+ return;
9879
+ }
9784
9880
  }
9785
9881
  input.focus();
9786
9882
  }
@@ -9847,6 +9943,8 @@ function createPickerSwitcher({
9847
9943
  pickFirst = false;
9848
9944
  moved = false;
9849
9945
  activeKey = null;
9946
+ delete rovingKeys["row:"];
9947
+ delete rovingKeys["action:"];
9850
9948
  const last = kept.get(cacheKey());
9851
9949
  items = last ? [...last.items] : [];
9852
9950
  total = last ? last.total : null;
@@ -9902,6 +10000,24 @@ function createPickerSwitcher({
9902
10000
  panel.addEventListener("mousedown", (event) => {
9903
10001
  if (event.target !== input) event.preventDefault();
9904
10002
  });
10003
+ panel.addEventListener("focusin", ({ target, relatedTarget }) => {
10004
+ if (target === input) {
10005
+ if (panel.contains(relatedTarget)) {
10006
+ moved = false;
10007
+ settleActive();
10008
+ }
10009
+ return;
10010
+ }
10011
+ const stop = stops.find((s) => s.node === target);
10012
+ if (!stop) return;
10013
+ activeKey = stop.key;
10014
+ moved = true;
10015
+ selectId = null;
10016
+ const prefix = ["row:", "action:"].find((p) => stop.key.startsWith(p));
10017
+ if (prefix) rovingKeys[prefix] = stop.key;
10018
+ syncTabStops();
10019
+ paintActive();
10020
+ });
9905
10021
  input.addEventListener("input", () => {
9906
10022
  moved = false;
9907
10023
  scroller.scrollTop = 0;
@@ -9922,21 +10038,27 @@ function createPickerSwitcher({
9922
10038
  event.preventDefault();
9923
10039
  event.stopPropagation();
9924
10040
  };
10041
+ const stop = stops.find((s) => s.node === target);
9925
10042
  if (key === "Escape") {
9926
10043
  handled();
9927
10044
  close({ restoreFocus: true });
9928
10045
  } else if (key === "ArrowDown" || key === "ArrowUp") {
9929
10046
  handled();
9930
- if (document.activeElement !== input) input.focus();
9931
10047
  move(key === "ArrowDown" ? 1 : -1);
9932
10048
  } else if (key === "Enter" && target === input) {
9933
10049
  handled();
9934
10050
  enter();
9935
- } else if (key === "Delete" && shiftKey && target === input) {
10051
+ } else if ((key === "Enter" || key === " ") && stop) {
10052
+ handled();
10053
+ stop.run();
10054
+ } else if (key === "Delete" && shiftKey) {
9936
10055
  handled();
9937
10056
  stops.find((s) => s.key === activeKey)?.remove?.();
9938
10057
  } else if (key === "Backspace" && target !== input) {
9939
10058
  handled();
10059
+ } else if (target !== input && key.length === 1 && key !== " " && !event.ctrlKey && !event.metaKey && !event.altKey) {
10060
+ event.stopPropagation();
10061
+ input.focus();
9940
10062
  }
9941
10063
  });
9942
10064
  panel.addEventListener("focusout", (event) => {
@@ -11460,6 +11582,8 @@ var currentTarget = null;
11460
11582
  var listenersAttached = false;
11461
11583
  var mouseCheckInterval = null;
11462
11584
  var lastMousePosition = { x: 0, y: 0 };
11585
+ var focusTarget = null;
11586
+ var byKeyboard = false;
11463
11587
  function createTooltipElement() {
11464
11588
  if (tooltipElement) return tooltipElement;
11465
11589
  tooltipElement = document.createElement("div");
@@ -11538,11 +11662,12 @@ function showTooltip(target) {
11538
11662
  const hasLatex = target.classList && target.classList.contains("has-latex-tooltip");
11539
11663
  const hasPlain = target.classList && target.classList.contains("twm-has-tooltip");
11540
11664
  if (!hasLatex && !hasPlain) return;
11665
+ if (!target.getAttribute(hasLatex ? "data-latex-tooltip" : "data-tooltip")) return;
11666
+ stopMouseCheck();
11541
11667
  currentTarget = target;
11542
11668
  const tooltip = createTooltipElement();
11543
11669
  if (hasLatex) {
11544
11670
  const latex = target.getAttribute("data-latex-tooltip");
11545
- if (!latex) return;
11546
11671
  try {
11547
11672
  if (typeof katex !== "undefined") {
11548
11673
  katex.render(latex, tooltip, {
@@ -11559,7 +11684,6 @@ function showTooltip(target) {
11559
11684
  }
11560
11685
  } else if (hasPlain) {
11561
11686
  const text = target.getAttribute("data-tooltip");
11562
- if (!text) return;
11563
11687
  const formattedText = text.replace(/&#10;/g, "\n");
11564
11688
  const lines = formattedText.split("\n");
11565
11689
  tooltip.innerHTML = "";
@@ -11578,9 +11702,10 @@ function showTooltip(target) {
11578
11702
  const widthOverride = target.getAttribute("data-tooltip-max-width");
11579
11703
  tooltip.style.maxWidth = widthOverride || "";
11580
11704
  requestAnimationFrame(() => {
11705
+ if (currentTarget !== target) return;
11581
11706
  positionTooltip(target, tooltip);
11582
11707
  tooltip.classList.add("visible");
11583
- startMouseCheck();
11708
+ if (focusTarget !== target) startMouseCheck();
11584
11709
  });
11585
11710
  }
11586
11711
  function hideTooltip() {
@@ -11588,15 +11713,34 @@ function hideTooltip() {
11588
11713
  tooltipElement.classList.remove("visible");
11589
11714
  }
11590
11715
  currentTarget = null;
11716
+ focusTarget = null;
11717
+ stopMouseCheck();
11718
+ }
11719
+ function stopMouseCheck() {
11591
11720
  if (mouseCheckInterval) {
11592
11721
  clearInterval(mouseCheckInterval);
11593
11722
  mouseCheckInterval = null;
11594
11723
  }
11595
11724
  }
11725
+ function pointerGone() {
11726
+ const held = focusTarget;
11727
+ if (held && held === currentTarget) return;
11728
+ hideTooltip();
11729
+ if (!held) return;
11730
+ focusTarget = held;
11731
+ showTooltip(held);
11732
+ requestAnimationFrame(() => {
11733
+ if (held === focusTarget && held === currentTarget) followFocus();
11734
+ });
11735
+ }
11736
+ function followFocus() {
11737
+ const { left, top, width, height } = focusTarget.getBoundingClientRect();
11738
+ const inSight = focusTarget.contains(document.elementFromPoint(left + width / 2, top + height / 2));
11739
+ tooltipElement.classList.toggle("visible", inSight);
11740
+ if (inSight) positionTooltip(focusTarget, tooltipElement);
11741
+ }
11596
11742
  function startMouseCheck() {
11597
- if (mouseCheckInterval) {
11598
- clearInterval(mouseCheckInterval);
11599
- }
11743
+ stopMouseCheck();
11600
11744
  mouseCheckInterval = setInterval(() => {
11601
11745
  if (!currentTarget) {
11602
11746
  clearInterval(mouseCheckInterval);
@@ -11607,10 +11751,10 @@ function startMouseCheck() {
11607
11751
  if (elementUnderMouse) {
11608
11752
  const isStillOnTarget = elementUnderMouse === currentTarget || currentTarget.contains(elementUnderMouse) || elementUnderMouse.closest(".twm-has-tooltip, .has-latex-tooltip") === currentTarget;
11609
11753
  if (!isStillOnTarget) {
11610
- hideTooltip();
11754
+ pointerGone();
11611
11755
  }
11612
11756
  } else {
11613
- hideTooltip();
11757
+ pointerGone();
11614
11758
  }
11615
11759
  }, 3e3);
11616
11760
  }
@@ -11636,9 +11780,33 @@ function attachListeners() {
11636
11780
  if (!(target instanceof Element)) return;
11637
11781
  const tooltipTarget = target.closest(".has-latex-tooltip, .twm-has-tooltip");
11638
11782
  if (!tooltipTarget) return;
11639
- hideTooltip();
11783
+ pointerGone();
11784
+ }, true);
11785
+ window.addEventListener("keydown", (event) => {
11786
+ if (!event.ctrlKey && !event.metaKey && !event.altKey) byKeyboard = true;
11787
+ }, true);
11788
+ window.addEventListener("pointerdown", () => {
11789
+ byKeyboard = false;
11790
+ }, true);
11791
+ document.addEventListener("focusin", (event) => {
11792
+ const target = event.target;
11793
+ if (!(target instanceof Element) || !byKeyboard) return;
11794
+ const tooltipTarget = target.closest(".has-latex-tooltip, .twm-has-tooltip");
11795
+ if (!tooltipTarget) return;
11796
+ focusTarget = tooltipTarget;
11797
+ showTooltip(tooltipTarget);
11798
+ }, true);
11799
+ document.addEventListener("focusout", (event) => {
11800
+ if (!focusTarget) return;
11801
+ const next = event.relatedTarget;
11802
+ if (next instanceof Node && focusTarget.contains(next)) return;
11803
+ if (focusTarget === currentTarget) hideTooltip();
11804
+ else focusTarget = null;
11805
+ }, true);
11806
+ document.addEventListener("scroll", () => {
11807
+ if (focusTarget && focusTarget === currentTarget) followFocus();
11808
+ else pointerGone();
11640
11809
  }, true);
11641
- document.addEventListener("scroll", hideTooltip, true);
11642
11810
  document.addEventListener("keydown", (event) => {
11643
11811
  if (event.key === "Escape") {
11644
11812
  hideTooltip();