flexdesk 0.17.18 → 0.18.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 (118) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/README.md +1 -0
  3. package/css/flexdesk.css +7544 -5916
  4. package/css/reset.css +1 -1
  5. package/css/tokens.css +23 -1
  6. package/dist/charts.js +10 -6
  7. package/dist/charts.js.map +1 -1
  8. package/dist/chunk-5W4EEKSK.js +778 -0
  9. package/dist/chunk-5W4EEKSK.js.map +7 -0
  10. package/dist/chunk-DNJ3N4T4.js +82 -0
  11. package/dist/chunk-DNJ3N4T4.js.map +7 -0
  12. package/dist/{chunk-REJ5HLIM.js → chunk-HYJYYEPK.js} +2 -2
  13. package/dist/{chunk-7U4IB65H.js → chunk-LLBJ2SJH.js} +5 -3
  14. package/dist/{chunk-7U4IB65H.js.map → chunk-LLBJ2SJH.js.map} +1 -1
  15. package/dist/{chunk-7OL6F2AM.js → chunk-P74F6I6P.js} +6 -4
  16. package/dist/{chunk-7OL6F2AM.js.map → chunk-P74F6I6P.js.map} +1 -1
  17. package/dist/chunk-PAI2ZUWV.js +260 -0
  18. package/dist/chunk-PAI2ZUWV.js.map +7 -0
  19. package/dist/chunk-Q7BALEPX.js +527 -0
  20. package/dist/chunk-Q7BALEPX.js.map +7 -0
  21. package/dist/chunk-QXAOTGCC.js +40 -0
  22. package/dist/chunk-QXAOTGCC.js.map +7 -0
  23. package/dist/{chunk-XJNADRGR.js → chunk-RKVYIXDC.js} +664 -91
  24. package/dist/chunk-RKVYIXDC.js.map +7 -0
  25. package/dist/{chunk-2Z2ENHR3.js → chunk-U5GKJUOC.js} +3 -3
  26. package/dist/{chunk-2Z2ENHR3.js.map → chunk-U5GKJUOC.js.map} +1 -1
  27. package/dist/chunk-UKP22RUG.js +219 -0
  28. package/dist/chunk-UKP22RUG.js.map +7 -0
  29. package/dist/chunk-VLOAI3HS.js +43 -0
  30. package/dist/chunk-VLOAI3HS.js.map +7 -0
  31. package/dist/{chunk-WUFLGETN.js → chunk-WVFGV5FT.js} +2 -39
  32. package/dist/{chunk-WUFLGETN.js.map → chunk-WVFGV5FT.js.map} +3 -3
  33. package/dist/{chunk-PGZ4EHSE.js → chunk-ZL3VKINP.js} +41 -390
  34. package/dist/chunk-ZL3VKINP.js.map +7 -0
  35. package/dist/editor.js +3 -2
  36. package/dist/editor.js.map +1 -1
  37. package/dist/flexdesk.css +7544 -5916
  38. package/dist/flow.js +29 -97
  39. package/dist/flow.js.map +3 -3
  40. package/dist/git.js +1356 -0
  41. package/dist/git.js.map +7 -0
  42. package/dist/reset.css +1 -1
  43. package/dist/tiles.js +20 -15
  44. package/dist/tiles.js.map +2 -2
  45. package/dist/tokens.css +23 -1
  46. package/dist/widgets.js +3967 -1188
  47. package/dist/widgets.js.map +4 -4
  48. package/dist/wm.js +120 -32
  49. package/dist/wm.js.map +2 -2
  50. package/docs/README.md +9 -4
  51. package/docs/best-practices.md +2 -2
  52. package/docs/components/chat-shell.md +198 -0
  53. package/docs/components/conversation.md +10 -0
  54. package/docs/components/filter-builder.md +246 -0
  55. package/docs/components/forms-and-dialogs.md +22 -0
  56. package/docs/components/git.md +355 -0
  57. package/docs/components/input-fields.md +167 -2
  58. package/docs/components/keymap.md +8 -0
  59. package/docs/components/menus.md +82 -2
  60. package/docs/components/panels-and-desktops.md +16 -5
  61. package/docs/components/shell.md +104 -1
  62. package/docs/components/workspace-picker.md +149 -0
  63. package/docs/concepts/building-and-packaging.md +2 -2
  64. package/docs/concepts/packages-and-entry-points.md +14 -13
  65. package/docs/concepts/state-guard.md +3 -3
  66. package/docs/quickstart.md +1 -1
  67. package/git.js +13 -0
  68. package/package.json +6 -1
  69. package/src/flow/kit/dom.js +28 -119
  70. package/src/flow/kit/step_picker.js +1 -1
  71. package/src/flow/kit/value_picker.js +1 -1
  72. package/src/flow/outline/menu.js +1 -1
  73. package/src/git/diff_viewer.js +212 -0
  74. package/src/git/diff_window.js +51 -0
  75. package/src/git/git_graph.js +401 -0
  76. package/src/git/git_ops.js +61 -0
  77. package/src/git/git_panel.js +755 -0
  78. package/src/git/index.js +9 -0
  79. package/src/git/line_diff.js +202 -0
  80. package/src/tiles/tile_base.js +5 -4
  81. package/src/tiling/desktops.js +10 -8
  82. package/src/tiling/keymap.js +6 -5
  83. package/src/tiling/kind_taxonomy.js +15 -2
  84. package/src/tiling/shell.js +124 -17
  85. package/src/tiling/tile_renderer.js +2 -3
  86. package/src/ui/components/action_dropdown.js +4 -78
  87. package/src/ui/components/anchored_layer.js +255 -0
  88. package/src/ui/components/chat_shell.js +920 -0
  89. package/src/ui/components/chat_state_machine.js +223 -0
  90. package/src/ui/components/choice_controls.js +45 -2
  91. package/src/ui/components/color_field.js +3 -2
  92. package/src/ui/components/color_list_field.js +3 -2
  93. package/src/ui/components/combobox.js +422 -0
  94. package/src/ui/components/conversation_view.js +17 -2
  95. package/src/ui/components/data_table.js +2 -2
  96. package/src/ui/components/filter_builder.js +766 -0
  97. package/src/ui/components/filter_model.js +614 -0
  98. package/src/ui/components/managed_window.js +24 -47
  99. package/src/ui/components/modal.js +79 -109
  100. package/src/ui/components/option_list.js +160 -0
  101. package/src/ui/components/popover.js +94 -0
  102. package/src/ui/components/recent_items.js +169 -0
  103. package/src/ui/components/select.js +322 -0
  104. package/src/ui/components/tag_input.js +77 -8
  105. package/src/ui/components/workspace_picker.js +1155 -0
  106. package/src/ui/utils/fire_event.js +17 -0
  107. package/src/ui/utils/focusable.js +25 -0
  108. package/src/ui/utils/modal_host.js +27 -0
  109. package/widgets.js +12 -2
  110. package/css/base.css +0 -10547
  111. package/css/overrides.css +0 -2596
  112. package/dist/chunk-7JI7HHSY.js +0 -107
  113. package/dist/chunk-7JI7HHSY.js.map +0 -7
  114. package/dist/chunk-MIW7D6WC.js +0 -526
  115. package/dist/chunk-MIW7D6WC.js.map +0 -7
  116. package/dist/chunk-PGZ4EHSE.js.map +0 -7
  117. package/dist/chunk-XJNADRGR.js.map +0 -7
  118. /package/dist/{chunk-REJ5HLIM.js.map → chunk-HYJYYEPK.js.map} +0 -0
