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
package/dist/tiles.js CHANGED
@@ -1,11 +1,11 @@
1
1
  import {
2
2
  ActionDropdown
3
- } from "./chunk-TLZUUFOE.js";
3
+ } from "./chunk-O5OHMWBB.js";
4
4
  import {
5
5
  openRawTracesWindow
6
- } from "./chunk-DRYCDMEG.js";
7
- import "./chunk-CT4YXXLP.js";
8
- import "./chunk-UCJ2WD4D.js";
6
+ } from "./chunk-XKDTIT4Q.js";
7
+ import "./chunk-QIU5S2RU.js";
8
+ import "./chunk-LH5TSOZW.js";
9
9
  import "./chunk-FL5KFNQH.js";
10
10
  import {
11
11
  TileRegistry,
@@ -708,6 +708,9 @@ var TileBase = class {
708
708
  * @param {Object} [options.eventBus] - Event bus for cross-component communication
709
709
  * @param {Object} [options.config] - Widget-specific configuration
710
710
  * @param {boolean} [options.headless] - If true, mount without tile chrome (header, controls)
711
+ * @param {{resolve: Function}} [options.dataSource] - Where this tile asks for its
712
+ * own rows. Handed down by `TileGrid`; absent for a grid whose data is
713
+ * broadcast with `setData()`, and then nothing ever calls `loadData()`.
711
714
  */
712
715
  constructor({
713
716
  id,
@@ -717,9 +720,11 @@ var TileBase = class {
717
720
  readonly = false,
718
721
  headless = false,
719
722
  host = null,
720
- stateGuard = null
723
+ stateGuard = null,
724
+ dataSource = null
721
725
  }) {
722
726
  this.host = host;
727
+ this.dataSource = dataSource;
723
728
  this.stateGuard = stateGuard;
724
729
  this.id = id;
725
730
  this.grid = grid;
@@ -733,6 +738,8 @@ var TileBase = class {
733
738
  this.data = null;
734
739
  this.fullData = null;
735
740
  this._disposed = false;
741
+ this._lastLoadedAt = null;
742
+ this._loadSeq = 0;
736
743
  this.comparisonData = null;
737
744
  }
738
745
  /**
@@ -929,6 +936,33 @@ var TileBase = class {
929
936
  this.render(this.data);
930
937
  }
931
938
  }
939
+ /**
940
+ * Load this tile's own data from `this.dataSource` and render it.
941
+ *
942
+ * A NO-OP HERE ON PURPOSE. The base class knows nothing about what a binding
943
+ * is or what a dataset looks like — those belong to the consumer, and the
944
+ * consumer that has them (Tables' `TablesTile`) implements this in ~30 lines
945
+ * over `showLoading()` / `render()` / `showError()`. What upstream owns is the
946
+ * CALL SITES: `TileGrid` invokes this after mount, after a resize settles and
947
+ * after a config save, but only when a `dataSource` was injected. Making it a
948
+ * hook rather than an implementation is what keeps EcoAgent's broadcast path
949
+ * (`setData` -> `update`) bit-identical: with no data source nothing here ever
950
+ * runs.
951
+ *
952
+ * @param {{force?: boolean}} [_opts] - `force` bypasses the source's cache.
953
+ * @returns {Promise<void>}
954
+ */
955
+ async loadData(_opts = {}) {
956
+ }
957
+ /**
958
+ * Called once after a drag-resize settles, for widgets whose content does not
959
+ * reflow on its own — a Plotly figure sizes itself at draw time and stays that
960
+ * size until something calls `Plotly.Plots.resize`. Deliberately NOT wired to a
961
+ * ResizeObserver: the grid already knows when a resize ended, and an observer
962
+ * per tile fires during the drag, which is a relayout per pointer-move.
963
+ */
964
+ onResize() {
965
+ }
932
966
  /** Add, update, or remove the info (ⓘ) button in the header to match current docs state. */
933
967
  _syncInfoButton() {
934
968
  const header = this.element?.querySelector(".tile-header");
@@ -978,6 +1012,65 @@ var TileBase = class {
978
1012
  this.data = null;
979
1013
  this.fullData = null;
980
1014
  }
1015
+ /**
1016
+ * The entries in the tile's ⋯ menu, as `{action, label, icon, hidden}`.
1017
+ *
1018
+ * This existed as a LITERAL inside `_buildChrome`, and the literal was
1019
+ * EcoAgent's: "Add to Documentation" and "Show in Documentation". Both are
1020
+ * meaningful only for a tile whose `config.sourceCellId` names a notebook
1021
+ * cell, and the first one's hide condition is `sourceCellId && already-added` —
1022
+ * so on a tile that has no `sourceCellId` at all it renders VISIBLE, and
1023
+ * choosing it emits `tile:add-to-documentation` at an application that has no
1024
+ * documentation. Every consumer other than EcoAgent therefore shipped a menu
1025
+ * item that did nothing, and could not remove it without editing this file.
1026
+ *
1027
+ * Returning `[]` drops the whole wrapper — button, dropdown and all — because
1028
+ * a ⋯ button that opens an empty menu reads as a broken feature rather than
1029
+ * an absent one. The default is the two entries above, so EcoAgent's chrome
1030
+ * is unchanged.
1031
+ *
1032
+ * Selection is routed through `_onMenuAction()`; override both together.
1033
+ * @returns {Array<{action: string, label: string, icon?: string, hidden?: boolean}>}
1034
+ */
1035
+ menuItems() {
1036
+ const linked = !!(this.config?.sourceCellId && this.grid?.documentationCellIds?.has(this.config.sourceCellId));
1037
+ return [
1038
+ {
1039
+ action: "add-to-documentation",
1040
+ label: "Add to Documentation",
1041
+ icon: "post_add",
1042
+ hidden: linked
1043
+ },
1044
+ {
1045
+ action: "show-in-documentation",
1046
+ label: "Show in Documentation",
1047
+ icon: "description",
1048
+ hidden: !linked
1049
+ }
1050
+ ];
1051
+ }
1052
+ /**
1053
+ * Act on a ⋯ menu selection. One dispatch point for every entry, so an
1054
+ * override of `menuItems()` cannot add a row that has nothing behind it, and
1055
+ * an unrecognised action is silently ignored rather than throwing into a
1056
+ * click handler.
1057
+ * @param {string} action
1058
+ * @protected
1059
+ */
1060
+ _onMenuAction(action) {
1061
+ if (action === "add-to-documentation") {
1062
+ this.eventBus?.emit?.("tile:add-to-documentation", {
1063
+ tileType: this.constructor.TYPE,
1064
+ config: { ...this.config }
1065
+ });
1066
+ } else if (action === "show-in-documentation") {
1067
+ if (this.config?.sourceCellId) {
1068
+ this.eventBus?.emit?.("tile:show-in-documentation", {
1069
+ sourceCellId: this.config.sourceCellId
1070
+ });
1071
+ }
1072
+ }
1073
+ }
981
1074
  /**
982
1075
  * Build the tile chrome (header, content area, resize handles).
983
1076
  * @private
@@ -996,6 +1089,19 @@ var TileBase = class {
996
1089
  style="background:transparent;border:none;padding:2px;color:rgba(255,255,255,0.55);cursor:help;display:inline-flex;align-items:center;justify-content:center;">
997
1090
  <span class="material-symbols-outlined" style="font-size:18px;">info</span>
998
1091
  </button>` : "";
1092
+ const menuEntries = (this.readonly ? [] : this.menuItems()) ?? [];
1093
+ const menuHtml = menuEntries.length ? `<div class="tile-menu-wrapper">
1094
+ <button class="tile-menu-btn twm-has-tooltip" data-tooltip="More actions">
1095
+ <span class="material-symbols-outlined">more_vert</span>
1096
+ </button>
1097
+ <div class="tile-menu-dropdown" hidden>
1098
+ ${menuEntries.map((item) => `
1099
+ <button class="tile-menu-item" data-action="${this.#escapeAttr(item.action)}"${item.hidden ? ' style="display:none"' : ""}>
1100
+ ${item.icon ? `<span class="material-symbols-outlined">${this.#escapeAttr(item.icon)}</span>` : ""}
1101
+ <span>${this.#escapeAttr(item.label ?? "")}</span>
1102
+ </button>`).join("")}
1103
+ </div>
1104
+ </div>` : "";
999
1105
  if (this.readonly) {
1000
1106
  header.innerHTML = `
1001
1107
  <span class="tile-title">${this.constructor.TITLE}</span>
@@ -1014,21 +1120,7 @@ var TileBase = class {
1014
1120
  <button class="tile-config-btn twm-has-tooltip" data-tooltip="Configure">
1015
1121
  <span class="material-symbols-outlined">settings</span>
1016
1122
  </button>
1017
- <div class="tile-menu-wrapper">
1018
- <button class="tile-menu-btn twm-has-tooltip" data-tooltip="More actions">
1019
- <span class="material-symbols-outlined">more_vert</span>
1020
- </button>
1021
- <div class="tile-menu-dropdown" hidden>
1022
- <button class="tile-menu-item" data-action="add-to-documentation" style="${this.config?.sourceCellId && this.grid?.documentationCellIds?.has(this.config.sourceCellId) ? "display:none" : ""}">
1023
- <span class="material-symbols-outlined">post_add</span>
1024
- <span>Add to Documentation</span>
1025
- </button>
1026
- <button class="tile-menu-item" data-action="show-in-documentation" style="${this.config?.sourceCellId && this.grid?.documentationCellIds?.has(this.config.sourceCellId) ? "" : "display:none"}">
1027
- <span class="material-symbols-outlined">description</span>
1028
- <span>Show in Documentation</span>
1029
- </button>
1030
- </div>
1031
- </div>
1123
+ ${menuHtml}
1032
1124
  ${infoBtnHtml}
1033
1125
  ${expandBtnHtml}
1034
1126
  <button class="tile-remove-btn twm-has-tooltip" data-tooltip="Remove widget">
@@ -1069,23 +1161,13 @@ var TileBase = class {
1069
1161
  });
1070
1162
  }
1071
1163
  });
1072
- menuDropdown.querySelector('[data-action="add-to-documentation"]')?.addEventListener("click", (e) => {
1073
- e.stopPropagation();
1074
- menuDropdown.hidden = true;
1075
- this.eventBus?.emit?.("tile:add-to-documentation", {
1076
- tileType: this.constructor.TYPE,
1077
- config: { ...this.config }
1164
+ menuDropdown.querySelectorAll(".tile-menu-item").forEach((itemEl) => {
1165
+ itemEl.addEventListener("click", (e) => {
1166
+ e.stopPropagation();
1167
+ menuDropdown.hidden = true;
1168
+ this._onMenuAction(itemEl.dataset.action);
1078
1169
  });
1079
1170
  });
1080
- menuDropdown.querySelector('[data-action="show-in-documentation"]')?.addEventListener("click", (e) => {
1081
- e.stopPropagation();
1082
- menuDropdown.hidden = true;
1083
- if (this.config?.sourceCellId) {
1084
- this.eventBus?.emit?.("tile:show-in-documentation", {
1085
- sourceCellId: this.config.sourceCellId
1086
- });
1087
- }
1088
- });
1089
1171
  }
1090
1172
  const removeBtn = header.querySelector(".tile-remove-btn");
1091
1173
  removeBtn?.addEventListener("click", (e) => {
@@ -1179,6 +1261,13 @@ var TileBase = class {
1179
1261
  }
1180
1262
  /**
1181
1263
  * Show empty state in content area.
1264
+ *
1265
+ * The message is ESCAPED. Both this and `showError` interpolated straight
1266
+ * into `innerHTML`, and the string reaching `showError` is the one place a
1267
+ * caller has least control over — a server's error text, echoed back with a
1268
+ * column name or a filter value in it. Callers pass plain text; this decides
1269
+ * what that means in HTML.
1270
+ *
1182
1271
  * @param {string} [message] - Custom message
1183
1272
  */
1184
1273
  showEmpty(message = "No data available") {
@@ -1186,13 +1275,13 @@ var TileBase = class {
1186
1275
  this.contentElement.innerHTML = `
1187
1276
  <div class="tile-empty">
1188
1277
  <span class="material-symbols-outlined">inbox</span>
1189
- <span>${message}</span>
1278
+ <span>${this.#escapeAttr(message)}</span>
1190
1279
  </div>
1191
1280
  `;
1192
1281
  }
1193
1282
  }
1194
1283
  /**
1195
- * Show error state in content area.
1284
+ * Show error state in content area. The message is escaped — see `showEmpty`.
1196
1285
  * @param {string} [message] - Error message
1197
1286
  */
1198
1287
  showError(message = "Failed to load data") {
@@ -1200,7 +1289,7 @@ var TileBase = class {
1200
1289
  this.contentElement.innerHTML = `
1201
1290
  <div class="twm-tile-error">
1202
1291
  <span class="material-symbols-outlined">error</span>
1203
- <span>${message}</span>
1292
+ <span>${this.#escapeAttr(message)}</span>
1204
1293
  </div>
1205
1294
  `;
1206
1295
  }
@@ -1300,7 +1389,8 @@ var TileBase = class {
1300
1389
  });
1301
1390
  }
1302
1391
  /**
1303
- * Escape a string for safe insertion into an HTML attribute.
1392
+ * Escape a string for safe insertion into HTML — an attribute value or a text
1393
+ * node, since the five characters that matter are the same five in both.
1304
1394
  * @private
1305
1395
  */
1306
1396
  #escapeAttr(s) {
@@ -1340,6 +1430,9 @@ var TileGrid = class {
1340
1430
  * @param {Function} [options.onAddWidget] - Callback for add widget action
1341
1431
  * @param {Function} [options.onResetLayout] - Callback for reset layout action
1342
1432
  * @param {Function} [options.onExport] - Callback for export action
1433
+ * @param {{resolve: Function}} [options.dataSource] - Per-tile data provider,
1434
+ * handed to every tile. With none, the grid is the broadcast-only grid
1435
+ * it has always been and no tile is ever asked to load anything.
1343
1436
  */
1344
1437
  constructor({
1345
1438
  container,
@@ -1359,10 +1452,14 @@ var TileGrid = class {
1359
1452
  // no host simply gets the Blob-download fallback the host contract
1360
1453
  // promises. It is not an error to have none.
1361
1454
  host = null,
1362
- stateGuard = null
1455
+ stateGuard = null,
1456
+ // Injected, never constructed here: the grid does not know what
1457
+ // a binding is, only that a tile may have somewhere to ask.
1458
+ dataSource = null
1363
1459
  }) {
1364
1460
  this.host = host;
1365
1461
  this.stateGuard = stateGuard;
1462
+ this.dataSource = dataSource;
1366
1463
  this.container = container;
1367
1464
  this.columns = columns;
1368
1465
  this.rowHeight = rowHeight;
@@ -1479,7 +1576,8 @@ var TileGrid = class {
1479
1576
  config,
1480
1577
  readonly: this.readonly,
1481
1578
  host: this.host,
1482
- stateGuard: this.stateGuard
1579
+ stateGuard: this.stateGuard,
1580
+ dataSource: this.dataSource
1483
1581
  });
1484
1582
  if (!tile) return null;
1485
1583
  const layoutEntry = {
@@ -1499,6 +1597,8 @@ var TileGrid = class {
1499
1597
  if (this.data) {
1500
1598
  tile.fullData = this.fullData;
1501
1599
  tile.update(this.data);
1600
+ } else if (this.dataSource) {
1601
+ tile.loadData?.();
1502
1602
  }
1503
1603
  this._emitLayoutChanged();
1504
1604
  return tile;
@@ -1526,6 +1626,30 @@ var TileGrid = class {
1526
1626
  tile.update(data);
1527
1627
  });
1528
1628
  }
1629
+ /**
1630
+ * Ask every self-fetching tile to load again, bypassing whatever the data
1631
+ * source has cached.
1632
+ *
1633
+ * The counterpart of `setData` for the pull model: `setData` is how a caller
1634
+ * with one dataset for the whole board pushes it, and this is how a caller
1635
+ * whose tiles each own a query says "that underlying data changed". Passing
1636
+ * `ids` narrows it to the tiles a change actually touched, which is what an
1637
+ * invalidation carrying a table id can work out and a broadcast cannot.
1638
+ *
1639
+ * Not awaited and returns nothing: the tiles render themselves as their own
1640
+ * requests land, and a caller that waited for all of them would be waiting on
1641
+ * the slowest tile to show the fastest one.
1642
+ *
1643
+ * @param {string[]|Set<string>|null} [ids] - Tile ids, or null for all.
1644
+ */
1645
+ reloadAll(ids = null) {
1646
+ if (!this.dataSource) return;
1647
+ const wanted = ids ? new Set(ids) : null;
1648
+ this.tiles.forEach((tile, tileId) => {
1649
+ if (wanted && !wanted.has(tileId)) return;
1650
+ tile.loadData?.({ force: true });
1651
+ });
1652
+ }
1529
1653
  /**
1530
1654
  * Get current layout.
1531
1655
  * @returns {Array} Layout array
@@ -1760,6 +1884,8 @@ var TileGrid = class {
1760
1884
  if (this.data) {
1761
1885
  state.tile.fullData = this.fullData;
1762
1886
  state.tile.update(this.data);
1887
+ } else if (this.dataSource) {
1888
+ state.tile.onResize?.();
1763
1889
  }
1764
1890
  }
1765
1891
  }
@@ -1926,6 +2052,7 @@ var TileGrid = class {
1926
2052
  if (tile) {
1927
2053
  tile.fullData = this.fullData;
1928
2054
  tile.update(this.data, newConfig);
2055
+ if (this.dataSource) tile.loadData?.({ force: true });
1929
2056
  const layoutItem = this.layout.find((l) => l.id === tileId);
1930
2057
  if (layoutItem) {
1931
2058
  layoutItem.config = newConfig;