flexdesk 0.17.15 → 0.17.16

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 (42) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/css/base.css +224 -4
  3. package/css/flexdesk.css +314 -12
  4. package/dist/charts.js +104 -34
  5. package/dist/charts.js.map +3 -3
  6. package/dist/{chunk-2PM6ZMNS.js → chunk-2Z2ENHR3.js} +2 -2
  7. package/dist/{chunk-AD7FIA3Z.js → chunk-7JI7HHSY.js} +2 -2
  8. package/dist/{chunk-TLIV6QSU.js → chunk-7OL6F2AM.js} +3 -3
  9. package/dist/{chunk-MADMGRX2.js → chunk-PGZ4EHSE.js} +156 -9
  10. package/dist/chunk-PGZ4EHSE.js.map +7 -0
  11. package/dist/{chunk-OEG6NPAN.js → chunk-XJNADRGR.js} +2 -2
  12. package/dist/flexdesk.css +314 -12
  13. package/dist/flow.js +3 -3
  14. package/dist/tiles.js +84 -9
  15. package/dist/tiles.js.map +2 -2
  16. package/dist/widgets.js +448 -5
  17. package/dist/widgets.js.map +4 -4
  18. package/dist/wm.js +102 -14
  19. package/dist/wm.js.map +2 -2
  20. package/docs/README.md +1 -1
  21. package/docs/components/charts.md +70 -3
  22. package/docs/components/forms-and-dialogs.md +61 -0
  23. package/docs/components/menus.md +85 -1
  24. package/docs/components/shell.md +37 -0
  25. package/docs/components/tiles.md +37 -0
  26. package/package.json +1 -1
  27. package/src/charts/plot_config_editor.js +125 -29
  28. package/src/tiles/tile_base.js +48 -2
  29. package/src/tiles/tile_grid.js +42 -1
  30. package/src/tiling/kind_taxonomy.js +45 -0
  31. package/src/tiling/wm.js +86 -11
  32. package/src/ui/components/category_picker.js +556 -0
  33. package/src/ui/components/computing_status_window.js +3 -0
  34. package/src/ui/components/confirm_dialog.js +3 -0
  35. package/src/ui/components/managed_window.js +204 -9
  36. package/src/ui/components/wizard.js +4 -1
  37. package/widgets.js +2 -1
  38. package/dist/chunk-MADMGRX2.js.map +0 -7
  39. /package/dist/{chunk-2PM6ZMNS.js.map → chunk-2Z2ENHR3.js.map} +0 -0
  40. /package/dist/{chunk-AD7FIA3Z.js.map → chunk-7JI7HHSY.js.map} +0 -0
  41. /package/dist/{chunk-TLIV6QSU.js.map → chunk-7OL6F2AM.js.map} +0 -0
  42. /package/dist/{chunk-OEG6NPAN.js.map → chunk-XJNADRGR.js.map} +0 -0