@@ -0,0 +1,527 @@
1
+ // src/ui/utils/modal_host.js
2
+ var host = null;
3
+ function setModalHost(el2) {
4
+ host = el2 || null;
5
+ }
6
+ function modalHost() {
7
+ return host;
8
+ }
9
+
10
+ // src/ui/components/anchored_layer.js
11
+ var EDGE_MARGIN = 8;
12
+ var GAP = 8;
13
+ var NO_BOX = { left: 0, right: 0, top: 0, bottom: 0, width: 0, height: 0 };
14
+ function positionAnchored(anchor, panel, { align = "start", gap = GAP, margin = EDGE_MARGIN, matchWidth = false } = {}) {
15
+ if (!panel) return;
16
+ Object.assign(panel.style, {
17
+ position: "fixed",
18
+ top: "auto",
19
+ bottom: "auto",
20
+ left: "auto",
21
+ right: "auto",
22
+ maxHeight: "",
23
+ overflowY: ""
24
+ });
25
+ const box = anchor?.getBoundingClientRect?.() || NO_BOX;
26
+ if (matchWidth) panel.style.minWidth = `${Math.round(box.width)}px`;
27
+ const size = panel.getBoundingClientRect();
28
+ const doc = anchor?.ownerDocument || document;
29
+ const view = doc.defaultView || window;
30
+ const vw = doc.documentElement.clientWidth || view.innerWidth;
31
+ const vh = doc.documentElement.clientHeight || view.innerHeight;
32
+ const spaceBelow = vh - box.bottom - gap - margin;
33
+ const spaceAbove = box.top - gap - margin;
34
+ let top;
35
+ let maxHeight;
36
+ if (size.height <= spaceBelow) {
37
+ top = box.bottom + gap;
38
+ maxHeight = spaceBelow;
39
+ } else if (size.height <= spaceAbove) {
40
+ top = box.top - gap - size.height;
41
+ maxHeight = spaceAbove;
42
+ } else if (spaceBelow >= spaceAbove) {
43
+ top = box.bottom + gap;
44
+ maxHeight = spaceBelow;
45
+ } else {
46
+ maxHeight = spaceAbove;
47
+ top = margin;
48
+ }
49
+ let left;
50
+ if (align === "end") {
51
+ left = box.right - size.width;
52
+ if (left < margin) left = box.left;
53
+ } else {
54
+ left = box.left;
55
+ if (left + size.width > vw - margin) left = box.right - size.width;
56
+ }
57
+ left = Math.max(margin, Math.min(left, vw - size.width - margin));
58
+ Object.assign(panel.style, {
59
+ position: "fixed",
60
+ top: `${Math.round(top)}px`,
61
+ left: `${Math.round(left)}px`,
62
+ maxHeight: `${Math.round(maxHeight)}px`,
63
+ overflowY: "auto"
64
+ });
65
+ }
66
+ var FOREIGN_FLOATS = ".twm-action-dropdown-menu, .twm-category-picker, .twm-context-menu";
67
+ var FOREIGN_ESCAPE = ".twm-action-dropdown-menu:not([hidden]), .twm-category-picker:not([hidden])";
68
+ var MODAL_WINDOW = ".twm-managed-window--modal";
69
+ var LAYERS = [];
70
+ var holds = (node, target) => !!node && typeof node.contains === "function" && node.contains(target);
71
+ var shownModals = (doc) => [...doc.querySelectorAll(MODAL_WINDOW)].filter((win) => win.style.display !== "none");
72
+ var isInAnchoredLayer = (node) => LAYERS.some((layer) => holds(layer.el, node));
73
+ function openAnchoredLayer({
74
+ anchor = null,
75
+ el: el2,
76
+ align = "start",
77
+ gap = GAP,
78
+ matchWidth = false,
79
+ inside = null,
80
+ returnFocus = true,
81
+ onClose = null
82
+ }) {
83
+ if (anchor && anchor.isConnected === false) return null;
84
+ while (LAYERS.length && !LAYERS[LAYERS.length - 1].owns(anchor)) LAYERS[LAYERS.length - 1].close("replaced");
85
+ const doc = anchor?.ownerDocument || document;
86
+ const view = doc.defaultView || window;
87
+ (modalHost() || doc.body).appendChild(el2);
88
+ const modalsUnder = new Set(shownModals(doc));
89
+ let closed = false;
90
+ const place = () => {
91
+ if (closed) return;
92
+ if (anchor && anchor.isConnected === false) {
93
+ layer.close("detached");
94
+ return;
95
+ }
96
+ positionAnchored(anchor, el2, { align, gap, matchWidth });
97
+ };
98
+ const owns = (target) => {
99
+ if (holds(el2, target)) return true;
100
+ for (let i = LAYERS.indexOf(layer) + 1; i < LAYERS.length; i += 1) if (holds(LAYERS[i].el, target)) return true;
101
+ if (typeof target?.closest !== "function") return false;
102
+ if (target.closest(FOREIGN_FLOATS)) return true;
103
+ const modal = target.closest(MODAL_WINDOW);
104
+ if (modal && !modalsUnder.has(modal)) return true;
105
+ return !!inside && !!target.closest(inside);
106
+ };
107
+ const onPress = (ev) => {
108
+ if (!holds(anchor, ev.target) && !owns(ev.target)) layer.close("outside");
109
+ };
110
+ const onDocKey = (ev) => {
111
+ if (!holds(el2, ev.target)) onKey(ev);
112
+ };
113
+ const onKey = (ev) => {
114
+ if (ev.key !== "Escape" || ev.isComposing || ev.defaultPrevented) return;
115
+ if (LAYERS[LAYERS.length - 1] !== layer) return;
116
+ if (doc.querySelector(FOREIGN_ESCAPE)) return;
117
+ if (shownModals(doc).some((win) => !modalsUnder.has(win))) return;
118
+ ev.preventDefault();
119
+ ev.stopPropagation();
120
+ layer.close("escape");
121
+ };
122
+ const onScroll = (ev) => {
123
+ if (!holds(el2, ev.target)) place();
124
+ };
125
+ const sized = typeof ResizeObserver === "function" ? new ResizeObserver(() => place()) : null;
126
+ const layer = {
127
+ el: el2,
128
+ anchor,
129
+ place,
130
+ owns,
131
+ close(reason = "closed") {
132
+ if (closed) return;
133
+ while (LAYERS[LAYERS.length - 1] !== layer) LAYERS[LAYERS.length - 1].close("parent");
134
+ closed = true;
135
+ const active = doc.activeElement;
136
+ const held = holds(el2, active) || active === doc.body;
137
+ doc.removeEventListener("pointerdown", onPress, true);
138
+ doc.removeEventListener("mousedown", onPress, true);
139
+ doc.removeEventListener("keydown", onDocKey, true);
140
+ el2.removeEventListener("keydown", onKey);
141
+ view.removeEventListener("resize", place);
142
+ view.removeEventListener("scroll", onScroll, true);
143
+ sized?.disconnect();
144
+ el2.remove();
145
+ LAYERS.pop();
146
+ if (returnFocus && held && reason !== "blur" && anchor?.isConnected) anchor.focus?.({ preventScroll: true });
147
+ onClose?.(reason);
148
+ }
149
+ };
150
+ LAYERS.push(layer);
151
+ setTimeout(() => {
152
+ if (closed) return;
153
+ doc.addEventListener("pointerdown", onPress, true);
154
+ doc.addEventListener("mousedown", onPress, true);
155
+ }, 0);
156
+ doc.addEventListener("keydown", onDocKey, true);
157
+ el2.addEventListener("keydown", onKey);
158
+ view.addEventListener("resize", place);
159
+ view.addEventListener("scroll", onScroll, true);
160
+ sized?.observe(el2);
161
+ place();
162
+ return layer;
163
+ }
164
+
165
+ // src/ui/components/choice_controls.js
166
+ var shownValue = (v) => v === void 0 ? "" : typeof v === "string" ? v : JSON.stringify(v);
167
+ function el(tag, className, text) {
168
+ const n = document.createElement(tag);
169
+ if (className) n.className = className;
170
+ if (text !== void 0 && text !== null) n.textContent = String(text);
171
+ return n;
172
+ }
173
+ var isGroup = (o) => o !== null && typeof o === "object" && Array.isArray(o.options) && "group" in o;
174
+ function choiceOption(o) {
175
+ if (o === null || typeof o !== "object") {
176
+ return {
177
+ value: o,
178
+ label: shownValue(o),
179
+ description: null,
180
+ disabled: false,
181
+ reason: null,
182
+ icon: null,
183
+ hint: null,
184
+ keywords: null,
185
+ source: o
186
+ };
187
+ }
188
+ return {
189
+ value: o.value,
190
+ label: o.label != null ? String(o.label) : shownValue(o.value),
191
+ description: o.description != null ? String(o.description) : null,
192
+ disabled: Boolean(o.disabled),
193
+ reason: o.reason != null && o.reason !== "" ? String(o.reason) : null,
194
+ icon: o.icon ? String(o.icon) : null,
195
+ hint: o.hint != null && o.hint !== "" ? String(o.hint) : null,
196
+ keywords: o.keywords != null ? String(o.keywords) : null,
197
+ source: o
198
+ };
199
+ }
200
+ function flatChoices(options) {
201
+ const out = [];
202
+ for (const o of options || []) {
203
+ if (isGroup(o)) for (const inner of o.options) out.push(choiceOption(inner));
204
+ else out.push(choiceOption(o));
205
+ }
206
+ return out;
207
+ }
208
+ function choiceSections(options) {
209
+ const out = [];
210
+ for (const o of options || []) {
211
+ if (isGroup(o)) {
212
+ if (o.options.length) out.push({ heading: String(o.group ?? ""), items: o.options.map(choiceOption) });
213
+ } else if (out.length && out[out.length - 1].heading === null) {
214
+ out[out.length - 1].items.push(choiceOption(o));
215
+ } else {
216
+ out.push({ heading: null, items: [choiceOption(o)] });
217
+ }
218
+ }
219
+ return out;
220
+ }
221
+ function optionMatches(option, text) {
222
+ const needle = String(text ?? "").trim().toLowerCase();
223
+ if (!needle) return true;
224
+ return [option.label, shownValue(option.value), option.description, option.keywords].some((part) => String(part ?? "").toLowerCase().includes(needle));
225
+ }
226
+ function filterSections(sections, text) {
227
+ return sections.map((s) => ({ heading: s.heading, items: s.items.filter((o) => optionMatches(o, text)) })).filter((s) => s.items.length);
228
+ }
229
+ function groupHeads(options) {
230
+ const heads = /* @__PURE__ */ new Map();
231
+ let at = 0;
232
+ for (const o of options || []) {
233
+ if (!isGroup(o)) {
234
+ at += 1;
235
+ continue;
236
+ }
237
+ if (o.options.length) heads.set(at, String(o.group ?? ""));
238
+ at += o.options.length;
239
+ }
240
+ return heads;
241
+ }
242
+ var groupHeading = (text) => el("div", "twm-choice__group", text);
243
+ function sameChoice(v, value, loose = false) {
244
+ if (v === value) return true;
245
+ if (!loose || v === void 0 || value === void 0 || v === null || value === null) return false;
246
+ return String(v) === String(value);
247
+ }
248
+ function choiceIndex(list, v, loose = false, from = 0) {
249
+ for (let i = from; i < list.length; i += 1) if (list[i].value === v) return i;
250
+ if (!loose) return -1;
251
+ for (let i = from; i < list.length; i += 1) if (sameChoice(list[i].value, v, true)) return i;
252
+ return -1;
253
+ }
254
+ function findChoice(list, v, loose = false) {
255
+ const i = choiceIndex(list, v, loose);
256
+ return i >= 0 ? list[i] : void 0;
257
+ }
258
+ var optionText = (o) => o.disabled && o.reason ? `${o.label} \u2014 ${o.reason}` : o.label;
259
+ function selectBox(options, value, {
260
+ empty = null,
261
+ label = "",
262
+ notFound = (v) => `${v} (not found)`,
263
+ keepMissing = true,
264
+ valueAttr = "index",
265
+ loose = false,
266
+ className = "twm-flow-input twm-flow-input--select"
267
+ } = {}) {
268
+ const box = el("select", className);
269
+ if (label) box.setAttribute("aria-label", label);
270
+ let entries = [];
271
+ const add = (parent, o) => {
272
+ const attr = valueAttr === "index" ? String(entries.length) : o.value === void 0 ? "" : String(o.value);
273
+ const opt = el("option", "", optionText(o));
274
+ opt.value = attr;
275
+ if (o.disabled) {
276
+ opt.disabled = true;
277
+ if (o.reason) opt.title = o.reason;
278
+ }
279
+ parent.appendChild(opt);
280
+ entries.push(o);
281
+ };
282
+ const firstEnabled = () => {
283
+ const i = entries.findIndex((e) => !e.disabled);
284
+ return i >= 0 ? i : 0;
285
+ };
286
+ const select = (v) => {
287
+ const start = empty !== null ? 1 : 0;
288
+ const at = choiceIndex(entries, v, loose, start);
289
+ if (at < 0 || entries[at].disabled || entries[at].stray) return false;
290
+ box.selectedIndex = at;
291
+ return true;
292
+ };
293
+ const place = (v) => {
294
+ if (select(v)) return;
295
+ if (keepMissing && v !== void 0 && v !== null && v !== "") {
296
+ add(box, {
297
+ value: v,
298
+ label: notFound(shownValue(v)),
299
+ description: null,
300
+ disabled: false,
301
+ reason: null,
302
+ stray: true
303
+ });
304
+ box.selectedIndex = entries.length - 1;
305
+ return;
306
+ }
307
+ box.selectedIndex = empty !== null ? 0 : firstEnabled();
308
+ };
309
+ const fill = (opts, v) => {
310
+ box.replaceChildren();
311
+ entries = [];
312
+ if (empty !== null) add(box, {
313
+ value: void 0,
314
+ label: String(empty),
315
+ description: null,
316
+ disabled: false,
317
+ reason: null
318
+ });
319
+ for (const o of opts || []) {
320
+ if (isGroup(o)) {
321
+ const g = el("optgroup");
322
+ g.label = String(o.group ?? "");
323
+ for (const inner of o.options) add(g, choiceOption(inner));
324
+ box.appendChild(g);
325
+ } else {
326
+ add(box, choiceOption(o));
327
+ }
328
+ }
329
+ place(v);
330
+ };
331
+ const entry = () => box.selectedIndex < 0 ? null : entries[box.selectedIndex] ?? null;
332
+ const valueOf = () => entry()?.value;
333
+ fill(options, value);
334
+ return {
335
+ box,
336
+ valueOf,
337
+ entry,
338
+ select,
339
+ setOptions(opts, v = valueOf()) {
340
+ fill(opts, v);
341
+ }
342
+ };
343
+ }
344
+ function toggleChips({
345
+ options,
346
+ value,
347
+ label,
348
+ readOnly,
349
+ onChange,
350
+ notFound = (v) => `${v} (not found)`,
351
+ keepMissing = true,
352
+ loose = false
353
+ }) {
354
+ const root = el("div", "twm-flow-togglechips");
355
+ root.setAttribute("role", "group");
356
+ if (label) root.setAttribute("aria-label", label);
357
+ let shown = [];
358
+ let list = [];
359
+ let chosen = /* @__PURE__ */ new Set();
360
+ let strays = [];
361
+ const emit = () => {
362
+ const out = valueOf();
363
+ onChange?.(out.length ? out : void 0);
364
+ };
365
+ const chipFor = (text, pressed, toggle, { stray = false, reason = null, disabled = false } = {}) => {
366
+ const b = el("button", `twm-flow-togglechip${stray ? " twm-flow-togglechip--missing" : ""}`, text);
367
+ b.type = "button";
368
+ b.setAttribute("aria-pressed", pressed ? "true" : "false");
369
+ b.disabled = Boolean(readOnly) || disabled;
370
+ if (reason) {
371
+ b.title = reason;
372
+ b.appendChild(el("span", "twm-choice__reason", reason));
373
+ }
374
+ b.addEventListener("click", () => {
375
+ const on = toggle();
376
+ b.setAttribute("aria-pressed", on ? "true" : "false");
377
+ emit();
378
+ });
379
+ return b;
380
+ };
381
+ const draw = (opts, stored) => {
382
+ shown = opts || [];
383
+ list = flatChoices(shown);
384
+ const heads = groupHeads(shown);
385
+ const wanted = Array.isArray(stored) ? stored : [];
386
+ const offered = (v) => {
387
+ const o = findChoice(list, v, loose);
388
+ return o && !o.disabled ? o : void 0;
389
+ };
390
+ chosen = new Set(wanted.map(offered).filter(Boolean).map((o) => o.value));
391
+ strays = keepMissing ? wanted.filter((v) => !offered(v)) : [];
392
+ root.replaceChildren();
393
+ list.forEach((o, i) => {
394
+ if (heads.has(i)) root.appendChild(groupHeading(heads.get(i)));
395
+ root.appendChild(chipFor(o.label, chosen.has(o.value), () => {
396
+ if (o.disabled) return false;
397
+ if (chosen.has(o.value)) chosen.delete(o.value);
398
+ else chosen.add(o.value);
399
+ return chosen.has(o.value);
400
+ }, { disabled: o.disabled, reason: o.disabled ? o.reason : null }));
401
+ });
402
+ for (const v of [...strays]) {
403
+ root.appendChild(chipFor(notFound(shownValue(v)), true, () => {
404
+ const i = strays.indexOf(v);
405
+ if (i >= 0) {
406
+ strays.splice(i, 1);
407
+ return false;
408
+ }
409
+ strays.push(v);
410
+ return true;
411
+ }, { stray: true }));
412
+ }
413
+ };
414
+ const valueOf = () => [...list.filter((o) => chosen.has(o.value)).map((o) => o.value), ...strays];
415
+ draw(options, value);
416
+ return {
417
+ el: root,
418
+ valueOf,
419
+ setValue: (stored) => draw(shown, stored),
420
+ setOptions: (opts) => draw(opts, valueOf())
421
+ };
422
+ }
423
+ function choiceList({
424
+ options,
425
+ value,
426
+ name,
427
+ label,
428
+ readOnly,
429
+ onChange,
430
+ notFound = (v) => `${v} (not found)`,
431
+ keepMissing = true,
432
+ loose = false
433
+ }) {
434
+ const root = el("div", "twm-flow-choices");
435
+ root.setAttribute("role", "radiogroup");
436
+ if (label) root.setAttribute("aria-label", label);
437
+ let list = [];
438
+ let radios = [];
439
+ const draw = (opts, current) => {
440
+ list = flatChoices(opts);
441
+ const heads = groupHeads(opts);
442
+ radios = [];
443
+ root.replaceChildren();
444
+ const named = choiceIndex(list, current, loose);
445
+ const at = named >= 0 && !list[named].disabled ? named : -1;
446
+ list.forEach((o, i) => {
447
+ if (heads.has(i)) root.appendChild(groupHeading(heads.get(i)));
448
+ const row = el("label", `twm-flow-choice${o.disabled ? " twm-choice--disabled" : ""}`);
449
+ const radio = el("input", "twm-flow-choice__radio");
450
+ radio.type = "radio";
451
+ radio.name = name;
452
+ radio.checked = i === at;
453
+ radio.disabled = Boolean(readOnly) || o.disabled;
454
+ radio.addEventListener("change", () => {
455
+ if (radio.checked) onChange?.(o.value);
456
+ });
457
+ radios.push(radio);
458
+ const words = el("span", "twm-flow-choice__words");
459
+ words.appendChild(el("span", "twm-flow-choice__label", o.label));
460
+ if (o.description !== null) words.appendChild(el("span", "twm-flow-choice__sub", o.description));
461
+ if (o.disabled && o.reason) words.appendChild(el("span", "twm-choice__reason", o.reason));
462
+ row.append(radio, words);
463
+ root.appendChild(row);
464
+ });
465
+ if (keepMissing && current !== void 0 && named < 0) {
466
+ root.appendChild(el("p", "twm-flow-field__note", notFound(shownValue(current))));
467
+ }
468
+ };
469
+ const checkedAt = () => radios.findIndex((r) => r.checked);
470
+ const entry = () => list[checkedAt()] ?? null;
471
+ const api = {
472
+ el: root,
473
+ focus: () => (radios.find((r) => r.checked && !r.disabled) || radios.find((r) => !r.disabled))?.focus({ preventScroll: true }),
474
+ entry,
475
+ valueOf: () => entry()?.value,
476
+ select(v) {
477
+ const named = choiceIndex(list, v, loose);
478
+ const at = named >= 0 && !list[named].disabled ? named : -1;
479
+ radios.forEach((r, i) => {
480
+ r.checked = i === at;
481
+ });
482
+ return at >= 0;
483
+ },
484
+ setOptions(opts) {
485
+ draw(opts, api.valueOf());
486
+ }
487
+ };
488
+ draw(options, value);
489
+ return api;
490
+ }
491
+
492
+ // src/ui/utils/focusable.js
493
+ function collectFocusable(root) {
494
+ if (!root) return [];
495
+ const sel = [
496
+ "a[href]",
497
+ "button:not([disabled])",
498
+ 'input:not([disabled]):not([type="hidden"])',
499
+ "select:not([disabled])",
500
+ "textarea:not([disabled])",
501
+ '[tabindex]:not([tabindex="-1"])'
502
+ ].join(",");
503
+ return Array.from(root.querySelectorAll(sel)).filter((el2) => {
504
+ if (el2.hidden) return false;
505
+ if (el2.closest("[hidden]")) return false;
506
+ const r = el2.getBoundingClientRect();
507
+ return r.width > 0 || r.height > 0;
508
+ });
509
+ }
510
+
511
+ export {
512
+ setModalHost,
513
+ modalHost,
514
+ positionAnchored,
515
+ isInAnchoredLayer,
516
+ openAnchoredLayer,
517
+ collectFocusable,
518
+ shownValue,
519
+ flatChoices,
520
+ choiceSections,
521
+ filterSections,
522
+ findChoice,
523
+ selectBox,
524
+ toggleChips,
525
+ choiceList
526
+ };
527
+ //# sourceMappingURL=chunk-Q7BALEPX.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/ui/utils/modal_host.js", "../src/ui/components/anchored_layer.js", "../src/ui/components/choice_controls.js", "../src/ui/utils/focusable.js"],
4
+ "sourcesContent": ["/**\n * C25. WHERE A DIALOG OR A FLOATING PANEL OPENS, when the application spans more\n * than one window.\n *\n * Every modal is a `ManagedWindow` with no `container`, so it mounts into\n * `document.body` \u2014 and \"document\" is the document the MODULE was loaded in. A\n * consumer that opens a second browser window with `window.open` and builds its\n * DOM from the opener's realm (which is how a pop-out grid works: same modules,\n * same JS context, a different document) therefore gets its dialogs in the\n * window it popped OUT of. The user presses \"Add column\" on their second monitor\n * and a dialog appears on the first, behind whatever is there.\n *\n * Passing a container at each call site was the other option and it is worse:\n * twenty-one of them across eleven files, every one of which would have to be\n * given a document it has no other reason to know about, and any one missed is\n * this bug again with no way to see it from here.\n *\n * So the HOST is ambient and the consumer sets it when its focus moves. Null \u2014\n * every consumer today \u2014 means `document.body`, exactly as before.\n *\n * A module of its own because modals and the anchored layers both ask it, and\n * `openForm` draws its combobox on an anchored layer. `modal.js` re-exports both\n * names: that is where they are public.\n */\nlet host = null;\nexport function setModalHost(el) { host = el || null; }\nexport function modalHost() { return host; }\n", "/**\n * ONE WAY TO HANG A PANEL OFF AN ELEMENT. (0.18.0)\n *\n * `positionAnchored` is the placement: `ActionDropdown.position`, `CategoryPicker`,\n * a tile's menu, `openPopover`, `createSelect` and `createCombobox` all place\n * through it, so none of them has a second idea of what \"below, else above,\n * inside the window\" means.\n *\n * `openAnchoredLayer` is the behaviour around the placement:\n *\n * - the panel is mounted on FlexDesk's modal host (`modalHost()`) or the anchor's\n * `document.body`, never inside the anchor's parent, so no `overflow: hidden`\n * ancestor clips it. It is taken off again on close: a closed layer leaves\n * nothing behind to forget;\n * - it is placed again when the window resizes, when anything outside it\n * scrolls and when its own size changes; an anchor that has left the document\n * closes it;\n * - it closes on a press outside it and on Escape.\n *\n * LAYERS STACK, because a popover's own controls hang panels too. A select inside\n * a popover is a layer whose anchor is inside the popover; that gives every rule\n * below one answer:\n *\n * - OPENING a layer closes every open layer that does not own its anchor (so\n * there is one popover at a time, and a popover's select does not close it);\n * - A PRESS on what a layer owns is not outside it, and neither is one on its anchor;\n * - ESCAPE goes to the top layer alone and to nothing else, or the `ManagedWindow`\n * around the field would close too and take a half-filled form with it. From\n * inside the panel it is heard as it bubbles out, so that the panel's content\n * may answer it first (a picker climbing a level); from anywhere else, on the\n * way in;\n * - CLOSING a layer closes the layers above it first.\n *\n * WHAT A LAYER OWNS is one question with one answer, for a press and for an anchor\n * alike: the panel; every layer above it; and what floats without being a layer, which\n * is named here: an `ActionDropdown` menu, a `CategoryPicker`, a context menu, a modal\n * window shown after the layer opened (a dialog its content opened), and whatever the\n * caller's `inside` selector names. A modal already showing when the layer opened is\n * UNDER it, however far down the chain of layers it was opened from: it owns none of\n * that and takes no key from it. An open menu or picker, and a modal over the layer,\n * get Escape first.\n *\n * An anchor that has left the document has nothing to hang a panel from: opening on\n * one opens nothing and closes nothing, and answers `null`.\n */\nimport { modalHost } from '../utils/modal_host.js';\n\n/** Viewport margin in px: a placed panel stays this far from the edges. */\nconst EDGE_MARGIN = 8;\n/** Gap between an anchor and its panel in px, unless the caller gives one. */\nconst GAP = 8;\n\nconst NO_BOX = { left: 0, right: 0, top: 0, bottom: 0, width: 0, height: 0 };\n\n/**\n * Place `panel` against `anchor`: `position: fixed`, so no ancestor clips it;\n * under the anchor when it fits there, else above it when it fits there, else on\n * the side with more room, scrolling inside (`max-height` + `overflow-y: auto`);\n * kept `margin` inside the window. The client size is the window: a page\n * scrollbar is not room to draw in.\n *\n * @param {HTMLElement|null} anchor Null places it at the window's top-left corner.\n * @param {HTMLElement} panel\n * @param {object} [opts]\n * @param {'start'|'end'} [opts.align='start'] Which anchor edge the panel lines up\n * with first: `'start'` its left, `'end'` its right. Either flips to the other\n * edge when the panel would leave the window.\n * @param {number} [opts.gap=8] Px between the anchor and the panel.\n * @param {number} [opts.margin=8] Px kept clear of the window's edges.\n * @param {boolean} [opts.matchWidth] The panel is at least as wide as the anchor.\n */\nexport function positionAnchored(anchor, panel, { align = 'start', gap = GAP, margin = EDGE_MARGIN, matchWidth = false } = {}) {\n if (!panel) return;\n\n // Reset any prior inline position so natural size can be measured.\n Object.assign(panel.style, {\n position: 'fixed',\n top: 'auto',\n bottom: 'auto',\n left: 'auto',\n right: 'auto',\n maxHeight: '',\n overflowY: '',\n });\n\n const box = anchor?.getBoundingClientRect?.() || NO_BOX;\n if (matchWidth) panel.style.minWidth = `${Math.round(box.width)}px`;\n const size = panel.getBoundingClientRect();\n const doc = anchor?.ownerDocument || document;\n const view = doc.defaultView || window;\n const vw = doc.documentElement.clientWidth || view.innerWidth;\n const vh = doc.documentElement.clientHeight || view.innerHeight;\n\n // \u2500\u2500 Vertical: prefer below, flip above if insufficient room \u2500\u2500\u2500\u2500\u2500\u2500\u2500\n const spaceBelow = vh - box.bottom - gap - margin;\n const spaceAbove = box.top - gap - margin;\n\n let top;\n let maxHeight;\n if (size.height <= spaceBelow) {\n top = box.bottom + gap;\n maxHeight = spaceBelow;\n } else if (size.height <= spaceAbove) {\n top = box.top - gap - size.height;\n maxHeight = spaceAbove;\n } else if (spaceBelow >= spaceAbove) {\n // More space below: scroll.\n top = box.bottom + gap;\n maxHeight = spaceBelow;\n } else {\n // More space above: scroll.\n maxHeight = spaceAbove;\n top = margin;\n }\n\n // \u2500\u2500 Horizontal: the chosen edge, the other if off-screen, then clamped \u2500\u2500\n let left;\n if (align === 'end') {\n left = box.right - size.width;\n if (left < margin) left = box.left;\n } else {\n left = box.left;\n if (left + size.width > vw - margin) left = box.right - size.width;\n }\n left = Math.max(margin, Math.min(left, vw - size.width - margin));\n\n Object.assign(panel.style, {\n position: 'fixed',\n top: `${Math.round(top)}px`,\n left: `${Math.round(left)}px`,\n maxHeight: `${Math.round(maxHeight)}px`,\n overflowY: 'auto',\n });\n}\n\n/** What floats without being a layer. See the header. */\nconst FOREIGN_FLOATS = '.twm-action-dropdown-menu, .twm-category-picker, .twm-context-menu';\n/** Which of those answer Escape themselves while open. */\nconst FOREIGN_ESCAPE = '.twm-action-dropdown-menu:not([hidden]), .twm-category-picker:not([hidden])';\nconst MODAL_WINDOW = '.twm-managed-window--modal';\n\n/** The open layers, bottom first. */\nconst LAYERS = [];\n\nconst holds = (node, target) => !!node && typeof node.contains === 'function' && node.contains(target);\n\nconst shownModals = (doc) => [...doc.querySelectorAll(MODAL_WINDOW)].filter((win) => win.style.display !== 'none');\n\n/** Is `node` in an open layer? A modal's focus trap leaves such focus alone: the panel is outside the modal's frame. */\nexport const isInAnchoredLayer = (node) => LAYERS.some((layer) => holds(layer.el, node));\n\n/**\n * Open `el` as a layer against `anchor`.\n *\n * @param {object} o\n * @param {HTMLElement|null} o.anchor\n * @param {HTMLElement} o.el The panel. Mounted here, removed on close; reusable.\n * @param {'start'|'end'} [o.align]\n * @param {number} [o.gap]\n * @param {boolean} [o.matchWidth]\n * @param {string} [o.inside] A selector for floating things of the caller's own:\n * a press in one is not outside this layer.\n * @param {boolean} [o.returnFocus=true] Closed with focus inside the panel, or on\n * nothing, focus goes back to the anchor, unless the reason is `'blur'`: focus\n * that left is going where it was sent.\n * @param {(reason: string) => void} [o.onClose] After it is gone. The reasons are\n * `'outside'`, `'escape'`, `'replaced'`, `'parent'`, `'detached'`, and whatever\n * the caller passes to `close`.\n * @returns {{el: HTMLElement, anchor: HTMLElement|null, place: () => void, owns: (node: Node) => boolean,\n * close: (reason?: string) => void}|null}\n * `null` when `anchor` has left the document: nothing opened, `onClose` will not be called.\n */\nexport function openAnchoredLayer({ anchor = null, el, align = 'start', gap = GAP, matchWidth = false, inside = null,\n returnFocus = true, onClose = null }) {\n if (anchor && anchor.isConnected === false) return null;\n\n while (LAYERS.length && !LAYERS[LAYERS.length - 1].owns(anchor)) LAYERS[LAYERS.length - 1].close('replaced');\n\n const doc = anchor?.ownerDocument || document;\n const view = doc.defaultView || window;\n (modalHost() || doc.body).appendChild(el);\n const modalsUnder = new Set(shownModals(doc));\n\n let closed = false;\n const place = () => {\n if (closed) return;\n if (anchor && anchor.isConnected === false) { layer.close('detached'); return; }\n positionAnchored(anchor, el, { align, gap, matchWidth });\n };\n\n const owns = (target) => {\n if (holds(el, target)) return true;\n for (let i = LAYERS.indexOf(layer) + 1; i < LAYERS.length; i += 1) if (holds(LAYERS[i].el, target)) return true;\n if (typeof target?.closest !== 'function') return false;\n if (target.closest(FOREIGN_FLOATS)) return true;\n const modal = target.closest(MODAL_WINDOW);\n if (modal && !modalsUnder.has(modal)) return true;\n return !!inside && !!target.closest(inside);\n };\n const onPress = (ev) => { if (!holds(anchor, ev.target) && !owns(ev.target)) layer.close('outside'); };\n const onDocKey = (ev) => { if (!holds(el, ev.target)) onKey(ev); };\n const onKey = (ev) => {\n if (ev.key !== 'Escape' || ev.isComposing || ev.defaultPrevented) return;\n if (LAYERS[LAYERS.length - 1] !== layer) return;\n if (doc.querySelector(FOREIGN_ESCAPE)) return;\n if (shownModals(doc).some((win) => !modalsUnder.has(win))) return;\n ev.preventDefault();\n ev.stopPropagation();\n layer.close('escape');\n };\n const onScroll = (ev) => { if (!holds(el, ev.target)) place(); };\n // Placed before a caller fills it, and it changes with a filter: placed again\n // whenever its size changes, or it would run past the window's edge.\n const sized = typeof ResizeObserver === 'function' ? new ResizeObserver(() => place()) : null;\n\n const layer = {\n el,\n anchor,\n place,\n owns,\n close(reason = 'closed') {\n if (closed) return;\n while (LAYERS[LAYERS.length - 1] !== layer) LAYERS[LAYERS.length - 1].close('parent');\n closed = true;\n const active = doc.activeElement;\n const held = holds(el, active) || active === doc.body;\n doc.removeEventListener('pointerdown', onPress, true);\n doc.removeEventListener('mousedown', onPress, true);\n doc.removeEventListener('keydown', onDocKey, true);\n el.removeEventListener('keydown', onKey);\n view.removeEventListener('resize', place);\n view.removeEventListener('scroll', onScroll, true);\n sized?.disconnect();\n el.remove();\n LAYERS.pop();\n if (returnFocus && held && reason !== 'blur' && anchor?.isConnected) anchor.focus?.({ preventScroll: true });\n onClose?.(reason);\n },\n };\n LAYERS.push(layer);\n\n // The press that OPENED it is still being dispatched; listen from the next turn.\n setTimeout(() => {\n if (closed) return;\n doc.addEventListener('pointerdown', onPress, true);\n doc.addEventListener('mousedown', onPress, true);\n }, 0);\n doc.addEventListener('keydown', onDocKey, true);\n el.addEventListener('keydown', onKey);\n view.addEventListener('resize', place);\n view.addEventListener('scroll', onScroll, true);\n sized?.observe(el);\n place();\n return layer;\n}\n", "/**\n * CHOICE CONTROLS \u2014 the one select, the one set of toggle chips and the one\n * radio list, shared by `openForm` and the flow kit's settings widgets.\n *\n * Each draws a fixed list of options and reads back the option's OWN value \u2014\n * a number stays a number, an object stays the object \u2014 never the text of an\n * `<option>` or a `value` attribute. That is why every control here keeps the\n * values beside the DOM it draws and resolves the reader's choice by position.\n *\n * An option is a bare value, or `{ value, label?, description?, disabled?,\n * reason? }`. Each also takes `{ group, options }` entries: `selectBox` draws\n * one as an `<optgroup>`, the chips and the radio list as a `.twm-choice__group`\n * heading over its options. A group with no options draws no heading there.\n *\n * A DISABLED OPTION IS SHOWN, WITH ITS REASON, AND IS NEVER CHOSEN BY VALUE:\n * a stored value that names one is not selected (the select falls to its first\n * enabled option, the radios to none, the chips leave it unpressed), unless\n * `keepMissing` keeps it as a value the list does not offer. The reader cannot\n * pick one either. What a caller does about a disabled option selected some\n * other way (a script setting `.value`) is the caller's: `selectBox.entry()`\n * and `choiceList.entry()` say which option is selected, disabled or not.\n *\n * `keepMissing` (the kit's rule, on by default): a stored value the list does\n * not offer is KEPT, marked with `notFound(text)`, so a settings panel cannot\n * silently turn it into the first entry on the next save. A form that should\n * offer only what is listed passes `keepMissing: false`.\n *\n * `loose`: a stored value matches an option whose value has the same TEXT\n * (`2` matches `'2'`), for a caller whose stored values may arrive as strings.\n * Off by default: the kit's values keep their type, and `'2'` is not `2`.\n * Text is only the fallback: an option whose value IS the stored value (`===`)\n * wins over an earlier one that only reads the same (`findChoice`).\n *\n * Nothing here names a consumer's vocabulary; every word on screen is the\n * caller's (`label`, `reason`, `description`, `notFound`).\n */\n\n/** A value as one line of text shows it: a string as itself, anything else as JSON. */\nexport const shownValue = (v) => (v === undefined ? '' : typeof v === 'string' ? v : JSON.stringify(v));\n\nfunction el(tag, className, text) {\n const n = document.createElement(tag);\n if (className) n.className = className;\n if (text !== undefined && text !== null) n.textContent = String(text);\n return n;\n}\n\nconst isGroup = (o) => o !== null && typeof o === 'object' && Array.isArray(o.options) && 'group' in o;\n\n/**\n * One option, normalised: `{value, label, description, disabled, reason, icon, hint,\n * keywords, source}`. `icon`, `hint` and `keywords` are the DOM-drawn select's and\n * combobox's; `source` is the entry as it was given, handed back when one is chosen.\n */\nfunction choiceOption(o) {\n if (o === null || typeof o !== 'object') {\n return { value: o, label: shownValue(o), description: null, disabled: false, reason: null, icon: null,\n hint: null, keywords: null, source: o };\n }\n return {\n value: o.value,\n label: o.label != null ? String(o.label) : shownValue(o.value),\n description: o.description != null ? String(o.description) : null,\n disabled: Boolean(o.disabled),\n reason: o.reason != null && o.reason !== '' ? String(o.reason) : null,\n icon: o.icon ? String(o.icon) : null,\n hint: o.hint != null && o.hint !== '' ? String(o.hint) : null,\n keywords: o.keywords != null ? String(o.keywords) : null,\n source: o,\n };\n}\n\n/** Every option of a list, groups flattened in order. */\nexport function flatChoices(options) {\n const out = [];\n for (const o of options || []) {\n if (isGroup(o)) for (const inner of o.options) out.push(choiceOption(inner));\n else out.push(choiceOption(o));\n }\n return out;\n}\n\n/**\n * A list as the sections it draws in, in order: `[{ heading, items }]`, `heading`\n * null for a run of options outside any group, `items` normalised. A group with no\n * options is left out.\n */\nexport function choiceSections(options) {\n const out = [];\n for (const o of options || []) {\n if (isGroup(o)) {\n if (o.options.length) out.push({ heading: String(o.group ?? ''), items: o.options.map(choiceOption) });\n } else if (out.length && out[out.length - 1].heading === null) {\n out[out.length - 1].items.push(choiceOption(o));\n } else {\n out.push({ heading: null, items: [choiceOption(o)] });\n }\n }\n return out;\n}\n\n/** Does `text` occur, ignoring case, in the option's label, value, description or keywords? */\nexport function optionMatches(option, text) {\n const needle = String(text ?? '').trim().toLowerCase();\n if (!needle) return true;\n return [option.label, shownValue(option.value), option.description, option.keywords]\n .some((part) => String(part ?? '').toLowerCase().includes(needle));\n}\n\n/** `sections` with only the options `text` matches; a section left empty goes. */\nexport function filterSections(sections, text) {\n return sections\n .map((s) => ({ heading: s.heading, items: s.items.filter((o) => optionMatches(o, text)) }))\n .filter((s) => s.items.length);\n}\n\n/**\n * Where the chips and the radio list draw a group heading: the position in\n * the flattened list of each non-empty group's first option, to its heading.\n */\nfunction groupHeads(options) {\n const heads = new Map();\n let at = 0;\n for (const o of options || []) {\n if (!isGroup(o)) { at += 1; continue; }\n if (o.options.length) heads.set(at, String(o.group ?? ''));\n at += o.options.length;\n }\n return heads;\n}\n\nconst groupHeading = (text) => el('div', 'twm-choice__group', text);\n\n/** Does stored `value` name option value `v`? */\nfunction sameChoice(v, value, loose = false) {\n if (v === value) return true;\n if (!loose || v === undefined || value === undefined || v === null || value === null) return false;\n return String(v) === String(value);\n}\n\n/**\n * The position in `list` (normalised options) of the option stored value `v`\n * names, or -1: the first whose value IS `v`, else \u2014 with `loose` \u2014 the first\n * whose value has the same text. `from` skips the leading entries.\n */\nfunction choiceIndex(list, v, loose = false, from = 0) {\n for (let i = from; i < list.length; i += 1) if (list[i].value === v) return i;\n if (!loose) return -1;\n for (let i = from; i < list.length; i += 1) if (sameChoice(list[i].value, v, true)) return i;\n return -1;\n}\n\n/** The option of `list` stored value `v` names (`choiceIndex`), or undefined. */\nexport function findChoice(list, v, loose = false) {\n const i = choiceIndex(list, v, loose);\n return i >= 0 ? list[i] : undefined;\n}\n\n/** An option's text: its label, and the reason it is unavailable when it is. */\nconst optionText = (o) => (o.disabled && o.reason ? `${o.label} \u2014 ${o.reason}` : o.label);\n\n/**\n * A `<select>` over `options`.\n *\n * `empty` (text) draws a first option that stands for \"no value\" (`undefined`).\n * `valueAttr`: what each `<option value>` carries \u2014 `'index'` (its position;\n * the kit's) or `'value'` (the option's value as text, so a script that sets\n * `.value = 'b'` picks option `'b'`). The value READ BACK is the option's own\n * either way.\n *\n * Returns `{ box, valueOf(), entry(), select(value), setOptions(options, value?) }`:\n * `select` re-selects by value (true when the value is offered and enabled);\n * `setOptions` redraws the list and places `value` (default: the current\n * value) the way the first draw places one, so with `keepMissing` a value the\n * list lacks shows as its own not-found entry.\n */\nexport function selectBox(options, value, {\n empty = null, label = '', notFound = (v) => `${v} (not found)`, keepMissing = true,\n valueAttr = 'index', loose = false, className = 'twm-flow-input twm-flow-input--select',\n} = {}) {\n const box = el('select', className);\n if (label) box.setAttribute('aria-label', label);\n let entries = [];\n\n const add = (parent, o) => {\n const attr = valueAttr === 'index' ? String(entries.length) : (o.value === undefined ? '' : String(o.value));\n // `createElement`, not `new Option`: the constructor is a window\n // global, and a page that installed only `document` has none.\n const opt = el('option', '', optionText(o));\n opt.value = attr;\n if (o.disabled) {\n opt.disabled = true;\n if (o.reason) opt.title = o.reason;\n }\n parent.appendChild(opt);\n entries.push(o);\n };\n const firstEnabled = () => {\n const i = entries.findIndex((e) => !e.disabled);\n return i >= 0 ? i : 0;\n };\n const select = (v) => {\n const start = empty !== null ? 1 : 0;\n const at = choiceIndex(entries, v, loose, start);\n if (at < 0 || entries[at].disabled || entries[at].stray) return false;\n box.selectedIndex = at;\n return true;\n };\n const place = (v) => {\n if (select(v)) return;\n if (keepMissing && v !== undefined && v !== null && v !== '') {\n // A stored value the list no longer offers is KEPT as its own option:\n // it must not silently become the first entry on the next save.\n add(box, { value: v, label: notFound(shownValue(v)), description: null, disabled: false, reason: null,\n stray: true });\n box.selectedIndex = entries.length - 1;\n return;\n }\n box.selectedIndex = empty !== null ? 0 : firstEnabled();\n };\n const fill = (opts, v) => {\n box.replaceChildren();\n entries = [];\n if (empty !== null) add(box, { value: undefined, label: String(empty), description: null, disabled: false,\n reason: null });\n for (const o of opts || []) {\n if (isGroup(o)) {\n const g = el('optgroup');\n g.label = String(o.group ?? '');\n for (const inner of o.options) add(g, choiceOption(inner));\n box.appendChild(g);\n } else {\n add(box, choiceOption(o));\n }\n }\n place(v);\n };\n const entry = () => (box.selectedIndex < 0 ? null : entries[box.selectedIndex] ?? null);\n const valueOf = () => entry()?.value;\n\n fill(options, value);\n return {\n box,\n valueOf,\n entry,\n select,\n setOptions(opts, v = valueOf()) { fill(opts, v); },\n };\n}\n\n/**\n * Toggle chips over a fixed list: a multi-select. The value is the list of\n * pressed values in the list's order, and `onChange(list | undefined)` hears\n * each toggle (`undefined` when none is pressed). With `keepMissing`, a stored\n * entry the list lacks is kept, marked, until it is toggled off. A disabled\n * option is a chip that cannot be pressed, its reason under its label.\n *\n * Returns `{ el, valueOf(), setValue(list), setOptions(options) }`;\n * `setOptions` keeps the pressed values the new list still offers.\n */\nexport function toggleChips({ options, value, label, readOnly, onChange, notFound = (v) => `${v} (not found)`,\n keepMissing = true, loose = false }) {\n const root = el('div', 'twm-flow-togglechips');\n root.setAttribute('role', 'group');\n if (label) root.setAttribute('aria-label', label);\n let shown = []; // the options as given, groups and all\n let list = [];\n let chosen = new Set();\n let strays = [];\n\n const emit = () => {\n const out = valueOf();\n onChange?.(out.length ? out : undefined);\n };\n const chipFor = (text, pressed, toggle, { stray = false, reason = null, disabled = false } = {}) => {\n const b = el('button', `twm-flow-togglechip${stray ? ' twm-flow-togglechip--missing' : ''}`, text);\n b.type = 'button';\n b.setAttribute('aria-pressed', pressed ? 'true' : 'false');\n b.disabled = Boolean(readOnly) || disabled;\n if (reason) {\n b.title = reason;\n b.appendChild(el('span', 'twm-choice__reason', reason));\n }\n b.addEventListener('click', () => {\n const on = toggle();\n b.setAttribute('aria-pressed', on ? 'true' : 'false');\n emit();\n });\n return b;\n };\n const draw = (opts, stored) => {\n shown = opts || [];\n list = flatChoices(shown);\n const heads = groupHeads(shown);\n const wanted = Array.isArray(stored) ? stored : [];\n const offered = (v) => {\n const o = findChoice(list, v, loose);\n return o && !o.disabled ? o : undefined;\n };\n chosen = new Set(wanted.map(offered).filter(Boolean).map((o) => o.value));\n strays = keepMissing ? wanted.filter((v) => !offered(v)) : [];\n root.replaceChildren();\n list.forEach((o, i) => {\n if (heads.has(i)) root.appendChild(groupHeading(heads.get(i)));\n root.appendChild(chipFor(o.label, chosen.has(o.value), () => {\n if (o.disabled) return false; // never chosen, even with the button forced\n if (chosen.has(o.value)) chosen.delete(o.value); else chosen.add(o.value);\n return chosen.has(o.value);\n }, { disabled: o.disabled, reason: o.disabled ? o.reason : null }));\n });\n for (const v of [...strays]) {\n root.appendChild(chipFor(notFound(shownValue(v)), true, () => {\n const i = strays.indexOf(v);\n if (i >= 0) { strays.splice(i, 1); return false; }\n strays.push(v);\n return true;\n }, { stray: true }));\n }\n };\n const valueOf = () => [...list.filter((o) => chosen.has(o.value)).map((o) => o.value), ...strays];\n draw(options, value);\n return {\n el: root,\n valueOf,\n setValue: (stored) => draw(shown, stored),\n setOptions: (opts) => draw(opts, valueOf()),\n };\n}\n\n/**\n * A one-of-N as radio rows, each with its label \u2014 and a sentence under it when\n * the option has a `description`, and its `reason` when it is disabled. `name`\n * groups the radios (the caller's, so it is unique where it needs to be).\n *\n * Returns `{ el, focus(), valueOf(), entry(), select(value), setOptions(options) }`.\n * `onChange(value)` hears the reader's choice. With `keepMissing`, a stored\n * value the list lacks is named in a note under the list (and kept by the\n * caller, since no radio holds it).\n */\nexport function choiceList({ options, value, name, label, readOnly, onChange, notFound = (v) => `${v} (not found)`,\n keepMissing = true, loose = false }) {\n const root = el('div', 'twm-flow-choices');\n root.setAttribute('role', 'radiogroup');\n if (label) root.setAttribute('aria-label', label);\n let list = [];\n let radios = [];\n\n const draw = (opts, current) => {\n list = flatChoices(opts);\n const heads = groupHeads(opts);\n radios = [];\n root.replaceChildren();\n const named = choiceIndex(list, current, loose);\n const at = named >= 0 && !list[named].disabled ? named : -1;\n list.forEach((o, i) => {\n if (heads.has(i)) root.appendChild(groupHeading(heads.get(i)));\n const row = el('label', `twm-flow-choice${o.disabled ? ' twm-choice--disabled' : ''}`);\n const radio = el('input', 'twm-flow-choice__radio');\n radio.type = 'radio';\n radio.name = name;\n radio.checked = i === at;\n radio.disabled = Boolean(readOnly) || o.disabled;\n radio.addEventListener('change', () => { if (radio.checked) onChange?.(o.value); });\n radios.push(radio);\n const words = el('span', 'twm-flow-choice__words');\n words.appendChild(el('span', 'twm-flow-choice__label', o.label));\n if (o.description !== null) words.appendChild(el('span', 'twm-flow-choice__sub', o.description));\n if (o.disabled && o.reason) words.appendChild(el('span', 'twm-choice__reason', o.reason));\n row.append(radio, words);\n root.appendChild(row);\n });\n if (keepMissing && current !== undefined && named < 0) {\n root.appendChild(el('p', 'twm-flow-field__note', notFound(shownValue(current))));\n }\n };\n const checkedAt = () => radios.findIndex((r) => r.checked);\n const entry = () => list[checkedAt()] ?? null;\n const api = {\n el: root,\n focus: () => (radios.find((r) => r.checked && !r.disabled) || radios.find((r) => !r.disabled))\n ?.focus({ preventScroll: true }),\n entry,\n valueOf: () => entry()?.value,\n select(v) {\n const named = choiceIndex(list, v, loose);\n const at = named >= 0 && !list[named].disabled ? named : -1;\n radios.forEach((r, i) => { r.checked = i === at; });\n return at >= 0;\n },\n setOptions(opts) { draw(opts, api.valueOf()); },\n };\n draw(options, value);\n return api;\n}\n", "/**\n * Collect the focusable elements inside `root`, in DOM order: what a focus trap\n * cycles through. Hidden and zero-size elements are skipped, since Tab visits only\n * what the reader can see.\n *\n * @param {HTMLElement|null} root\n * @returns {HTMLElement[]}\n */\nexport function collectFocusable(root) {\n if (!root) return [];\n const sel = [\n 'a[href]',\n 'button:not([disabled])',\n 'input:not([disabled]):not([type=\"hidden\"])',\n 'select:not([disabled])',\n 'textarea:not([disabled])',\n '[tabindex]:not([tabindex=\"-1\"])',\n ].join(',');\n return Array.from(root.querySelectorAll(sel)).filter((el) => {\n if (el.hidden) return false;\n if (el.closest('[hidden]')) return false;\n const r = el.getBoundingClientRect();\n return r.width > 0 || r.height > 0;\n });\n}\n"],
5
+ "mappings": ";AAwBA,IAAI,OAAO;AACJ,SAAS,aAAaA,KAAI;AAAE,SAAOA,OAAM;AAAM;AAC/C,SAAS,YAAY;AAAE,SAAO;AAAM;;;ACsB3C,IAAM,cAAc;AAEpB,IAAM,MAAM;AAEZ,IAAM,SAAS,EAAE,MAAM,GAAG,OAAO,GAAG,KAAK,GAAG,QAAQ,GAAG,OAAO,GAAG,QAAQ,EAAE;AAmBpE,SAAS,iBAAiB,QAAQ,OAAO,EAAE,QAAQ,SAAS,MAAM,KAAK,SAAS,aAAa,aAAa,MAAM,IAAI,CAAC,GAAG;AAC3H,MAAI,CAAC,MAAO;AAGZ,SAAO,OAAO,MAAM,OAAO;AAAA,IACvB,UAAU;AAAA,IACV,KAAK;AAAA,IACL,QAAQ;AAAA,IACR,MAAM;AAAA,IACN,OAAO;AAAA,IACP,WAAW;AAAA,IACX,WAAW;AAAA,EACf,CAAC;AAED,QAAM,MAAM,QAAQ,wBAAwB,KAAK;AACjD,MAAI,WAAY,OAAM,MAAM,WAAW,GAAG,KAAK,MAAM,IAAI,KAAK,CAAC;AAC/D,QAAM,OAAO,MAAM,sBAAsB;AACzC,QAAM,MAAM,QAAQ,iBAAiB;AACrC,QAAM,OAAO,IAAI,eAAe;AAChC,QAAM,KAAK,IAAI,gBAAgB,eAAe,KAAK;AACnD,QAAM,KAAK,IAAI,gBAAgB,gBAAgB,KAAK;AAGpD,QAAM,aAAa,KAAK,IAAI,SAAS,MAAM;AAC3C,QAAM,aAAa,IAAI,MAAM,MAAM;AAEnC,MAAI;AACJ,MAAI;AACJ,MAAI,KAAK,UAAU,YAAY;AAC3B,UAAM,IAAI,SAAS;AACnB,gBAAY;AAAA,EAChB,WAAW,KAAK,UAAU,YAAY;AAClC,UAAM,IAAI,MAAM,MAAM,KAAK;AAC3B,gBAAY;AAAA,EAChB,WAAW,cAAc,YAAY;AAEjC,UAAM,IAAI,SAAS;AACnB,gBAAY;AAAA,EAChB,OAAO;AAEH,gBAAY;AACZ,UAAM;AAAA,EACV;AAGA,MAAI;AACJ,MAAI,UAAU,OAAO;AACjB,WAAO,IAAI,QAAQ,KAAK;AACxB,QAAI,OAAO,OAAQ,QAAO,IAAI;AAAA,EAClC,OAAO;AACH,WAAO,IAAI;AACX,QAAI,OAAO,KAAK,QAAQ,KAAK,OAAQ,QAAO,IAAI,QAAQ,KAAK;AAAA,EACjE;AACA,SAAO,KAAK,IAAI,QAAQ,KAAK,IAAI,MAAM,KAAK,KAAK,QAAQ,MAAM,CAAC;AAEhE,SAAO,OAAO,MAAM,OAAO;AAAA,IACvB,UAAU;AAAA,IACV,KAAK,GAAG,KAAK,MAAM,GAAG,CAAC;AAAA,IACvB,MAAM,GAAG,KAAK,MAAM,IAAI,CAAC;AAAA,IACzB,WAAW,GAAG,KAAK,MAAM,SAAS,CAAC;AAAA,IACnC,WAAW;AAAA,EACf,CAAC;AACL;AAGA,IAAM,iBAAiB;AAEvB,IAAM,iBAAiB;AACvB,IAAM,eAAe;AAGrB,IAAM,SAAS,CAAC;AAEhB,IAAM,QAAQ,CAAC,MAAM,WAAW,CAAC,CAAC,QAAQ,OAAO,KAAK,aAAa,cAAc,KAAK,SAAS,MAAM;AAErG,IAAM,cAAc,CAAC,QAAQ,CAAC,GAAG,IAAI,iBAAiB,YAAY,CAAC,EAAE,OAAO,CAAC,QAAQ,IAAI,MAAM,YAAY,MAAM;AAG1G,IAAM,oBAAoB,CAAC,SAAS,OAAO,KAAK,CAAC,UAAU,MAAM,MAAM,IAAI,IAAI,CAAC;AAuBhF,SAAS,kBAAkB;AAAA,EAAE,SAAS;AAAA,EAAM,IAAAC;AAAA,EAAI,QAAQ;AAAA,EAAS,MAAM;AAAA,EAAK,aAAa;AAAA,EAAO,SAAS;AAAA,EAC5E,cAAc;AAAA,EAAM,UAAU;AAAK,GAAG;AACtE,MAAI,UAAU,OAAO,gBAAgB,MAAO,QAAO;AAEnD,SAAO,OAAO,UAAU,CAAC,OAAO,OAAO,SAAS,CAAC,EAAE,KAAK,MAAM,EAAG,QAAO,OAAO,SAAS,CAAC,EAAE,MAAM,UAAU;AAE3G,QAAM,MAAM,QAAQ,iBAAiB;AACrC,QAAM,OAAO,IAAI,eAAe;AAChC,GAAC,UAAU,KAAK,IAAI,MAAM,YAAYA,GAAE;AACxC,QAAM,cAAc,IAAI,IAAI,YAAY,GAAG,CAAC;AAE5C,MAAI,SAAS;AACb,QAAM,QAAQ,MAAM;AAChB,QAAI,OAAQ;AACZ,QAAI,UAAU,OAAO,gBAAgB,OAAO;AAAE,YAAM,MAAM,UAAU;AAAG;AAAA,IAAQ;AAC/E,qBAAiB,QAAQA,KAAI,EAAE,OAAO,KAAK,WAAW,CAAC;AAAA,EAC3D;AAEA,QAAM,OAAO,CAAC,WAAW;AACrB,QAAI,MAAMA,KAAI,MAAM,EAAG,QAAO;AAC9B,aAAS,IAAI,OAAO,QAAQ,KAAK,IAAI,GAAG,IAAI,OAAO,QAAQ,KAAK,EAAG,KAAI,MAAM,OAAO,CAAC,EAAE,IAAI,MAAM,EAAG,QAAO;AAC3G,QAAI,OAAO,QAAQ,YAAY,WAAY,QAAO;AAClD,QAAI,OAAO,QAAQ,cAAc,EAAG,QAAO;AAC3C,UAAM,QAAQ,OAAO,QAAQ,YAAY;AACzC,QAAI,SAAS,CAAC,YAAY,IAAI,KAAK,EAAG,QAAO;AAC7C,WAAO,CAAC,CAAC,UAAU,CAAC,CAAC,OAAO,QAAQ,MAAM;AAAA,EAC9C;AACA,QAAM,UAAU,CAAC,OAAO;AAAE,QAAI,CAAC,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,KAAK,GAAG,MAAM,EAAG,OAAM,MAAM,SAAS;AAAA,EAAG;AACrG,QAAM,WAAW,CAAC,OAAO;AAAE,QAAI,CAAC,MAAMA,KAAI,GAAG,MAAM,EAAG,OAAM,EAAE;AAAA,EAAG;AACjE,QAAM,QAAQ,CAAC,OAAO;AAClB,QAAI,GAAG,QAAQ,YAAY,GAAG,eAAe,GAAG,iBAAkB;AAClE,QAAI,OAAO,OAAO,SAAS,CAAC,MAAM,MAAO;AACzC,QAAI,IAAI,cAAc,cAAc,EAAG;AACvC,QAAI,YAAY,GAAG,EAAE,KAAK,CAAC,QAAQ,CAAC,YAAY,IAAI,GAAG,CAAC,EAAG;AAC3D,OAAG,eAAe;AAClB,OAAG,gBAAgB;AACnB,UAAM,MAAM,QAAQ;AAAA,EACxB;AACA,QAAM,WAAW,CAAC,OAAO;AAAE,QAAI,CAAC,MAAMA,KAAI,GAAG,MAAM,EAAG,OAAM;AAAA,EAAG;AAG/D,QAAM,QAAQ,OAAO,mBAAmB,aAAa,IAAI,eAAe,MAAM,MAAM,CAAC,IAAI;AAEzF,QAAM,QAAQ;AAAA,IACV,IAAAA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,MAAM,SAAS,UAAU;AACrB,UAAI,OAAQ;AACZ,aAAO,OAAO,OAAO,SAAS,CAAC,MAAM,MAAO,QAAO,OAAO,SAAS,CAAC,EAAE,MAAM,QAAQ;AACpF,eAAS;AACT,YAAM,SAAS,IAAI;AACnB,YAAM,OAAO,MAAMA,KAAI,MAAM,KAAK,WAAW,IAAI;AACjD,UAAI,oBAAoB,eAAe,SAAS,IAAI;AACpD,UAAI,oBAAoB,aAAa,SAAS,IAAI;AAClD,UAAI,oBAAoB,WAAW,UAAU,IAAI;AACjD,MAAAA,IAAG,oBAAoB,WAAW,KAAK;AACvC,WAAK,oBAAoB,UAAU,KAAK;AACxC,WAAK,oBAAoB,UAAU,UAAU,IAAI;AACjD,aAAO,WAAW;AAClB,MAAAA,IAAG,OAAO;AACV,aAAO,IAAI;AACX,UAAI,eAAe,QAAQ,WAAW,UAAU,QAAQ,YAAa,QAAO,QAAQ,EAAE,eAAe,KAAK,CAAC;AAC3G,gBAAU,MAAM;AAAA,IACpB;AAAA,EACJ;AACA,SAAO,KAAK,KAAK;AAGjB,aAAW,MAAM;AACb,QAAI,OAAQ;AACZ,QAAI,iBAAiB,eAAe,SAAS,IAAI;AACjD,QAAI,iBAAiB,aAAa,SAAS,IAAI;AAAA,EACnD,GAAG,CAAC;AACJ,MAAI,iBAAiB,WAAW,UAAU,IAAI;AAC9C,EAAAA,IAAG,iBAAiB,WAAW,KAAK;AACpC,OAAK,iBAAiB,UAAU,KAAK;AACrC,OAAK,iBAAiB,UAAU,UAAU,IAAI;AAC9C,SAAO,QAAQA,GAAE;AACjB,QAAM;AACN,SAAO;AACX;;;ACxNO,IAAM,aAAa,CAAC,MAAO,MAAM,SAAY,KAAK,OAAO,MAAM,WAAW,IAAI,KAAK,UAAU,CAAC;AAErG,SAAS,GAAG,KAAK,WAAW,MAAM;AAC9B,QAAM,IAAI,SAAS,cAAc,GAAG;AACpC,MAAI,UAAW,GAAE,YAAY;AAC7B,MAAI,SAAS,UAAa,SAAS,KAAM,GAAE,cAAc,OAAO,IAAI;AACpE,SAAO;AACX;AAEA,IAAM,UAAU,CAAC,MAAM,MAAM,QAAQ,OAAO,MAAM,YAAY,MAAM,QAAQ,EAAE,OAAO,KAAK,WAAW;AAOrG,SAAS,aAAa,GAAG;AACrB,MAAI,MAAM,QAAQ,OAAO,MAAM,UAAU;AACrC,WAAO;AAAA,MAAE,OAAO;AAAA,MAAG,OAAO,WAAW,CAAC;AAAA,MAAG,aAAa;AAAA,MAAM,UAAU;AAAA,MAAO,QAAQ;AAAA,MAAM,MAAM;AAAA,MACxF,MAAM;AAAA,MAAM,UAAU;AAAA,MAAM,QAAQ;AAAA,IAAE;AAAA,EACnD;AACA,SAAO;AAAA,IACH,OAAO,EAAE;AAAA,IACT,OAAO,EAAE,SAAS,OAAO,OAAO,EAAE,KAAK,IAAI,WAAW,EAAE,KAAK;AAAA,IAC7D,aAAa,EAAE,eAAe,OAAO,OAAO,EAAE,WAAW,IAAI;AAAA,IAC7D,UAAU,QAAQ,EAAE,QAAQ;AAAA,IAC5B,QAAQ,EAAE,UAAU,QAAQ,EAAE,WAAW,KAAK,OAAO,EAAE,MAAM,IAAI;AAAA,IACjE,MAAM,EAAE,OAAO,OAAO,EAAE,IAAI,IAAI;AAAA,IAChC,MAAM,EAAE,QAAQ,QAAQ,EAAE,SAAS,KAAK,OAAO,EAAE,IAAI,IAAI;AAAA,IACzD,UAAU,EAAE,YAAY,OAAO,OAAO,EAAE,QAAQ,IAAI;AAAA,IACpD,QAAQ;AAAA,EACZ;AACJ;AAGO,SAAS,YAAY,SAAS;AACjC,QAAM,MAAM,CAAC;AACb,aAAW,KAAK,WAAW,CAAC,GAAG;AAC3B,QAAI,QAAQ,CAAC,EAAG,YAAW,SAAS,EAAE,QAAS,KAAI,KAAK,aAAa,KAAK,CAAC;AAAA,QACtE,KAAI,KAAK,aAAa,CAAC,CAAC;AAAA,EACjC;AACA,SAAO;AACX;AAOO,SAAS,eAAe,SAAS;AACpC,QAAM,MAAM,CAAC;AACb,aAAW,KAAK,WAAW,CAAC,GAAG;AAC3B,QAAI,QAAQ,CAAC,GAAG;AACZ,UAAI,EAAE,QAAQ,OAAQ,KAAI,KAAK,EAAE,SAAS,OAAO,EAAE,SAAS,EAAE,GAAG,OAAO,EAAE,QAAQ,IAAI,YAAY,EAAE,CAAC;AAAA,IACzG,WAAW,IAAI,UAAU,IAAI,IAAI,SAAS,CAAC,EAAE,YAAY,MAAM;AAC3D,UAAI,IAAI,SAAS,CAAC,EAAE,MAAM,KAAK,aAAa,CAAC,CAAC;AAAA,IAClD,OAAO;AACH,UAAI,KAAK,EAAE,SAAS,MAAM,OAAO,CAAC,aAAa,CAAC,CAAC,EAAE,CAAC;AAAA,IACxD;AAAA,EACJ;AACA,SAAO;AACX;AAGO,SAAS,cAAc,QAAQ,MAAM;AACxC,QAAM,SAAS,OAAO,QAAQ,EAAE,EAAE,KAAK,EAAE,YAAY;AACrD,MAAI,CAAC,OAAQ,QAAO;AACpB,SAAO,CAAC,OAAO,OAAO,WAAW,OAAO,KAAK,GAAG,OAAO,aAAa,OAAO,QAAQ,EAC9E,KAAK,CAAC,SAAS,OAAO,QAAQ,EAAE,EAAE,YAAY,EAAE,SAAS,MAAM,CAAC;AACzE;AAGO,SAAS,eAAe,UAAU,MAAM;AAC3C,SAAO,SACF,IAAI,CAAC,OAAO,EAAE,SAAS,EAAE,SAAS,OAAO,EAAE,MAAM,OAAO,CAAC,MAAM,cAAc,GAAG,IAAI,CAAC,EAAE,EAAE,EACzF,OAAO,CAAC,MAAM,EAAE,MAAM,MAAM;AACrC;AAMA,SAAS,WAAW,SAAS;AACzB,QAAM,QAAQ,oBAAI,IAAI;AACtB,MAAI,KAAK;AACT,aAAW,KAAK,WAAW,CAAC,GAAG;AAC3B,QAAI,CAAC,QAAQ,CAAC,GAAG;AAAE,YAAM;AAAG;AAAA,IAAU;AACtC,QAAI,EAAE,QAAQ,OAAQ,OAAM,IAAI,IAAI,OAAO,EAAE,SAAS,EAAE,CAAC;AACzD,UAAM,EAAE,QAAQ;AAAA,EACpB;AACA,SAAO;AACX;AAEA,IAAM,eAAe,CAAC,SAAS,GAAG,OAAO,qBAAqB,IAAI;AAGlE,SAAS,WAAW,GAAG,OAAO,QAAQ,OAAO;AACzC,MAAI,MAAM,MAAO,QAAO;AACxB,MAAI,CAAC,SAAS,MAAM,UAAa,UAAU,UAAa,MAAM,QAAQ,UAAU,KAAM,QAAO;AAC7F,SAAO,OAAO,CAAC,MAAM,OAAO,KAAK;AACrC;AAOA,SAAS,YAAY,MAAM,GAAG,QAAQ,OAAO,OAAO,GAAG;AACnD,WAAS,IAAI,MAAM,IAAI,KAAK,QAAQ,KAAK,EAAG,KAAI,KAAK,CAAC,EAAE,UAAU,EAAG,QAAO;AAC5E,MAAI,CAAC,MAAO,QAAO;AACnB,WAAS,IAAI,MAAM,IAAI,KAAK,QAAQ,KAAK,EAAG,KAAI,WAAW,KAAK,CAAC,EAAE,OAAO,GAAG,IAAI,EAAG,QAAO;AAC3F,SAAO;AACX;AAGO,SAAS,WAAW,MAAM,GAAG,QAAQ,OAAO;AAC/C,QAAM,IAAI,YAAY,MAAM,GAAG,KAAK;AACpC,SAAO,KAAK,IAAI,KAAK,CAAC,IAAI;AAC9B;AAGA,IAAM,aAAa,CAAC,MAAO,EAAE,YAAY,EAAE,SAAS,GAAG,EAAE,KAAK,WAAM,EAAE,MAAM,KAAK,EAAE;AAiB5E,SAAS,UAAU,SAAS,OAAO;AAAA,EACtC,QAAQ;AAAA,EAAM,QAAQ;AAAA,EAAI,WAAW,CAAC,MAAM,GAAG,CAAC;AAAA,EAAgB,cAAc;AAAA,EAC9E,YAAY;AAAA,EAAS,QAAQ;AAAA,EAAO,YAAY;AACpD,IAAI,CAAC,GAAG;AACJ,QAAM,MAAM,GAAG,UAAU,SAAS;AAClC,MAAI,MAAO,KAAI,aAAa,cAAc,KAAK;AAC/C,MAAI,UAAU,CAAC;AAEf,QAAM,MAAM,CAAC,QAAQ,MAAM;AACvB,UAAM,OAAO,cAAc,UAAU,OAAO,QAAQ,MAAM,IAAK,EAAE,UAAU,SAAY,KAAK,OAAO,EAAE,KAAK;AAG1G,UAAM,MAAM,GAAG,UAAU,IAAI,WAAW,CAAC,CAAC;AAC1C,QAAI,QAAQ;AACZ,QAAI,EAAE,UAAU;AACZ,UAAI,WAAW;AACf,UAAI,EAAE,OAAQ,KAAI,QAAQ,EAAE;AAAA,IAChC;AACA,WAAO,YAAY,GAAG;AACtB,YAAQ,KAAK,CAAC;AAAA,EAClB;AACA,QAAM,eAAe,MAAM;AACvB,UAAM,IAAI,QAAQ,UAAU,CAAC,MAAM,CAAC,EAAE,QAAQ;AAC9C,WAAO,KAAK,IAAI,IAAI;AAAA,EACxB;AACA,QAAM,SAAS,CAAC,MAAM;AAClB,UAAM,QAAQ,UAAU,OAAO,IAAI;AACnC,UAAM,KAAK,YAAY,SAAS,GAAG,OAAO,KAAK;AAC/C,QAAI,KAAK,KAAK,QAAQ,EAAE,EAAE,YAAY,QAAQ,EAAE,EAAE,MAAO,QAAO;AAChE,QAAI,gBAAgB;AACpB,WAAO;AAAA,EACX;AACA,QAAM,QAAQ,CAAC,MAAM;AACjB,QAAI,OAAO,CAAC,EAAG;AACf,QAAI,eAAe,MAAM,UAAa,MAAM,QAAQ,MAAM,IAAI;AAG1D,UAAI,KAAK;AAAA,QAAE,OAAO;AAAA,QAAG,OAAO,SAAS,WAAW,CAAC,CAAC;AAAA,QAAG,aAAa;AAAA,QAAM,UAAU;AAAA,QAAO,QAAQ;AAAA,QACtF,OAAO;AAAA,MAAK,CAAC;AACxB,UAAI,gBAAgB,QAAQ,SAAS;AACrC;AAAA,IACJ;AACA,QAAI,gBAAgB,UAAU,OAAO,IAAI,aAAa;AAAA,EAC1D;AACA,QAAM,OAAO,CAAC,MAAM,MAAM;AACtB,QAAI,gBAAgB;AACpB,cAAU,CAAC;AACX,QAAI,UAAU,KAAM,KAAI,KAAK;AAAA,MAAE,OAAO;AAAA,MAAW,OAAO,OAAO,KAAK;AAAA,MAAG,aAAa;AAAA,MAAM,UAAU;AAAA,MACrE,QAAQ;AAAA,IAAK,CAAC;AAC7C,eAAW,KAAK,QAAQ,CAAC,GAAG;AACxB,UAAI,QAAQ,CAAC,GAAG;AACZ,cAAM,IAAI,GAAG,UAAU;AACvB,UAAE,QAAQ,OAAO,EAAE,SAAS,EAAE;AAC9B,mBAAW,SAAS,EAAE,QAAS,KAAI,GAAG,aAAa,KAAK,CAAC;AACzD,YAAI,YAAY,CAAC;AAAA,MACrB,OAAO;AACH,YAAI,KAAK,aAAa,CAAC,CAAC;AAAA,MAC5B;AAAA,IACJ;AACA,UAAM,CAAC;AAAA,EACX;AACA,QAAM,QAAQ,MAAO,IAAI,gBAAgB,IAAI,OAAO,QAAQ,IAAI,aAAa,KAAK;AAClF,QAAM,UAAU,MAAM,MAAM,GAAG;AAE/B,OAAK,SAAS,KAAK;AACnB,SAAO;AAAA,IACH;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,WAAW,MAAM,IAAI,QAAQ,GAAG;AAAE,WAAK,MAAM,CAAC;AAAA,IAAG;AAAA,EACrD;AACJ;AAYO,SAAS,YAAY;AAAA,EAAE;AAAA,EAAS;AAAA,EAAO;AAAA,EAAO;AAAA,EAAU;AAAA,EAAU,WAAW,CAAC,MAAM,GAAG,CAAC;AAAA,EACjE,cAAc;AAAA,EAAM,QAAQ;AAAM,GAAG;AAC/D,QAAM,OAAO,GAAG,OAAO,sBAAsB;AAC7C,OAAK,aAAa,QAAQ,OAAO;AACjC,MAAI,MAAO,MAAK,aAAa,cAAc,KAAK;AAChD,MAAI,QAAQ,CAAC;AACb,MAAI,OAAO,CAAC;AACZ,MAAI,SAAS,oBAAI,IAAI;AACrB,MAAI,SAAS,CAAC;AAEd,QAAM,OAAO,MAAM;AACf,UAAM,MAAM,QAAQ;AACpB,eAAW,IAAI,SAAS,MAAM,MAAS;AAAA,EAC3C;AACA,QAAM,UAAU,CAAC,MAAM,SAAS,QAAQ,EAAE,QAAQ,OAAO,SAAS,MAAM,WAAW,MAAM,IAAI,CAAC,MAAM;AAChG,UAAM,IAAI,GAAG,UAAU,sBAAsB,QAAQ,kCAAkC,EAAE,IAAI,IAAI;AACjG,MAAE,OAAO;AACT,MAAE,aAAa,gBAAgB,UAAU,SAAS,OAAO;AACzD,MAAE,WAAW,QAAQ,QAAQ,KAAK;AAClC,QAAI,QAAQ;AACR,QAAE,QAAQ;AACV,QAAE,YAAY,GAAG,QAAQ,sBAAsB,MAAM,CAAC;AAAA,IAC1D;AACA,MAAE,iBAAiB,SAAS,MAAM;AAC9B,YAAM,KAAK,OAAO;AAClB,QAAE,aAAa,gBAAgB,KAAK,SAAS,OAAO;AACpD,WAAK;AAAA,IACT,CAAC;AACD,WAAO;AAAA,EACX;AACA,QAAM,OAAO,CAAC,MAAM,WAAW;AAC3B,YAAQ,QAAQ,CAAC;AACjB,WAAO,YAAY,KAAK;AACxB,UAAM,QAAQ,WAAW,KAAK;AAC9B,UAAM,SAAS,MAAM,QAAQ,MAAM,IAAI,SAAS,CAAC;AACjD,UAAM,UAAU,CAAC,MAAM;AACnB,YAAM,IAAI,WAAW,MAAM,GAAG,KAAK;AACnC,aAAO,KAAK,CAAC,EAAE,WAAW,IAAI;AAAA,IAClC;AACA,aAAS,IAAI,IAAI,OAAO,IAAI,OAAO,EAAE,OAAO,OAAO,EAAE,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC;AACxE,aAAS,cAAc,OAAO,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC;AAC5D,SAAK,gBAAgB;AACrB,SAAK,QAAQ,CAAC,GAAG,MAAM;AACnB,UAAI,MAAM,IAAI,CAAC,EAAG,MAAK,YAAY,aAAa,MAAM,IAAI,CAAC,CAAC,CAAC;AAC7D,WAAK,YAAY,QAAQ,EAAE,OAAO,OAAO,IAAI,EAAE,KAAK,GAAG,MAAM;AACzD,YAAI,EAAE,SAAU,QAAO;AACvB,YAAI,OAAO,IAAI,EAAE,KAAK,EAAG,QAAO,OAAO,EAAE,KAAK;AAAA,YAAQ,QAAO,IAAI,EAAE,KAAK;AACxE,eAAO,OAAO,IAAI,EAAE,KAAK;AAAA,MAC7B,GAAG,EAAE,UAAU,EAAE,UAAU,QAAQ,EAAE,WAAW,EAAE,SAAS,KAAK,CAAC,CAAC;AAAA,IACtE,CAAC;AACD,eAAW,KAAK,CAAC,GAAG,MAAM,GAAG;AACzB,WAAK,YAAY,QAAQ,SAAS,WAAW,CAAC,CAAC,GAAG,MAAM,MAAM;AAC1D,cAAM,IAAI,OAAO,QAAQ,CAAC;AAC1B,YAAI,KAAK,GAAG;AAAE,iBAAO,OAAO,GAAG,CAAC;AAAG,iBAAO;AAAA,QAAO;AACjD,eAAO,KAAK,CAAC;AACb,eAAO;AAAA,MACX,GAAG,EAAE,OAAO,KAAK,CAAC,CAAC;AAAA,IACvB;AAAA,EACJ;AACA,QAAM,UAAU,MAAM,CAAC,GAAG,KAAK,OAAO,CAAC,MAAM,OAAO,IAAI,EAAE,KAAK,CAAC,EAAE,IAAI,CAAC,MAAM,EAAE,KAAK,GAAG,GAAG,MAAM;AAChG,OAAK,SAAS,KAAK;AACnB,SAAO;AAAA,IACH,IAAI;AAAA,IACJ;AAAA,IACA,UAAU,CAAC,WAAW,KAAK,OAAO,MAAM;AAAA,IACxC,YAAY,CAAC,SAAS,KAAK,MAAM,QAAQ,CAAC;AAAA,EAC9C;AACJ;AAYO,SAAS,WAAW;AAAA,EAAE;AAAA,EAAS;AAAA,EAAO;AAAA,EAAM;AAAA,EAAO;AAAA,EAAU;AAAA,EAAU,WAAW,CAAC,MAAM,GAAG,CAAC;AAAA,EACvE,cAAc;AAAA,EAAM,QAAQ;AAAM,GAAG;AAC9D,QAAM,OAAO,GAAG,OAAO,kBAAkB;AACzC,OAAK,aAAa,QAAQ,YAAY;AACtC,MAAI,MAAO,MAAK,aAAa,cAAc,KAAK;AAChD,MAAI,OAAO,CAAC;AACZ,MAAI,SAAS,CAAC;AAEd,QAAM,OAAO,CAAC,MAAM,YAAY;AAC5B,WAAO,YAAY,IAAI;AACvB,UAAM,QAAQ,WAAW,IAAI;AAC7B,aAAS,CAAC;AACV,SAAK,gBAAgB;AACrB,UAAM,QAAQ,YAAY,MAAM,SAAS,KAAK;AAC9C,UAAM,KAAK,SAAS,KAAK,CAAC,KAAK,KAAK,EAAE,WAAW,QAAQ;AACzD,SAAK,QAAQ,CAAC,GAAG,MAAM;AACnB,UAAI,MAAM,IAAI,CAAC,EAAG,MAAK,YAAY,aAAa,MAAM,IAAI,CAAC,CAAC,CAAC;AAC7D,YAAM,MAAM,GAAG,SAAS,kBAAkB,EAAE,WAAW,0BAA0B,EAAE,EAAE;AACrF,YAAM,QAAQ,GAAG,SAAS,wBAAwB;AAClD,YAAM,OAAO;AACb,YAAM,OAAO;AACb,YAAM,UAAU,MAAM;AACtB,YAAM,WAAW,QAAQ,QAAQ,KAAK,EAAE;AACxC,YAAM,iBAAiB,UAAU,MAAM;AAAE,YAAI,MAAM,QAAS,YAAW,EAAE,KAAK;AAAA,MAAG,CAAC;AAClF,aAAO,KAAK,KAAK;AACjB,YAAM,QAAQ,GAAG,QAAQ,wBAAwB;AACjD,YAAM,YAAY,GAAG,QAAQ,0BAA0B,EAAE,KAAK,CAAC;AAC/D,UAAI,EAAE,gBAAgB,KAAM,OAAM,YAAY,GAAG,QAAQ,wBAAwB,EAAE,WAAW,CAAC;AAC/F,UAAI,EAAE,YAAY,EAAE,OAAQ,OAAM,YAAY,GAAG,QAAQ,sBAAsB,EAAE,MAAM,CAAC;AACxF,UAAI,OAAO,OAAO,KAAK;AACvB,WAAK,YAAY,GAAG;AAAA,IACxB,CAAC;AACD,QAAI,eAAe,YAAY,UAAa,QAAQ,GAAG;AACnD,WAAK,YAAY,GAAG,KAAK,wBAAwB,SAAS,WAAW,OAAO,CAAC,CAAC,CAAC;AAAA,IACnF;AAAA,EACJ;AACA,QAAM,YAAY,MAAM,OAAO,UAAU,CAAC,MAAM,EAAE,OAAO;AACzD,QAAM,QAAQ,MAAM,KAAK,UAAU,CAAC,KAAK;AACzC,QAAM,MAAM;AAAA,IACR,IAAI;AAAA,IACJ,OAAO,OAAO,OAAO,KAAK,CAAC,MAAM,EAAE,WAAW,CAAC,EAAE,QAAQ,KAAK,OAAO,KAAK,CAAC,MAAM,CAAC,EAAE,QAAQ,IACtF,MAAM,EAAE,eAAe,KAAK,CAAC;AAAA,IACnC;AAAA,IACA,SAAS,MAAM,MAAM,GAAG;AAAA,IACxB,OAAO,GAAG;AACN,YAAM,QAAQ,YAAY,MAAM,GAAG,KAAK;AACxC,YAAM,KAAK,SAAS,KAAK,CAAC,KAAK,KAAK,EAAE,WAAW,QAAQ;AACzD,aAAO,QAAQ,CAAC,GAAG,MAAM;AAAE,UAAE,UAAU,MAAM;AAAA,MAAI,CAAC;AAClD,aAAO,MAAM;AAAA,IACjB;AAAA,IACA,WAAW,MAAM;AAAE,WAAK,MAAM,IAAI,QAAQ,CAAC;AAAA,IAAG;AAAA,EAClD;AACA,OAAK,SAAS,KAAK;AACnB,SAAO;AACX;;;ACjYO,SAAS,iBAAiB,MAAM;AACnC,MAAI,CAAC,KAAM,QAAO,CAAC;AACnB,QAAM,MAAM;AAAA,IACR;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACJ,EAAE,KAAK,GAAG;AACV,SAAO,MAAM,KAAK,KAAK,iBAAiB,GAAG,CAAC,EAAE,OAAO,CAACC,QAAO;AACzD,QAAIA,IAAG,OAAQ,QAAO;AACtB,QAAIA,IAAG,QAAQ,UAAU,EAAG,QAAO;AACnC,UAAM,IAAIA,IAAG,sBAAsB;AACnC,WAAO,EAAE,QAAQ,KAAK,EAAE,SAAS;AAAA,EACrC,CAAC;AACL;",
6
+ "names": ["el", "el", "el"]
7
+ }
@@ -0,0 +1,40 @@
1
+ // src/ui/utils/menu_viewport.js
2
+ var MENU_VIEWPORT_MARGIN = 8;
3
+ function placeMenuInViewport(menu, x, y, margin = MENU_VIEWPORT_MARGIN) {
4
+ const view = menu.ownerDocument?.defaultView || window;
5
+ const vw = view.innerWidth;
6
+ const vh = view.innerHeight;
7
+ const maxHeight = Math.max(0, vh - 2 * margin);
8
+ menu.style.maxHeight = `${maxHeight}px`;
9
+ menu.scrollTop = 0;
10
+ const rect = menu.getBoundingClientRect();
11
+ const left = Math.min(x, vw - rect.width - margin);
12
+ const top = Math.min(y, vh - rect.height - margin);
13
+ menu.style.left = `${Math.max(0, left)}px`;
14
+ menu.style.top = `${Math.max(0, top)}px`;
15
+ }
16
+ function focusMenuItem(item) {
17
+ if (!item) return;
18
+ item.focus({ preventScroll: true });
19
+ item.scrollIntoView?.({ block: "nearest", inline: "nearest" });
20
+ }
21
+ function moveMenuFocus(e, items) {
22
+ if (!items.length) return false;
23
+ const at = items.indexOf(items[0].ownerDocument.activeElement);
24
+ let next = null;
25
+ if (e.key === "ArrowDown") next = items[(at + 1 + items.length) % items.length];
26
+ else if (e.key === "ArrowUp") next = items[(at - 1 + items.length) % items.length];
27
+ else if (e.key === "Home") next = items[0];
28
+ else if (e.key === "End") next = items[items.length - 1];
29
+ if (!next) return false;
30
+ e.preventDefault();
31
+ focusMenuItem(next);
32
+ return true;
33
+ }
34
+
35
+ export {
36
+ placeMenuInViewport,
37
+ focusMenuItem,
38
+ moveMenuFocus
39
+ };
40
+ //# sourceMappingURL=chunk-QXAOTGCC.js.map