flexdesk 0.10.0 → 0.11.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.
@@ -11,7 +11,7 @@ function modalHost() {
11
11
  return _modalHost;
12
12
  }
13
13
  var _modalSeq = 1;
14
- function openForm({ title, fields = [], defaults = {}, submitLabel = "OK" } = {}) {
14
+ function openForm({ title, fields = [], defaults = {}, submitLabel = "OK", onFieldChange = null } = {}) {
15
15
  return new Promise((resolve) => {
16
16
  const body = document.createElement("div");
17
17
  body.className = "twm-modal__body-host";
@@ -66,7 +66,9 @@ function openForm({ title, fields = [], defaults = {}, submitLabel = "OK" } = {}
66
66
  const formFields = fields.filter((f) => !f.section);
67
67
  for (const f of formFields) {
68
68
  if (f.type === "select" && f.create) _wireComboboxHint(body, f);
69
+ if (f.type === "picker") _wirePicker(body, f);
69
70
  }
71
+ const rowHidden = (name) => !!body.querySelector(`[data-field-row="${name}"]`)?.hidden;
70
72
  const clearFieldError = (name) => {
71
73
  const row = body.querySelector(`[data-field-row="${name}"]`);
72
74
  const errEl2 = body.querySelector(`[data-field-error="${name}"]`);
@@ -90,6 +92,58 @@ function openForm({ title, fields = [], defaults = {}, submitLabel = "OK" } = {}
90
92
  el?.addEventListener("input", () => clearFieldError(f.name));
91
93
  el?.addEventListener("change", () => clearFieldError(f.name));
92
94
  }
95
+ if (typeof onFieldChange === "function") {
96
+ const fieldOf = (name) => formFields.find((f) => f.name === name) || null;
97
+ const api = {
98
+ setVisible(name, on) {
99
+ const row = body.querySelector(`[data-field-row="${name}"]`);
100
+ if (!row) return;
101
+ row.hidden = !on;
102
+ row.style.display = on ? "" : "none";
103
+ clearFieldError(name);
104
+ for (const el of row.querySelectorAll("input, select, textarea")) {
105
+ if (!on && el.required) {
106
+ el.required = false;
107
+ el.dataset.twmRequired = "1";
108
+ } else if (on && el.dataset.twmRequired) {
109
+ el.required = true;
110
+ delete el.dataset.twmRequired;
111
+ }
112
+ }
113
+ },
114
+ get(name) {
115
+ const el = body.querySelector(`[name="${name}"]`);
116
+ if (!el) return void 0;
117
+ return el.type === "checkbox" ? !!el.checked : el.value;
118
+ },
119
+ set(name, value) {
120
+ const el = body.querySelector(`[name="${name}"]`);
121
+ if (!el) return;
122
+ if (el.type === "checkbox") {
123
+ el.checked = !!value;
124
+ return;
125
+ }
126
+ el.value = value == null ? "" : String(value);
127
+ const f = fieldOf(name);
128
+ if (f?.type === "picker") {
129
+ const display = body.querySelector(`[data-picker-display="${name}"]`);
130
+ if (display) display.value = _pickerLabel(f, el.value);
131
+ }
132
+ }
133
+ };
134
+ const fire = (name) => {
135
+ try {
136
+ onFieldChange(name, name == null ? void 0 : api.get(name), api);
137
+ } catch (err) {
138
+ console.error("[modal] onFieldChange threw", err);
139
+ }
140
+ };
141
+ for (const f of formFields) {
142
+ const el = body.querySelector(`[name="${f.name}"]`);
143
+ el?.addEventListener("change", () => fire(f.name));
144
+ }
145
+ fire(null);
146
+ }
93
147
  body.querySelector("form").addEventListener("submit", async (e) => {
94
148
  e.preventDefault();
95
149
  const submitBtn = body.querySelector('button[type="submit"]');
@@ -107,20 +161,21 @@ function openForm({ title, fields = [], defaults = {}, submitLabel = "OK" } = {}
107
161
  v = el.value;
108
162
  }
109
163
  if (f.type === "select" && f.create) {
164
+ const spec = _createSpec(f);
110
165
  const known = new Set(_optionValues(f.options));
111
166
  if (v && !known.has(v)) {
112
167
  if (submitBtn) submitBtn.disabled = true;
113
168
  let created;
114
169
  let failMsg = null;
115
170
  try {
116
- created = await f.create.onCreate(v);
171
+ created = await spec.onCreate(v);
117
172
  } catch (err) {
118
173
  created = null;
119
174
  failMsg = err?.message || String(err);
120
175
  }
121
176
  if (submitBtn) submitBtn.disabled = false;
122
177
  if (created == null) {
123
- const hint = f.create && f.create.hint || f.label.toLowerCase();
178
+ const hint = spec.hint || f.label.toLowerCase();
124
179
  showError(failMsg ? `Couldn't create ${hint} "${v}": ${failMsg}` : `Couldn't create ${hint} "${v}". See the toast for details.`);
125
180
  return;
126
181
  }
@@ -132,12 +187,16 @@ function openForm({ title, fields = [], defaults = {}, submitLabel = "OK" } = {}
132
187
  let firstBadName = null;
133
188
  for (const f of formFields) {
134
189
  clearFieldError(f.name);
135
- if (typeof f.validator !== "function") continue;
190
+ if (rowHidden(f.name)) continue;
136
191
  let msg = null;
137
- try {
138
- msg = f.validator(data[f.name], data);
139
- } catch (err) {
140
- msg = err?.message || String(err);
192
+ if (f.type === "picker" && f.required && !data[f.name]) msg = "Required.";
193
+ else if (typeof f.validator !== "function") continue;
194
+ else {
195
+ try {
196
+ msg = f.validator(data[f.name], data);
197
+ } catch (err) {
198
+ msg = err?.message || String(err);
199
+ }
141
200
  }
142
201
  if (msg) {
143
202
  setFieldError(f.name, msg);
@@ -145,13 +204,15 @@ function openForm({ title, fields = [], defaults = {}, submitLabel = "OK" } = {}
145
204
  }
146
205
  }
147
206
  if (firstBadName) {
148
- body.querySelector(`[name="${firstBadName}"]`)?.focus();
207
+ body.querySelector(`[data-picker="${firstBadName}"]`)?.focus();
208
+ body.querySelector(`[name="${firstBadName}"]:not([type="hidden"])`)?.focus();
149
209
  return;
150
210
  }
151
211
  close(data);
152
212
  });
153
213
  requestAnimationFrame(() => {
154
- body.querySelector("input, select, textarea")?.focus();
214
+ const first = [...body.querySelectorAll('input:not([type="hidden"]), select, textarea')].find((el) => !el.closest("[data-field-row]")?.hidden);
215
+ first?.focus();
155
216
  });
156
217
  });
157
218
  }
@@ -409,12 +470,57 @@ function _estimateWidth(fields) {
409
470
  function _optionValues(options) {
410
471
  return (options || []).map((o) => typeof o === "object" ? o.value : o);
411
472
  }
473
+ function _createSpec(f) {
474
+ if (!f || !f.create) return null;
475
+ if (f.create === true) return { onCreate: async (v) => v, hint: null };
476
+ return {
477
+ onCreate: typeof f.create.onCreate === "function" ? f.create.onCreate : async (v) => v,
478
+ hint: f.create.hint ?? null
479
+ };
480
+ }
481
+ function _pickerLabel(f, value) {
482
+ if (typeof f.displayFor !== "function") return value == null ? "" : String(value);
483
+ try {
484
+ return String(f.displayFor(value) ?? "");
485
+ } catch (err) {
486
+ console.warn("[modal] displayFor threw", err);
487
+ return String(value ?? "");
488
+ }
489
+ }
490
+ function _wirePicker(host, f) {
491
+ const hidden = host.querySelector(`input[type="hidden"][name="${f.name}"]`);
492
+ const display = host.querySelector(`[data-picker-display="${f.name}"]`);
493
+ const btn = host.querySelector(`[data-picker="${f.name}"]`);
494
+ if (!hidden || !display || !btn || typeof f.pick !== "function") return;
495
+ let busy = false;
496
+ const open = async () => {
497
+ if (busy) return;
498
+ busy = true;
499
+ let picked;
500
+ try {
501
+ picked = await f.pick(hidden.value);
502
+ } catch (err) {
503
+ console.warn("[modal] pick threw", err);
504
+ picked = null;
505
+ } finally {
506
+ busy = false;
507
+ }
508
+ if (picked == null) return;
509
+ const isPair = typeof picked === "object";
510
+ hidden.value = String((isPair ? picked.value : picked) ?? "");
511
+ display.value = isPair && picked.label != null ? String(picked.label) : _pickerLabel(f, hidden.value);
512
+ hidden.dispatchEvent(new Event("change", { bubbles: true }));
513
+ };
514
+ btn.addEventListener("click", open);
515
+ display.addEventListener("click", open);
516
+ }
412
517
  function _wireComboboxHint(host, f) {
413
518
  const input = host.querySelector(`input[name="${f.name}"]`);
414
519
  const hintEl = host.querySelector(`.twm-combobox__hint[data-for="${f.name}"]`);
415
520
  if (!input || !hintEl) return;
521
+ if (f.create === true) return;
416
522
  const known = new Set(_optionValues(f.options));
417
- const label = f.create && f.create.hint || "new entry";
523
+ const label = _createSpec(f).hint || "new entry";
418
524
  const update = () => {
419
525
  const v = input.value.trim();
420
526
  if (v && !known.has(v)) {
@@ -429,7 +535,8 @@ function _wireComboboxHint(host, f) {
429
535
  }
430
536
  function _renderEntry(f, value) {
431
537
  if (f && f.section != null) {
432
- return `<div class="twm-modal__section">${_esc(f.section)}</div>`;
538
+ const attr = f.name ? ` data-field-row="${f.name}"` : "";
539
+ return `<div class="twm-modal__section"${attr}>${_esc(f.section)}</div>`;
433
540
  }
434
541
  return _renderField(f, value);
435
542
  }
@@ -467,6 +574,20 @@ function _renderField(f, value) {
467
574
  return wrap(`<textarea name="${f.name}" ${rows} ${placeholder2} ${required}>${_esc(v2)}</textarea>`);
468
575
  }
469
576
  const v = value == null ? "" : String(value);
577
+ if (f.type === "picker") {
578
+ const placeholder2 = f.placeholder ? `placeholder="${_esc(f.placeholder)}"` : "";
579
+ return wrap(`
580
+ <div class="twm-picker">
581
+ <input type="hidden" name="${f.name}" value="${_esc(v)}">
582
+ <input class="twm-picker__display" data-picker-display="${f.name}" type="text"
583
+ value="${_esc(_pickerLabel(f, v))}" ${placeholder2} readonly>
584
+ <button type="button" class="twm-btn twm-picker__btn" data-picker="${f.name}"
585
+ aria-label="Search" title="Search">
586
+ <span class="material-symbols-outlined" aria-hidden="true">search</span>
587
+ </button>
588
+ </div>
589
+ `);
590
+ }
470
591
  if (f.type === "select" && f.create) {
471
592
  const dlId = `ea-dl-${f.name}-${_modalSeq}`;
472
593
  const opts = _optionValues(f.options).map((ov) => `<option value="${_esc(ov)}"></option>`).join("");
@@ -505,4 +626,4 @@ export {
505
626
  openConfirm,
506
627
  openModal
507
628
  };
508
- //# sourceMappingURL=chunk-6NG7YVBG.js.map
629
+ //# sourceMappingURL=chunk-6EZPPN4E.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/ui/components/modal.js"],
4
+ "sourcesContent": ["/**\n * Modal helpers \u2014 prompt with form, yes/no confirm.\n *\n * Built on the canonical `ManagedWindow` (with `modal: true`) so dialogs\n * go through the same window system as everything else: same chrome,\n * same backdrop, same Esc-to-close behaviour, same z-stacking.\n *\n * Returns Promises that resolve with the form data (or null on cancel)\n * / a boolean (confirm).\n *\n * Field schema:\n * { name, label, type='text', default, step?, options?, required?,\n * placeholder?, hint?, validator?, rows?, create?, pick?, displayFor? }\n *\n * - type: 'text' | 'number' | 'password' | 'select' | 'textarea' |\n * 'checkbox' | 'picker'\n * - hint: small dim help line rendered under the field\n * - validator: fn(value, allValues) => string | null (inline error)\n * - rows: number of textarea rows (only used for type='textarea')\n *\n * Section dividers \u2014 entries of the form `{ section: 'Label' }` are\n * rendered as a small uppercase heading that groups the fields below\n * them. Useful for longer forms. A section that also carries a `name`\n * can be hidden and shown by `onFieldChange`'s `setVisible`.\n *\n * Picker \u2014 a value the user LOOKS UP rather than types (an id chosen in a\n * search dialog, say). The row shows a read-only box and a search button;\n * the button (or a click on the box) awaits `pick(currentValue)`, which\n * resolves to `{ value, label }`, a bare value, or null to leave the field\n * alone. The value is what is submitted, the label is what is shown, and\n * `displayFor(value)` gives the label for the value the form opens with:\n *\n * {\n * name: 'parent', label: 'Parent', type: 'picker',\n * pick: async (current) => ({ value: 'EPIC-0001', label: 'EPIC-0001 \u2014 Auth' }),\n * displayFor: (value) => titleOf(value),\n * }\n *\n * Dependent-property pick-or-create \u2014 a `select` field that carries a\n * `create` spec renders as a **type-ahead combobox** instead of a\n * dropdown: the user picks an existing option OR types a new value.\n * On submit, a value that isn't an existing option is created via\n * `create.onCreate(value)` before the form resolves:\n *\n * {\n * name: 'asset_kind', label: 'Asset kind', type: 'select',\n * options: [...], // existing values (autocomplete)\n * create: {\n * onCreate: async (typed) => finalValue | null, // make the entity\n * hint: 'new asset kind', // shown when the typed value is new\n * },\n * }\n *\n * `create: true` is the degenerate case \u2014 \"pick one of these, or type\n * anything\" \u2014 where the typed value IS the value and there is no entity to\n * create, so there is no \"will create\" hint either. A bare `true` used to\n * reach `f.create.onCreate(v)` and fail the submit with \"onCreate is not a\n * function\", blaming the user for typing their own name.\n */\n\nimport { ManagedWindow } from '../../ui/components/managed_window.js';\n\n/**\n * C25. WHERE A DIALOG OPENS, when the application spans more than one window.\n *\n * Every modal here is a `ManagedWindow` with no `container`, so it mounts into\n * `document.body` \u2014 and \"document\" is the document this MODULE was loaded in.\n * A consumer that opens a second browser window with `window.open` and builds\n * its DOM from the opener's realm (which is how a pop-out grid works: same\n * modules, same JS context, a different document) therefore gets its dialogs in\n * the window it popped OUT of. The user presses \"Add column\" on their second\n * monitor 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 */\nlet _modalHost = null;\nexport function setModalHost(el) { _modalHost = el || null; }\nexport function modalHost() { return _modalHost; }\n\nlet _modalSeq = 1;\n\n/**\n * @param {object} o\n * @param {Function?} o.onFieldChange `(name, value, api)` \u2014 called after any\n * field's `change`, and ONCE on open with `name === null` so the form starts\n * consistent with its defaults. `api` is\n * `{ setVisible(name, on), get(name), set(name, value) }`, addressing fields\n * and named sections by name. `get` reads the control as it stands (a\n * checkbox as a boolean, everything else as its string); `set` writes it and\n * does NOT call `onFieldChange` back, so a hook that fills one field from\n * another cannot loop.\n *\n * It exists for the form whose Type select spans two record kinds and must\n * show the fields of the kind actually chosen. Rebuilding the form on every\n * change would lose what the user had already typed, so every field is\n * built once and the irrelevant rows are hidden. A hidden row keeps its\n * value and is still submitted \u2014 the caller drops what its chosen kind does\n * not use \u2014 but it is not ASKED about: its `required` and its `validator`\n * stand down until it is shown again, because a constraint on a field the\n * user cannot see is a submit button that silently does nothing.\n *\n * Absent \u2014 every caller before it \u2014 nothing here runs.\n */\nexport function openForm({ title, fields = [], defaults = {}, submitLabel = 'OK', onFieldChange = null } = {}) {\n return new Promise((resolve) => {\n // Build the form body \u2014 same markup as the legacy overlay,\n // minus the outer .ea-modal wrapper (ManagedWindow owns the\n // chrome now).\n const body = document.createElement('div');\n body.className = 'twm-modal__body-host';\n body.innerHTML = `\n <form class=\"twm-modal__form\">\n ${fields.map((f) => _renderEntry(f, defaults[f.name] ?? f.default)).join('')}\n <div class=\"twm-modal__error\" data-role=\"form-error\" hidden></div>\n <div class=\"twm-modal__actions\">\n <button type=\"button\" class=\"twm-btn\" data-action=\"cancel\">Cancel</button>\n <button type=\"submit\" class=\"twm-btn twm-btn--primary\">${submitLabel}</button>\n </div>\n </form>\n `;\n\n const win = new ManagedWindow({\n container: _modalHost,\n id: `twm-modal-${_modalSeq++}`,\n title: title || 'Dialog',\n icon: 'edit_note',\n content: body,\n modal: true,\n canMinimize: false,\n canMaximize: false,\n canResize: false,\n canDrag: true,\n defaultWidth: _estimateWidth(fields),\n defaultHeight: _estimateHeight(fields),\n onClose: () => { if (!_resolved) { _resolved = true; resolve(null); } },\n });\n win.show();\n\n let _resolved = false;\n const close = (result) => {\n if (_resolved) return;\n _resolved = true;\n resolve(result);\n try { win.close({ force: true }); } catch {}\n };\n\n const errEl = body.querySelector('[data-role=\"form-error\"]');\n const showError = (msg) => {\n if (!errEl) return;\n errEl.textContent = msg;\n errEl.hidden = !msg;\n };\n body.querySelector('[data-action=\"cancel\"]')?.addEventListener('click', () => close(null));\n\n const formFields = fields.filter((f) => !f.section);\n for (const f of formFields) {\n if (f.type === 'select' && f.create) _wireComboboxHint(body, f);\n if (f.type === 'picker') _wirePicker(body, f);\n }\n const rowHidden = (name) => !!body.querySelector(`[data-field-row=\"${name}\"]`)?.hidden;\n\n const clearFieldError = (name) => {\n const row = body.querySelector(`[data-field-row=\"${name}\"]`);\n const errEl = body.querySelector(`[data-field-error=\"${name}\"]`);\n if (row) row.classList.remove('twm-is-invalid');\n if (errEl) { errEl.textContent = ''; errEl.hidden = true; }\n };\n const setFieldError = (name, msg) => {\n const row = body.querySelector(`[data-field-row=\"${name}\"]`);\n const errEl = body.querySelector(`[data-field-error=\"${name}\"]`);\n if (row) row.classList.add('twm-is-invalid');\n if (errEl) { errEl.textContent = msg; errEl.hidden = false; }\n };\n // Clear per-field errors on input so the user sees the row recover.\n for (const f of formFields) {\n const el = body.querySelector(`[name=\"${f.name}\"]`);\n el?.addEventListener('input', () => clearFieldError(f.name));\n el?.addEventListener('change', () => clearFieldError(f.name));\n }\n\n // Adaptive fields \u2014 see `onFieldChange` above.\n if (typeof onFieldChange === 'function') {\n const fieldOf = (name) => formFields.find((f) => f.name === name) || null;\n const api = {\n setVisible(name, on) {\n const row = body.querySelector(`[data-field-row=\"${name}\"]`);\n if (!row) return;\n // The attribute alone loses to `.twm-modal__row { display:\n // grid }` in any stylesheet that has not got the `[hidden]`\n // rule flexdesk.css carries, so the inline style says it too.\n row.hidden = !on;\n row.style.display = on ? '' : 'none';\n clearFieldError(name);\n // A required control nobody can see blocks the submit with\n // no message the user can find. Remembered on the control,\n // so showing the row gives back exactly what it had.\n for (const el of row.querySelectorAll('input, select, textarea')) {\n if (!on && el.required) { el.required = false; el.dataset.twmRequired = '1'; }\n else if (on && el.dataset.twmRequired) { el.required = true; delete el.dataset.twmRequired; }\n }\n },\n get(name) {\n const el = body.querySelector(`[name=\"${name}\"]`);\n if (!el) return undefined;\n return el.type === 'checkbox' ? !!el.checked : el.value;\n },\n set(name, value) {\n const el = body.querySelector(`[name=\"${name}\"]`);\n if (!el) return;\n if (el.type === 'checkbox') { el.checked = !!value; return; }\n el.value = value == null ? '' : String(value);\n const f = fieldOf(name);\n if (f?.type === 'picker') {\n const display = body.querySelector(`[data-picker-display=\"${name}\"]`);\n if (display) display.value = _pickerLabel(f, el.value);\n }\n },\n };\n const fire = (name) => {\n try { onFieldChange(name, name == null ? undefined : api.get(name), api); }\n catch (err) { console.error('[modal] onFieldChange threw', err); }\n };\n for (const f of formFields) {\n const el = body.querySelector(`[name=\"${f.name}\"]`);\n el?.addEventListener('change', () => fire(f.name));\n }\n // Once on open, so a preselected value is reflected immediately\n // rather than only after the user touches something.\n fire(null);\n }\n\n body.querySelector('form').addEventListener('submit', async (e) => {\n e.preventDefault();\n const submitBtn = body.querySelector('button[type=\"submit\"]');\n showError('');\n const data = {};\n for (const f of formFields) {\n const el = body.querySelector(`[name=\"${f.name}\"]`);\n if (!el) continue;\n let v;\n if (f.type === 'checkbox') {\n v = !!el.checked;\n } else if (f.type === 'number') {\n v = (el.value === '' ? null : Number(el.value));\n } else {\n v = el.value;\n }\n if (f.type === 'select' && f.create) {\n const spec = _createSpec(f);\n const known = new Set(_optionValues(f.options));\n if (v && !known.has(v)) {\n if (submitBtn) submitBtn.disabled = true;\n let created;\n let failMsg = null;\n try { created = await spec.onCreate(v); }\n catch (err) { created = null; failMsg = err?.message || String(err); }\n if (submitBtn) submitBtn.disabled = false;\n if (created == null) {\n const hint = spec.hint || f.label.toLowerCase();\n showError(failMsg\n ? `Couldn't create ${hint} \"${v}\": ${failMsg}`\n : `Couldn't create ${hint} \"${v}\". See the toast for details.`);\n return;\n }\n v = created;\n }\n }\n data[f.name] = v;\n }\n // Per-field validators run after collection so they can see\n // peer values (e.g. confirm-password matches password).\n let firstBadName = null;\n for (const f of formFields) {\n clearFieldError(f.name);\n if (rowHidden(f.name)) continue;\n let msg = null;\n // A picker's value lives in a hidden input, which the browser's\n // own `required` check skips \u2014 so it is checked here.\n if (f.type === 'picker' && f.required && !data[f.name]) msg = 'Required.';\n else if (typeof f.validator !== 'function') continue;\n else {\n try { msg = f.validator(data[f.name], data); } catch (err) { msg = err?.message || String(err); }\n }\n if (msg) {\n setFieldError(f.name, msg);\n if (!firstBadName) firstBadName = f.name;\n }\n }\n if (firstBadName) {\n body.querySelector(`[data-picker=\"${firstBadName}\"]`)?.focus();\n body.querySelector(`[name=\"${firstBadName}\"]:not([type=\"hidden\"])`)?.focus();\n return;\n }\n close(data);\n });\n\n // Focus first field on next frame so the managed-window mount\n // settles before we steal focus.\n // Not a picker's hidden value, and not a field in a row that\n // `onFieldChange` hid on open.\n requestAnimationFrame(() => {\n const first = [...body.querySelectorAll('input:not([type=\"hidden\"]), select, textarea')]\n .find((el) => !el.closest('[data-field-row]')?.hidden);\n first?.focus();\n });\n });\n}\n\n\nexport function openConfirm({\n title, message, confirmLabel = 'OK',\n cancelLabel = 'Cancel',\n danger = false, icon = null,\n} = {}) {\n return new Promise((resolve) => {\n const body = document.createElement('div');\n body.className = 'twm-modal__body-host';\n const cls = danger ? 'twm-btn twm-btn--danger' : 'twm-btn twm-btn--primary';\n body.innerHTML = `\n <div class=\"twm-modal__body\">${message}</div>\n <div class=\"twm-modal__actions\">\n <button type=\"button\" class=\"twm-btn\" data-action=\"cancel\">${cancelLabel}</button>\n <button type=\"button\" class=\"${cls}\" data-action=\"confirm\">${confirmLabel}</button>\n </div>\n `;\n\n let _resolved = false;\n const win = new ManagedWindow({\n container: _modalHost,\n id: `twm-confirm-${_modalSeq++}`,\n title: title || 'Confirm',\n icon: icon || (danger ? 'warning' : 'help'),\n content: body,\n modal: true,\n canMinimize: false,\n canMaximize: false,\n canResize: false,\n canDrag: true,\n defaultWidth: 380,\n defaultHeight: 180,\n onClose: () => { if (!_resolved) { _resolved = true; resolve(false); } },\n });\n win.show();\n const close = (v) => {\n if (_resolved) return;\n _resolved = true;\n resolve(v);\n try { win.close({ force: true }); } catch {}\n };\n body.querySelector('[data-action=\"cancel\"]').addEventListener('click', () => close(false));\n body.querySelector('[data-action=\"confirm\"]').addEventListener('click', () => close(true));\n requestAnimationFrame(() => {\n body.querySelector(`[data-action=\"${danger ? 'cancel' : 'confirm'}\"]`)?.focus();\n });\n });\n}\n\n\n/**\n * `openModal` \u2014 rich-content modal. The third helper alongside\n * `openForm` (field-based) and `openConfirm` (yes/no).\n *\n * Use when the modal body is arbitrary DOM the caller renders itself\n * \u2014 a chart, a Monaco editor, a table, a diff view. The shell still\n * goes through ManagedWindow so the chrome, focus trap, Esc handling,\n * and z-stacking match every other modal in the app.\n *\n * Schema:\n * - `title` window title (string)\n * - `icon` optional material-symbols-outlined glyph for the\n * title bar; defaults to `info`\n * - `content` HTMLElement appended into the body. Caller owns\n * rendering \u2014 `openModal` doesn't size or style the\n * internal content beyond making it scrollable.\n * - `actions` `[{ label, value, primary?, danger?, icon? }, \u2026]`. If\n * omitted, defaults to a single `[Close]` action that\n * resolves with `null`. The first action with\n * `primary: true` is the rightmost; otherwise the\n * last action wins. Esc / X / backdrop resolve with\n * `null` regardless of what's in `actions`. `icon` is\n * an optional material-symbols-outlined ligature drawn\n * BEFORE the label; when it is given the label lives in\n * a `.twm-btn__label` span, because an icon font's\n * ligature name is itself text and would otherwise be\n * read back as part of the label.\n * - `width`,\n * `height` initial size in px. Defaults: 640 \u00D7 480.\n * - `onMount` optional `(contentEl) => void` invoked once after\n * the content is appended \u2014 handy when the caller\n * needs the bounding rect to size a chart / Monaco /\n * SVG inside the body.\n * - `backdropBlur` backdrop blur radius in px (`0` disables it).\n * - `backdropOpacity` backdrop dim, 0 (clear) \u2026 1 (opaque). Use a low\n * value (with `backdropBlur: 0`) to keep the background\n * legible \u2014 e.g. a live-applied editor where you want to\n * watch the content behind update. Omit both to inherit\n * the default look (blur 2px over a 50% dim).\n * - `maximizable` C29. Draw a maximise button in the title bar.\n * Default `false`, which is byte-for-byte what every\n * existing caller has always got.\n * - `onMaximizeChange` `(maximized, contentEl) => void`, called on each\n * flip. Only useful with `maximizable`.\n *\n * \u2550\u2550 C29. WHY A DIALOG'S MAXIMISE IS THE GEOMETRIC ONE \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n *\n * Everywhere else in this library maximise means BACK TO TILE: the window\n * stops being a window and its content returns to the tile it came out of.\n * That is R14, it is the product owner's ruling \u2014 *\"maximize here means back\n * to tile\"* \u2014 and it holds for the chrome button, the title bar's\n * double-click, an embedder's window menu and the aero-snap top edge alike.\n *\n * A DIALOG HAS NO TILE. It was never lifted out of one; it is modal, it owns\n * the screen until it resolves, and \"back to tile\" names a destination that\n * does not exist for it. So here maximise can only mean the rectangle, and\n * that is not a contradiction of the ruling but its boundary: THE RULING IS\n * ABOUT WINDOWS THAT CAME FROM A TILE. Do not \"fix\" this into a dock \u2014 there\n * is nothing to dock into, and `bringBackWindow` would refuse it anyway.\n *\n * Mechanically that means setting no `onMaximize` at all rather than reaching\n * for `toggleMaximize({claimable: false})`: with nothing claiming the gesture,\n * the ordinary path IS the rectangle, and the escape hatch is for consumers\n * who have claimed it.\n *\n * The dialog stays a dialog while maximised \u2014 `modal: true` is untouched, so\n * the backdrop, the Esc handler and the Tab focus trap (all of which hang off\n * `this.element` and the top-most-window test, not off geometry) go on working\n * exactly as they did.\n *\n * Returns a Promise resolving with the chosen action's `value`, or\n * `null` if the user dismissed the modal.\n */\nexport function openModal({\n title = 'Dialog',\n icon = 'info',\n content = null,\n actions = null,\n width = 640,\n height = 480,\n onMount = null,\n backdropBlur = undefined,\n backdropOpacity = undefined,\n maximizable = false,\n onMaximizeChange = null,\n} = {}) {\n return new Promise((resolve) => {\n const body = document.createElement('div');\n body.className = 'twm-modal__body-host';\n\n const bodyInner = document.createElement('div');\n bodyInner.className = 'twm-modal__body twm-modal__body--rich';\n if (content instanceof HTMLElement) bodyInner.appendChild(content);\n body.appendChild(bodyInner);\n\n // Actions row \u2014 default = single Close button. Esc / backdrop /\n // X all bypass this and resolve with null.\n const acts = Array.isArray(actions) && actions.length > 0\n ? actions\n : [{ label: 'Close', value: null }];\n const actionsEl = document.createElement('div');\n actionsEl.className = 'twm-modal__actions';\n acts.forEach((a, i) => {\n const btn = document.createElement('button');\n btn.type = 'button';\n let cls = 'twm-btn';\n if (a.primary) cls += ' twm-btn--primary';\n if (a.danger) cls += ' twm-btn--danger';\n btn.className = cls;\n // C26. AN ACTION MAY CARRY A GLYPH \u2014 additively, and only when it\n // asks for one. Without `icon` the button is byte-identical to what\n // every existing caller gets today: one text node, so a consumer\n // reading `btn.textContent` to find its own button keeps working.\n //\n // With one, the label moves into its own span so that reading it\n // back is still possible: an icon font's ligature name IS text\n // content (\"save\" renders as a glyph but reads as the word), so a\n // bare `textContent` on a button with an icon would answer\n // \"saveSave\". `.twm-btn__label` is the answer to \"what does this\n // button say\", and `aria-hidden` on the glyph is what keeps a screen\n // reader from saying the ligature name out loud beside the label.\n if (a.icon) {\n const glyph = document.createElement('span');\n glyph.className = 'material-symbols-outlined twm-btn__glyph';\n glyph.textContent = String(a.icon);\n glyph.setAttribute('aria-hidden', 'true');\n const text = document.createElement('span');\n text.className = 'twm-btn__label';\n text.textContent = String(a.label || '');\n btn.append(glyph, text);\n } else {\n btn.textContent = String(a.label || '');\n }\n btn.dataset.actionIdx = String(i);\n // C27. AN ACTION MAY START DISABLED, and be enabled later.\n //\n // Every action here dismisses the dialog and resolves the promise \u2014\n // that is the documented contract, and it is why a caller cannot\n // \"refuse\" a submit: by the time it sees the value, the form and\n // everything typed into it are gone. So a dialog whose primary\n // action is not yet valid has had exactly two options: let the user\n // press it and lose their work to a toast, or reach into this markup\n // from outside, which is the coupling C6 exists to remove.\n //\n // `disabled` plus the controller handed to `onMount` is the third.\n // Absent \u2014 every existing caller \u2014 the button is enabled exactly as\n // before.\n if (a.disabled) btn.disabled = true;\n if (a.value !== undefined && a.value !== null) {\n btn.dataset.actionValue = String(a.value);\n }\n actionsEl.appendChild(btn);\n });\n body.appendChild(actionsEl);\n\n let _resolved = false;\n // DECLARED BEFORE THE WINDOW, INSTALLED AFTER IT. `onClose` below closes\n // over `_dropMaxListener`, and `show()` runs between the two \u2014 so\n // leaving the declaration down where the listener is installed would\n // stretch a temporal dead zone across a call that could come back\n // through `onClose`. The cost of getting that wrong is a ReferenceError\n // inside a dismissal, which is the least debuggable place in this file.\n let _onMax = null;\n const _dropMaxListener = () => {\n if (!_onMax) return;\n window.removeEventListener('managed-window-maximized', _onMax);\n _onMax = null;\n };\n const win = new ManagedWindow({\n container: _modalHost,\n id: `twm-modal-${_modalSeq++}`,\n title,\n icon,\n content: body,\n modal: true,\n backdropBlur,\n backdropOpacity,\n canMinimize: false,\n // C29. OPT-IN, and `false` is still the default \u2014 a confirmation\n // and a two-field form have nothing to do with the extra room, and\n // a button that grows a dialog nobody wanted grown is noise in the\n // one place a user looks for the X. `onMaximize` is deliberately\n // NOT set: see the C29 note in the header \u2014 with nothing claiming\n // the gesture, `toggleMaximize` runs the rectangle, which is the\n // only thing maximise can mean for a window with no tile.\n canMaximize: !!maximizable,\n canResize: true,\n canDrag: true,\n defaultWidth: width,\n defaultHeight: height,\n // Esc, the X and the backdrop all land here without passing through\n // `close`, so the listener is dropped here as well as there \u2014 the\n // three dismissals a user reaches for most are exactly the ones\n // that would otherwise leak it.\n onClose: () => {\n _dropMaxListener();\n if (!_resolved) { _resolved = true; resolve(null); }\n },\n });\n win.show();\n\n // C29. THE STATE, FOR A CALLER WHOSE LAYOUT DEPENDS ON IT.\n //\n // `managed-window-maximized` (C16) is the framework's own edge and it\n // is global, so it is filtered on the id \u2014 the alternative, wrapping\n // `win.toggleMaximize`, would miss `_restoreState`'s replay of a\n // persisted maximised window and any consumer calling the method\n // directly. Removed on close: a dialog is short-lived and twenty of\n // them over a session leaving listeners behind is a leak with no\n // symptom until the twenty-first.\n //\n // The CSS half needs no listener at all \u2014 `ManagedWindow` toggles\n // `.twm-managed-window--maximized` on its own element, which is an\n // ancestor of everything in here, so a stylesheet can respond without\n // any of this. The callback is for the layout a stylesheet cannot\n // reach: content whose height was fixed by the CALLER's own rules.\n if (maximizable && typeof onMaximizeChange === 'function') {\n _onMax = (e) => {\n if (e.detail?.id !== win.id) return;\n try { onMaximizeChange(!!e.detail.maximized, bodyInner); }\n catch (err) { console.warn('[modal] onMaximizeChange threw', err); }\n };\n window.addEventListener('managed-window-maximized', _onMax);\n }\n\n const close = (value) => {\n if (_resolved) return;\n _resolved = true;\n _dropMaxListener();\n resolve(value);\n try { win.close({ force: true }); } catch {}\n };\n actionsEl.addEventListener('click', (e) => {\n const btn = e.target.closest('[data-action-idx]');\n if (!btn) return;\n const idx = Number(btn.dataset.actionIdx);\n const a = acts[idx];\n if (!a) return;\n // `keepOpen` actions are side-effects (Copy, Apply\n // Without Close, etc.): if the action's `onClick` returns\n // anything (or just runs), the modal stays open. Use this\n // for Copy buttons and the like.\n if (a.keepOpen) {\n // `onClick` receives `close` so the handler can\n // validate, do an async side-effect, and only then\n // dismiss the modal with whatever value it chooses.\n // Returning early without calling `close` leaves the\n // modal open (which is the whole point of keepOpen).\n try { a.onClick?.(close); } catch (err) { console.warn(err); }\n return;\n }\n close(a.value);\n });\n\n /**\n * C27. What `onMount` is handed alongside the body, so a form can keep\n * its own submit honest without knowing this file's markup.\n *\n * Addressed by an action's VALUE rather than its index: an index is a\n * fact about the order the caller happened to list them in, and a caller\n * that inserted a \"Save and add another\" in the middle would silently\n * start disabling Cancel.\n */\n const controls = {\n // NOT `CSS.escape`. It is a browser global that jsdom does not\n // provide, and this file is mounted under jsdom by four render\n // tests \u2014 so reaching for it turns \"the dialog is valid\" into a\n // ReferenceError in every one of them, and into nothing at all in a\n // headless consumer. An attribute selector needs `\"` and `\\`\n // escaped and nothing else.\n actionButton: (value) => actionsEl.querySelector(\n `[data-action-value=\"${String(value).replace(/[\"\\\\]/g, '\\\\$&')}\"]`),\n setActionEnabled(value, enabled) {\n const btn = controls.actionButton(value);\n if (btn) btn.disabled = !enabled;\n return !!btn;\n },\n };\n\n // onMount runs after the next frame so layout has settled and\n // the inner content has a real bounding box for sizing.\n if (typeof onMount === 'function') {\n requestAnimationFrame(() => {\n try { onMount(bodyInner, controls); } catch (err) { console.warn(err); }\n });\n }\n // C28. THE FIRST FIELD, IF THERE IS ONE. Otherwise the primary button.\n //\n // A dialog that asks for a name should let you type it: *\"on any 'new'\n // dialog I would like to have the first input focused by default, so\n // that I can directly start typing.\"* Focusing the action button first\n // means every new table, project and column begins with a click into a\n // field that was the only place the caret could sensibly have been.\n //\n // The button remains the fallback, which is what keeps Enter useful on\n // a dialog that asks nothing \u2014 a confirmation has no field, and there\n // the primary action IS the answer.\n //\n // DISABLED CONTROLS ARE SKIPPED, and so is anything `readonly`: a form\n // whose first control is a read-only statement panel (C27's disabled\n // primary is the sibling case) would swallow the caret into a box that\n // cannot take it.\n requestAnimationFrame(() => {\n const field = bodyInner.querySelector(\n 'input:not([type=\"hidden\"]):not([disabled]):not([readonly]),'\n + ' textarea:not([disabled]):not([readonly]),'\n + ' select:not([disabled])');\n if (field) {\n field.focus();\n // The caret at the END of whatever is already there, not\n // selecting it: an edit dialog opens on a value the user means\n // to amend, and a selected value is one keystroke from gone.\n try { field.setSelectionRange?.(field.value.length, field.value.length); }\n catch { /* a `select`, or an input type with no selection */ }\n return;\n }\n const idx = acts.findIndex((a) => a.primary);\n const which = idx >= 0 ? idx : acts.length - 1;\n actionsEl.querySelector(`[data-action-idx=\"${which}\"]`)?.focus();\n });\n });\n}\n\n\n// \u2500\u2500\u2500 helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfunction _estimateHeight(fields) {\n // Header (~36) + actions row (~46) + section dividers (~32 each)\n // + per-field rows (~46 text, ~140 textarea, ~32 checkbox)\n // + body padding. Clamped to 200..80vh.\n let body = 0;\n for (const f of fields) {\n if (f.section) body += 32;\n else if (f.type === 'textarea') {\n const rows = Number(f.rows) || 4;\n body += 32 + rows * 18;\n if (f.hint) body += 16;\n }\n else if (f.type === 'checkbox') body += 32;\n else { body += 46; if (f.hint) body += 16; }\n }\n const base = 36 + 46 + 28 + body;\n const max = Math.floor(window.innerHeight * 0.8);\n return Math.max(200, Math.min(base, max));\n}\n\nfunction _estimateWidth(fields) {\n // Forms with a textarea or section divider get the wider layout so\n // multi-line content + grouped fields breathe; otherwise 480 px is\n // enough for label + single-line input pairs.\n const wide = fields.some((f) => f && (f.type === 'textarea' || f.section));\n return wide ? 640 : 480;\n}\n\nfunction _optionValues(options) {\n return (options || []).map((o) => (typeof o === 'object') ? o.value : o);\n}\n\n/** Normalise a `create` spec. `true` means the typed value is accepted as-is. */\nfunction _createSpec(f) {\n if (!f || !f.create) return null;\n if (f.create === true) return { onCreate: async (v) => v, hint: null };\n return {\n onCreate: typeof f.create.onCreate === 'function' ? f.create.onCreate : async (v) => v,\n hint: f.create.hint ?? null,\n };\n}\n\n/** What a picker shows for `value`: the caller's `displayFor`, else the value. */\nfunction _pickerLabel(f, value) {\n if (typeof f.displayFor !== 'function') return value == null ? '' : String(value);\n try { return String(f.displayFor(value) ?? ''); }\n catch (err) { console.warn('[modal] displayFor threw', err); return String(value ?? ''); }\n}\n\n/** Hook a picker field's search button up to the caller's `pick()`, and keep\n * the hidden value and the visible display in step. */\nfunction _wirePicker(host, f) {\n const hidden = host.querySelector(`input[type=\"hidden\"][name=\"${f.name}\"]`);\n const display = host.querySelector(`[data-picker-display=\"${f.name}\"]`);\n const btn = host.querySelector(`[data-picker=\"${f.name}\"]`);\n if (!hidden || !display || !btn || typeof f.pick !== 'function') return;\n\n let busy = false;\n const open = async () => {\n if (busy) return; // one search at a time\n busy = true;\n let picked;\n try { picked = await f.pick(hidden.value); }\n catch (err) { console.warn('[modal] pick threw', err); picked = null; }\n finally { busy = false; }\n if (picked == null) return; // cancelled \u2014 leave it alone\n const isPair = typeof picked === 'object';\n hidden.value = String((isPair ? picked.value : picked) ?? '');\n display.value = isPair && picked.label != null\n ? String(picked.label) : _pickerLabel(f, hidden.value);\n // A hidden input fires no `change` of its own; this is the one the\n // field-error clear and `onFieldChange` listen for.\n hidden.dispatchEvent(new Event('change', { bubbles: true }));\n };\n btn.addEventListener('click', open);\n // The read-only box is the obvious thing to click; make it work too.\n display.addEventListener('click', open);\n}\n\n\n/** Show / hide the \"\u21B3 will create \u2026\" hint as the user types into a\n * create-capable combobox. */\nfunction _wireComboboxHint(host, f) {\n const input = host.querySelector(`input[name=\"${f.name}\"]`);\n const hintEl = host.querySelector(`.twm-combobox__hint[data-for=\"${f.name}\"]`);\n if (!input || !hintEl) return;\n // With `create: true` the typed value simply IS the value \u2014 there is no\n // entity being made, so promising to \"create\" one is a lie that reads as\n // a warning.\n if (f.create === true) return;\n const known = new Set(_optionValues(f.options));\n const label = _createSpec(f).hint || 'new entry';\n const update = () => {\n const v = input.value.trim();\n if (v && !known.has(v)) {\n hintEl.textContent = `\u21B3 will create ${label} \u201C${v}\u201D`;\n hintEl.hidden = false;\n } else {\n hintEl.hidden = true;\n }\n };\n input.addEventListener('input', update);\n update();\n}\n\n\nfunction _renderEntry(f, value) {\n if (f && f.section != null) {\n // A named section is addressable by `setVisible`, so a heading can be\n // hidden along with the group it introduces.\n const attr = f.name ? ` data-field-row=\"${f.name}\"` : '';\n return `<div class=\"twm-modal__section\"${attr}>${_esc(f.section)}</div>`;\n }\n return _renderField(f, value);\n}\n\nfunction _renderField(f, value) {\n const required = f.required ? 'required' : '';\n const rowMod = (f.type === 'textarea') ? ' twm-modal__row--multiline' : '';\n const hintHtml = f.hint\n ? `<div class=\"twm-modal__hint--field\">${_esc(f.hint)}</div>` : '';\n const errHtml = `<div class=\"twm-modal__field-error\" data-field-error=\"${f.name}\" hidden></div>`;\n const wrap = (inner) => `\n <label class=\"twm-modal__row${rowMod}\" data-field-row=\"${f.name}\">\n <span>${_esc(f.label || '')}</span>\n ${inner}\n ${hintHtml}\n ${errHtml}\n </label>\n `;\n\n if (f.type === 'checkbox') {\n const checked = (value === true || value === 'true') ? 'checked' : '';\n return `\n <label class=\"twm-modal__row\" data-field-row=\"${f.name}\">\n <span></span>\n <span class=\"twm-modal__check\">\n <input name=\"${f.name}\" type=\"checkbox\" ${checked}>\n <span>${_esc(f.label || '')}</span>\n </span>\n ${hintHtml}\n ${errHtml}\n </label>\n `;\n }\n\n if (f.type === 'textarea') {\n const v = value == null ? '' : String(value);\n const rows = f.rows != null ? `rows=\"${Number(f.rows) || 4}\"` : 'rows=\"4\"';\n const placeholder = f.placeholder ? `placeholder=\"${_esc(f.placeholder)}\"` : '';\n return wrap(`<textarea name=\"${f.name}\" ${rows} ${placeholder} ${required}>${_esc(v)}</textarea>`);\n }\n\n const v = value == null ? '' : String(value);\n if (f.type === 'picker') {\n // The display text and the stored value are different things (an id\n // vs. \"EPIC-0001 \u2014 Auth rewrite\"), so the value rides in a hidden\n // input \u2014 the one the submit reads by name \u2014 and the visible box is\n // read-only.\n const placeholder = f.placeholder ? `placeholder=\"${_esc(f.placeholder)}\"` : '';\n return wrap(`\n <div class=\"twm-picker\">\n <input type=\"hidden\" name=\"${f.name}\" value=\"${_esc(v)}\">\n <input class=\"twm-picker__display\" data-picker-display=\"${f.name}\" type=\"text\"\n value=\"${_esc(_pickerLabel(f, v))}\" ${placeholder} readonly>\n <button type=\"button\" class=\"twm-btn twm-picker__btn\" data-picker=\"${f.name}\"\n aria-label=\"Search\" title=\"Search\">\n <span class=\"material-symbols-outlined\" aria-hidden=\"true\">search</span>\n </button>\n </div>\n `);\n }\n if (f.type === 'select' && f.create) {\n const dlId = `ea-dl-${f.name}-${_modalSeq}`;\n const opts = _optionValues(f.options)\n .map((ov) => `<option value=\"${_esc(ov)}\"></option>`).join('');\n const placeholder = f.placeholder\n ? `placeholder=\"${_esc(f.placeholder)}\"` : 'placeholder=\"type to pick or create\u2026\"';\n return wrap(`\n <div class=\"twm-combobox\">\n <input name=\"${f.name}\" type=\"text\" list=\"${dlId}\"\n value=\"${_esc(v)}\" ${placeholder} ${required}\n autocomplete=\"off\">\n <datalist id=\"${dlId}\">${opts}</datalist>\n <span class=\"twm-combobox__hint\" data-for=\"${f.name}\" hidden></span>\n </div>\n `);\n }\n if (f.type === 'select') {\n const opts = (f.options || []).map((o) => {\n const ov = (typeof o === 'object') ? o.value : o;\n const ol = (typeof o === 'object') ? o.label : o;\n return `<option value=\"${_esc(ov)}\" ${ov === v ? 'selected' : ''}>${_esc(ol)}</option>`;\n }).join('');\n return wrap(`<select name=\"${f.name}\" ${required}>${opts}</select>`);\n }\n const step = f.step != null ? `step=\"${f.step}\"` : '';\n const placeholder = f.placeholder ? `placeholder=\"${_esc(f.placeholder)}\"` : '';\n const type = (f.type === 'number' || f.type === 'password') ? f.type : 'text';\n return wrap(`<input name=\"${f.name}\" type=\"${type}\" value=\"${_esc(v)}\" ${step} ${placeholder} ${required}>`);\n}\n\nfunction _esc(s) {\n return String(s == null ? '' : s)\n .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;');\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["errEl", "v", "placeholder"]
7
+ }
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  DataTable,
3
3
  createRafResizeObserver
4
- } from "./chunk-JLCJWCRS.js";
4
+ } from "./chunk-6CGH73BC.js";
5
5
  import {
6
6
  ManagedWindow
7
7
  } from "./chunk-6JSOVNID.js";
@@ -2370,4 +2370,4 @@ export {
2370
2370
  openPlotPopoutWindow,
2371
2371
  openRawTracesWindow
2372
2372
  };
2373
- //# sourceMappingURL=chunk-WKTYKXWF.js.map
2373
+ //# sourceMappingURL=chunk-EHHTKD7B.js.map
package/dist/flexdesk.css CHANGED
@@ -279,6 +279,22 @@
279
279
  flex-direction: column;
280
280
  gap: var(--space-lg);
281
281
  }
282
+
283
+ /* NotificationCenter `overflow: 'collapse'`: the toasts past maxVisible, as one. */
284
+ .twm-notification--summary {
285
+ display: block;
286
+ width: 100%;
287
+ padding: 6px 12px;
288
+ font: inherit;
289
+ font-size: 12px;
290
+ text-align: left;
291
+ color: var(--text-secondary, #9a9a9a);
292
+ background: var(--surface-2, #2d2d2d);
293
+ border: 1px dashed var(--border-strong, #383838);
294
+ border-radius: var(--radius-md, 6px);
295
+ cursor: pointer;
296
+ }
297
+ .twm-notification--summary:hover { color: var(--text-primary, #e0e0e0); }
282
298
  .twm-notification__header {
283
299
  display: flex;
284
300
  align-items: center;
@@ -5065,6 +5081,36 @@ button.twm-about-dialog__btn--close:focus {
5065
5081
  color: var(--ea-accent);
5066
5082
  font-style: italic;
5067
5083
  }
5084
+ /* Picker = a read-only display of the looked-up value + its search button.
5085
+ * The display takes the field-well styling of the text inputs above. */
5086
+ .twm-managed-window--modal .twm-picker {
5087
+ display: flex;
5088
+ align-items: center;
5089
+ gap: 4px;
5090
+ min-width: 0;
5091
+ }
5092
+ .twm-managed-window--modal .twm-picker .twm-picker__display {
5093
+ flex: 1 1 auto;
5094
+ min-width: 0;
5095
+ cursor: pointer;
5096
+ }
5097
+ .twm-managed-window--modal .twm-picker__btn {
5098
+ flex: 0 0 auto;
5099
+ min-height: 22px;
5100
+ padding: 0 6px;
5101
+ border-radius: 0;
5102
+ }
5103
+ .twm-managed-window--modal .twm-picker__btn .material-symbols-outlined {
5104
+ font-size: 15px;
5105
+ }
5106
+ /* A row (or named section) hidden by openForm's `setVisible`. `[hidden]`
5107
+ * carries only the UA sheet's `display: none`, which the grid and flex rules
5108
+ * on `.twm-modal__row` / `.twm-modal__section` beat — so a hidden row stayed
5109
+ * on screen. */
5110
+ .twm-modal__row[hidden],
5111
+ .twm-modal__section[hidden] {
5112
+ display: none !important;
5113
+ }
5068
5114
  /* Action strip — true footer band, flush against the modal edges,
5069
5115
  * darker chrome bg, top border + 1px highlight above. Right-aligned
5070
5116
  * buttons, primary stays rightmost. */
@@ -8490,7 +8536,7 @@ tr:hover .twm-row-action { opacity: 1; }
8490
8536
  /* ── the popover both pickers are drawn in ──────────────────────────── */
8491
8537
  .twm-flow-popover {
8492
8538
  position: fixed;
8493
- z-index: var(--z-popover);
8539
+ z-index: var(--z-over-windows, 7100); /* over the managed window it opened from */
8494
8540
  display: flex;
8495
8541
  flex-direction: column;
8496
8542
  box-sizing: border-box;
@@ -9817,7 +9863,7 @@ tr:hover .twm-row-action { opacity: 1; }
9817
9863
  /* the grip drag: a ghost under the pointer, and a drop line naming the place */
9818
9864
  .twm-flow-outline__ghost {
9819
9865
  position: fixed;
9820
- z-index: var(--z-popover);
9866
+ z-index: var(--z-over-windows, 7100); /* over the managed window it opened from */
9821
9867
  box-sizing: border-box;
9822
9868
  display: flex;
9823
9869
  align-items: center;
package/dist/flow.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  modalHost
3
- } from "./chunk-6NG7YVBG.js";
3
+ } from "./chunk-6EZPPN4E.js";
4
4
  import {
5
5
  DataTable
6
- } from "./chunk-JLCJWCRS.js";
6
+ } from "./chunk-6CGH73BC.js";
7
7
  import "./chunk-6JSOVNID.js";
8
8
  import "./chunk-FL5KFNQH.js";
9
9
  import {
package/dist/tiles.js CHANGED
@@ -16,8 +16,8 @@ import {
16
16
  } from "./chunk-O5OHMWBB.js";
17
17
  import {
18
18
  openRawTracesWindow
19
- } from "./chunk-WKTYKXWF.js";
20
- import "./chunk-JLCJWCRS.js";
19
+ } from "./chunk-EHHTKD7B.js";
20
+ import "./chunk-6CGH73BC.js";
21
21
  import "./chunk-6JSOVNID.js";
22
22
  import "./chunk-FL5KFNQH.js";
23
23
  import {
package/dist/tokens.css CHANGED
@@ -115,6 +115,10 @@
115
115
  --z-popover: 200;
116
116
  --z-modal: 1000;
117
117
  --z-toast: 10000;
118
+ /* What opens FROM a managed window and must draw over it: managed windows
119
+ stack from 6000 to 6999 (managed_window.js), so a popover at
120
+ --z-popover would open behind the window that opened it. */
121
+ --z-over-windows: 7100;
118
122
 
119
123
  /* ── Transitions ──────────────────────────────────────────── */
120
124
  --transition-fast: 120ms ease;
package/dist/widgets.js CHANGED
@@ -9,14 +9,14 @@ import {
9
9
  hideContextMenu,
10
10
  setHelpProvider,
11
11
  showContextMenu
12
- } from "./chunk-H4LT43RS.js";
12
+ } from "./chunk-5QPGYHBN.js";
13
13
  import {
14
14
  modalHost,
15
15
  openConfirm,
16
16
  openForm,
17
17
  openModal,
18
18
  setModalHost
19
- } from "./chunk-6NG7YVBG.js";
19
+ } from "./chunk-6EZPPN4E.js";
20
20
  import {
21
21
  ActionDropdown
22
22
  } from "./chunk-O5OHMWBB.js";
@@ -36,7 +36,7 @@ import {
36
36
  parseDateValue,
37
37
  parseDuration,
38
38
  resolvePaging
39
- } from "./chunk-JLCJWCRS.js";
39
+ } from "./chunk-6CGH73BC.js";
40
40
  import {
41
41
  ManagedWindow,
42
42
  ROW_CONTROL_SELECTOR,
@@ -5073,10 +5073,20 @@ var NotificationCenter = class _NotificationCenter extends ComponentBase {
5073
5073
  maxVisible = 3,
5074
5074
  historyLimit = 50,
5075
5075
  defaultDurationMs = 6e3,
5076
- logLevels = DEFAULT_LOG_LEVELS
5076
+ logLevels = DEFAULT_LOG_LEVELS,
5077
+ // What happens past `maxVisible`. 'drop' (the default, as before 0.11):
5078
+ // the oldest toast leaves. 'collapse': the toasts on screen stay, the
5079
+ // rest wait behind one "+N more" toast, and each comes in as one closes
5080
+ // — a sticky alert nobody has read is never pushed off unseen.
5081
+ overflow = "drop",
5082
+ // Called with the held-back toasts when the "+N more" toast is clicked.
5083
+ summaryAction = null
5077
5084
  } = {}) {
5078
5085
  super({ eventBus, logger });
5079
5086
  this.maxVisible = maxVisible;
5087
+ this.overflow = overflow === "collapse" ? "collapse" : "drop";
5088
+ this.summaryAction = typeof summaryAction === "function" ? summaryAction : null;
5089
+ this.held = [];
5080
5090
  this.historyLimit = historyLimit;
5081
5091
  this.defaultDurationMs = defaultDurationMs;
5082
5092
  this.logLevels = new Set(logLevels ?? DEFAULT_LOG_LEVELS);
@@ -5143,6 +5153,13 @@ var NotificationCenter = class _NotificationCenter extends ComponentBase {
5143
5153
  show(notification) {
5144
5154
  const normalized = this.#normalizeNotification(notification);
5145
5155
  this.#consoleLog(normalized);
5156
+ if (this.overflow === "collapse" && this.notifications.length >= this.maxVisible) {
5157
+ this.held.push(normalized);
5158
+ this.#recordHistory(normalized);
5159
+ this.#render();
5160
+ this.#announce(normalized);
5161
+ return normalized.id;
5162
+ }
5146
5163
  this.notifications.push(normalized);
5147
5164
  if (this.notifications.length > this.maxVisible) {
5148
5165
  const removed = this.notifications.shift();
@@ -5158,11 +5175,21 @@ var NotificationCenter = class _NotificationCenter extends ComponentBase {
5158
5175
  dismiss(notificationId, reason = "dismissed") {
5159
5176
  const idx = this.notifications.findIndex((item) => item.id === notificationId);
5160
5177
  if (idx === -1) {
5161
- return false;
5178
+ const h = this.held.findIndex((item) => item.id === notificationId);
5179
+ if (h === -1) return false;
5180
+ const [gone] = this.held.splice(h, 1);
5181
+ this.#recordHistory({ ...gone, dismissed: true, reason });
5182
+ this.#render();
5183
+ return true;
5162
5184
  }
5163
5185
  const [removed] = this.notifications.splice(idx, 1);
5164
5186
  this.#stopTimer(notificationId);
5165
5187
  this.#recordHistory({ ...removed, dismissed: true, reason });
5188
+ if (this.held.length && this.notifications.length < this.maxVisible) {
5189
+ const next = this.held.shift();
5190
+ this.notifications.push(next);
5191
+ this.#scheduleAutoDismiss(next);
5192
+ }
5166
5193
  this.#render();
5167
5194
  return true;
5168
5195
  }
@@ -5422,6 +5449,24 @@ var NotificationCenter = class _NotificationCenter extends ComponentBase {
5422
5449
  });
5423
5450
  fragment.appendChild(item);
5424
5451
  });
5452
+ if (this.held.length) {
5453
+ const more = document.createElement("button");
5454
+ more.type = "button";
5455
+ more.className = "notification twm-notification--summary";
5456
+ more.setAttribute("data-notification-summary", String(this.held.length));
5457
+ more.textContent = `+${this.held.length} more`;
5458
+ more.title = this.held.map((h) => h.title || h.message).join("\n");
5459
+ more.addEventListener("click", () => {
5460
+ const held = [...this.held];
5461
+ try {
5462
+ this.summaryAction?.(held);
5463
+ } catch (err) {
5464
+ this.logger?.warn?.("summaryAction failed", err);
5465
+ }
5466
+ this.eventBus?.emit?.("notification:summary", { held });
5467
+ });
5468
+ fragment.appendChild(more);
5469
+ }
5425
5470
  this.listEl.appendChild(fragment);
5426
5471
  }
5427
5472
  #formatTimestamp(timestamp) {