flexdesk 0.6.3 → 0.7.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.
package/dist/widgets.js CHANGED
@@ -26,6 +26,7 @@ import {
26
26
  ISO_DATETIME,
27
27
  PAGING_HARD_LIMIT,
28
28
  PAGING_SOFT_LIMIT,
29
+ createChipsFilter,
29
30
  createRafResizeObserver,
30
31
  formatDate,
31
32
  formatDuration,
@@ -35,7 +36,7 @@ import {
35
36
  parseDateValue,
36
37
  parseDuration,
37
38
  resolvePaging
38
- } from "./chunk-WLASPRGP.js";
39
+ } from "./chunk-JLCJWCRS.js";
39
40
  import {
40
41
  ManagedWindow,
41
42
  ROW_CONTROL_SELECTOR,
@@ -2238,6 +2239,237 @@ function _populate(header, config) {
2238
2239
  }
2239
2240
  }
2240
2241
 
2242
+ // src/ui/components/fact_table.js
2243
+ var SEQ = 0;
2244
+ var isNode = (v) => typeof Node !== "undefined" && v instanceof Node;
2245
+ var FactTable = class {
2246
+ /**
2247
+ * @param {HTMLElement|null} host appended to, if given
2248
+ * @param {FactTableConfig} [config]
2249
+ */
2250
+ constructor(host, config = {}) {
2251
+ this.config = {
2252
+ rows: [],
2253
+ headers: null,
2254
+ compare: false,
2255
+ labelColumn: true,
2256
+ wrapLabels: false,
2257
+ wrap: "anywhere",
2258
+ mono: false,
2259
+ numeric: [],
2260
+ nullDisplay: "",
2261
+ missing: "\u2014",
2262
+ rowClass: null,
2263
+ rowAttrs: null,
2264
+ emptyState: null,
2265
+ label: null,
2266
+ className: null,
2267
+ ...config
2268
+ };
2269
+ this._rows = Array.isArray(this.config.rows) ? this.config.rows.slice() : [];
2270
+ this._id = ++SEQ;
2271
+ const table = document.createElement("table");
2272
+ table.className = "twm-facts" + (this.config.compare ? " twm-facts--compare" : "") + (this.config.labelColumn ? "" : " twm-facts--no-label") + (this.config.wrapLabels ? " twm-facts--wrap-labels" : "") + (this.config.wrap === "words" ? " twm-facts--wrap-words" : this.config.wrap === "none" ? " twm-facts--wrap-none" : "") + (this.config.mono ? " twm-facts--mono" : "") + (this.config.className ? ` ${this.config.className}` : "");
2273
+ if (this.config.label) table.setAttribute("aria-label", this.config.label);
2274
+ this.element = table;
2275
+ this._draw();
2276
+ if (host) host.appendChild(table);
2277
+ }
2278
+ /** Replace the rows and draw them. */
2279
+ setRows(rows) {
2280
+ this._rows = Array.isArray(rows) ? rows.slice() : [];
2281
+ this._draw();
2282
+ }
2283
+ getRows() {
2284
+ return this._rows.slice();
2285
+ }
2286
+ /** The `<tr>` drawn for row `idx`, or null. */
2287
+ getRow(idx) {
2288
+ return this.element.tBodies[0]?.rows[idx]?.__factIndex === idx ? this.element.tBodies[0].rows[idx] : null;
2289
+ }
2290
+ dispose() {
2291
+ this.element.remove();
2292
+ }
2293
+ // ── drawing ──────────────────────────────────────────────────────────
2294
+ /** How many columns the sheet has. */
2295
+ _width() {
2296
+ const { compare, headers } = this.config;
2297
+ if (compare) return 4;
2298
+ let n = 2;
2299
+ if (Array.isArray(headers)) n = Math.max(n, headers.length);
2300
+ for (const r of this._rows) if (Array.isArray(r)) n = Math.max(n, r.length);
2301
+ return n;
2302
+ }
2303
+ _draw() {
2304
+ const table = this.element;
2305
+ table.replaceChildren();
2306
+ const width = this._width();
2307
+ const { headers, compare } = this.config;
2308
+ if (Array.isArray(headers) && headers.length) {
2309
+ const thead = document.createElement("thead");
2310
+ const tr = document.createElement("tr");
2311
+ const names = compare && headers.length === 3 ? [headers[0], headers[1], "", headers[2]] : headers;
2312
+ for (let i = 0; i < width; i++) {
2313
+ const th = document.createElement("th");
2314
+ th.scope = "col";
2315
+ appendContent(th, names[i] ?? "");
2316
+ if (this._numeric(i)) th.classList.add("twm-facts__num");
2317
+ tr.appendChild(th);
2318
+ }
2319
+ thead.appendChild(tr);
2320
+ table.appendChild(thead);
2321
+ }
2322
+ const tbody = document.createElement("tbody");
2323
+ this._rows.forEach((row, idx) => tbody.appendChild(this._row(row, idx, width)));
2324
+ if (!this._rows.length) {
2325
+ let content = this.config.emptyState;
2326
+ if (typeof content === "function") content = content();
2327
+ if (content != null && content !== false && content !== "") {
2328
+ const tr = document.createElement("tr");
2329
+ tr.className = "twm-facts__empty-row";
2330
+ const td = document.createElement("td");
2331
+ td.className = "twm-facts__empty";
2332
+ td.colSpan = width;
2333
+ appendContent(td, content);
2334
+ tr.appendChild(td);
2335
+ tbody.appendChild(tr);
2336
+ }
2337
+ }
2338
+ table.classList.toggle("twm-facts--empty", this._rows.length === 0);
2339
+ table.appendChild(tbody);
2340
+ }
2341
+ _numeric(col) {
2342
+ const n = this.config.numeric;
2343
+ return Array.isArray(n) && n.includes(col);
2344
+ }
2345
+ _row(row, idx, width) {
2346
+ const tr = document.createElement("tr");
2347
+ tr.className = "twm-facts__row";
2348
+ tr.__factIndex = idx;
2349
+ tr.__fact = row;
2350
+ if (Array.isArray(row)) {
2351
+ for (let i = 0; i < width; i++) {
2352
+ const td = this._cell(i === 0 && this.config.labelColumn ? "label" : "value", row[i]);
2353
+ if (this._numeric(i)) td.classList.add("twm-facts__num");
2354
+ if (i === row.length - 1 && row.length < width) {
2355
+ td.colSpan = width - i;
2356
+ tr.appendChild(td);
2357
+ break;
2358
+ }
2359
+ tr.appendChild(td);
2360
+ }
2361
+ } else {
2362
+ const r = row && typeof row === "object" ? row : { label: row };
2363
+ if (r.key !== void 0 && r.key !== null) tr.dataset.key = String(r.key);
2364
+ const label = this._cell(this.config.labelColumn ? "label" : "value", r.label);
2365
+ if (r.labelTitle) label.title = String(r.labelTitle);
2366
+ tr.appendChild(label);
2367
+ const tune = (td) => {
2368
+ if (r.mono) td.classList.add("twm-facts__mono");
2369
+ if (r.numeric) td.classList.add("twm-facts__num");
2370
+ return td;
2371
+ };
2372
+ const hasBefore = Object.prototype.hasOwnProperty.call(r, "before");
2373
+ const hasAfter = Object.prototype.hasOwnProperty.call(r, "after");
2374
+ if (this.config.compare && (hasBefore || hasAfter)) {
2375
+ const status = r.status || statusOf(r, hasBefore, hasAfter);
2376
+ tr.classList.add(`twm-facts__row--${String(status).replace(/[^\w-]/g, "")}`);
2377
+ tr.dataset.status = String(status);
2378
+ const before = tune(hasBefore ? this._cell("value", r.before) : this._missing());
2379
+ before.classList.add("twm-facts__before");
2380
+ const arrow = document.createElement("td");
2381
+ arrow.className = "twm-facts__arrow";
2382
+ arrow.setAttribute("aria-hidden", "true");
2383
+ arrow.textContent = "\u2192";
2384
+ const after = tune(hasAfter ? this._cell("value", r.after) : this._missing());
2385
+ after.classList.add("twm-facts__after");
2386
+ tr.append(before, arrow, after);
2387
+ } else {
2388
+ const value = tune(this._cell("value", r.value));
2389
+ if (width > 2) value.colSpan = width - 1;
2390
+ tr.appendChild(value);
2391
+ }
2392
+ }
2393
+ this._decorate(tr, row, idx);
2394
+ return tr;
2395
+ }
2396
+ _cell(kind, content) {
2397
+ const isLabel = kind === "label";
2398
+ const td = document.createElement(isLabel ? "th" : "td");
2399
+ td.className = isLabel ? "twm-facts__label" : "twm-facts__value";
2400
+ if (isLabel) td.scope = "row";
2401
+ if (content === null || content === void 0) {
2402
+ const said = isLabel ? "" : this.config.nullDisplay;
2403
+ if (said) {
2404
+ td.textContent = String(said);
2405
+ td.classList.add("twm-facts__null");
2406
+ }
2407
+ } else {
2408
+ appendContent(td, content);
2409
+ }
2410
+ return td;
2411
+ }
2412
+ _missing() {
2413
+ const td = document.createElement("td");
2414
+ td.className = "twm-facts__value twm-facts__missing";
2415
+ td.textContent = String(this.config.missing ?? "");
2416
+ return td;
2417
+ }
2418
+ _decorate(tr, row, idx) {
2419
+ const { rowClass, rowAttrs } = this.config;
2420
+ if (typeof rowClass === "function") {
2421
+ const c = rowClass(row, idx);
2422
+ const list = Array.isArray(c) ? c : String(c ?? "").split(/\s+/);
2423
+ for (const name of list) if (name) tr.classList.add(String(name));
2424
+ }
2425
+ if (typeof rowAttrs === "function") {
2426
+ for (const [name, value] of Object.entries(rowAttrs(row, idx) || {})) {
2427
+ if (value == null || value === false) continue;
2428
+ try {
2429
+ tr.setAttribute(name, value === true ? "" : String(value));
2430
+ } catch (err) {
2431
+ console.error(`[FactTable] rowAttrs: bad attribute "${name}"`, err);
2432
+ }
2433
+ }
2434
+ }
2435
+ }
2436
+ };
2437
+ function createFactTable(host, config = {}) {
2438
+ return new FactTable(host, config);
2439
+ }
2440
+ function statusOf(r, hasBefore, hasAfter) {
2441
+ if (!hasBefore) return "added";
2442
+ if (!hasAfter) return "removed";
2443
+ if (isNode(r.before) || isNode(r.after)) return "changed";
2444
+ if (Object.is(r.before, r.after)) return "same";
2445
+ if (r.before && r.after && typeof r.before === "object" && typeof r.after === "object") {
2446
+ try {
2447
+ return JSON.stringify(r.before) === JSON.stringify(r.after) ? "same" : "changed";
2448
+ } catch {
2449
+ return "changed";
2450
+ }
2451
+ }
2452
+ return "changed";
2453
+ }
2454
+ function appendContent(el, content) {
2455
+ if (content === null || content === void 0) return;
2456
+ if (isNode(content)) {
2457
+ el.appendChild(content);
2458
+ return;
2459
+ }
2460
+ let text;
2461
+ if (typeof content === "object") {
2462
+ try {
2463
+ text = JSON.stringify(content, null, 2);
2464
+ } catch {
2465
+ text = String(content);
2466
+ }
2467
+ } else {
2468
+ text = String(content);
2469
+ }
2470
+ el.appendChild(document.createTextNode(text));
2471
+ }
2472
+
2241
2473
  // src/ui/components/gallery_picker.js
