flexdesk 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/README.md +26 -0
  2. package/css/base.css +2243 -872
  3. package/css/flexdesk.css +1375 -14
  4. package/css/overrides.css +44 -0
  5. package/css/tokens.css +45 -0
  6. package/dist/charts.js +5 -3
  7. package/dist/charts.js.map +1 -1
  8. package/dist/{chunk-DVU44T77.js → chunk-ELXVW542.js} +196 -75
  9. package/dist/chunk-ELXVW542.js.map +7 -0
  10. package/dist/chunk-LH5TSOZW.js +1237 -0
  11. package/dist/chunk-LH5TSOZW.js.map +7 -0
  12. package/dist/{chunk-TLZUUFOE.js → chunk-O5OHMWBB.js} +10 -2
  13. package/dist/chunk-O5OHMWBB.js.map +7 -0
  14. package/dist/{chunk-CT4YXXLP.js → chunk-QIU5S2RU.js} +371 -73
  15. package/dist/chunk-QIU5S2RU.js.map +7 -0
  16. package/dist/chunk-QNQHQ24V.js +408 -0
  17. package/dist/chunk-QNQHQ24V.js.map +7 -0
  18. package/dist/{chunk-DRYCDMEG.js → chunk-XKDTIT4Q.js} +168 -12
  19. package/dist/chunk-XKDTIT4Q.js.map +7 -0
  20. package/dist/editor.js +3 -380
  21. package/dist/editor.js.map +3 -3
  22. package/dist/flexdesk.css +1375 -14
  23. package/dist/tiles.js +168 -41
  24. package/dist/tiles.js.map +2 -2
  25. package/dist/tokens.css +45 -0
  26. package/dist/widgets.js +44 -14
  27. package/dist/widgets.js.map +2 -2
  28. package/dist/wm.js +3140 -157
  29. package/dist/wm.js.map +4 -4
  30. package/package.json +3 -2
  31. package/src/charts/chart_types.js +167 -0
  32. package/src/charts/plotly_wrapper.js +178 -10
  33. package/src/editor/notebook_tab_bar.js +39 -3
  34. package/src/tiles/tile_base.js +143 -35
  35. package/src/tiles/tile_grid.js +52 -1
  36. package/src/tiling/command_palette.js +71 -18
  37. package/src/tiling/desktops.js +36 -12
  38. package/src/tiling/keymap.js +24 -4
  39. package/src/tiling/shell.js +156 -25
  40. package/src/tiling/tab_strip.js +184 -0
  41. package/src/tiling/tile_breadcrumb.js +34 -2
  42. package/src/tiling/tile_renderer.js +1386 -21
  43. package/src/tiling/tile_tab_menu.js +101 -0
  44. package/src/tiling/tile_tree.js +115 -11
  45. package/src/tiling/wm.js +2375 -84
  46. package/src/tiling/zoom.js +248 -0
  47. package/src/ui/components/action_dropdown.js +34 -3
  48. package/src/ui/components/autocomplete_field.js +65 -13
  49. package/src/ui/components/context_menu.js +79 -8
  50. package/src/ui/components/data_table.js +508 -84
  51. package/src/ui/components/managed_window.js +928 -36
  52. package/src/ui/components/modal.js +214 -8
  53. package/dist/chunk-CT4YXXLP.js.map +0 -7
  54. package/dist/chunk-DRYCDMEG.js.map +0 -7
  55. package/dist/chunk-DVU44T77.js.map +0 -7
  56. package/dist/chunk-TLZUUFOE.js.map +0 -7
  57. package/dist/chunk-UCJ2WD4D.js +0 -625
  58. package/dist/chunk-UCJ2WD4D.js.map +0 -7
