@iyulab/flex-table 0.34.3 → 0.35.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/CHANGELOG.md CHANGED
@@ -1,5 +1,31 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.35.0] - 2026-09-15
4
+
5
+ ### Changed
6
+
7
+ - **Every header now has a 24×24 column menu button (`⋮`), which replaces the 14px filter button.**
8
+ The menu — the same one a header right-click opens — gathers the per-column actions: **Sort
9
+ ascending** / **Sort descending** (for sortable columns — the keyboard path to sorting), **Filter…**
10
+ and **Clear filter** (with `show-filters`), **Hide column** / **Show: …**, **Auto-fit width**,
11
+ **Wider** and **Narrower** (±20px, the menu stays open for repeated steps). Opening a filter is now
12
+ two clicks. The button is highlighted while its column is filtered, as the filter button was.
13
+ The old filter button was below the WCAG 2.2 SC 2.5.8 minimum and sat right next to the resize handle;
14
+ the handle keeps its 6px width and the column menu is its equivalent, which also means resizing
15
+ no longer requires a drag (SC 2.5.7).
16
+
17
+ ### Fixed
18
+
19
+ - **Sorting from the cell context menu broke the `sort-change` contract.** It reported
20
+ `{ sortCriteria }` instead of `{ criteria }`, re-sorted the data locally even with
21
+ `data-mode="server"`, and ignored `sortable: false`. It now behaves exactly like a header click.
22
+ - **The header and cell context menus could not be used from the keyboard, and could open
23
+ off-screen.** Their items are now buttons in a `role="menu"`: arrow keys, Home and End move between
24
+ them, Escape closes and returns focus (to the column menu button, or to the grid). The cell menu
25
+ focuses its first item when it opens. Both menus are moved back inside the visible area once their
26
+ real size is known — the cell menu previously flipped on a guessed 200×280 size, and a menu fitted
27
+ to the window width could still sit under the vertical scrollbar.
28
+
3
29
  ## [0.34.3] - 2026-09-13
4
30
 
5
31
  ### Fixed
package/README.md CHANGED
@@ -52,7 +52,8 @@ npm install @iyulab/flex-table
52
52
  - **Row Selection** — Checkbox-based row selection (`selectable`, single/multi mode)
53
53
  - **Clipboard** — Ctrl+C/X/V with TSV format (Excel/Google Sheets compatible, RFC 4180)
54
54
  - **Sorting** — Click header to sort (asc/desc/none), Shift+click for multi-sort
55
- - **Column Resize** — Drag header border, double-click to auto-fit, Alt+Arrow keyboard resize
55
+ - **Column Menu** — A 24×24 button in every header opens the column's menu: sort, filter, hide/show, auto-fit, wider/narrower (also opens on header right-click)
56
+ - **Column Resize** — Drag header border, double-click to auto-fit, Alt+Arrow keyboard resize, or the column menu
56
57
  - **Column Operations** — `addColumn()`, `deleteColumn()`, `moveColumn()` with undo
57
58
  - **Pinned Columns** — Freeze columns to left or right (`pinned: 'left' | 'right'`)
58
59
  - **Filtering** — Programmatic API + built-in header filter UI (`show-filters`)
@@ -103,7 +104,7 @@ guarantee about a *constrained* host. `height-model.browser.test.ts` pins both s
103
104
  | `showRowNumbers` | `show-row-numbers` | `boolean` | `false` | Show row number column |
104
105
  | `theme` | `theme` | `'light' \| 'dark'` | auto | Force theme; auto-detects `prefers-color-scheme` |
105
106
  | `editable` | `editable` | `boolean` | `true` | Global read-only mode when `false`. Defaults to `true` — a purely read-only grid should set this explicitly rather than relying on per-column `editable: false` alone, since it's also what makes Enter fire `row-activate` (see [Events](#events)) instead of entering edit mode |
106
- | `showFilters` | `show-filters` | `boolean` | `false` | Show built-in header filter dropdowns |
107
+ | `showFilters` | `show-filters` | `boolean` | `false` | Offer the built-in filter dropdowns through each column menu («Filter…») |
107
108
  | `maxRows` | `max-rows` | `number` | `0` | Max row count (0 = unlimited); blocks `addRow()` and paste expansion |
108
109
  | `maxUndoSize` | `max-undo-size` | `number` | `100` | Max undo history stack size |
109
110
  | `selectable` | `selectable` | `boolean` | `false` | Enable row-level checkbox selection |
@@ -404,6 +405,23 @@ package dependency; CSS custom properties are resolved at render time, not impor
404
405
  | Ctrl+Shift+Z / Ctrl+Y | Redo |
405
406
  | Alt+ArrowLeft / Alt+ArrowRight | Resize current column (±20px) |
406
407
  | Ctrl+Click header | Select entire column |
408
+ | Enter / Space on a column menu button | Open the column menu |
409
+ | ArrowUp / ArrowDown / Home / End (column menu) | Move between menu items |
410
+ | Escape (column menu) | Close the menu and return focus to its button |
411
+
412
+ ## Accessibility
413
+
414
+ The baseline is **WCAG 2.2**. The table lists what this package **measures in tests** — it is not a
415
+ conformance claim for the success criteria it does not list.
416
+
417
+ | Success criterion | Guarantee | Measured by |
418
+ |---|---|---|
419
+ | SC 2.5.8 Target Size (Minimum) | Sortable headers, column menu buttons, the open column menu, the open filter dropdown (text and number), the find/replace bar, the cell context menu and row selection checkboxes are at least 24×24 CSS px or meet the spacing exception (24px between centers, counting the neighbouring resize handles), and are actually hit at that position. Two small targets use the equivalent-control exception: the 6px column resize handle (every resize is also in the column menu) and the hidden-column marker (its column menu offers **Show: …**) | `tests/browser/target-size.browser.test.ts` (real Chromium) |
420
+ | SC 2.1.1 Keyboard (pointer-cursor check) | Nothing the grid renders shows a pointer cursor without being an interactive element. Sorting by header click also has a keyboard path — the column menu's **Sort ascending** / **Sort descending** (the check alone cannot see that, because the header cell wraps the menu button) | `tests/browser/target-size.browser.test.ts` · `src/flex-table.test.ts` |
421
+ | SC 2.5.7 Dragging Movements | Resizing never requires a drag — the column menu's **Auto-fit width**, **Wider** and **Narrower** are single clicks | `tests/browser/column-menu.browser.test.ts` |
422
+
423
+ Not yet measured: the boolean and date filter dropdowns and the comment popup. Color contrast comes
424
+ from the `@iyulab/components` tokens this package reads.
407
425
 
408
426
  ## Usage Guide
409
427
 
@@ -569,7 +587,9 @@ Use `updateRows()` for programmatic edits — it provides undo/redo and dispatch
569
587
 
570
588
  ### Built-in Filter UI
571
589
 
572
- Enable with `show-filters` attribute. Filter dropdowns appear in column headers:
590
+ Enable with `show-filters` attribute. Each column menu (the `⋮` button in the header) then offers **Filter…**,
591
+ which opens that column's filter dropdown, and **Clear filter** while the column is filtered. The menu
592
+ button is highlighted while its column has an active filter.
573
593
 
574
594
  - **text**: case-insensitive substring search
575
595
  - **number**: min/max range inputs
@@ -447,20 +447,39 @@ var l = r`
447
447
 
448
448
  /* --- Filter UI --- */
449
449
 
450
- .ft-filter-btn {
450
+ /* 열 메뉴 버튼 — 필터·숨기기·폭 조절이 모두 여기서 열린다. 24px 은 WCAG 2.2 SC 2.5.8 하한이고,
451
+ 6px 리사이즈 핸들의 «등가 수단» 이 이 버튼의 메뉴다. */
452
+ .ft-column-menu-btn {
451
453
  all: unset;
454
+ box-sizing: border-box;
455
+ display: inline-flex;
456
+ align-items: center;
457
+ justify-content: center;
458
+ inline-size: 24px;
459
+ block-size: 24px;
460
+ margin-left: auto;
461
+ flex-shrink: 0;
462
+ border-radius: 4px;
452
463
  cursor: pointer;
453
- font-size: 10px;
464
+ font-size: 16px;
465
+ line-height: 1;
454
466
  color: var(--ft-sort-indicator-color);
455
- padding: 0 2px;
456
- margin-left: auto;
457
467
  opacity: 0.7;
458
- flex-shrink: 0;
459
468
  }
460
469
 
461
- .ft-filter-btn:hover { opacity: 1; }
470
+ .ft-column-menu-btn:hover,
471
+ .ft-column-menu-btn[aria-expanded="true"] {
472
+ opacity: 1;
473
+ background: var(--ft-header-hover-bg);
474
+ }
475
+
476
+ .ft-column-menu-btn:focus-visible {
477
+ opacity: 1;
478
+ outline: 2px solid var(--ft-active-color);
479
+ outline-offset: -2px;
480
+ }
462
481
 
463
- .ft-filter-btn.ft-filter-active {
482
+ .ft-column-menu-btn.ft-filter-active {
464
483
  color: var(--ft-active-color);
465
484
  opacity: 1;
466
485
  }
@@ -639,21 +658,36 @@ var l = r`
639
658
  min-width: 160px;
640
659
  border-radius: 4px;
641
660
  overflow: hidden;
661
+ display: flex;
662
+ flex-direction: column;
663
+ padding-block: 4px;
642
664
  }
643
665
 
644
666
  .ft-header-menu-item {
667
+ all: unset;
668
+ box-sizing: border-box;
669
+ display: block;
670
+ min-block-size: 28px;
645
671
  padding: 6px 12px;
646
672
  cursor: pointer;
647
673
  font-size: var(--ft-font-size, 14px);
648
674
  color: var(--ft-text-color);
649
675
  white-space: nowrap;
676
+ text-align: start;
650
677
  }
651
678
 