2242
2474
  function esc(s) {
2243
2475
  return String(s ?? "").replace(/[&<>"']/g, (c) => ({
@@ -2529,6 +2761,440 @@ var NotificationHistory = class {
2529
2761
  }
2530
2762
  };
2531
2763
 
2764
+ // src/ui/components/record_list.js
2765
+ var OWN_ARROWS = 'input, select, textarea, [contenteditable=""], [contenteditable="true"]';
2766
+ var safeWord = (s) => String(s).replace(/[^\w-]/g, "");
2767
+ var SEQ2 = 0;
2768
+ var RecordList = class {
2769
+ /**
2770
+ * @param {HTMLElement|null} host appended to, if given
2771
+ * @param {RecordListConfig} [config]
2772
+ */
2773
+ constructor(host, config = {}) {
2774
+ this.config = {
2775
+ records: [],
2776
+ describe: null,
2777
+ getKey: null,
2778
+ onActivate: null,
2779
+ activateOn: ["click", "enter"],
2780
+ keyboard: void 0,
2781
+ activeKey: null,
2782
+ rowClass: null,
2783
+ rowAttrs: null,
2784
+ emptyState: null,
2785
+ factSeparator: " \u2014 ",
2786
+ facts: "wrap",
2787
+ verbClass: null,
2788
+ label: null,
2789
+ className: null,
2790
+ ...config
2791
+ };
2792
+ this._records = Array.isArray(this.config.records) ? this.config.records.slice() : [];
2793
+ this._activeKey = this.config.activeKey ?? null;
2794
+ this._tabKey = null;
2795
+ this._items = /* @__PURE__ */ new Map();
2796
+ this._disposed = false;
2797
+ this._id = `twm-record-list-${++SEQ2}`;
2798
+ const root = document.createElement("div");
2799
+ root.className = "twm-record-list" + (this.config.facts === "ellipsis" ? " twm-record-list--clip" : "") + (this._activates() ? " twm-record-list--clickable" : "") + (this.config.className ? ` ${this.config.className}` : "");
2800
+ const list = document.createElement("ul");
2801
+ list.className = "twm-record-list__items";
2802
+ if (this.config.label) list.setAttribute("aria-label", this.config.label);
2803
+ root.appendChild(list);
2804
+ this.element = root;
2805
+ this.listElement = list;
2806
+ this._onClick = (ev) => this._gesture(ev, "click");
2807
+ this._onDblClick = (ev) => this._gesture(ev, "dblclick");
2808
+ this._onKeyDown = (ev) => this._key(ev);
2809
+ this._onFocusIn = (ev) => {
2810
+ const li = this._itemOf(ev.target);
2811
+ if (li && ev.target === li) this._setTabStop(li.__recordKey);
2812
+ };
2813
+ list.addEventListener("click", this._onClick);
2814
+ list.addEventListener("dblclick", this._onDblClick);
2815
+ list.addEventListener("keydown", this._onKeyDown);
2816
+ list.addEventListener("focusin", this._onFocusIn);
2817
+ this._draw();
2818
+ if (host) host.appendChild(root);
2819
+ }
2820
+ // ── public ───────────────────────────────────────────────────────────
2821
+ /** Replace the records and draw them. Focus inside the list is kept on
2822
+ * the same record, and on the same verb of it, when it is still there. */
2823
+ setRecords(records) {
2824
+ if (this._disposed) return;
2825
+ this._records = Array.isArray(records) ? records.slice() : [];
2826
+ this._draw();
2827
+ }
2828
+ /** The records, as last given. */
2829
+ getRecords() {
2830
+ return this._records.slice();
2831
+ }
2832
+ /** Draw ONE record again (`describe` asked afresh), or every one with no key. */
2833
+ refresh(key) {
2834
+ if (this._disposed) return;
2835
+ if (key === void 0) {
2836
+ this._draw();
2837
+ return;
2838
+ }
2839
+ const old = this._items.get(this._norm(key));
2840
+ if (!old) return;
2841
+ const idx = old.__recordIndex;
2842
+ const focus = this._focusMemo();
2843
+ const fresh = this._item(this._records[idx], idx);
2844
+ old.replaceWith(fresh);
2845
+ this._items.set(fresh.__recordKey, fresh);
2846
+ this._paintTabStop();
2847
+ this._restoreFocus(focus);
2848
+ }
2849
+ /** Mark the record that is open (`null` for none). */
2850
+ setActiveKey(key) {
2851
+ this._activeKey = key ?? null;
2852
+ for (const li of this._items.values()) this._paintActive(li);
2853
+ if (this._activeKey !== null && this._items.has(this._norm(this._activeKey))) {
2854
+ this._setTabStop(this._norm(this._activeKey));
2855
+ }
2856
+ }
2857
+ getActiveKey() {
2858
+ return this._activeKey;
2859
+ }
2860
+ /** The `<li>` drawn for a key, or null. */
2861
+ getItem(key) {
2862
+ return this._items.get(this._norm(key)) || null;
2863
+ }
2864
+ /** Move the keyboard focus to a record. */
2865
+ focusRecord(key) {
2866
+ const li = this.getItem(key);
2867
+ if (!li) return false;
2868
+ this._setTabStop(li.__recordKey);
2869
+ li.focus();
2870
+ return true;
2871
+ }
2872
+ dispose() {
2873
+ if (this._disposed) return;
2874
+ this._disposed = true;
2875
+ const list = this.listElement;
2876
+ list.removeEventListener("click", this._onClick);
2877
+ list.removeEventListener("dblclick", this._onDblClick);
2878
+ list.removeEventListener("keydown", this._onKeyDown);
2879
+ list.removeEventListener("focusin", this._onFocusIn);
2880
+ this._items.clear();
2881
+ this.element.remove();
2882
+ }
2883
+ // ── drawing ──────────────────────────────────────────────────────────
2884
+ _activates() {
2885
+ return typeof this.config.onActivate === "function";
2886
+ }
2887
+ _keyboard() {
2888
+ return this.config.keyboard ?? this._activates();
2889
+ }
2890
+ _gestures() {
2891
+ const g = this.config.activateOn;
2892
+ return new Set(Array.isArray(g) ? g : [g]);
2893
+ }
2894
+ /** Keys compare as strings: a record keyed `7` and an `activeKey` of `'7'` are one. */
2895
+ _norm(key) {
2896
+ return key === null || key === void 0 ? key : String(key);
2897
+ }
2898
+ _spec(record, idx) {
2899
+ const { describe } = this.config;
2900
+ const spec = typeof describe === "function" ? describe(record, idx) : record;
2901
+ return spec && typeof spec === "object" ? spec : { name: spec };
2902
+ }
2903
+ _keyOf(record, idx, spec) {
2904
+ const { getKey } = this.config;
2905
+ let key;
2906
+ if (spec.key !== void 0 && spec.key !== null) key = spec.key;
2907
+ else if (typeof getKey === "function") key = getKey(record, idx);
2908
+ else if (record && typeof record === "object" && (record.key ?? record.id) != null) key = record.key ?? record.id;
2909
+ else key = idx;
2910
+ return String(key);
2911
+ }
2912
+ _draw() {
2913
+ const focus = this._focusMemo();
2914
+ const list = this.listElement;
2915
+ list.replaceChildren();
2916
+ this._items.clear();
2917
+ this.element.querySelector(":scope > .twm-record-list__empty")?.remove();
2918
+ this._records.forEach((record, idx) => {
2919
+ const li = this._item(record, idx);
2920
+ if (this._items.has(li.__recordKey)) {
2921
+ console.error(`[RecordList] two records share the key "${li.__recordKey}"`);
2922
+ }
2923
+ this._items.set(li.__recordKey, li);
2924
+ list.appendChild(li);
2925
+ });
2926
+ this.element.classList.toggle("twm-record-list--empty", this._records.length === 0);
2927
+ if (!this._records.length) this._drawEmpty();
2928
+ this._paintTabStop();
2929
+ this._restoreFocus(focus);
2930
+ }
2931
+ _drawEmpty() {
2932
+ let content = this.config.emptyState;
2933
+ if (typeof content === "function") content = content();
2934
+ if (content == null || content === false || content === "") return;
2935
+ const p = document.createElement("p");
2936
+ p.className = "twm-record-list__empty";
2937
+ if (typeof Node !== "undefined" && content instanceof Node) p.appendChild(content);
2938
+ else p.textContent = String(content);
2939
+ this.element.appendChild(p);
2940
+ }
2941
+ _item(record, idx) {
2942
+ const spec = this._spec(record, idx);
2943
+ const key = this._keyOf(record, idx, spec);
2944
+ const li = document.createElement("li");
2945
+ li.className = "twm-record";
2946
+ li.__record = record;
2947
+ li.__recordIndex = idx;
2948
+ li.__recordKey = key;
2949
+ li.dataset.recordKey = key;
2950
+ if (this._keyboard()) li.tabIndex = -1;
2951
+ if (spec.state) {
2952
+ li.dataset.state = safeWord(spec.state);
2953
+ li.classList.add(`twm-record--state-${safeWord(spec.state)}`);
2954
+ }
2955
+ const main = document.createElement("div");
2956
+ main.className = "twm-record__main";
2957
+ const name = document.createElement("div");
2958
+ name.className = "twm-record__name" + (spec.mono ? " twm-record__name--mono" : "");
2959
+ appendContent2(name, spec.name);
2960
+ main.appendChild(name);
2961
+ const factsText = this._facts(spec.facts);
2962
+ if (factsText !== null) {
2963
+ const facts = document.createElement("div");
2964
+ facts.className = "twm-record__facts";
2965
+ appendContent2(facts, factsText);
2966
+ if (this.config.facts === "ellipsis") facts.title = facts.textContent;
2967
+ main.appendChild(facts);
2968
+ }
2969
+ if (spec.error != null && spec.error !== false && spec.error !== "") {
2970
+ const err = document.createElement("div");
2971
+ err.className = "twm-record__error";
2972
+ appendContent2(err, spec.error);
2973
+ main.appendChild(err);
2974
+ }
2975
+ li.appendChild(main);
2976
+ const pills = (spec.pills || []).filter((p) => p != null && p !== false && p !== "");
2977
+ if (pills.length) {
2978
+ const box = document.createElement("div");
2979
+ box.className = "twm-record__pills";
2980
+ for (const p of pills) box.appendChild(pill(p));
2981
+ li.appendChild(box);
2982
+ }
2983
+ const verbs = (spec.verbs || []).filter(Boolean);
2984
+ if (verbs.length) {
2985
+ const box = document.createElement("div");
2986
+ box.className = "twm-record__verbs";
2987
+ const reasons = /* @__PURE__ */ new Map();
2988
+ for (const v of verbs) {
2989
+ const b = this._verb(v, record);
2990
+ box.appendChild(b);
2991
+ if (v.refusal) {
2992
+ const said = String(v.refusal);
2993
+ if (!reasons.has(said)) reasons.set(said, { labels: [], buttons: [] });
2994
+ reasons.get(said).labels.push(v.label);
2995
+ reasons.get(said).buttons.push(b);
2996
+ }
2997
+ }
2998
+ li.appendChild(box);
2999
+ if (reasons.size) {
3000
+ const lines = document.createElement("div");
3001
+ lines.className = "twm-record__refusals";
3002
+ let n = 0;
3003
+ for (const [said, { labels, buttons }] of reasons) {
3004
+ const line = document.createElement("div");
3005
+ line.className = "twm-record__refusal";
3006
+ line.id = `${this._id}-r${idx}-${n++}`;
3007
+ line.dataset.refusalFor = labels.join(", ");
3008
+ line.textContent = labels.length === verbs.length && verbs.length > 1 ? said : `${labels.join(", ")}: ${said}`;
3009
+ for (const b of buttons) b.setAttribute("aria-describedby", line.id);
3010
+ lines.appendChild(line);
3011
+ }
3012
+ main.appendChild(lines);
3013
+ }
3014
+ }
3015
+ this._decorate(li, record, idx);
3016
+ return li;
3017
+ }
3018
+ _facts(facts) {
3019
+ if (facts == null || facts === false) return null;
3020
+ if (typeof Node !== "undefined" && facts instanceof Node) return facts;
3021
+ if (!Array.isArray(facts)) {
3022
+ const s = String(facts);
3023
+ return s === "" ? null : s;
3024
+ }
3025
+ const parts = facts.filter((f) => f != null && f !== false && f !== "");
3026
+ if (!parts.length) return null;
3027
+ if (parts.every((p) => !(typeof Node !== "undefined" && p instanceof Node))) {
3028
+ return parts.map(String).join(this.config.factSeparator);
3029
+ }
3030
+ const frag = document.createDocumentFragment();
3031
+ parts.forEach((p, i) => {
3032
+ if (i) frag.appendChild(document.createTextNode(this.config.factSeparator));
3033
+ appendContent2(frag, p);
3034
+ });
3035
+ return frag;
3036
+ }
3037
+ _verb(v, record) {
3038
+ const b = document.createElement("button");
3039
+ b.type = "button";
3040
+ const { verbClass } = this.config;
3041
+ b.className = typeof verbClass === "function" ? String(verbClass(v) || "") : typeof verbClass === "string" ? verbClass : "twm-btn" + (v.primary ? " twm-btn--primary" : "") + (v.danger ? " twm-btn--danger" : "");
3042
+ b.classList.add("twm-record__verb");
3043
+ b.dataset.verb = String(v.name ?? v.label);
3044
+ if (v.icon) {
3045
+ const glyph = document.createElement("span");
3046
+ glyph.className = "material-symbols-outlined twm-record__verb-icon";
3047
+ glyph.setAttribute("aria-hidden", "true");
3048
+ glyph.textContent = String(v.icon);
3049
+ b.appendChild(glyph);
3050
+ }
3051
+ const label = document.createElement("span");
3052
+ label.className = "twm-record__verb-label";
3053
+ label.textContent = String(v.label ?? "");
3054
+ b.appendChild(label);
3055
+ if (v.refusal) {
3056
+ b.disabled = true;
3057
+ b.title = String(v.refusal);
3058
+ b.dataset.refusal = String(v.refusal);
3059
+ } else {
3060
+ if (v.title) b.title = String(v.title);
3061
+ if (typeof v.onClick === "function") b.addEventListener("click", (ev) => v.onClick(record, ev));
3062
+ }
3063
+ return b;
3064
+ }
3065
+ _decorate(li, record, idx) {
3066
+ const { rowClass, rowAttrs } = this.config;
3067
+ if (typeof rowClass === "function") {
3068
+ const c = rowClass(record, idx);
3069
+ const list = Array.isArray(c) ? c : String(c ?? "").split(/\s+/);
3070
+ for (const name of list) if (name) li.classList.add(String(name));
3071
+ }
3072
+ if (typeof rowAttrs === "function") {
3073
+ for (const [name, value] of Object.entries(rowAttrs(record, idx) || {})) {
3074
+ if (value == null || value === false) continue;
3075
+ try {
3076
+ li.setAttribute(name, value === true ? "" : String(value));
3077
+ } catch (err) {
3078
+ console.error(`[RecordList] rowAttrs: bad attribute "${name}"`, err);
3079
+ }
3080
+ }
3081
+ }
3082
+ this._paintActive(li);
3083
+ }
3084
+ _paintActive(li) {
3085
+ const on = this._activeKey !== null && li.__recordKey === this._norm(this._activeKey);
3086
+ li.classList.toggle("twm-record--active", on);
3087
+ if (on) {
3088
+ if (li.getAttribute("aria-current") !== "true") {
3089
+ li.setAttribute("aria-current", "true");
3090
+ li.__twmAriaCurrent = true;
3091
+ }
3092
+ } else if (li.__twmAriaCurrent) {
3093
+ li.__twmAriaCurrent = false;
3094
+ li.removeAttribute("aria-current");
3095
+ }
3096
+ }
3097
+ // ── focus ────────────────────────────────────────────────────────────
3098
+ /** The ONE record that takes Tab: the one last focused, else the open
3099
+ * one, else the first. */
3100
+ _paintTabStop() {
3101
+ if (!this._keyboard()) return;
3102
+ let key = this._tabKey;
3103
+ if (key === null || !this._items.has(key)) {
3104
+ const active = this._norm(this._activeKey);
3105
+ key = active !== null && this._items.has(active) ? active : this._items.keys().next().value ?? null;
3106
+ }
3107
+ this._setTabStop(key);
3108
+ }
3109
+ _setTabStop(key) {
3110
+ if (!this._keyboard()) return;
3111
+ this._tabKey = key ?? null;
3112
+ for (const [k, li] of this._items) li.tabIndex = k === this._tabKey ? 0 : -1;
3113
+ }
3114
+ _focusMemo() {
3115
+ const doc = this.element.ownerDocument;
3116
+ const active = doc?.activeElement;
3117
+ if (!active || !this.listElement.contains(active)) return null;
3118
+ const li = this._itemOf(active);
3119
+ if (!li) return null;
3120
+ return { key: li.__recordKey, verb: active.closest(".twm-record__verb")?.dataset.verb ?? null };
3121
+ }
3122
+ _restoreFocus(memo) {
3123
+ if (!memo) return;
3124
+ const li = this._items.get(memo.key);
3125
+ if (!li) return;
3126
+ if (memo.verb !== null) {
3127
+ const b = [...li.querySelectorAll(".twm-record__verb")].find((x) => x.dataset.verb === memo.verb);
3128
+ if (b && !b.disabled) {
3129
+ b.focus();
3130
+ return;
3131
+ }
3132
+ }
3133
+ if (this._keyboard()) {
3134
+ this._setTabStop(li.__recordKey);
3135
+ li.focus();
3136
+ }
3137
+ }
3138
+ _itemOf(target) {
3139
+ const li = target?.closest?.(".twm-record");
3140
+ return li && this.listElement.contains(li) && li.parentNode === this.listElement ? li : null;
3141
+ }
3142
+ // ── gestures ─────────────────────────────────────────────────────────
3143
+ _gesture(ev, gesture) {
3144
+ if (!this._activates() || !this._gestures().has(gesture)) return;
3145
+ const li = this._itemOf(ev.target);
3146
+ if (!li) return;
3147
+ if (!isRowActivation(ev, gesture, { scope: this.listElement })) return;
3148
+ this.config.onActivate(li.__record, li.__recordIndex, ev);
3149
+ }
3150
+ _key(ev) {
3151
+ if (ev.defaultPrevented) return;
3152
+ const li = this._itemOf(ev.target);
3153
+ if (!li) return;
3154
+ if (ev.key === "Enter") {
3155
+ if (ev.target !== li || !this._activates() || !this._gestures().has("enter")) return;
3156
+ if (ev.altKey || ev.ctrlKey || ev.metaKey || ev.shiftKey) return;
3157
+ ev.preventDefault();
3158
+ this.config.onActivate(li.__record, li.__recordIndex, ev);
3159
+ return;
3160
+ }
3161
+ if (!this._keyboard()) return;
3162
+ if (ev.target !== li && (ev.target.closest(OWN_ARROWS) || !isRowControl(ev.target, li))) return;
3163
+ const items = [...this._items.values()];
3164
+ const at = items.indexOf(li);
3165
+ let next = null;
3166
+ if (ev.key === "ArrowDown") next = items[Math.min(items.length - 1, at + 1)];
3167
+ else if (ev.key === "ArrowUp") next = items[Math.max(0, at - 1)];
3168
+ else if (ev.key === "Home") next = items[0];
3169
+ else if (ev.key === "End") next = items[items.length - 1];
3170
+ else return;
3171
+ ev.preventDefault();
3172
+ if (next) {
3173
+ this._setTabStop(next.__recordKey);
3174
+ next.focus();
3175
+ }
3176
+ }
3177
+ };
3178
+ function createRecordList(host, config = {}) {
3179
+ return new RecordList(host, config);
3180
+ }
3181
+ function pill(p) {
3182
+ const s = typeof p === "object" && !(typeof Node !== "undefined" && p instanceof Node) ? p : { text: p };
3183
+ const span = document.createElement("span");
3184
+ span.className = "twm-record__pill" + (s.tone ? ` twm-record__pill--${safeWord(s.tone)}` : "");
3185
+ if (typeof Node !== "undefined" && s.text instanceof Node) span.appendChild(s.text);
3186
+ else if (typeof Node !== "undefined" && p instanceof Node) span.appendChild(p);
3187
+ else span.textContent = String(s.text ?? "");
3188
+ if (s.title) span.title = String(s.title);
3189
+ if (s.tone) span.dataset.tone = safeWord(s.tone);
3190
+ return span;
3191
+ }
3192
+ function appendContent2(el, content) {
3193
+ if (content == null || content === false) return;
3194
+ if (typeof Node !== "undefined" && content instanceof Node) el.appendChild(content);
3195
+ else el.appendChild(document.createTextNode(String(content)));
3196
+ }
3197
+
2532
3198
  // src/ui/components/slide_out_panel.js
2533
3199
  var SlideOutPanel = class extends ComponentBase {
2534
3200
  /** @type {HTMLElement} */
@@ -5827,6 +6493,7 @@ export {
5827
6493
  DEFAULT_TYPES,
5828
6494
  DataTable,
5829
6495
  DragReorder,
6496
+ FactTable,
5830
6497
  HelpModal,
5831
6498
  ISO_DATE,
5832
6499
  ISO_DATETIME,
@@ -5840,6 +6507,7 @@ export {
5840
6507
  PageBase,
5841
6508
  PanelStateMachine,
5842
6509
  ROW_CONTROL_SELECTOR,
6510
+ RecordList,
5843
6511
  SCALAR_TYPES,
5844
6512
  SlideOutPanel,
5845
6513
  SliderField,
@@ -5849,8 +6517,11 @@ export {
5849
6517
  WindowChromeController,
5850
6518
  WindowTaskbar,
5851
6519
  attachInlineRenamer,
6520
+ createChipsFilter,
5852
6521
  createDetailHeader,
6522
+ createFactTable,
5853
6523
  createRafResizeObserver,
6524
+ createRecordList,
5854
6525
  createScrollLock,
5855
6526
  createTableStateStore,
5856
6527
  createTreeCategory,