@@ -5,6 +5,9 @@ import {
5
5
  // src/ui/components/managed_window.js
6
6
  var DEFAULT_WINDOW_STORAGE_KEY = "flexdesk.managedWindows.v1";
7
7
  var _storageKey = DEFAULT_WINDOW_STORAGE_KEY;
8
+ var DEFAULT_MAX_SAVED_WINDOWS = 50;
9
+ var MINTED_ID_PREFIX = "twm-mw-";
10
+ var _maxSavedWindows = DEFAULT_MAX_SAVED_WINDOWS;
8
11
  var BASE_Z_INDEX = 6e3;
9
12
  var MAX_Z_INDEX = 6999;
10
13
  var TOP_BAR_HEIGHT = 35;
@@ -17,6 +20,7 @@ var SNAP_EDGE = 12;
17
20
  var DEFAULT_ICON = "web_asset";
18
21
  var _zIndexCounter = 0;
19
22
  var _activeWindows = /* @__PURE__ */ new Map();
23
+ var _escAnswered = /* @__PURE__ */ new WeakSet();
20
24
  function _collectFocusable(root) {
21
25
  if (!root) return [];
22
26
  const sel = [
@@ -39,7 +43,11 @@ function _loadState() {
39
43
  if (_stateCache) return _stateCache;
40
44
  try {
41
45
  const raw = localStorage.getItem(_storageKey);
42
- _stateCache = raw ? JSON.parse(raw) : {};
46
+ const parsed = raw ? JSON.parse(raw) : {};
47
+ _stateCache = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
48
+ for (const k of Object.keys(_stateCache)) {
49
+ if (k.startsWith(MINTED_ID_PREFIX)) delete _stateCache[k];
50
+ }
43
51
  } catch {
44
52
  _stateCache = {};
45
53
  }
@@ -59,9 +67,50 @@ function _getWindowState(id) {
59
67
  }
60
68
  function _setWindowState(id, windowState) {
61
69
  const state = _loadState();
62
- state[id] = windowState;
70
+ let latest = 0;
71
+ for (const k of Object.keys(state)) latest = Math.max(latest, Number(state[k]?.u) || 0);
72
+ for (const k of Object.keys(state)) {
73
+ if (!(Number(state[k]?.u) > 0) && state[k] && typeof state[k] === "object") {
74
+ state[k] = { ...state[k], u: ++latest };
75
+ }
76
+ }
77
+ state[id] = { ...windowState, u: latest + 1 };
78
+ const ids = Object.keys(state);
79
+ if (ids.length > _maxSavedWindows) {
80
+ ids.sort((a, b) => (Number(state[a]?.u) || 0) - (Number(state[b]?.u) || 0));
81
+ for (const k of ids.slice(0, ids.length - _maxSavedWindows)) delete state[k];
82
+ }
63
83
  _saveState(state);
64
84
  }
85
+ function _focusFirstIn(root) {
86
+ if (!root) return false;
87
+ const sel = [
88
+ "a[href]",
89
+ "button:not([disabled])",
90
+ 'input:not([disabled]):not([type="hidden"])',
91
+ "select:not([disabled])",
92
+ "textarea:not([disabled])",
93
+ '[contenteditable="true"]',
94
+ '[tabindex]:not([tabindex="-1"])'
95
+ ].join(",");
96
+ for (const el2 of root.querySelectorAll(sel)) {
97
+ if (el2.hidden || el2.closest("[hidden]")) continue;
98
+ try {
99
+ el2.focus({ preventScroll: true });
100
+ } catch {
101
+ continue;
102
+ }
103
+ if (el2.ownerDocument.activeElement === el2) return true;
104
+ }
105
+ return false;
106
+ }
107
+ function _escAnswerer() {
108
+ const visible = Array.from(_activeWindows.values()).filter((w) => w.isVisible && !w.isMinimized && w.element).sort((a, b) => b.zIndex - a.zIndex);
109
+ if (visible[0]?.modal) return visible[0];
110
+ const active = document.activeElement;
111
+ const holder = active ? visible.find((w) => w.element.contains(active)) : null;
112
+ return holder || visible.find((w) => w.modal) || null;
113
+ }
65
114
  var ManagedWindow = class {
66
115
  /**
67
116
  * @param {Object} options
@@ -99,6 +148,15 @@ var ManagedWindow = class {
99
148
  * away.
100
149
  * @param {Array} [options.titlebarButtons] - Extra buttons left of
101
150
  * minimize: `{icon, title, onClick}`.
151
+ * @param {boolean} [options.persistGeometry=true] - Save and restore this
152
+ * window's position and size under its id. Pass false for a window whose
153
+ * id is minted per opening, so it never writes an entry.
154
+ * @param {boolean|'window'} [options.focusOnOpen=true] - When the window
155
+ * opens, move focus to the first focusable element of its content (the
156
+ * window itself when it has none; always the window itself with
157
+ * `'window'`), and on close give focus back to the element that had it
158
+ * before, if that element is still in the document and focus is still in
159
+ * this window or nowhere.
102
160
  */
103
161
  constructor(options) {
104
162
  this.id = options.id;
@@ -135,6 +193,10 @@ var ManagedWindow = class {
135
193
  this._minimizeTimer = null;
136
194
  this._restoreTimer = null;
137
195
  this.titlebarButtons = Array.isArray(options.titlebarButtons) ? options.titlebarButtons : [];
196
+ this.persistGeometry = options.persistGeometry ?? true;
197
+ this.focusOnOpen = options.focusOnOpen ?? true;
198
+ this._returnFocusTo = null;
199
+ this._closed = false;
138
200
  this.element = null;
139
201
  this.backdropElement = null;
140
202
  this.contentContainer = null;
@@ -159,6 +221,10 @@ var ManagedWindow = class {
159
221
  * Show the window.
160
222
  */
161
223
  show() {
224
+ if (this._closed) {
225
+ console.warn(`[ManagedWindow] show() on closed window "${this.id}"; build a new ManagedWindow to reopen it`);
226
+ return;
227
+ }
162
228
  if (this.isVisible && !this.isMinimized) {
163
229
  if (this.element && this.element.style.display === "none") {
164
230
  this.element.style.display = "";
@@ -166,6 +232,7 @@ var ManagedWindow = class {
166
232
  this.bringToFront();
167
233
  return;
168
234
  }
235
+ let opened = false;
169
236
  if (!this.element) {
170
237
  this._build();
171
238
  this._restoreState();
@@ -190,9 +257,36 @@ var ManagedWindow = class {
190
257
  }
191
258
  this._installContainerResizeObserver();
192
259
  this.isVisible = true;
260
+ opened = true;
193
261
  }
194
262
  this.bringToFront();
195
263
  document.addEventListener("keydown", this._boundOnKeyDown);
264
+ if (opened && this.focusOnOpen) this._moveFocusIn();
265
+ }
266
+ /** BUG-0053. Focus into the content on open, remembering where it was. */
267
+ _moveFocusIn() {
268
+ const active = document.activeElement;
269
+ this._returnFocusTo = active && active !== document.body && !this.element.contains(active) ? active : null;
270
+ if (this.focusOnOpen !== "window" && _focusFirstIn(this.contentContainer)) return;
271
+ try {
272
+ this.element.focus({ preventScroll: true });
273
+ } catch {
274
+ }
275
+ }
276
+ /** BUG-0053. Give focus back on close — only when it is still in this
277
+ * window (or nowhere): a window closed while the user works elsewhere
278
+ * must not pull focus away from where they are. */
279
+ _returnFocus() {
280
+ const back = this._returnFocusTo;
281
+ this._returnFocusTo = null;
282
+ if (!back || !back.isConnected) return;
283
+ const active = document.activeElement;
284
+ const held = !active || active === document.body || this.element && this.element.contains(active);
285
+ if (!held) return;
286
+ try {
287
+ back.focus({ preventScroll: true });
288
+ } catch {
289
+ }
196
290
  }
197
291
  /**
198
292
  * Hide/close the window.
@@ -219,6 +313,7 @@ var ManagedWindow = class {
219
313
  if (this.onClose) {
220
314
  this.onClose();
221
315
  }
316
+ if (this.focusOnOpen) this._returnFocus();
222
317
  if (this.element && this.element.parentNode) {
223
318
  this.element.parentNode.removeChild(this.element);
224
319
  }
@@ -235,6 +330,38 @@ var ManagedWindow = class {
235
330
  window.dispatchEvent(new CustomEvent("managed-window-closed", {
236
331
  detail: { id: this.id, container: this.container }
237
332
  }));
333
+ this._release();
334
+ }
335
+ /** BUG-0053. A closed window leaves the registry and lets go of what it
336
+ * held: its content, its DOM and every consumer callback (an `onClose`
337
+ * typically captures its owner). Until now the registry kept all of it
338
+ * alive until the same id opened again, and `get(id)` answered with the
339
+ * dead window. */
340
+ _release() {
341
+ if (_activeWindows.get(this.id) === this) _activeWindows.delete(this.id);
342
+ this._closed = true;
343
+ if (!this._dragState && !this._resizeState) {
344
+ document.removeEventListener("pointermove", this._boundOnPointerMove);
345
+ document.removeEventListener("pointerup", this._boundOnPointerUp);
346
+ document.removeEventListener("pointercancel", this._boundOnPointerUp);
347
+ }
348
+ this.content = null;
349
+ this.element = null;
350
+ this.contentContainer = null;
351
+ this.backdropElement = null;
352
+ this.onClose = null;
353
+ this.beforeClose = null;
354
+ this.onMinimize = null;
355
+ this.onMaximize = null;
356
+ this.snapController = null;
357
+ this.dragHost = null;
358
+ this.dragBounds = null;
359
+ this.titlebarButtons = [];
360
+ }
361
+ /** True once the window has closed. A closed window cannot be shown
362
+ * again; `ManagedWindow.get(id)` no longer returns it. */
363
+ get isClosed() {
364
+ return this._closed;
238
365
  }
239
366
  /**
240
367
  * Minimize to taskbar with animation.
@@ -521,6 +648,7 @@ var ManagedWindow = class {
521
648
  this.element.classList.add("twm-managed-window--modal");
522
649
  }
523
650
  this.element.setAttribute("data-window-id", this.id);
651
+ this.element.tabIndex = -1;
524
652
  const topbar = document.createElement("div");
525
653
  topbar.className = "twm-managed-window__topbar";
526
654
  const icon = document.createElement("span");
@@ -613,7 +741,15 @@ var ManagedWindow = class {
613
741
  if (this.canMaximize) {
614
742
  topbar.addEventListener("dblclick", () => this.toggleMaximize());
615
743
  }
616
- this.element.addEventListener("pointerdown", () => this.bringToFront());
744
+ this.element.addEventListener("pointerdown", () => {
745
+ this.bringToFront();
746
+ if (this.element && !this.element.contains(document.activeElement)) {
747
+ try {
748
+ this.element.focus({ preventScroll: true });
749
+ } catch {
750
+ }
751
+ }
752
+ });
617
753
  }
618
754
  _addResizeHandles() {
619
755
  const directions = ["n", "s", "e", "w", "ne", "nw", "se", "sw"];
@@ -883,7 +1019,7 @@ var ManagedWindow = class {
883
1019
  const bounds = this._bounds();
884
1020
  const maxWidth = bounds.width;
885
1021
  const maxHeight = bounds.height;
886
- const saved = this.modal ? null : _getWindowState(this.id);
1022
+ const saved = this.modal || !this.persistGeometry ? null : _getWindowState(this.id);
887
1023
  if (saved) {
888
1024
  this.x = saved.x ?? this.x;
889
1025
  this.y = saved.y ?? this.y;
@@ -904,7 +1040,7 @@ var ManagedWindow = class {
904
1040
  }
905
1041
  }
906
1042
  _saveCurrentState() {
907
- if (this.modal) return;
1043
+ if (this.modal || !this.persistGeometry || this._closed) return;
908
1044
  _setWindowState(this.id, {
909
1045
  x: this._preMaximizeState?.x ?? this.x,
910
1046
  y: this._preMaximizeState?.y ?? this.y,
@@ -1183,11 +1319,15 @@ var ManagedWindow = class {
1183
1319
  _onKeyDown(e) {
1184
1320
  if (!this.isVisible || this.isMinimized) return;
1185
1321
  const topWindow = Array.from(_activeWindows.values()).filter((w) => w.isVisible && !w.isMinimized).sort((a, b) => b.zIndex - a.zIndex)[0];
1186
- if (topWindow !== this) return;
1187
1322
  if (e.key === "Escape") {
1188
- this.close();
1323
+ if (e.defaultPrevented || e.isComposing || _escAnswered.has(e)) return;
1324
+ if (_escAnswerer() === this) {
1325
+ _escAnswered.add(e);
1326
+ this.close();
1327
+ }
1189
1328
  return;
1190
1329
  }
1330
+ if (topWindow !== this) return;
1191
1331
  if (e.key === "Tab" && this.modal && this.element) {
1192
1332
  const focusables = _collectFocusable(this.element);
1193
1333
  if (focusables.length === 0) {
@@ -1219,9 +1359,15 @@ var ManagedWindow = class {
1219
1359
  *
1220
1360
  * @param {object} [opts]
1221
1361
  * @param {string} [opts.storageKey=DEFAULT_WINDOW_STORAGE_KEY]
1362
+ * @param {number} [opts.maxSavedWindows=DEFAULT_MAX_SAVED_WINDOWS] How many
1363
+ * ids keep saved geometry; the least recently saved go first.
1222
1364
  */
1223
- static configure({ storageKey = DEFAULT_WINDOW_STORAGE_KEY } = {}) {
1365
+ static configure({
1366
+ storageKey = DEFAULT_WINDOW_STORAGE_KEY,
1367
+ maxSavedWindows = DEFAULT_MAX_SAVED_WINDOWS
1368
+ } = {}) {
1224
1369
  _storageKey = storageKey;
1370
+ _maxSavedWindows = Math.max(1, Math.floor(maxSavedWindows) || DEFAULT_MAX_SAVED_WINDOWS);
1225
1371
  _stateCache = null;
1226
1372
  }
1227
1373
  /** @returns {string} The localStorage key window geometry uses. */
@@ -1894,6 +2040,7 @@ function isRowActivation(ev, gesture = "click", { scope = null } = {}) {
1894
2040
 
1895
2041
  export {
1896
2042
  DEFAULT_WINDOW_STORAGE_KEY,
2043
+ DEFAULT_MAX_SAVED_WINDOWS,
1897
2044
  ManagedWindow,
1898
2045
  ToggleField,
1899
2046
  isHexColor,
@@ -1910,4 +2057,4 @@ export {
1910
2057
  endsTextSelection,
1911
2058
  isRowActivation
1912
2059
  };
1913
- //# sourceMappingURL=chunk-MADMGRX2.js.map
2060
+ //# sourceMappingURL=chunk-PGZ4EHSE.js.map