@@ -15,6 +15,7 @@ function createRafResizeObserver(callback) {
15
15
 
16
16
  // src/ui/components/data_table.js
17
17
  var DEFAULT_PAGE_SIZE = 100;
18
+ var AUTOSIZE_MAX_PX = 520;
18
19
  var DataTable = class {
19
20
  /**
20
21
  * Create a DataTable instance
@@ -64,6 +65,12 @@ var DataTable = class {
64
65
  // ephemeral. WHERE the store persists is the embedder's business.
65
66
  persistKey: null,
66
67
  stateStore: null,
68
+ // How the automatic fit pass sizes an undragged column — see the
69
+ // typedef. 'container' is exactly what every consumer got before
70
+ // this key existed, so the default is not a preference: it is the
71
+ // promise that adding the key changed no existing layout by a
72
+ // pixel. Only a consumer that asks for 'content' sees anything new.
73
+ columnFit: "container",
67
74
  ...config
68
75
  };
69
76
  if (this.config.persistKey && !this.config.stateStore) {
@@ -1189,38 +1196,7 @@ var DataTable = class {
1189
1196
  if (selected.has(globalIdx)) {
1190
1197
  tr.classList.add("selected");
1191
1198
  }
1192
- if (showRowNumbers) {
1193
- const td = document.createElement("td");
1194
- td.className = "num";
1195
- td.textContent = String(globalIdx + 1);
1196
- tr.appendChild(td);
1197
- }
1198
- for (let colIdx = 0; colIdx < row.length; colIdx++) {
1199
- const td = document.createElement("td");
1200
- td.className = this._columnTypes[colIdx] || "text";
1201
- const value = row[colIdx];
1202
- if (this.config.renderCell) {
1203
- const handled = this.config.renderCell(td, value, colIdx, globalIdx, row);
1204
- if (!handled) {
1205
- td.textContent = this._formatValue(value, colIdx);
1206
- }
1207
- } else {
1208
- td.textContent = this._formatValue(value, colIdx);
1209
- }
1210
- if (this.config.onCellContextMenu) {
1211
- const cellColIdx = colIdx;
1212
- td.addEventListener("contextmenu", (ev) => {
1213
- this.config.onCellContextMenu(
1214
- cellColIdx,
1215
- globalIdx,
1216
- value,
1217
- td,
1218
- ev
1219
- );
1220
- });
1221
- }
1222
- tr.appendChild(td);
1223
- }
1199
+ this._fillRowCells(tr, row, globalIdx, showRowNumbers);
1224
1200
  if (this.config.onRowClick) {
1225
1201
  tr.addEventListener("click", (ev) => {
1226
1202
  this.config.onRowClick(globalIdx, row, ev);
@@ -1237,21 +1213,148 @@ var DataTable = class {
1237
1213
  this._tbodyEl = tbody;
1238
1214
  return table;
1239
1215
  }
1216
+ /** Build (or rebuild) one row's cells in place.
1217
+ *
1218
+ * Extracted from the body loop so that `updateRow` and the initial
1219
+ * render share ONE cell-building path. Two paths would drift, and the
1220
+ * drift would show as a cell that renders differently after a live
1221
+ * update than it did on load. */
1222
+ _fillRowCells(tr, row, globalIdx, showRowNumbers) {
1223
+ tr.replaceChildren();
1224
+ if (showRowNumbers) {
1225
+ const td = document.createElement("td");
1226
+ td.className = "num";
1227
+ td.textContent = String(globalIdx + 1);
1228
+ tr.appendChild(td);
1229
+ }
1230
+ for (let colIdx = 0; colIdx < row.length; colIdx++) {
1231
+ const td = document.createElement("td");
1232
+ td.className = this._columnTypes[colIdx] || "text";
1233
+ const value = row[colIdx];
1234
+ if (this.config.renderCell) {
1235
+ const handled = this.config.renderCell(td, value, colIdx, globalIdx, row);
1236
+ if (!handled) {
1237
+ td.textContent = this._formatValue(value, colIdx);
1238
+ }
1239
+ } else {
1240
+ td.textContent = this._formatValue(value, colIdx);
1241
+ }
1242
+ if (this.config.onCellContextMenu) {
1243
+ const cellColIdx = colIdx;
1244
+ td.addEventListener("contextmenu", (ev) => {
1245
+ this.config.onCellContextMenu(
1246
+ cellColIdx,
1247
+ globalIdx,
1248
+ value,
1249
+ td,
1250
+ ev
1251
+ );
1252
+ });
1253
+ }
1254
+ tr.appendChild(td);
1255
+ }
1256
+ }
1257
+ /** Re-render ONE row in place, preserving everything around it.
1258
+ *
1259
+ * `render()` rebuilds the entire `<tbody>`, which takes the scroll
1260
+ * position, any open editor, the keyboard focus and the measured column
1261
+ * widths with it. That is fine for a sort or a page change and wrong for
1262
+ * a single-cell commit or a live update arriving over a socket — the
1263
+ * common case in an editable grid, where a full rebuild once per keystroke
1264
+ * is both visible and destructive.
1265
+ *
1266
+ * What survives, by construction:
1267
+ * - scroll position, because the tbody is not replaced;
1268
+ * - the separately-rendered thead/tbody column widths, because the
1269
+ * explicit widths live on the header cells and on the FIRST body row,
1270
+ * and all three of the properties `_setCellWidth` writes are re-applied
1271
+ * here when that first row is the one being replaced;
1272
+ * - selection, because the `selected` class is recomputed from the
1273
+ * selection set rather than carried on the old element;
1274
+ * - keyboard focus, because the focused element's position is recorded
1275
+ * before the replace and restored after.
1276
+ *
1277
+ * @param {number} index row index as tracked by `tr.__rowIndex`
1278
+ * @param {any[]} row the new cell values
1279
+ * @returns {boolean} false when the row is not currently rendered
1280
+ * (it is on another page, or outside the render
1281
+ * window) — which is NOT an error: the caller has
1282
+ * nothing to update on screen.
1283
+ */
1284
+ updateRow(index, row) {
1285
+ const tbody = this._tbodyEl;
1286
+ if (!tbody) return false;
1287
+ let tr = null;
1288
+ for (const candidate of tbody.children) {
1289
+ if (candidate.__rowIndex === index) {
1290
+ tr = candidate;
1291
+ break;
1292
+ }
1293
+ }
1294
+ if (!tr) return false;
1295
+ if (Array.isArray(this.config.rows) && this.config.rows[index]) {
1296
+ this.config.rows[index] = row;
1297
+ }
1298
+ const active = document.activeElement;
1299
+ let focusedCol = -1;
1300
+ if (active && tr.contains(active)) {
1301
+ focusedCol = Array.prototype.indexOf.call(tr.children, active.closest("td"));
1302
+ }
1303
+ const isFirstRow = tr === tbody.firstElementChild;
1304
+ const widths = isFirstRow ? Array.prototype.map.call(tr.children, (td) => ({
1305
+ width: td.style.width,
1306
+ minWidth: td.style.minWidth,
1307
+ maxWidth: td.style.maxWidth
1308
+ })) : null;
1309
+ const showRowNumbers = this.config.showRowNumbers !== false && tr.firstElementChild?.classList.contains("num");
1310
+ this._fillRowCells(tr, row, index, showRowNumbers);
1311
+ if (widths) {
1312
+ widths.forEach((saved, i) => {
1313
+ const cell = tr.children[i];
1314
+ if (!saved.width || !cell) return;
1315
+ cell.style.width = saved.width;
1316
+ cell.style.minWidth = saved.minWidth;
1317
+ cell.style.maxWidth = saved.maxWidth;
1318
+ });
1319
+ }
1320
+ tr.classList.toggle("selected", this._state?.selected?.has(index) === true);
1321
+ if (focusedCol >= 0 && tr.children[focusedCol]) {
1322
+ tr.children[focusedCol].focus?.();
1323
+ }
1324
+ return true;
1325
+ }
1240
1326
  /** Sync the (separate) header table's column widths to the body
1241
1327
  * table's measured widths. Without this, the two tables compute
1242
1328
  * widths independently and the columns drift apart. Locks both
1243
1329
  * tables to `table-layout: fixed` and writes explicit width onto
1244
1330
  * each header cell of every header row (header + filter row). */
1245
- _syncHeaderWidths() {
1331
+ /**
1332
+ * Measure every column's NATURAL content width, in one transient reflow.
1333
+ *
1334
+ * Extracted from `_syncHeaderWidths` so that auto-size can ask the same
1335
+ * question the fit pass asks, and get the same answer. Two measurement
1336
+ * passes would drift, and the drift would show as a double-click that
1337
+ * sized a column differently from the render that follows it.
1338
+ *
1339
+ * The pass ignores the CSS caps (the 150px-pinned first column, the 80px
1340
+ * min on the rest) and the filter-row inputs: `twm-dt-measuring` flips both
1341
+ * tables to `table-layout:auto; width:max-content` with those caps off (via
1342
+ * `!important`) for a single reflow, then reverts before paint — it is
1343
+ * never visible. Clearing inline widths first stops the last sync's forced
1344
+ * widths from constraining the measure.
1345
+ *
1346
+ * @returns {number[]|null} width per DOM column index, or null when there
1347
+ * is nothing laid out to measure.
1348
+ */
1349
+ _measureNaturalWidths() {
1246
1350
  const headerTable = this._headerTableEl;
1247
1351
  const bodyTable = this._tableEl;
1248
- if (!headerTable || !bodyTable) return;
1352
+ if (!headerTable || !bodyTable) return null;
1249
1353
  const firstRow = bodyTable.querySelector("tbody > tr");
1250
- if (!firstRow) return;
1354
+ if (!firstRow) return null;
1251
1355
  const bodyCells = firstRow.children;
1252
- if (!bodyCells.length) return;
1253
- const headerRows = headerTable.querySelectorAll("thead > tr");
1254
- headerRows.forEach((tr) => {
1356
+ if (!bodyCells.length) return null;
1357
+ headerTable.querySelectorAll("thead > tr").forEach((tr) => {
1255
1358
  for (const th of tr.children) this._clearCellWidth(th);
1256
1359
  });
1257
1360
  for (const td of bodyCells) this._clearCellWidth(td);
@@ -1263,20 +1366,120 @@ var DataTable = class {
1263
1366
  const labelRow = headerTable.querySelector("thead > tr");
1264
1367
  const cols = bodyCells.length;
1265
1368
  const natural = new Array(cols);
1369
+ const zoom = bodyTable.currentCSSZoom || 1;
1266
1370
  for (let i = 0; i < cols; i++) {
1267
- const body = bodyCells[i].getBoundingClientRect().width;
1268
- const head = labelRow && labelRow.children[i] ? labelRow.children[i].getBoundingClientRect().width : 0;
1371
+ const body = bodyCells[i].getBoundingClientRect().width / zoom;
1372
+ const head = labelRow && labelRow.children[i] ? labelRow.children[i].getBoundingClientRect().width / zoom : 0;
1269
1373
  natural[i] = Math.max(body, head);
1270
1374
  }
1271
1375
  headerTable.classList.remove("twm-dt-measuring");
1272
1376
  bodyTable.classList.remove("twm-dt-measuring");
1377
+ return natural;
1378
+ }
1379
+ /**
1380
+ * Fit one column to its widest visible value and pin it there.
1381
+ *
1382
+ * The gesture is a double-click on the column's resize grip, which is where
1383
+ * every spreadsheet has put it — and the grip is this component's, which is
1384
+ * why the behaviour is too. A consumer that wanted this had no hook to hang
1385
+ * it on: `_installColumnResizers` bound `mousedown` and a `click` that only
1386
+ * suppressed the sort, and the measurement, the pin, the table-width
1387
+ * invariant and the write-through to `stateStore` are all private here.
1388
+ *
1389
+ * @param {number} domIdx column index INCLUDING the row-number column when
1390
+ * `showRowNumbers` is on — the same index space as `_colWidths`.
1391
+ * @param {{maxWidth?: number}} [opts] ceiling; defaults to AUTOSIZE_MAX_PX.
1392
+ * @returns {boolean} whether it had a layout to measure.
1393
+ */
1394
+ autoSizeColumn(domIdx, opts = {}) {
1395
+ const natural = this._measureNaturalWidths();
1396
+ if (!natural || domIdx < 0 || domIdx >= natural.length) return false;
1397
+ this._colWidths[domIdx] = this._autoWidth(natural[domIdx], opts);
1398
+ this._colWidthsSig = this._colSig();
1399
+ this._savePersisted();
1400
+ this._syncHeaderWidths();
1401
+ return true;
1402
+ }
1403
+ /**
1404
+ * The same for every column at once.
1405
+ *
1406
+ * It REPLACES existing drag overrides rather than sizing around them: "fit
1407
+ * every column to its content" that quietly excepted the three columns you
1408
+ * had dragged would be a button whose result depends on history nobody can
1409
+ * see.
1410
+ *
1411
+ * The result may well be wider than the container — forty columns of real
1412
+ * content usually are — and that is the intended answer, not a failure:
1413
+ * `_syncHeaderWidths` honours overrides verbatim, sizes both tables to their
1414
+ * total and the body wrap scrolls sideways with the header following it.
1415
+ * Squeezing them to fit is what the automatic fit pass does on every render
1416
+ * already, so a button that did that would be a button that does nothing.
1417
+ */
1418
+ autoSizeColumns(opts = {}) {
1419
+ const natural = this._measureNaturalWidths();
1420
+ if (!natural) return false;
1421
+ this._colWidths = {};
1422
+ for (let i = 0; i < natural.length; i++) {
1423
+ this._colWidths[i] = this._autoWidth(natural[i], opts);
1424
+ }
1425
+ this._colWidthsSig = this._colSig();
1426
+ this._savePersisted();
1427
+ this._syncHeaderWidths();
1428
+ return true;
1429
+ }
1430
+ /** One measured width, floored at the drag-resize minimum and capped, with
1431
+ * the same sub-pixel pad `_fitColumnWidths` adds against ellipsis. */
1432
+ _autoWidth(natural, opts = {}) {
1433
+ const max = opts.maxWidth ?? AUTOSIZE_MAX_PX;
1434
+ return Math.min(max, Math.max(40, Math.ceil((natural || 0) + 2)));
1435
+ }
1436
+ /**
1437
+ * Does every one of the `n` DOM columns already carry an override taken
1438
+ * against the column set that is on screen right now?
1439
+ *
1440
+ * The signature half is not belt-and-braces. `render()` drops `_colWidths`
1441
+ * when `_colSig` changes, but `_syncHeaderWidths` is also reached straight
1442
+ * from the ResizeObserver, which never goes through `render()` — so a set
1443
+ * of overrides measured against the PREVIOUS headers can still be sitting
1444
+ * in `_colWidths` when this is asked. Answering "pinned" then would skip
1445
+ * the measure and hand `_fitColumnWidths` widths keyed to columns that are
1446
+ * no longer there, each one landing on its neighbour.
1447
+ *
1448
+ * `== null` rather than a falsy test, to match `_fitColumnWidths`' own
1449
+ * `overrides[i] != null`: a legitimately-stored 0 must be read the same way
1450
+ * in both places or the two disagree about which columns are flexible.
1451
+ */
1452
+ _allColumnsPinned(n) {
1453
+ if (!n || this._colWidthsSig !== this._colSig()) return false;
1454
+ for (let i = 0; i < n; i++) {
1455
+ if (this._colWidths[i] == null) return false;
1456
+ }
1457
+ return true;
1458
+ }
1459
+ /** Sync the (separate) header table's column widths to the body
1460
+ * table's measured widths. Without this, the two tables compute
1461
+ * widths independently and the columns drift apart. Locks both
1462
+ * tables to `table-layout: fixed` and writes explicit width onto
1463
+ * each header cell of every header row (header + filter row). */
1464
+ _syncHeaderWidths() {
1465
+ const headerTable = this._headerTableEl;
1466
+ const bodyTable = this._tableEl;
1467
+ if (!headerTable || !bodyTable) return;
1468
+ const firstRow = bodyTable.querySelector("tbody > tr");
1469
+ if (!firstRow) return;
1470
+ const bodyCells = firstRow.children;
1471
+ if (!bodyCells.length) return;
1472
+ const headerRows = headerTable.querySelectorAll("thead > tr");
1473
+ const natural = this._allColumnsPinned(bodyCells.length) ? new Array(bodyCells.length).fill(0) : this._measureNaturalWidths();
1474
+ if (!natural) return;
1273
1475
  const wrap = this._tableWrapEl;
1274
1476
  const headerWrap = this._headerWrapEl;
1275
1477
  const avail = wrap ? wrap.clientWidth : 0;
1276
- const firstIdx = this.config.showRowNumbers && cols > 1 ? 1 : 0;
1478
+ const firstIdx = this.config.showRowNumbers && bodyCells.length > 1 ? 1 : 0;
1277
1479
  const widths = this._fitColumnWidths(natural, avail, {
1278
1480
  firstIdx,
1279
- overrides: this._colWidths
1481
+ overrides: this._colWidths,
1482
+ fit: this.config.columnFit
1280
1483
  });
1281
1484
  const total = widths.reduce((a, b) => a + b, 0);
1282
1485
  headerRows.forEach((tr) => {
@@ -1300,6 +1503,7 @@ var DataTable = class {
1300
1503
  if (wrap && headerWrap) {
1301
1504
  const sbw = Math.max(0, wrap.offsetWidth - wrap.clientWidth);
1302
1505
  headerWrap.style.paddingRight = sbw ? `${sbw}px` : "";
1506
+ this._wrapperEl?.style?.setProperty("--twm-dt-gutter", `${sbw}px`);
1303
1507
  }
1304
1508
  this._syncHeaderScroll();
1305
1509
  this._updateCellTooltips();
@@ -1318,9 +1522,17 @@ var DataTable = class {
1318
1522
  * the rest (trim the widest first) until it fits; only if even the
1319
1523
  * floors overflow do we give up and let the body scroll sideways.
1320
1524
  *
1525
+ * Under `fit: 'content'` the first and last of those goals invert: each
1526
+ * unpinned column asks for its own content width through the same
1527
+ * `_autoWidth` the grip double-click uses, and an overflow is the answer
1528
+ * rather than a problem — the caller wanted a table that scrolls sideways,
1529
+ * not one squeezed toward the floor. The grow-to-fill branch is shared by
1530
+ * both, because a table narrower than its container leaves a dead gap on
1531
+ * the right under either reading.
1532
+ *
1321
1533
  * @param {number[]} natural measured content width per column (px)
1322
1534
  * @param {number} avail usable width of the body wrap (px)
1323
- * @param {{firstIdx?:number, overrides?:Object}} [opts]
1535
+ * @param {{firstIdx?:number, overrides?:Object, fit?:'container'|'content'}} [opts]
1324
1536
  * @returns {number[]} final width per column (px)
1325
1537
  */
1326
1538
  _fitColumnWidths(natural, avail, opts = {}) {
@@ -1331,6 +1543,7 @@ var DataTable = class {
1331
1543
  const n = natural.length;
1332
1544
  const firstIdx = opts.firstIdx ?? 0;
1333
1545
  const overrides = opts.overrides || {};
1546
+ const toContent = opts.fit === "content";
1334
1547
  const desired = new Array(n);
1335
1548
  const pinned = new Array(n).fill(false);
1336
1549
  for (let i = 0; i < n; i++) {
@@ -1339,6 +1552,10 @@ var DataTable = class {
1339
1552
  pinned[i] = true;
1340
1553
  continue;
1341
1554
  }
1555
+ if (toContent) {
1556
+ desired[i] = this._autoWidth(natural[i]);
1557
+ continue;
1558
+ }
1342
1559
  const cap = i === firstIdx ? FIRST_CAP : CAP;
1343
1560
  desired[i] = Math.min(cap, Math.max(FLOOR, Math.ceil(natural[i] + PAD)));
1344
1561
  }
@@ -1349,13 +1566,15 @@ var DataTable = class {
1349
1566
  const flex = [];
1350
1567
  for (let i = 0; i < n; i++) if (!pinned[i]) flex.push(i);
1351
1568
  const slack = avail - sum;
1352
- if (slack > 0 && flex.length) {
1353
- const per = Math.floor(slack / flex.length);
1354
- for (const i of flex) widths[i] += per;
1355
- widths[flex[flex.length - 1]] += slack - per * flex.length;
1569
+ if (slack > 0) {
1570
+ const targets = flex.length ? flex : [n - 1];
1571
+ const per = Math.floor(slack / targets.length);
1572
+ for (const i of targets) widths[i] += per;
1573
+ widths[targets[targets.length - 1]] += slack - per * targets.length;
1356
1574
  }
1357
1575
  return widths;
1358
1576
  }
1577
+ if (toContent) return widths;
1359
1578
  let protectedSum = 0;
1360
1579
  const shrinkable = [];
1361
1580
  for (let i = 0; i < n; i++) {
@@ -1407,8 +1626,9 @@ var DataTable = class {
1407
1626
  }
1408
1627
  /** Wire the body wrap's horizontal scroll to the header. The header
1409
1628
  * sits in an overflow:hidden wrap, so it can't scroll sideways on
1410
- * its own — translate it by the body's scrollLeft. Re-installed each
1411
- * render against the freshly-built wrap. */
1629
+ * its own — `_syncHeaderScroll` scrolls it programmatically to the
1630
+ * body's `scrollLeft`. Re-installed each render against the freshly-built
1631
+ * wrap. */
1412
1632
  _installHeaderScrollSync() {
1413
1633
  const wrap = this._tableWrapEl;
1414
1634
  if (!wrap) return;
@@ -1420,14 +1640,56 @@ var DataTable = class {
1420
1640
  wrap.addEventListener("scroll", this._onBodyScroll, { passive: true });
1421
1641
  this._syncHeaderScroll();
1422
1642
  }
1423
- /** Translate the header table to match the body's horizontal scroll
1424
- * so the columns stay aligned when the table overflows sideways. */
1643
+ /**
1644
+ * Scroll the header wrap to match the body's horizontal scroll so the
1645
+ * columns stay aligned when the table overflows sideways.
1646
+ *
1647
+ * ══ C29. A TRANSFORM IS WHAT MAKES A STICKY COLUMN IMPOSSIBLE ═══════
1648
+ *
1649
+ * This wrote `headerTable.style.transform = translateX(-scrollLeft)`. It
1650
+ * aligns the two tables perfectly and it forecloses `position: sticky`
1651
+ * entirely, because the two halves of the table then pin against different
1652
+ * things: a sticky cell in the BODY pins to the body wrap's scrollport,
1653
+ * while its header counterpart is moved by a transform inside a box that
1654
+ * never scrolls at all. Scroll sideways and the pinned body column stands
1655
+ * still while its header slides away with everything else — so a table
1656
+ * with a trailing verb column (the row-actions column Tables needs pinned
1657
+ * to the right edge) can have a sticky body or an aligned header, never
1658
+ * both. A transform also establishes a containing block for fixed/sticky
1659
+ * descendants, which breaks the header cell independently of the offset.
1660
+ *
1661
+ * AN `overflow: hidden` BOX IS STILL A SCROLL CONTAINER. It has no
1662
+ * scrollbar and no user affordance, and `scrollLeft` moves it exactly like
1663
+ * any other. Scrolling the wrap rather than transforming its child gives
1664
+ * the header a real scrollport at the same offset as the body's, which is
1665
+ * the one arrangement in which a sticky cell on each side pins to the same
1666
+ * place. Identical for ordinary content: same pixels, no transform, no new
1667
+ * containing block.
1668
+ *
1669
+ * THE CLAMP IS ALREADY PAID FOR. A scroll container clamps `scrollLeft` to
1670
+ * `scrollWidth - clientWidth`, and the body wrap carries a vertical
1671
+ * scrollbar the header wrap does not — so the header would clamp short by
1672
+ * the scrollbar width and desync at the far right. `_syncHeaderWidths`
1673
+ * already pads the header wrap by exactly that gutter (`paddingRight`,
1674
+ * :1905), and end padding counts toward `scrollWidth`, so the two maxima
1675
+ * coincide. The residual below is the honest belt for the day some engine
1676
+ * disagrees about that: it is zero in the ordinary case, so the transform
1677
+ * is not set and sticky keeps working, and it is the difference rather
1678
+ * than the whole offset if it is ever not.
1679
+ */
1425
1680
  _syncHeaderScroll() {
1426
1681
  const wrap = this._tableWrapEl;
1427
1682
  const headerTable = this._headerTableEl;
1428
1683
  if (!wrap || !headerTable) return;
1429
1684
  const x = wrap.scrollLeft;
1430
- headerTable.style.transform = x ? `translateX(${-x}px)` : "";
1685
+ const headerWrap = this._headerWrapEl;
1686
+ if (!headerWrap) {
1687
+ headerTable.style.transform = x ? `translateX(${-x}px)` : "";
1688
+ return;
1689
+ }
1690
+ headerWrap.scrollLeft = x;
1691
+ const residual = x - headerWrap.scrollLeft;
1692
+ headerTable.style.transform = residual ? `translateX(${-residual}px)` : "";
1431
1693
  }
1432
1694
  /** Drag-to-resize: hang a thin grab handle off the right edge of
1433
1695
  * every header cell. Dragging it writes a per-column width override
@@ -1447,6 +1709,11 @@ var DataTable = class {
1447
1709
  (ev) => this._beginColResize(ev, domIdx)
1448
1710
  );
1449
1711
  grip.addEventListener("click", (ev) => ev.stopPropagation());
1712
+ grip.addEventListener("dblclick", (ev) => {
1713
+ ev.preventDefault();
1714
+ ev.stopPropagation();
1715
+ this.autoSizeColumn(domIdx);
1716
+ });
1450
1717
  th.appendChild(grip);
1451
1718
  });
1452
1719
  }
@@ -1456,23 +1723,13 @@ var DataTable = class {
1456
1723
  const headerTable = this._headerTableEl;
1457
1724
  const bodyTable = this._tableEl;
1458
1725
  const headRow = headerTable && headerTable.querySelector("thead > tr");
1459
- const bodyRow = bodyTable && bodyTable.querySelector("tbody > tr");
1460
1726
  if (!headRow) return;
1461
1727
  const startWidths = [...headRow.children].map(
1462
1728
  (c) => c.getBoundingClientRect().width
1463
1729
  );
1464
1730
  headerTable.style.tableLayout = "fixed";
1465
1731
  if (bodyTable) bodyTable.style.tableLayout = "fixed";
1466
- const applyCol = (i, w) => {
1467
- this._colWidths[i] = w;
1468
- headerTable.querySelectorAll("thead > tr").forEach((tr) => {
1469
- if (tr.children[i]) this._setCellWidth(tr.children[i], w);
1470
- });
1471
- if (bodyRow && bodyRow.children[i]) {
1472
- this._setCellWidth(bodyRow.children[i], w);
1473
- }
1474
- };
1475
- startWidths.forEach((w, i) => applyCol(i, w));
1732
+ startWidths.forEach((w, i) => this._pinColumnWidth(i, w));
1476
1733
  this._applyTableWidth();
1477
1734
  const startX = ev.clientX;
1478
1735
  const MIN = 40;
@@ -1482,8 +1739,8 @@ var DataTable = class {
1482
1739
  MIN,
1483
1740
  Math.round(startWidths[domIdx] + (mv.clientX - startX))
1484
1741
  );
1485
- applyCol(domIdx, w);
1486
- this._applyTableWidth();
1742
+ this._pinColumnWidth(domIdx, w);
1743
+ this._applyTableWidth(domIdx);
1487
1744
  this._syncHeaderScroll();
1488
1745
  };
1489
1746
  const onUp = () => {
@@ -1496,19 +1753,60 @@ var DataTable = class {
1496
1753
  document.addEventListener("mousemove", onMove);
1497
1754
  document.addEventListener("mouseup", onUp);
1498
1755
  }
1499
- /** Size both tables to the sum of the header row's explicit column
1500
- * widths so a widened column grows the table (→ horizontal scroll)
1501
- * rather than stealing from its neighbours. */
1502
- _applyTableWidth() {
1756
+ /** Pin one column to an explicit width across BOTH tables (every
1757
+ * header row + the body's first row) and record it as a user
1758
+ * override so `_syncHeaderWidths` honors it on later renders. The
1759
+ * single place that writes a column width during a drag / fill. */
1760
+ _pinColumnWidth(i, w) {
1761
+ const headerTable = this._headerTableEl;
1762
+ const bodyTable = this._tableEl;
1763
+ if (!headerTable) return;
1764
+ this._colWidths[i] = w;
1765
+ headerTable.querySelectorAll("thead > tr").forEach((tr) => {
1766
+ if (tr.children[i]) this._setCellWidth(tr.children[i], w);
1767
+ });
1768
+ const bodyRow = bodyTable && bodyTable.querySelector("tbody > tr");
1769
+ if (bodyRow && bodyRow.children[i]) this._setCellWidth(bodyRow.children[i], w);
1770
+ }
1771
+ /** Size both tables so a widened column grows the table (→ horizontal
1772
+ * scroll) rather than stealing from its neighbours — while enforcing
1773
+ * the TABLE WIDTH INVARIANT: the table is never narrower than its
1774
+ * container. Any width freed by dragging a column in stretches a
1775
+ * flexible column (the last one, stepping off the column being
1776
+ * dragged) so the table always spans ≥100% of the wrap.
1777
+ *
1778
+ * @param {number|null} dragIdx column the user is actively dragging,
1779
+ * so the fill lands on a DIFFERENT column and doesn't fight the
1780
+ * drag. Null (non-drag callers) lets the last column flex. */
1781
+ _applyTableWidth(dragIdx = null) {
1503
1782
  const headerTable = this._headerTableEl;
1504
1783
  const bodyTable = this._tableEl;
1505
1784
  const headRow = headerTable && headerTable.querySelector("thead > tr");
1506
1785
  if (!headRow) return;
1507
- let total = 0;
1508
- for (const th of headRow.children) {
1786
+ const cells = [...headRow.children];
1787
+ const cols = cells.length;
1788
+ if (cols === 0) return;
1789
+ const FLOOR = 40;
1790
+ const widthOf = (th) => {
1509
1791
  const px = parseFloat(th.style.width);
1510
- total += Number.isFinite(px) ? px : th.getBoundingClientRect().width;
1792
+ return Number.isFinite(px) ? px : th.getBoundingClientRect().width;
1793
+ };
1794
+ const cur = cells.map(widthOf);
1795
+ const wrap = this._tableWrapEl;
1796
+ const avail = wrap ? wrap.clientWidth : 0;
1797
+ let flexIdx = cols - 1;
1798
+ if (dragIdx != null && flexIdx === dragIdx) flexIdx -= 1;
1799
+ if (avail > 1 && flexIdx >= 0 && flexIdx !== dragIdx) {
1800
+ let rest = 0;
1801
+ for (let i = 0; i < cols; i++) if (i !== flexIdx) rest += cur[i];
1802
+ const fill = Math.max(FLOOR, Math.round(avail - rest));
1803
+ if (Math.round(fill) !== Math.round(cur[flexIdx])) {
1804
+ this._pinColumnWidth(flexIdx, fill);
1805
+ cur[flexIdx] = fill;
1806
+ }
1511
1807
  }
1808
+ let total = 0;
1809
+ for (const w of cur) total += w;
1512
1810
  total = Math.ceil(total);
1513
1811
  headerTable.style.width = `${total}px`;
1514
1812
  if (bodyTable) bodyTable.style.width = `${total}px`;
@@ -1767,4 +2065,4 @@ export {
1767
2065
  createRafResizeObserver,
1768
2066
  DataTable
1769
2067
  };
1770
- //# sourceMappingURL=chunk-CT4YXXLP.js.map
2068
+ //# sourceMappingURL=chunk-QIU5S2RU.js.map