652
- .ft-header-menu-item:hover {
679
+ .ft-header-menu-item:hover,
680
+ .ft-header-menu-item:focus-visible {
653
681
  background: var(--ft-selection-bg);
654
682
  color: var(--ft-active-color);
655
683
  }
656
684
 
685
+ .ft-header-menu-separator {
686
+ block-size: 1px;
687
+ margin-block: 4px;
688
+ background: var(--ft-border-color);
689
+ }
690
+
657
691
  /* Hidden column indicator in header */
658
692
  .ft-hidden-col-indicator {
659
693
  position: absolute;
@@ -693,20 +727,28 @@ var l = r`
693
727
  }
694
728
 
695
729
  .ft-context-menu-item {
730
+ all: unset;
731
+ box-sizing: border-box;
732
+ display: block;
733
+ inline-size: 100%;
734
+ min-block-size: 28px;
696
735
  padding: 6px 14px;
697
736
  cursor: pointer;
698
737
  font-size: var(--ft-font-size, 14px);
699
738
  color: var(--ft-text-color);
700
739
  white-space: nowrap;
701
740
  user-select: none;
741
+ text-align: start;
702
742
  }
703
743
 
704
- .ft-context-menu-item:hover {
744
+ .ft-context-menu-item:hover,
745
+ .ft-context-menu-item:focus-visible {
705
746
  background: var(--ft-selection-bg);
706
747
  color: var(--ft-active-color);
707
748
  }
708
749
 
709
- .ft-context-menu-item.ft-context-menu-danger:hover {
750
+ .ft-context-menu-item.ft-context-menu-danger:hover,
751
+ .ft-context-menu-item.ft-context-menu-danger:focus-visible {
710
752
  background: #fdd;
711
753
  color: #c0392b;
712
754
  }
@@ -1247,56 +1289,56 @@ var A = (() => {
1247
1289
  }
1248
1290
  return e;
1249
1291
  })();
1250
- function j(e) {
1292
+ function ee(e) {
1251
1293
  let t = 4294967295;
1252
1294
  for (let n = 0; n < e.length; n++) t = A[(t ^ e[n]) & 255] ^ t >>> 8;
1253
1295
  return (t ^ 4294967295) >>> 0;
1254
1296
  }
1255
- var M = new TextEncoder();
1256
- function ee(e) {
1297
+ var j = new TextEncoder();
1298
+ function te(e) {
1257
1299
  let t = [], n = [], r = 0;
1258
1300
  for (let i of e) {
1259
- let e = M.encode(i.name), a = j(i.data), o = i.data.length, s = new ArrayBuffer(30 + e.length), c = new DataView(s);
1301
+ let e = j.encode(i.name), a = ee(i.data), o = i.data.length, s = new ArrayBuffer(30 + e.length), c = new DataView(s);
1260
1302
  c.setUint32(0, 67324752, !0), c.setUint16(4, 20, !0), c.setUint16(6, 0, !0), c.setUint16(8, 0, !0), c.setUint16(10, 0, !0), c.setUint16(12, 33, !0), c.setUint32(14, a, !0), c.setUint32(18, o, !0), c.setUint32(22, o, !0), c.setUint16(26, e.length, !0), c.setUint16(28, 0, !0), new Uint8Array(s, 30).set(e), t.push(new Uint8Array(s)), t.push(i.data);
1261
1303
  let l = new ArrayBuffer(46 + e.length), u = new DataView(l);
1262
1304
  u.setUint32(0, 33639248, !0), u.setUint16(4, 20, !0), u.setUint16(6, 20, !0), u.setUint16(8, 0, !0), u.setUint16(10, 0, !0), u.setUint16(12, 0, !0), u.setUint16(14, 33, !0), u.setUint32(16, a, !0), u.setUint32(20, o, !0), u.setUint32(24, o, !0), u.setUint16(28, e.length, !0), u.setUint16(30, 0, !0), u.setUint16(32, 0, !0), u.setUint16(34, 0, !0), u.setUint16(36, 0, !0), u.setUint32(38, 0, !0), u.setUint32(42, r, !0), new Uint8Array(l, 46).set(e), n.push(new Uint8Array(l)), r += 30 + e.length + o;
1263
1305
  }
1264
- let i = N(n), a = /* @__PURE__ */ new ArrayBuffer(22), o = new DataView(a);
1265
- return o.setUint32(0, 101010256, !0), o.setUint16(4, 0, !0), o.setUint16(6, 0, !0), o.setUint16(8, e.length, !0), o.setUint16(10, e.length, !0), o.setUint32(12, i.length, !0), o.setUint32(16, r, !0), o.setUint16(20, 0, !0), N([
1306
+ let i = M(n), a = /* @__PURE__ */ new ArrayBuffer(22), o = new DataView(a);
1307
+ return o.setUint32(0, 101010256, !0), o.setUint16(4, 0, !0), o.setUint16(6, 0, !0), o.setUint16(8, e.length, !0), o.setUint16(10, e.length, !0), o.setUint32(12, i.length, !0), o.setUint32(16, r, !0), o.setUint16(20, 0, !0), M([
1266
1308
  ...t,
1267
1309
  i,
1268
1310
  new Uint8Array(a)
1269
1311
  ]);
1270
1312
  }
1271
- function N(e) {
1313
+ function M(e) {
1272
1314
  let t = e.reduce((e, t) => e + t.length, 0), n = new Uint8Array(t), r = 0;
1273
1315
  for (let t of e) n.set(t, r), r += t.length;
1274
1316
  return n;
1275
1317
  }
1276
- function P(e) {
1318
+ function N(e) {
1277
1319
  return e.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
1278
1320
  }
1279
- function F(e) {
1321
+ function P(e) {
1280
1322
  let t = "";
1281
1323
  for (e++; e > 0;) t = String.fromCharCode(64 + (e % 26 || 26)) + t, e = Math.floor((e - 1) / 26);
1282
1324
  return t;
1283
1325
  }
1284
- function I(e, t) {
1285
- return F(t) + (e + 1);
1326
+ function F(e, t) {
1327
+ return P(t) + (e + 1);
1286
1328
  }
1287
- function te(e) {
1329
+ function ne(e) {
1288
1330
  let t = Date.UTC(1899, 11, 30);
1289
1331
  return (e.getTime() - t) / 864e5;
1290
1332
  }
1291
- var ne = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n<Types xmlns=\"http://schemas.openxmlformats.org/package/2006/content-types\">\n <Default Extension=\"rels\" ContentType=\"application/vnd.openxmlformats-package.relationships+xml\"/>\n <Default Extension=\"xml\" ContentType=\"application/xml\"/>\n <Override PartName=\"/xl/workbook.xml\" ContentType=\"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml\"/>\n <Override PartName=\"/xl/worksheets/sheet1.xml\" ContentType=\"application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml\"/>\n <Override PartName=\"/xl/styles.xml\" ContentType=\"application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml\"/>\n</Types>", re = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n<Relationships xmlns=\"http://schemas.openxmlformats.org/package/2006/relationships\">\n <Relationship Id=\"rId1\" Type=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument\" Target=\"xl/workbook.xml\"/>\n</Relationships>", ie = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n<workbook xmlns=\"http://schemas.openxmlformats.org/spreadsheetml/2006/main\" xmlns:r=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships\">\n <sheets><sheet name=\"Sheet1\" sheetId=\"1\" r:id=\"rId1\"/></sheets>\n</workbook>", ae = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n<Relationships xmlns=\"http://schemas.openxmlformats.org/package/2006/relationships\">\n <Relationship Id=\"rId1\" Type=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet\" Target=\"worksheets/sheet1.xml\"/>\n <Relationship Id=\"rId2\" Type=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles\" Target=\"styles.xml\"/>\n</Relationships>", oe = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n<styleSheet xmlns=\"http://schemas.openxmlformats.org/spreadsheetml/2006/main\">\n <numFmts count=\"1\"><numFmt numFmtId=\"164\" formatCode=\"yyyy-mm-dd\"/></numFmts>\n <fonts count=\"2\">\n <font><sz val=\"11\"/><name val=\"Calibri\"/></font>\n <font><sz val=\"11\"/><b/><name val=\"Calibri\"/></font>\n </fonts>\n <fills count=\"2\">\n <fill><patternFill patternType=\"none\"/></fill>\n <fill><patternFill patternType=\"gray125\"/></fill>\n </fills>\n <borders count=\"1\"><border><left/><right/><top/><bottom/><diagonal/></border></borders>\n <cellStyleXfs count=\"1\"><xf numFmtId=\"0\" fontId=\"0\" fillId=\"0\" borderId=\"0\"/></cellStyleXfs>\n <cellXfs count=\"3\">\n <xf numFmtId=\"0\" fontId=\"0\" fillId=\"0\" borderId=\"0\" xfId=\"0\"/>\n <xf numFmtId=\"164\" fontId=\"0\" fillId=\"0\" borderId=\"0\" xfId=\"0\" applyNumberFormat=\"1\"/>\n <xf numFmtId=\"0\" fontId=\"1\" fillId=\"0\" borderId=\"0\" xfId=\"0\" applyFont=\"1\"/>\n </cellXfs>\n</styleSheet>";
1292
- function se(e, t) {
1293
- let n = [], r = t.map((e, t) => `<c r="${I(0, t)}" t="str" s="2"><v>${P(e.header)}</v></c>`);
1333
+ var re = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n<Types xmlns=\"http://schemas.openxmlformats.org/package/2006/content-types\">\n <Default Extension=\"rels\" ContentType=\"application/vnd.openxmlformats-package.relationships+xml\"/>\n <Default Extension=\"xml\" ContentType=\"application/xml\"/>\n <Override PartName=\"/xl/workbook.xml\" ContentType=\"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml\"/>\n <Override PartName=\"/xl/worksheets/sheet1.xml\" ContentType=\"application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml\"/>\n <Override PartName=\"/xl/styles.xml\" ContentType=\"application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml\"/>\n</Types>", ie = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n<Relationships xmlns=\"http://schemas.openxmlformats.org/package/2006/relationships\">\n <Relationship Id=\"rId1\" Type=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument\" Target=\"xl/workbook.xml\"/>\n</Relationships>", ae = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n<workbook xmlns=\"http://schemas.openxmlformats.org/spreadsheetml/2006/main\" xmlns:r=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships\">\n <sheets><sheet name=\"Sheet1\" sheetId=\"1\" r:id=\"rId1\"/></sheets>\n</workbook>", oe = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n<Relationships xmlns=\"http://schemas.openxmlformats.org/package/2006/relationships\">\n <Relationship Id=\"rId1\" Type=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet\" Target=\"worksheets/sheet1.xml\"/>\n <Relationship Id=\"rId2\" Type=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles\" Target=\"styles.xml\"/>\n</Relationships>", se = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n<styleSheet xmlns=\"http://schemas.openxmlformats.org/spreadsheetml/2006/main\">\n <numFmts count=\"1\"><numFmt numFmtId=\"164\" formatCode=\"yyyy-mm-dd\"/></numFmts>\n <fonts count=\"2\">\n <font><sz val=\"11\"/><name val=\"Calibri\"/></font>\n <font><sz val=\"11\"/><b/><name val=\"Calibri\"/></font>\n </fonts>\n <fills count=\"2\">\n <fill><patternFill patternType=\"none\"/></fill>\n <fill><patternFill patternType=\"gray125\"/></fill>\n </fills>\n <borders count=\"1\"><border><left/><right/><top/><bottom/><diagonal/></border></borders>\n <cellStyleXfs count=\"1\"><xf numFmtId=\"0\" fontId=\"0\" fillId=\"0\" borderId=\"0\"/></cellStyleXfs>\n <cellXfs count=\"3\">\n <xf numFmtId=\"0\" fontId=\"0\" fillId=\"0\" borderId=\"0\" xfId=\"0\"/>\n <xf numFmtId=\"164\" fontId=\"0\" fillId=\"0\" borderId=\"0\" xfId=\"0\" applyNumberFormat=\"1\"/>\n <xf numFmtId=\"0\" fontId=\"1\" fillId=\"0\" borderId=\"0\" xfId=\"0\" applyFont=\"1\"/>\n </cellXfs>\n</styleSheet>";
1334
+ function ce(e, t) {
1335
+ let n = [], r = t.map((e, t) => `<c r="${F(0, t)}" t="str" s="2"><v>${N(e.header)}</v></c>`);
1294
1336
  return n.push(`<row r="1">${r.join("")}</row>`), e.forEach((e, r) => {
1295
1337
  let i = t.map((t, n) => {
1296
- let i = I(r + 1, n), a = e[t.key];
1338
+ let i = F(r + 1, n), a = e[t.key];
1297
1339
  if (a == null) return `<c r="${i}" t="str"><v></v></c>`;
1298
1340
  let o = t.type ?? "text";
1299
- return o === "number" && typeof a == "number" ? `<c r="${i}"><v>${a}</v></c>` : (o === "date" || o === "datetime") && a instanceof Date ? `<c r="${i}" s="1"><v>${te(a).toFixed(6)}</v></c>` : o === "boolean" ? `<c r="${i}" t="b"><v>${+!!a}</v></c>` : `<c r="${i}" t="str"><v>${P(String(a))}</v></c>`;
1341
+ return o === "number" && typeof a == "number" ? `<c r="${i}"><v>${a}</v></c>` : (o === "date" || o === "datetime") && a instanceof Date ? `<c r="${i}" s="1"><v>${ne(a).toFixed(6)}</v></c>` : o === "boolean" ? `<c r="${i}" t="b"><v>${+!!a}</v></c>` : `<c r="${i}" t="str"><v>${N(String(a))}</v></c>`;
1300
1342
  });
1301
1343
  n.push(`<row r="${r + 2}">${i.join("")}</row>`);
1302
1344
  }), `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
@@ -1304,55 +1346,55 @@ function se(e, t) {
1304
1346
  <sheetData>${n.join("")}</sheetData>
1305
1347
  </worksheet>`;
1306
1348
  }
1307
- function ce(e, t) {
1308
- let n = se(e, t);
1309
- return ee([
1349
+ function le(e, t) {
1350
+ let n = ce(e, t);
1351
+ return te([
1310
1352
  {
1311
1353
  name: "[Content_Types].xml",
1312
- data: M.encode(ne)
1354
+ data: j.encode(re)
1313
1355
  },
1314
1356
  {
1315
1357
  name: "_rels/.rels",
1316
- data: M.encode(re)
1358
+ data: j.encode(ie)
1317
1359
  },
1318
1360
  {
1319
1361
  name: "xl/workbook.xml",
1320
- data: M.encode(ie)
1362
+ data: j.encode(ae)
1321
1363
  },
1322
1364
  {
1323
1365
  name: "xl/_rels/workbook.xml.rels",
1324
- data: M.encode(ae)
1366
+ data: j.encode(oe)
1325
1367
  },
1326
1368
  {
1327
1369
  name: "xl/worksheets/sheet1.xml",
1328
- data: M.encode(n)
1370
+ data: j.encode(n)
1329
1371
  },
1330
1372
  {
1331
1373
  name: "xl/styles.xml",
1332
- data: M.encode(oe)
1374
+ data: j.encode(se)
1333
1375
  }
1334
1376
  ]);
1335
1377
  }
1336
1378
  //#endregion
1337
1379
  //#region src/export/export.ts
1338
- function L(e, t, n) {
1380
+ function I(e, t, n) {
1339
1381
  switch (n) {
1340
- case "csv": return z(e, t, ",");
1341
- case "tsv": return z(e, t, " ");
1342
- case "json": return V(e, t);
1343
- case "xlsx": return ce(e, t);
1382
+ case "csv": return R(e, t, ",");
1383
+ case "tsv": return R(e, t, " ");
1384
+ case "json": return B(e, t);
1385
+ case "xlsx": return le(e, t);
1344
1386
  }
1345
1387
  }
1346
- function R(e, t) {
1388
+ function L(e, t) {
1347
1389
  return e == null ? "" : (t.type === "date" || t.type === "datetime") && e instanceof Date ? e.toISOString() : String(e);
1348
1390
  }
1349
- function z(e, t, n) {
1350
- return [t.map((e) => B(e.header, n)).join(n), ...e.map((e) => t.map((t) => B(R(e[t.key], t), n)).join(n))].join("\n");
1391
+ function R(e, t, n) {
1392
+ return [t.map((e) => z(e.header, n)).join(n), ...e.map((e) => t.map((t) => z(L(e[t.key], t), n)).join(n))].join("\n");
1351
1393
  }
1352
- function B(e, t) {
1394
+ function z(e, t) {
1353
1395
  return e.includes(t) || e.includes("\"") || e.includes("\n") || e.includes("\r") ? "\"" + e.replace(/"/g, "\"\"") + "\"" : e;
1354
1396
  }
1355
- function V(e, t) {
1397
+ function B(e, t) {
1356
1398
  let n = e.map((e) => {
1357
1399
  let n = {};
1358
1400
  for (let r of t) {
@@ -1363,35 +1405,35 @@ function V(e, t) {
1363
1405
  });
1364
1406
  return JSON.stringify(n, null, 2);
1365
1407
  }
1366
- function H(e, t, n) {
1408
+ function V(e, t, n) {
1367
1409
  let r = new Blob([e], { type: n }), i = URL.createObjectURL(r), a = document.createElement("a");
1368
1410
  a.href = i, a.download = t, a.style.display = "none", document.body.appendChild(a), a.click(), document.body.removeChild(a), setTimeout(() => URL.revokeObjectURL(i), 100);
1369
1411
  }
1370
- var U = {
1412
+ var H = {
1371
1413
  csv: "text/csv;charset=utf-8",
1372
1414
  tsv: "text/tab-separated-values;charset=utf-8",
1373
1415
  json: "application/json;charset=utf-8",
1374
1416
  xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
1375
- }, W = {
1417
+ }, U = {
1376
1418
  csv: ".csv",
1377
1419
  tsv: ".tsv",
1378
1420
  json: ".json",
1379
1421
  xlsx: ".xlsx"
1380
1422
  };
1381
- function le(e) {
1382
- return U[e];
1383
- }
1384
1423
  function ue(e) {
1385
- return W[e];
1424
+ return H[e];
1425
+ }
1426
+ function de(e) {
1427
+ return U[e];
1386
1428
  }
1387
1429
  //#endregion
1388
1430
  //#region src/export/xlsx-reader.ts
1389
- function de(e) {
1431
+ function fe(e) {
1390
1432
  for (let t = e.byteLength - 22; t >= 0; t--) if (e.getUint32(t, !0) === 101010256) return t;
1391
1433
  throw Error("XLSX: End of central directory not found — not a valid ZIP file");
1392
1434
  }
1393
- function fe(e) {
1394
- let t = de(e), n = e.getUint32(t + 16, !0), r = e.getUint32(t + 12, !0), i = new TextDecoder("utf-8"), a = [], o = n, s = n + r;
1435
+ function pe(e) {
1436
+ let t = fe(e), n = e.getUint32(t + 16, !0), r = e.getUint32(t + 12, !0), i = new TextDecoder("utf-8"), a = [], o = n, s = n + r;
1395
1437
  for (; o < s && e.getUint32(o, !0) === 33639248;) {
1396
1438
  let t = e.getUint16(o + 10, !0), n = e.getUint32(o + 20, !0), r = e.getUint32(o + 24, !0), s = e.getUint16(o + 28, !0), c = e.getUint16(o + 30, !0), l = e.getUint16(o + 32, !0), u = e.getUint32(o + 42, !0), d = i.decode(new Uint8Array(e.buffer, o + 46, s));
1397
1439
  a.push({
@@ -1404,7 +1446,7 @@ function fe(e) {
1404
1446
  }
1405
1447
  return a;
1406
1448
  }
1407
- async function pe(e) {
1449
+ async function me(e) {
1408
1450
  let t = new DecompressionStream("deflate-raw"), n = t.writable.getWriter(), r = t.readable.getReader();
1409
1451
  n.write(e), n.close();
1410
1452
  let i = [];
@@ -1417,40 +1459,40 @@ async function pe(e) {
1417
1459
  for (let e of i) o.set(e, s), s += e.length;
1418
1460
  return o;
1419
1461
  }
1420
- async function me(e, t) {
1462
+ async function he(e, t) {
1421
1463
  let n = t.localHeaderOffset, r = e.getUint16(n + 26, !0), i = e.getUint16(n + 28, !0), a = n + 30 + r + i, o = new Uint8Array(e.buffer, a, t.compressedSize);
1422
1464
  if (t.compressionMethod === 0) return o.slice();
1423
- if (t.compressionMethod === 8) return pe(o);
1465
+ if (t.compressionMethod === 8) return me(o);
1424
1466
  throw Error(`XLSX: Unsupported compression method ${t.compressionMethod}`);
1425
1467
  }
1426
- async function he(e) {
1427
- let t = new DataView(e), n = fe(t), r = /* @__PURE__ */ new Map();
1428
- for (let e of n) e.compressedSize === 0 && e.name.endsWith("/") || r.set(e.name, await me(t, e));
1468
+ async function ge(e) {
1469
+ let t = new DataView(e), n = pe(t), r = /* @__PURE__ */ new Map();
1470
+ for (let e of n) e.compressedSize === 0 && e.name.endsWith("/") || r.set(e.name, await he(t, e));
1429
1471
  return r;
1430
1472
  }
1431
- var ge = new TextDecoder("utf-8");
1432
- function G(e) {
1433
- let t = ge.decode(e);
1473
+ var _e = new TextDecoder("utf-8");
1474
+ function W(e) {
1475
+ let t = _e.decode(e);
1434
1476
  return new DOMParser().parseFromString(t, "application/xml");
1435
1477
  }
1436
- function _e(e) {
1478
+ function ve(e) {
1437
1479
  if (!e) return [];
1438
- let t = G(e);
1480
+ let t = W(e);
1439
1481
  return Array.from(t.querySelectorAll("si")).map((e) => Array.from(e.querySelectorAll("t")).map((e) => e.textContent ?? "").join(""));
1440
1482
  }
1441
- function ve(e) {
1483
+ function ye(e) {
1442
1484
  let t = (e - 1) * 864e5 + Date.UTC(1899, 11, 30);
1443
1485
  return new Date(t).toISOString().slice(0, 10);
1444
1486
  }
1445
- function ye(e, t) {
1446
- let n = G(e).querySelectorAll("row");
1487
+ function be(e, t) {
1488
+ let n = W(e).querySelectorAll("row");
1447
1489
  if (n.length === 0) return [];
1448
1490
  let r = 0, i = /* @__PURE__ */ new Map();
1449
1491
  for (let e of n) {
1450
1492
  let n = parseInt(e.getAttribute("r") ?? "1", 10) - 1, a = /* @__PURE__ */ new Map();
1451
1493
  i.set(n, a);
1452
1494
  for (let n of e.querySelectorAll("c")) {
1453
- let e = be(n.getAttribute("r") ?? "");
1495
+ let e = xe(n.getAttribute("r") ?? "");
1454
1496
  e > r && (r = e);
1455
1497
  let i = n.getAttribute("t") ?? "", o = n.getAttribute("s") ?? "", s = n.querySelector("v")?.textContent ?? "", c = n.querySelector("is > t")?.textContent ?? null, l;
1456
1498
  if (i === "s") l = t[parseInt(s, 10)] ?? "";
@@ -1459,7 +1501,7 @@ function ye(e, t) {
1459
1501
  else if (s === "") l = "";
1460
1502
  else {
1461
1503
  let e = parseFloat(s);
1462
- l = !isNaN(e) && o !== "" && parseInt(o, 10) > 0 && e >= 1 && e <= 73050 ? ve(e) : s;
1504
+ l = !isNaN(e) && o !== "" && parseInt(o, 10) > 0 && e >= 1 && e <= 73050 ? ye(e) : s;
1463
1505
  }
1464
1506
  a.set(e, l);
1465
1507
  }
@@ -1473,13 +1515,13 @@ function ye(e, t) {
1473
1515
  }
1474
1516
  return o;
1475
1517
  }
1476
- function be(e) {
1518
+ function xe(e) {
1477
1519
  let t = 0, n = 0;
1478
1520
  for (; n < e.length && e[n] >= "A" && e[n] <= "Z";) t = t * 26 + (e.charCodeAt(n) - 64), n++;
1479
1521
  return t - 1;
1480
1522
  }
1481
- async function K(e) {
1482
- let t = await he(e), n = _e(t.get("xl/sharedStrings.xml")), r = t.get("xl/worksheets/sheet1.xml");
1523
+ async function G(e) {
1524
+ let t = await ge(e), n = ve(t.get("xl/sharedStrings.xml")), r = t.get("xl/worksheets/sheet1.xml");
1483
1525
  if (!r) {
1484
1526
  for (let [e, n] of t) if (e.toLowerCase().includes("worksheets/sheet")) {
1485
1527
  r = n;
@@ -1487,7 +1529,7 @@ async function K(e) {
1487
1529
  }
1488
1530
  }
1489
1531
  if (!r) throw Error("XLSX: No worksheet found");
1490
- let i = ye(r, n);
1532
+ let i = be(r, n);
1491
1533
  return i.length === 0 ? {
1492
1534
  headers: [],
1493
1535
  rows: []
@@ -1498,7 +1540,7 @@ async function K(e) {
1498
1540
  }
1499
1541
  //#endregion
1500
1542
  //#region \0@oxc-project+runtime@0.149.0/helpers/esm/decorate.js
1501
- function q(e, t, n, r) {
1543
+ function K(e, t, n, r) {
1502
1544
  var i = arguments.length, a = i < 3 ? t : r === null ? r = Object.getOwnPropertyDescriptor(t, n) : r, o;
1503
1545
  if (typeof Reflect == "object" && typeof Reflect.decorate == "function") a = Reflect.decorate(e, t, n, r);
1504
1546
  else for (var s = e.length - 1; s >= 0; s--) (o = e[s]) && (a = (i < 3 ? o(a) : i > 3 ? o(t, n, a) : o(t, n)) || a);
@@ -1506,14 +1548,14 @@ function q(e, t, n, r) {
1506
1548
  }
1507
1549
  //#endregion
1508
1550
  //#region src/flex-table.ts
1509
- var J = {
1551
+ var q = {
1510
1552
  eq: "=",
1511
1553
  neq: "≠",
1512
1554
  gt: ">",
1513
1555
  lt: "<",
1514
1556
  gte: "≥",
1515
1557
  lte: "≤"
1516
- }, Y = 120, X = 40, Z = 32, Q = 5, xe = 200, $ = class extends n {
1558
+ }, J = 120, Y = 40, X = 20, Z = 32, Q = 5, Se = 200, $ = class extends n {
1517
1559
  constructor(...e) {
1518
1560
  super(...e), this.columns = [], this._data = [], this._inUndoRedo = !1, this.clearUndoOnDataChange = !1, this.clearSelectionOnDataChange = !1, this._rowHeightFromCss = Z, this.showRowNumbers = !1, this.theme = void 0, this.maxRows = 0, this.editable = !0, this.showFilters = !1, this.showContextMenu = !1, this.frozenRows = 0, this.emptyMessage = "No data", this.noMatchingMessage = "No matching data", this.loading = !1, this.importEnabled = !1, this.selectable = !1, this.dataMode = "client", this.footerData = null, this.stylesheets = [], this._scrollTop = 0, this._scrollLeft = 0, this._viewportHeight = 0, this._viewportWidth = 0, this._colLeftOffsets = [], this._totalRowWidth = 0, this._baseStyleSheets = [], this._activeCell = null, this._editingCell = null, this._sortCriteria = [], this._selection = new x(), this._editing = new S(), this._rowSelection = new C(), this._undo = new E(), this._filters = [], this._filteredIndices = [], this._sortedIndices = [], this._openFilterKey = null, this._rowSelectionVersion = 0, this._lastCheckboxRowIndex = null, this._checkboxShiftPending = !1, this._autocompleteState = null, this._headerMenu = null, this._bodyContextMenu = null, this._commentPopup = null, this._viewDirty = !0, this._isDragging = !1, this._wasDrag = !1, this._resizing = null, this._resizeCleanup = null, this._wasHeaderDrag = !1, this._wasRowDrag = !1, this._colDrag = null, this._colDragIndicatorLeft = null, this._fillDrag = null, this._rowDrag = null, this._rowDragIndicatorY = null, this._findState = null, this._columnWidths = /* @__PURE__ */ new Map(), this._hostResizeObserver = null, this._comments = /* @__PURE__ */ new Map(), this._isDragOver = !1, this._warnedUnconstrained = !1, this._onCommentPopupOutsideClick = () => {
1519
1561
  this._commitCommentPopup();
@@ -1880,12 +1922,12 @@ var J = {
1880
1922
  if (!t) return "";
1881
1923
  let n = this.visibleColumns.slice(t.startCol, t.endCol + 1), r = [];
1882
1924
  for (let e = t.startRow; e <= t.endRow; e++) r.push(this.data[this._toDataIndex(e)]);
1883
- return L(r, n, e);
1925
+ return I(r, n, e);
1884
1926
  }
1885
- return L(this._sortedIndices.map((e) => this.data[e]), this.visibleColumns, e);
1927
+ return I(this._sortedIndices.map((e) => this.data[e]), this.visibleColumns, e);
1886
1928
  }
1887
1929
  exportToFile(e, t) {
1888
- H(this.exportToString(e), t ?? `export${ue(e)}`, le(e));
1930
+ V(this.exportToString(e), t ?? `export${de(e)}`, ue(e));
1889
1931
  }
1890
1932
  setComment(e, t, n) {
1891
1933
  let r = this.getComment(e, t);
@@ -1942,7 +1984,7 @@ var J = {
1942
1984
  async importFromFile(e) {
1943
1985
  let t = e.name.toLowerCase();
1944
1986
  if (t.endsWith(".xlsx")) {
1945
- let t = await K(await e.arrayBuffer());
1987
+ let t = await G(await e.arrayBuffer());
1946
1988
  this._applyImportedSheet(t);
1947
1989
  } else if (t.endsWith(".csv") || t.endsWith(".tsv")) {
1948
1990
  let t = O(await e.text());
@@ -2034,8 +2076,8 @@ var J = {
2034
2076
  return this._columnWidths.get(e);
2035
2077
  }
2036
2078
  _getColWidth(e) {
2037
- let t = this._columnWidths.get(e.key) ?? e.width ?? Y;
2038
- return Math.max(t, e.minWidth ?? X);
2079
+ let t = this._columnWidths.get(e.key) ?? e.width ?? J;
2080
+ return Math.max(t, e.minWidth ?? Y);
2039
2081
  }
2040
2082
  get headerHeight() {
2041
2083
  return this.rowHeight + 8;
@@ -2115,7 +2157,7 @@ var J = {
2115
2157
  (!(e.size <= 2 && !e.has("data") && !e.has("columns") && !e.has("_openFilterKey") && !e.has("_editingCell") && !e.has("_activeCell") && (e.has("_scrollTop") || e.has("_scrollLeft"))) || this._viewDirty) && (this._recomputeView(), this._viewDirty = !1), this._updateColOffsets(), this._selection.setDimensions(this._visibleRowCount, this.visibleColumns.length), this._rowSelection.setRowCount(this._visibleRowCount);
2116
2158
  }
2117
2159
  updated(e) {
2118
- this._focusEditor(), this._adjustFilterDropdown(), e.has("stylesheets") && this._syncStylesheets();
2160
+ this._focusEditor(), this._adjustFilterDropdown(), e.has("_headerMenu") && this._keepMenuInView(".ft-header-menu"), e.has("_bodyContextMenu") && this._bodyContextMenu && (this._keepMenuInView(".ft-body-context-menu"), this.shadowRoot?.querySelector(".ft-body-context-menu [role=\"menuitem\"]")?.focus()), e.has("stylesheets") && this._syncStylesheets();
2119
2161
  let t = String(this._visibleRowCount), n = String(this.visibleColumns.length);
2120
2162
  if (this.getAttribute("aria-rowcount") !== t && this.setAttribute("aria-rowcount", t), this.getAttribute("aria-colcount") !== n && this.setAttribute("aria-colcount", n), this.loading ? this.getAttribute("aria-busy") !== "true" && this.setAttribute("aria-busy", "true") : this.hasAttribute("aria-busy") && this.removeAttribute("aria-busy"), e.has("_commentPopup") && this._commentPopup) {
2121
2163
  let e = this.shadowRoot?.querySelector(".ft-comment-popup textarea");
@@ -2157,7 +2199,7 @@ var J = {
2157
2199
  e !== this._viewportHeight && (this._viewportHeight = e), t !== this._viewportWidth && (this._viewportWidth = t), this._warnIfUnconstrained();
2158
2200
  }
2159
2201
  _warnIfUnconstrained() {
2160
- this._warnedUnconstrained || process.env.NODE_ENV === "production" || this._visibleRowCount < xe || this.scrollHeight > this.clientHeight + 1 || (this._warnedUnconstrained = !0, console.warn(`[@iyulab/flex-table] ${this._visibleRowCount} rows but the host has no height constraint — it grew to fit every row, so virtual scrolling is off and all rows are rendered. Give the host a height (e.g. height: 400px, or flex: 1 1 auto with min-height: 0 inside a sized parent) so it becomes the scroll container.`));
2202
+ this._warnedUnconstrained || process.env.NODE_ENV === "production" || this._visibleRowCount < Se || this.scrollHeight > this.clientHeight + 1 || (this._warnedUnconstrained = !0, console.warn(`[@iyulab/flex-table] ${this._visibleRowCount} rows but the host has no height constraint — it grew to fit every row, so virtual scrolling is off and all rows are rendered. Give the host a height (e.g. height: 400px, or flex: 1 1 auto with min-height: 0 inside a sized parent) so it becomes the scroll container.`));
2161
2203
  }
2162
2204
  _onScroll() {
2163
2205
  this._scrollTop = this.scrollTop, this._scrollLeft = this.scrollLeft;
@@ -2481,17 +2523,21 @@ var J = {
2481
2523
  if (!e.altKey || !this._activeCell || e.key !== "ArrowLeft" && e.key !== "ArrowRight") return !1;
2482
2524
  e.preventDefault();
2483
2525
  let n = t[this._activeCell.col];
2484
- if (!n) return !0;
2485
- let r = this._columnWidths.get(n.key) ?? n.width ?? Y, i = e.key === "ArrowRight" ? 20 : -20, a = n.minWidth ?? X, o = Math.max(a, r + i);
2486
- return this._columnWidths.set(n.key, o), this.requestUpdate(), this.dispatchEvent(new CustomEvent("column-resize", {
2526
+ return n && this._resizeColumnBy(n.key, e.key === "ArrowRight" ? X : -20), !0;
2527
+ }
2528
+ _resizeColumnBy(e, t) {
2529
+ let n = this.visibleColumns.findIndex((t) => t.key === e), r = this.visibleColumns[n];
2530
+ if (!r) return;
2531
+ let i = this._columnWidths.get(r.key) ?? r.width ?? J, a = r.minWidth ?? Y, o = Math.max(a, i + t);
2532
+ this._columnWidths.set(r.key, o), this.requestUpdate(), this.dispatchEvent(new CustomEvent("column-resize", {
2487
2533
  detail: {
2488
- key: n.key,
2534
+ key: r.key,
2489
2535
  width: o,
2490
- colIndex: this._activeCell.col
2536
+ colIndex: n
2491
2537
  },
2492
2538
  bubbles: !0,
2493
2539
  composed: !0
2494
- })), !0;
2540
+ }));
2495
2541
  }
2496
2542
  _handleNavigation(e) {
2497
2543
  switch (e.key) {
@@ -2827,34 +2873,39 @@ var J = {
2827
2873
  <span>${e.header}</span>
2828
2874
  ${r ? i`<span class="ft-sort-indicator">${r.direction === "asc" ? "▲" : "▼"}</span>` : ""}
2829
2875
  ${o >= 0 ? i`<span class="ft-sort-order">${o + 1}</span>` : ""}
2830
- ${this.showFilters ? i`
2831
- <button class="ft-filter-btn ${s ? "ft-filter-active" : ""}"
2832
- title="Filter"
2833
- aria-label=${`Filter ${e.header}`}
2834
- aria-expanded=${this._openFilterKey === e.key ? "true" : "false"}
2835
- @click=${(t) => this._onFilterBtnClick(t, e)}>
2836
- \u25BD
2837
- </button>
2838
- ` : ""}
2876
+ <button class="ft-column-menu-btn ${s ? "ft-filter-active" : ""}"
2877
+ type="button"
2878
+ title="Column menu"
2879
+ aria-label=${`Column menu: ${e.header}`}
2880
+ aria-haspopup="menu"
2881
+ aria-expanded=${this._headerMenu?.key === e.key ? "true" : "false"}
2882
+ @mousedown=${(e) => e.stopPropagation()}
2883
+ @click=${(t) => this._onColumnMenuBtnClick(t, e)}>
2884
+ \u22EE
2885
+ </button>
2839
2886
  <div class="ft-resize-handle"
2840
2887
  @mousedown=${(e) => {
2841
2888
  e.stopPropagation(), this._wasHeaderDrag = !0, this._onResizeStart(e, t);
2842
2889
  }}
2843
- @dblclick=${(e) => this._onResizeAutoFit(e, t)}></div>
2890
+ @dblclick=${(t) => {
2891
+ t.preventDefault(), t.stopPropagation(), this._autoFitColumn(e.key);
2892
+ }}></div>
2844
2893
  </div>
2845
2894
  ${this._openFilterKey === e.key ? this._renderFilterDropdown(e) : ""}
2846
2895
  `;
2847
2896
  }
2897
+ _hiddenNeighbors(e) {
2898
+ let t = this.columns, n = t.findIndex((t) => t.key === e.key), r = [];
2899
+ for (let e = n - 1; e >= 0 && t[e].hidden; e--) r.push(t[e]);
2900
+ for (let e = n + 1; e < t.length && t[e].hidden; e++) r.push(t[e]);
2901
+ return r;
2902
+ }
2848
2903
  _onHeaderContextMenu(e, t) {
2849
- e.preventDefault();
2850
- let n = this.columns, r = n.findIndex((e) => e.key === t.key), i = [];
2851
- for (let e = r - 1; e >= 0 && n[e].hidden; e--) i.push(n[e]);
2852
- for (let e = r + 1; e < n.length && n[e].hidden; e++) i.push(n[e]);
2853
- this._headerMenu = {
2904
+ e.preventDefault(), this._openFilterKey = null, this._headerMenu = {
2854
2905
  key: t.key,
2855
2906
  x: e.clientX,
2856
2907
  y: e.clientY,
2857
- hiddenNeighbors: i
2908
+ hiddenNeighbors: this._hiddenNeighbors(t)
2858
2909
  }, this.dispatchEvent(new CustomEvent("header-context-menu", {
2859
2910
  detail: {
2860
2911
  key: t.key,
@@ -2868,118 +2919,139 @@ var J = {
2868
2919
  document.addEventListener("click", this._onDocumentClick, { once: !0 });
2869
2920
  });
2870
2921
  }
2922
+ _onColumnMenuBtnClick(e, t) {
2923
+ if (e.preventDefault(), e.stopPropagation(), this._headerMenu?.key === t.key) {
2924
+ this._headerMenu = null;
2925
+ return;
2926
+ }
2927
+ let n = e.currentTarget.getBoundingClientRect();
2928
+ this._openFilterKey = null, this._headerMenu = {
2929
+ key: t.key,
2930
+ x: n.left,
2931
+ y: n.bottom,
2932
+ hiddenNeighbors: this._hiddenNeighbors(t)
2933
+ }, requestAnimationFrame(() => {
2934
+ document.addEventListener("click", this._onDocumentClick, { once: !0 });
2935
+ }), this.updateComplete.then(() => this._menuItems()[0]?.focus());
2936
+ }
2937
+ _menuItems() {
2938
+ return Array.from(this.shadowRoot?.querySelectorAll(".ft-header-menu [role=\"menuitem\"]") ?? []);
2939
+ }
2940
+ _columnMenuButton(e) {
2941
+ let t = this.visibleColumns.findIndex((t) => t.key === e);
2942
+ return this.shadowRoot?.querySelector(`.ft-header-cell[data-col-index="${t}"] .ft-column-menu-btn`) ?? null;
2943
+ }
2944
+ _closeHeaderMenu(e) {
2945
+ let t = this._headerMenu?.key;
2946
+ this._headerMenu = null, e && t && this._columnMenuButton(t)?.focus();
2947
+ }
2948
+ _onHeaderMenuKeydown(e) {
2949
+ this._onMenuKeydown(e, ".ft-header-menu", (e) => this._closeHeaderMenu(e));
2950
+ }
2951
+ _onMenuKeydown(e, t, n) {
2952
+ let r = Array.from(this.shadowRoot?.querySelectorAll(`${t} [role="menuitem"]`) ?? []), i = r.indexOf(e.target), a = -1;
2953
+ switch (e.key) {
2954
+ case "ArrowDown":
2955
+ a = i < 0 ? 0 : (i + 1) % r.length;
2956
+ break;
2957
+ case "ArrowUp":
2958
+ a = i <= 0 ? r.length - 1 : i - 1;
2959
+ break;
2960
+ case "Home":
2961
+ a = 0;
2962
+ break;
2963
+ case "End":
2964
+ a = r.length - 1;
2965
+ break;
2966
+ case "Escape":
2967
+ e.preventDefault(), e.stopPropagation(), n(!0);
2968
+ return;
2969
+ case "Tab":
2970
+ n(!1);
2971
+ return;
2972
+ default: return;
2973
+ }
2974
+ e.preventDefault(), e.stopPropagation(), r[a]?.focus();
2975
+ }
2976
+ _openFilterFromMenu(e) {
2977
+ this._headerMenu = null, this._openFilterKey = e, requestAnimationFrame(() => {
2978
+ document.addEventListener("click", this._onDocumentClick, { once: !0 });
2979
+ }), this.updateComplete.then(() => {
2980
+ this.shadowRoot?.querySelector(".ft-filter-dropdown input, .ft-filter-dropdown select")?.focus();
2981
+ });
2982
+ }
2871
2983
  _showHiddenBefore(e) {
2872
2984
  let t = this.columns, n = t.findIndex((t) => t.key === e.key);
2873
2985
  for (let e = n - 1; e >= 0 && t[e].hidden; e--) this._setColumnHidden(t[e].key, !1);
2874
2986
  }
2875
2987
  _renderHeaderContextMenu() {
2876
2988
  if (!this._headerMenu) return a;
2877
- let { key: e, x: t, y: n, hiddenNeighbors: r } = this._headerMenu;
2878
- return this.columns.find((t) => t.key === e) ? i`
2879
- <div class="ft-header-menu" style="position: fixed; left: ${t}px; top: ${n}px; z-index: 200;"
2880
- @mousedown=${(e) => e.stopPropagation()}>
2881
- <div class="ft-header-menu-item"
2882
- @click=${() => {
2883
- this._setColumnHidden(e, !0), this._headerMenu = null;
2884
- }}>
2885
- Hide column
2886
- </div>
2887
- ${r.map((e) => i`
2888
- <div class="ft-header-menu-item"
2889
- @click=${() => {
2890
- this._setColumnHidden(e.key, !1), this._headerMenu = null;
2891
- }}>
2892
- Show: ${e.header}
2893
- </div>
2894
- `)}
2989
+ let { key: e, x: t, y: n, hiddenNeighbors: r } = this._headerMenu, o = this.columns.find((t) => t.key === e);
2990
+ if (!o) return a;
2991
+ let s = this._filters.some((t) => t.key === e), c = (e, t, n, r = !1) => i`
2992
+ <button type="button" role="menuitem" class="ft-header-menu-item" data-action=${e}
2993
+ @click=${(e) => {
2994
+ e.stopPropagation(), r || (this._headerMenu = null), n();
2995
+ }}>${t}</button>
2996
+ `;
2997
+ return i`
2998
+ <div class="ft-header-menu" role="menu" aria-label=${`${o.header} column`}
2999
+ style="position: fixed; left: ${t}px; top: ${n}px; z-index: 200;"
3000
+ @mousedown=${(e) => e.stopPropagation()}
3001
+ @keydown=${(e) => this._onHeaderMenuKeydown(e)}>
3002
+ ${o.sortable === !1 ? a : i`
3003
+ ${c("sort-asc", "Sort ascending", () => this._applySortFromMenu(e, "asc"))}
3004
+ ${c("sort-desc", "Sort descending", () => this._applySortFromMenu(e, "desc"))}
3005
+ <div class="ft-header-menu-separator" role="separator"></div>
3006
+ `}
3007
+ ${this.showFilters ? i`
3008
+ ${c("filter", "Filter…", () => this._openFilterFromMenu(e))}
3009
+ ${s ? c("clear-filter", "Clear filter", () => this._clearColumnFilter(e)) : a}
3010
+ <div class="ft-header-menu-separator" role="separator"></div>
3011
+ ` : a}
3012
+ ${c("hide", "Hide column", () => this._setColumnHidden(e, !0))}
3013
+ ${r.map((e) => c("show", i`Show: ${e.header}`, () => this._setColumnHidden(e.key, !1)))}
3014
+ <div class="ft-header-menu-separator" role="separator"></div>
3015
+ ${c("autofit", "Auto-fit width", () => this._autoFitColumn(e))}
3016
+ ${c("wider", "Wider", () => this._resizeColumnBy(e, X), !0)}
3017
+ ${c("narrower", "Narrower", () => this._resizeColumnBy(e, -20), !0)}
2895
3018
  </div>
2896
- ` : a;
3019
+ `;
2897
3020
  }
2898
3021
  _renderBodyContextMenu() {
2899
3022
  if (!this._bodyContextMenu) return a;
2900
3023
  let { colIndex: e, dataIndex: t, x: n, y: r } = this._bodyContextMenu, o = this.visibleColumns[e];
2901
3024
  if (!o) return a;
2902
- let s = this.data[t]?.[o.key], c = this._filters.some((e) => e.key === o.key), l = () => {
2903
- this._bodyContextMenu = null;
2904
- }, u = n + 200 > window.innerWidth ? n - 200 : n, d = r + 280 > window.innerHeight ? r - 280 : r;
3025
+ let s = this.data[t]?.[o.key], c = this._filters.some((e) => e.key === o.key), l = (e = !1) => {
3026
+ this._bodyContextMenu = null, e && this.focus();
3027
+ }, u = !!this.getComment(t, o.key), d = (e, t, n = !1) => i`
3028
+ <button type="button" role="menuitem"
3029
+ class="ft-context-menu-item ${n ? "ft-context-menu-danger" : ""}"
3030
+ @click=${() => {
3031
+ t(), l();
3032
+ }}>${e}</button>
3033
+ `, f = i`<div class="ft-context-menu-separator" role="separator"></div>`;
2905
3034
  return i`
2906
- <div class="ft-body-context-menu" style="position: fixed; left: ${u}px; top: ${d}px; z-index: 200;"
2907
- @mousedown=${(e) => e.stopPropagation()}>
2908
- <div class="ft-context-menu-item"
2909
- @click=${() => {
2910
- this._handleCopy(!1), l();
2911
- }}>
2912
- Copy
2913
- </div>
2914
- <div class="ft-context-menu-separator"></div>
2915
- <div class="ft-context-menu-item"
2916
- @click=${() => {
2917
- this.addRow(void 0, t), l();
2918
- }}>
2919
- Insert row above
2920
- </div>
2921
- <div class="ft-context-menu-item"
2922
- @click=${() => {
2923
- this.addRow(void 0, t + 1), l();
2924
- }}>
2925
- Insert row below
2926
- </div>
2927
- <div class="ft-context-menu-item ft-context-menu-danger"
2928
- @click=${() => {
2929
- this.deleteRows([t]), l();
2930
- }}>
2931
- Delete row
2932
- </div>
2933
- <div class="ft-context-menu-separator"></div>
2934
- <div class="ft-context-menu-item"
2935
- @click=${() => {
2936
- this._setColumnHidden(o.key, !0), l();
2937
- }}>
2938
- Hide column
2939
- </div>
2940
- <div class="ft-context-menu-separator"></div>
2941
- <div class="ft-context-menu-item"
2942
- @click=${() => {
2943
- this._applySortFromMenu(o.key, "asc"), l();
2944
- }}>
2945
- Sort ascending ↑
2946
- </div>
2947
- <div class="ft-context-menu-item"
2948
- @click=${() => {
2949
- this._applySortFromMenu(o.key, "desc"), l();
2950
- }}>
2951
- Sort descending ↓
2952
- </div>
2953
- <div class="ft-context-menu-separator"></div>
2954
- <div class="ft-context-menu-item"
2955
- @click=${() => {
2956
- this.setFilter(o.key, (e) => e === s), l();
2957
- }}>
2958
- Filter by this value
2959
- </div>
2960
- ${c ? i`
2961
- <div class="ft-context-menu-item"
2962
- @click=${() => {
2963
- this.removeFilter(o.key), l();
2964
- }}>
2965
- Clear filter
2966
- </div>
2967
- ` : ""}
2968
- <div class="ft-context-menu-separator"></div>
2969
- <div class="ft-context-menu-item"
2970
- @click=${() => {
2971
- this._openCommentPopup(t, o.key, n, r), l();
2972
- }}>
2973
- ${this.getComment(t, o.key) ? "Edit Comment" : "Add Comment"}
2974
- </div>
2975
- ${this.getComment(t, o.key) ? i`
2976
- <div class="ft-context-menu-item ft-context-menu-danger"
2977
- @click=${() => {
2978
- this.setComment(t, o.key, null), l();
2979
- }}>
2980
- Delete Comment
2981
- </div>
2982
- ` : ""}
3035
+ <div class="ft-body-context-menu" role="menu" aria-label="Cell actions"
3036
+ style="position: fixed; left: ${n}px; top: ${r}px; z-index: 200;"
3037
+ @mousedown=${(e) => e.stopPropagation()}
3038
+ @keydown=${(e) => this._onMenuKeydown(e, ".ft-body-context-menu", l)}>
3039
+ ${d("Copy", () => this._handleCopy(!1))}
3040
+ ${f}
3041
+ ${d("Insert row above", () => this.addRow(void 0, t))}
3042
+ ${d("Insert row below", () => this.addRow(void 0, t + 1))}
3043
+ ${d("Delete row", () => this.deleteRows([t]), !0)}
3044
+ ${f}
3045
+ ${d("Hide column", () => this._setColumnHidden(o.key, !0))}
3046
+ ${f}
3047
+ ${d("Sort ascending ↑", () => this._applySortFromMenu(o.key, "asc"))}
3048
+ ${d("Sort descending ↓", () => this._applySortFromMenu(o.key, "desc"))}
3049
+ ${f}
3050
+ ${d("Filter by this value", () => this.setFilter(o.key, (e) => e === s))}
3051
+ ${c ? d("Clear filter", () => this.removeFilter(o.key)) : a}
3052
+ ${f}
3053
+ ${d(u ? "Edit Comment" : "Add Comment", () => this._openCommentPopup(t, o.key, n, r))}
3054
+ ${u ? d("Delete Comment", () => this.setComment(t, o.key, null), !0) : a}
2983
3055
  </div>
2984
3056
  `;
2985
3057
  }
@@ -3022,15 +3094,22 @@ var J = {
3022
3094
  </div>
3023
3095
  `;
3024
3096
  }
3025
- _applySortFromMenu(e, n) {
3026
- this.columns.find((t) => t.key === e) && (this._sortCriteria = [{
3097
+ _applySortFromMenu(e, t) {
3098
+ let n = this.columns.find((t) => t.key === e);
3099
+ n && n.sortable !== !1 && (this._sortCriteria = [{
3027
3100
  key: e,
3028
- direction: n
3029
- }], this._sortedIndices = t(this.data, this._sortCriteria, this.columns), this.dispatchEvent(new CustomEvent("sort-change", {
3030
- detail: { sortCriteria: [...this._sortCriteria] },
3101
+ direction: t
3102
+ }], this.dataMode !== "server" && this._recomputeView(), this.requestUpdate(), this.dispatchEvent(new CustomEvent("sort-change", {
3103
+ detail: { criteria: [...this._sortCriteria] },
3031
3104
  bubbles: !0,
3032
3105
  composed: !0
3033
- })), this.requestUpdate());
3106
+ })));
3107
+ }
3108
+ _keepMenuInView(e) {
3109
+ let t = this.shadowRoot?.querySelector(e);
3110
+ if (!t) return;
3111
+ let n = t.getBoundingClientRect(), r = document.documentElement.clientWidth, i = document.documentElement.clientHeight;
3112
+ n.right > r - 4 && (t.style.left = `${Math.max(4, r - n.width - 4)}px`), n.bottom > i - 4 && (t.style.top = `${Math.max(4, i - n.height - 4)}px`);
3034
3113
  }
3035
3114
  _adjustFilterDropdown() {
3036
3115
  if (!this._openFilterKey) return;
@@ -3040,13 +3119,6 @@ var J = {
3040
3119
  let t = e.getBoundingClientRect(), n = window.innerHeight;
3041
3120
  t.bottom > n && (e.style.top = "auto", e.style.bottom = "100%");
3042
3121
  }
3043
- _onFilterBtnClick(e, t) {
3044
- e.preventDefault(), e.stopPropagation();
3045
- let n = this._openFilterKey === t.key ? null : t.key;
3046
- this._openFilterKey = n, n && requestAnimationFrame(() => {
3047
- document.addEventListener("click", this._onDocumentClick, { once: !0 });
3048
- });
3049
- }
3050
3122
  _renderFilterDropdown(e) {
3051
3123
  let t = e.type ?? "text";
3052
3124
  return i`
@@ -3229,7 +3301,7 @@ var J = {
3229
3301
  @keydown=${(e) => {
3230
3302
  e.key === "Escape" && (this._openFilterKey = null), e.stopPropagation();
3231
3303
  }}>
3232
- ${Object.keys(J).map((e) => i`<option value=${e}>${J[e]}</option>`)}
3304
+ ${Object.keys(q).map((e) => i`<option value=${e}>${q[e]}</option>`)}
3233
3305
  </select>
3234
3306
  <input class="ft-filter-input ft-num-cond-input" type="number" placeholder="Value"
3235
3307
  .value=${n.value == null ? "" : String(n.value)}
@@ -3334,13 +3406,12 @@ var J = {
3334
3406
  _clearColumnFilter(e) {
3335
3407
  this.removeFilter(e), this._textFilterState.delete(e), this._numberFilterState.delete(e), this._dateFilterState.delete(e), this._emptyFilterState.delete(e), this._openFilterKey = null;
3336
3408
  }
3337
- _onResizeAutoFit(e, t) {
3338
- e.preventDefault(), e.stopPropagation();
3339
- let n = this.visibleColumns[t];
3409
+ _autoFitColumn(e) {
3410
+ let t = this.visibleColumns.findIndex((t) => t.key === e), n = this.visibleColumns[t];
3340
3411
  if (!n) return;
3341
3412
  let r = this.shadowRoot?.querySelectorAll(`.ft-cell[data-col-index="${t}"]`), i = 0, a = this.shadowRoot?.querySelector(`.ft-header-cell[data-col-index="${t}"]`);
3342
3413
  if (a && (i = Math.max(i, a.scrollWidth)), r) for (let e of r) i = Math.max(i, e.scrollWidth);
3343
- let o = n.minWidth ?? X, s = Math.max(o, i + 8);
3414
+ let o = n.minWidth ?? Y, s = Math.max(o, i + 8);
3344
3415
  this._columnWidths.set(n.key, s), this.requestUpdate(), this.dispatchEvent(new CustomEvent("column-resize", {
3345
3416
  detail: {
3346
3417
  key: n.key,
@@ -3353,7 +3424,7 @@ var J = {
3353
3424
  }
3354
3425
  _onResizeStart(e, t) {
3355
3426
  e.preventDefault(), e.stopPropagation();
3356
- let n = this.visibleColumns[t], r = this._columnWidths.get(n.key) ?? n.width ?? Y;
3427
+ let n = this.visibleColumns[t], r = this._columnWidths.get(n.key) ?? n.width ?? J;
3357
3428
  this._resizing = {
3358
3429
  colIndex: t,
3359
3430
  startX: e.clientX,
@@ -3361,13 +3432,13 @@ var J = {
3361
3432
  };
3362
3433
  let i = (e) => {
3363
3434
  if (!this._resizing) return;
3364
- let t = e.clientX - this._resizing.startX, r = n.minWidth ?? X, i = Math.max(r, this._resizing.startWidth + t);
3435
+ let t = e.clientX - this._resizing.startX, r = n.minWidth ?? Y, i = Math.max(r, this._resizing.startWidth + t);
3365
3436
  this._columnWidths.set(n.key, i), this.requestUpdate();
3366
3437
  }, a = () => {
3367
3438
  document.removeEventListener("mousemove", i), document.removeEventListener("mouseup", o), this._resizeCleanup = null;
3368
3439
  }, o = () => {
3369
3440
  if (a(), this._resizing) {
3370
- let e = this._columnWidths.get(n.key) ?? Y;
3441
+ let e = this._columnWidths.get(n.key) ?? J;
3371
3442
  this.dispatchEvent(new CustomEvent("column-resize", {
3372
3443
  detail: {
3373
3444
  key: n.key,
@@ -4032,60 +4103,60 @@ var J = {
4032
4103
  return isNaN(t.getTime()) ? "" : `${t.getFullYear()}-${String(t.getMonth() + 1).padStart(2, "0")}-${String(t.getDate()).padStart(2, "0")}T${String(t.getHours()).padStart(2, "0")}:${String(t.getMinutes()).padStart(2, "0")}`;
4033
4104
  }
4034
4105
  };
4035
- q([s({ type: Array })], $.prototype, "columns", void 0), q([s({
4106
+ K([s({ type: Array })], $.prototype, "columns", void 0), K([s({
4036
4107
  type: Array,
4037
4108
  hasChanged: () => !0
4038
- })], $.prototype, "data", null), q([s({
4109
+ })], $.prototype, "data", null), K([s({
4039
4110
  type: Boolean,
4040
4111
  attribute: "clear-undo-on-data-change"
4041
- })], $.prototype, "clearUndoOnDataChange", void 0), q([s({
4112
+ })], $.prototype, "clearUndoOnDataChange", void 0), K([s({
4042
4113
  type: Boolean,
4043
4114
  attribute: "clear-selection-on-data-change"
4044
- })], $.prototype, "clearSelectionOnDataChange", void 0), q([s({
4115
+ })], $.prototype, "clearSelectionOnDataChange", void 0), K([s({
4045
4116
  type: Number,
4046
4117
  attribute: "row-height"
4047
- })], $.prototype, "rowHeight", null), q([s({
4118
+ })], $.prototype, "rowHeight", null), K([s({
4048
4119
  type: Boolean,
4049
4120
  attribute: "show-row-numbers"
4050
- })], $.prototype, "showRowNumbers", void 0), q([s({
4121
+ })], $.prototype, "showRowNumbers", void 0), K([s({
4051
4122
  type: String,
4052
4123
  reflect: !0
4053
- })], $.prototype, "theme", void 0), q([s({
4124
+ })], $.prototype, "theme", void 0), K([s({
4054
4125
  type: Number,
4055
4126
  attribute: "max-rows"
4056
- })], $.prototype, "maxRows", void 0), q([s({ type: Boolean })], $.prototype, "editable", void 0), q([s({
4127
+ })], $.prototype, "maxRows", void 0), K([s({ type: Boolean })], $.prototype, "editable", void 0), K([s({
4057
4128
  type: Boolean,
4058
4129
  attribute: "show-filters"
4059
- })], $.prototype, "showFilters", void 0), q([s({
4130
+ })], $.prototype, "showFilters", void 0), K([s({
4060
4131
  type: Boolean,
4061
4132
  attribute: "show-context-menu"
4062
- })], $.prototype, "showContextMenu", void 0), q([s({
4133
+ })], $.prototype, "showContextMenu", void 0), K([s({
4063
4134
  type: Number,
4064
4135
  attribute: "frozen-rows"
4065
- })], $.prototype, "frozenRows", void 0), q([s({
4136
+ })], $.prototype, "frozenRows", void 0), K([s({
4066
4137
  type: String,
4067
4138
  attribute: "empty-message"
4068
- })], $.prototype, "emptyMessage", void 0), q([s({
4139
+ })], $.prototype, "emptyMessage", void 0), K([s({
4069
4140
  type: String,
4070
4141
  attribute: "no-matching-message"
4071
- })], $.prototype, "noMatchingMessage", void 0), q([s({
4142
+ })], $.prototype, "noMatchingMessage", void 0), K([s({
4072
4143
  type: Boolean,
4073
4144
  reflect: !0
4074
- })], $.prototype, "loading", void 0), q([s({
4145
+ })], $.prototype, "loading", void 0), K([s({
4075
4146
  type: Boolean,
4076
4147
  attribute: "import-enabled"
4077
- })], $.prototype, "importEnabled", void 0), q([s({ type: Boolean })], $.prototype, "selectable", void 0), q([s({
4148
+ })], $.prototype, "importEnabled", void 0), K([s({ type: Boolean })], $.prototype, "selectable", void 0), K([s({
4078
4149
  type: String,
4079
4150
  attribute: "selection-mode"
4080
- })], $.prototype, "selectionMode", null), q([s({
4151
+ })], $.prototype, "selectionMode", null), K([s({
4081
4152
  type: String,
4082
4153
  attribute: "data-mode"
4083
- })], $.prototype, "dataMode", void 0), q([s({
4154
+ })], $.prototype, "dataMode", void 0), K([s({
4084
4155
  type: Object,
4085
4156
  attribute: "footer-data"
4086
- })], $.prototype, "footerData", void 0), q([s({ attribute: !1 })], $.prototype, "stylesheets", void 0), q([s({
4157
+ })], $.prototype, "footerData", void 0), K([s({ attribute: !1 })], $.prototype, "stylesheets", void 0), K([s({
4087
4158
  type: Number,
4088
4159
  attribute: "max-undo-size"
4089
- })], $.prototype, "maxUndoSize", null), q([c()], $.prototype, "_scrollTop", void 0), q([c()], $.prototype, "_scrollLeft", void 0), q([c()], $.prototype, "_viewportHeight", void 0), q([c()], $.prototype, "_viewportWidth", void 0), q([c()], $.prototype, "_activeCell", void 0), q([c()], $.prototype, "_editingCell", void 0), q([c()], $.prototype, "_sortCriteria", void 0), q([c()], $.prototype, "_openFilterKey", void 0), q([c()], $.prototype, "_rowSelectionVersion", void 0), q([c()], $.prototype, "_autocompleteState", void 0), q([c()], $.prototype, "_headerMenu", void 0), q([c()], $.prototype, "_bodyContextMenu", void 0), q([c()], $.prototype, "_commentPopup", void 0), q([c()], $.prototype, "_isDragOver", void 0), $ = q([o("flex-table")], $);
4160
+ })], $.prototype, "maxUndoSize", null), K([c()], $.prototype, "_scrollTop", void 0), K([c()], $.prototype, "_scrollLeft", void 0), K([c()], $.prototype, "_viewportHeight", void 0), K([c()], $.prototype, "_viewportWidth", void 0), K([c()], $.prototype, "_activeCell", void 0), K([c()], $.prototype, "_editingCell", void 0), K([c()], $.prototype, "_sortCriteria", void 0), K([c()], $.prototype, "_openFilterKey", void 0), K([c()], $.prototype, "_rowSelectionVersion", void 0), K([c()], $.prototype, "_autocompleteState", void 0), K([c()], $.prototype, "_headerMenu", void 0), K([c()], $.prototype, "_bodyContextMenu", void 0), K([c()], $.prototype, "_commentPopup", void 0), K([c()], $.prototype, "_isDragOver", void 0), $ = K([o("flex-table")], $);
4090
4161
  //#endregion
4091
- export { m as a, C as i, L as n, E as r, $ as t };
4162
+ export { m as a, C as i, I as n, E as r, $ as t };
@@ -366,6 +366,8 @@ export declare class FlexTable extends LitElement {
366
366
  private _onKeyDown;
367
367
  private _handleCtrlKey;
368
368
  private _handleAltKey;
369
+ /** Widen (positive) or narrow (negative) a visible column by `delta` px, never below its minimum. */
370
+ private _resizeColumnBy;
369
371
  private _handleNavigation;
370
372
  private _handleCopy;
371
373
  private _handlePaste;
@@ -402,7 +404,26 @@ export declare class FlexTable extends LitElement {
402
404
  */
403
405
  private _getPinnedRightLeft;
404
406
  private _renderHeaderCell;
407
+ /** Hidden columns directly adjacent to `col` (before and after) — the ones its menu can show again. */
408
+ private _hiddenNeighbors;
405
409
  private _onHeaderContextMenu;
410
+ /**
411
+ * The header's column menu button — the same menu a right-click opens, anchored under the button.
412
+ * It is the pointer-sized, keyboard-reachable way to every per-column action: filtering, hiding,
413
+ * and resizing without dragging the 6px handle (WCAG 2.2 SC 2.5.7 / 2.5.8).
414
+ */
415
+ private _onColumnMenuBtnClick;
416
+ private _menuItems;
417
+ private _columnMenuButton;
418
+ private _closeHeaderMenu;
419
+ private _onHeaderMenuKeydown;
420
+ /**
421
+ * Menu keyboard pattern shared by the column menu and the cell context menu: arrows and Home/End
422
+ * move focus between the items (wrapping), Escape closes and returns focus, Tab closes.
423
+ */
424
+ private _onMenuKeydown;
425
+ /** Open the filter dropdown for `key` from its column menu, focusing its first control. */
426
+ private _openFilterFromMenu;
406
427
  private _showHiddenBefore;
407
428
  private _renderHeaderContextMenu;
408
429
  private _renderBodyContextMenu;
@@ -411,9 +432,15 @@ export declare class FlexTable extends LitElement {
411
432
  private _commitCommentPopup;
412
433
  private _cancelCommentPopup;
413
434
  private _renderCommentPopup;
435
+ /**
436
+ * Sort by one column from a menu — the column menu (the keyboard path to sorting) and the cell
437
+ * context menu. Same contract as a header click: server mode only reports, and `sort-change`
438
+ * carries `{ criteria }`.
439
+ */
414
440
  private _applySortFromMenu;
441
+ /** Keep a fixed-position menu inside the viewport — a menu opened near the right or bottom edge would spill off-screen. */
442
+ private _keepMenuInView;
415
443
  private _adjustFilterDropdown;
416
- private _onFilterBtnClick;
417
444
  private _renderFilterDropdown;
418
445
  private _buildTextPredicate;
419
446
  private _applyTextFilter;
@@ -439,7 +466,8 @@ export declare class FlexTable extends LitElement {
439
466
  private _applyDateFilter;
440
467
  private _renderBooleanFilter;
441
468
  private _clearColumnFilter;
442
- private _onResizeAutoFit;
469
+ /** Size a column to its widest rendered content — the resize handle's double-click and the column menu. */
470
+ private _autoFitColumn;
443
471
  private _onResizeStart;
444
472
  private _onHeaderMouseDown;
445
473
  private _startColumnGhost;
@@ -1,2 +1,2 @@
1
- import { a as e, i as t, n, r, t as i } from "./flex-table-BbeD49ly.js";
1
+ import { a as e, i as t, n, r, t as i } from "./flex-table-Y4GyDV0Y.js";
2
2
  export { i as FlexTable, t as RowSelectionState, r as UndoStack, n as exportData, e as renderCell };
package/dist/react.js CHANGED
@@ -1,4 +1,4 @@
1
- import { t as e } from "./flex-table-BbeD49ly.js";
1
+ import { t as e } from "./flex-table-Y4GyDV0Y.js";
2
2
  import t from "react";
3
3
  import { createComponent as n } from "@lit/react";
4
4
  var r = n({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iyulab/flex-table",
3
- "version": "0.34.3",
3
+ "version": "0.35.0",
4
4
  "description": "A minimalist, input-centric data grid web component",
5
5
  "type": "module",
6
6
  "main": "./dist/flex-table.js",
@@ -46,7 +46,6 @@
46
46
  "typecheck": "tsc --noEmit && tsc -p tsconfig.test.json",
47
47
  "test": "vitest run",
48
48
  "test:watch": "vitest",
49
- "test:coverage": "vitest run --coverage",
50
49
  "lint": "eslint src/",
51
50
  "lint:fix": "eslint src/ --fix"
52
51
  },
@@ -91,16 +90,14 @@
91
90
  "@types/react-dom": "^19.2.7",
92
91
  "@typescript-eslint/eslint-plugin": "^8.64.0",
93
92
  "@typescript-eslint/parser": "^8.64.0",
94
- "@vitest/browser-playwright": "^4.1.10",
93
+ "@vitest/browser-playwright": "^5.0.0",
95
94
  "eslint": "^10.1.0",
96
95
  "eslint-plugin-lit": "^2.3.1",
97
- "jsdom": "^29.1.1",
98
- "loupe": "^3.2.1",
96
+ "jsdom": "^30.0.1",
99
97
  "react": "^19.2.7",
100
98
  "react-dom": "^19.2.8",
101
- "strip-literal": "^3.1.0",
102
99
  "typescript": "^6.0.2",
103
100
  "vite": "^8.1.4",
104
- "vitest": "^4.1.10"
101
+ "vitest": "^5.0.0"
105
102
  }
106
103
  }