flexdesk 0.17.11 → 0.17.13

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.
@@ -9,7 +9,7 @@ import {
9
9
  normalizeHex,
10
10
  selectBox,
11
11
  toggleChips
12
- } from "./chunk-CVMCZZX2.js";
12
+ } from "./chunk-MADMGRX2.js";
13
13
  import {
14
14
  DragReorder
15
15
  } from "./chunk-WUFLGETN.js";
@@ -1308,4 +1308,4 @@ export {
1308
1308
  openConfirm,
1309
1309
  openModal
1310
1310
  };
1311
- //# sourceMappingURL=chunk-BE2PU3LL.js.map
1311
+ //# sourceMappingURL=chunk-ZMH3OC7H.js.map
package/dist/flexdesk.css CHANGED
@@ -1695,6 +1695,25 @@ body.twm-dt-col-resizing {
1695
1695
  .twm-global-top-bar, .twm-global-top-bar .twm-bar-left, .twm-global-top-bar .twm-bar-center, .twm-global-top-bar .twm-bar-right {
1696
1696
  min-width: 0; /* allow flex children to shrink */
1697
1697
  }
1698
+ /* Global menu row (STORY-0119, 0.17.13): a shell's `chrome.menuBar` in a
1699
+ * full-width strip of its own, directly under .twm-global-top-bar in
1700
+ * body.twm-body's column (host.css), so the host below shrinks by its height.
1701
+ * It is a sibling of the top bar, not inside it: a window-drag region bound
1702
+ * to the top bar does not include it. No z-index, so it is no stacking
1703
+ * context: the top bar's dropdowns draw over it, and the .twm-menubar inside
1704
+ * keeps its own styles, its menus (position: fixed, --z-over-windows) drawing
1705
+ * over the page. Its height is the bar's plus the padding. */
1706
+ .twm-global-menu-bar {
1707
+ display: flex;
1708
+ align-items: center;
1709
+ width: 100%;
1710
+ box-sizing: border-box;
1711
+ padding: var(--space-xxs) var(--space-xs);
1712
+ background-color: var(--surface-1);
1713
+ color: var(--text-body);
1714
+ border-bottom: 1px solid var(--surface-0);
1715
+ overflow: visible; /* the bar's menus extend below */
1716
+ }
1698
1717
  .twm-bar-center {
1699
1718
  white-space: nowrap;
1700
1719
  overflow: hidden;
@@ -8528,8 +8547,9 @@ body.twm-dragging * { user-select: none; }
8528
8547
  * the WM host fills the remaining space.
8529
8548
  */
8530
8549
 
8531
- /* Body is a flex column: top bar, WM host (fill), bottom bar. We
8532
- * override the legacy `position: fixed` on both bars so they become
8550
+ /* Body is a flex column: top bar, menu row (when there is one), WM host
8551
+ * (fill), bottom bar. We override the legacy `position: fixed` on both
8552
+ * bars so they become
8533
8553
  * deterministic flex items and the host's height is exactly the
8534
8554
  * leftover space — no math, no overlap, no off-by-N pixels. Their
8535
8555
  * `z-index` stays put for dropdown stacking. */
@@ -8547,6 +8567,12 @@ body.twm-body > .twm-global-bottom-bar {
8547
8567
  inset: auto !important;
8548
8568
  flex: 0 0 auto;
8549
8569
  }
8570
+ /* The menu row (STORY-0119) sits between the top bar and the host, in
8571
+ * normal flow: a fixed-size strip, so the host's leftover space is less by
8572
+ * exactly its height. */
8573
+ body.twm-body > .twm-global-menu-bar {
8574
+ flex: 0 0 auto;
8575
+ }
8550
8576
  body.twm-body > .twm-host {
8551
8577
  flex: 1 1 auto;
8552
8578
  min-height: 0;
package/dist/flow.js CHANGED
@@ -1,15 +1,15 @@
1
1
  import {
2
2
  modalHost
3
- } from "./chunk-BE2PU3LL.js";
3
+ } from "./chunk-ZMH3OC7H.js";
4
4
  import {
5
5
  DataTable
6
- } from "./chunk-NIJEJAZ7.js";
6
+ } from "./chunk-2PM6ZMNS.js";
7
7
  import {
8
8
  choiceList,
9
9
  selectBox,
10
10
  shownValue,
11
11
  toggleChips
12
- } from "./chunk-CVMCZZX2.js";
12
+ } from "./chunk-MADMGRX2.js";
13
13
  import "./chunk-HNU6MM2B.js";
14
14
  import "./chunk-WUFLGETN.js";
15
15
  import {
package/dist/tiles.js CHANGED
@@ -17,11 +17,11 @@ import {
17
17
  } from "./chunk-MIW7D6WC.js";
18
18
  import {
19
19
  showContextMenu
20
- } from "./chunk-F35XSQWA.js";
20
+ } from "./chunk-IG74S3ZH.js";
21
21
  import {
22
22
  modalHost,
23
23
  openModal
24
- } from "./chunk-BE2PU3LL.js";
24
+ } from "./chunk-ZMH3OC7H.js";
25
25
  import {
26
26
  buildHistogramTrace,
27
27
  buildLegendLayout,
@@ -36,11 +36,11 @@ import {
36
36
  purge,
37
37
  resize,
38
38
  strideDownsampleShared
39
- } from "./chunk-2YTM3GJE.js";
40
- import "./chunk-NIJEJAZ7.js";
39
+ } from "./chunk-TLIV6QSU.js";
40
+ import "./chunk-2PM6ZMNS.js";
41
41
  import {
42
42
  isHexColor
43
- } from "./chunk-CVMCZZX2.js";
43
+ } from "./chunk-MADMGRX2.js";
44
44
  import "./chunk-HNU6MM2B.js";
45
45
  import "./chunk-WUFLGETN.js";
46
46
  import {
package/dist/widgets.js CHANGED
@@ -26,7 +26,7 @@ import {
26
26
  import {
27
27
  hideContextMenu,
28
28
  showContextMenu
29
- } from "./chunk-F35XSQWA.js";
29
+ } from "./chunk-IG74S3ZH.js";
30
30
  import {
31
31
  ColorListField,
32
32
  SortableList,
@@ -35,7 +35,7 @@ import {
35
35
  openForm,
36
36
  openModal,
37
37
  setModalHost
38
- } from "./chunk-BE2PU3LL.js";
38
+ } from "./chunk-ZMH3OC7H.js";
39
39
  import {
40
40
  ACTIONS,
41
41
  AutocompleteField,
@@ -82,7 +82,7 @@ import {
82
82
  resolvePaging,
83
83
  toTSV,
84
84
  tsvCell
85
- } from "./chunk-NIJEJAZ7.js";
85
+ } from "./chunk-2PM6ZMNS.js";
86
86
  import {
87
87
  ColorField,
88
88
  DEFAULT_WINDOW_STORAGE_KEY,
@@ -96,7 +96,7 @@ import {
96
96
  normalizeHex,
97
97
  selectBox,
98
98
  shownValue
99
- } from "./chunk-CVMCZZX2.js";
99
+ } from "./chunk-MADMGRX2.js";
100
100
  import {
101
101
  getSetting
102
102
  } from "./chunk-HNU6MM2B.js";
package/dist/wm.js CHANGED
@@ -8,14 +8,14 @@ import {
8
8
  } from "./chunk-REJ5HLIM.js";
9
9
  import {
10
10
  showContextMenu
11
- } from "./chunk-F35XSQWA.js";
11
+ } from "./chunk-IG74S3ZH.js";
12
12
  import {
13
13
  openForm
14
- } from "./chunk-BE2PU3LL.js";
14
+ } from "./chunk-ZMH3OC7H.js";
15
15
  import {
16
16
  ManagedWindow,
17
17
  isRowActivation
18
- } from "./chunk-CVMCZZX2.js";
18
+ } from "./chunk-MADMGRX2.js";
19
19
  import "./chunk-HNU6MM2B.js";
20
20
  import {
21
21
  NotebookTabBar
@@ -4,6 +4,40 @@
4
4
 
5
5
  Plotly-backed charts: the wrapper, the chart types, subplot layouts, downsampling, the plot grid, the plot-config editor and the two plot windows. Plotly itself is not in the bundle; call `setPlotlySource()` with the path to your copy. The plot window's `streamEvents` option is described with the [other application-neutral options](../concepts/domain-neutrality.md#no-application-globals-the-gallery-picker-and-plot-window-events-0170).
6
6
 
7
+ ## An empty cell can name what is missing: `emptyMessage` as a function (0.17.12)
8
+
9
+ [`renderPlotGrid`](#a-plot-config-in-any-container-renderplotgrid-0170),
10
+ `PlotPopoutWindow` and `openPlotPopoutWindow` take `emptyMessage` as a string,
11
+ as before, or as a function of the subplot:
12
+
13
+ ```js
14
+ const grid = renderPlotGrid(el, plotConfig, results, {
15
+ emptyMessage: (subplot, results, index) => {
16
+ const name = subplot.yAxes[0].series[0]?.variable;
17
+ return results ? `${name} not in this run` : 'No data yet.';
18
+ },
19
+ });
20
+ await grid.update({ results: next }); // the empty cells say what is missing in `next`
21
+ ```
22
+
23
+ The function is called each time a cell is drawn empty, with that cell's
24
+ subplot, the grid's results at that moment (as the grid was given them, so null or undefined when there are none) and the
25
+ cell's index in the layout: on the first draw, on every `update()` (new
26
+ `results`, a new `plotConfig`, `overlays` or `hiddenSeries`), on a resize that
27
+ draws a deferred cell, and when a subplot fails to draw. So the message follows
28
+ results that stream in or change, without disposing and rebuilding the grid. A
29
+ slot with no subplot still shows `noSubplotMessage`.
30
+
31
+ The plot window hands its `emptyMessage` to its grid, so the function is called
32
+ on the window's own redraws too: its refresh and stream events and
33
+ `updateConfig()`. `openPlotPopoutWindow` forwards it with its other options.
34
+ The Data tab's `tableEmptyMessage` stays a string.
35
+
36
+ A string `emptyMessage` is shown exactly as before, and no option or export
37
+ changed.
38
+
39
+ `tests/plot_grid.test.mjs` §8 pins it.
40
+
7
41
  ## Interpolation `'auto'` is a listed choice (0.17.9)
8
42
 
9
43
  A subplot's `interpolation` is `'linear'`, `'step'`, `'cubic'` or `'auto'`.
@@ -197,7 +231,9 @@ subplot: its chart type, interpolation, data points and stack groups, every
197
231
  y-axis (left or right, linear or log, min, max, step), reference lines and
198
232
  shaded bands, the NaN strategy, the x-axis clipped to the results' time
199
233
  range, or an x variable instead of time (`xAxis: { useTime: false, variable }`).
200
- A subplot with nothing to draw shows `emptyMessage` (default `No data yet.`); a
234
+ A subplot with nothing to draw shows `emptyMessage` (default `No data yet.`;
235
+ [a function of the subplot](#an-empty-cell-can-name-what-is-missing-emptymessage-as-a-function-01712)
236
+ since 0.17.12); a
201
237
  slot with no subplot shows `noSubplotMessage` (`No subplot configured.`); null
202
238
  results empty every subplot. A cell that is detached or has no size is not
203
239
  drawn: the grid's own ResizeObserver draws it once it has one. `dispose()`
@@ -101,6 +101,65 @@ The bar is `.twm-menubar` (not the older `.twm-menu-bar` icon strip).
101
101
  `tests/menu_bar.test.mjs` drives it in jsdom; `tests/menu_bar_css.test.mjs`
102
102
  holds its stylesheet.
103
103
 
104
+ ## The menu bar in its own row under the top bar (0.17.13)
105
+
106
+ A shell's menu bar can sit on a second line, as a full-width strip directly
107
+ under the top bar. Put `chrome.menuBar` in a `.twm-global-menu-bar` that is a
108
+ child of `body.twm-body`, between `.twm-global-top-bar` and the host:
109
+
110
+ ```html
111
+ <body class="twm-body">
112
+ <div class="twm-global-top-bar">
113
+ <div class="twm-bar-left">…</div>
114
+ <div class="twm-bar-center twm-top-nav">…</div>
115
+ <div class="twm-bar-right">…</div>
116
+ </div>
117
+ <div class="twm-global-menu-bar">
118
+ <div id="menus"></div> <!-- chrome.menuBar -->
119
+ </div>
120
+ <div class="twm-host">…</div> <!-- createShell's root -->
121
+ <div class="twm-global-bottom-bar">…</div>
122
+ </body>
123
+ ```
124
+
125
+ ```js
126
+ const shell = await createShell({
127
+ // …
128
+ menuBar: { menus, onAction },
129
+ chrome: { menuBar: document.getElementById('menus') },
130
+ });
131
+ ```
132
+
133
+ - **The host gives up the row's height.** `body.twm-body` is a flex column,
134
+ and the row is a fixed-size item in it (`flex: 0 0 auto`). The host still
135
+ takes whatever is left, so it is shorter by the row's height and nothing is
136
+ drawn under the row. The row's height is the bar's (24px) plus its padding.
137
+ - **It is styled with FlexDesk tokens.** It is a flex row with
138
+ `padding: var(--space-xxs) var(--space-xs)`, background `var(--surface-1)`,
139
+ text `var(--text-body)` and a bottom border `var(--surface-0)`, the same
140
+ border as the top bar. The `.twm-menubar` inside it keeps its own styles.
141
+ - **It is not part of the top bar.** It is a sibling of `.twm-global-top-bar`,
142
+ not inside it. A window-drag region bound to the top bar
143
+ (`WindowChromeController`'s `topBar`, which defaults to
144
+ `.twm-global-top-bar`) does not include the row. A press on the row never
145
+ drags the window, and its menus open and take clicks as usual. The row sets
146
+ no `z-index`, so the top bar's dropdowns are drawn over it, and the bar's
147
+ menus (`position: fixed`) are drawn over the page.
148
+ - **Windows on the page stop under it.** A `ManagedWindow` on `document.body`
149
+ (no `container`) is clamped, centred and maximised below the top bar and
150
+ the row, not under the row. Windows inside the shell already stay within
151
+ the host.
152
+ - **A modal leaves it usable, like the top bar.** A modal `ManagedWindow`'s
153
+ backdrop starts below the top bar (`top: 35px`), so the row, like the top
154
+ bar, is not dimmed and its menus can still be opened while a modal is open.
155
+
156
+ A shell without the row looks exactly as before: nothing changes until the
157
+ page has a `.twm-global-menu-bar`. The row works only inside `body.twm-body`.
158
+ Outside it, the top bar is `position: fixed` and would cover the row.
159
+ `tests/menu_row_layout.test.mjs` loads flexdesk.css in jsdom and checks the
160
+ row's rules, its place after the top bar, the drag region and the window
161
+ bounds.
162
+
104
163
  ## Action dropdowns: separators (0.17.0)
105
164
 
106
165
  `new ActionDropdown({ trigger, options, onSelect })` draws one button per
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flexdesk",
3
- "version": "0.17.11",
3
+ "version": "0.17.13",
4
4
  "description": "A tiling window manager and widget set for desktop-class web applications. Zero runtime dependencies.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -72,6 +72,16 @@ function resolveSeriesData(series, results) {
72
72
  return series._resultTime ? { y, x: series._resultTime } : { y };
73
73
  }
74
74
 
75
+ /**
76
+ * The text of an empty cell: `message` itself when it is a string, else what
77
+ * the function `message(subplot, results, index)` returns. PlotPopoutWindow
78
+ * hands its `emptyMessage` to the grid unchanged, so this is the one place the
79
+ * two forms are told apart.
80
+ */
81
+ function emptyMessageFor(message, subplot, results, index) {
82
+ return typeof message === 'function' ? message(subplot, results, index) : message;
83
+ }
84
+
75
85
  /**
76
86
  * Draw `plotConfig` into `container`.
77
87
  *
@@ -95,7 +105,10 @@ function resolveSeriesData(series, results) {
95
105
  * @param {Object} [options.theme] - Overrides for PLOT_GRID_THEME.
96
106
  * @param {boolean} [options.alwaysShowLegend=false] - Draw a 'hidden' legend at the top.
97
107
  * @param {Object} [options.layout] - Plotly layout fields merged over every figure's.
98
- * @param {string} [options.emptyMessage='No data yet.']
108
+ * @param {string|Function} [options.emptyMessage='No data yet.'] - What a subplot
109
+ * with nothing to draw says: a string, or `(subplot, results, index) => string`,
110
+ * called each time a cell is drawn empty (first draw, every `update()`, a
111
+ * failed draw) with the grid's results at that moment (as the grid was given them, so null or undefined when there are none).
99
112
  * @param {string} [options.noSubplotMessage='No subplot configured.']
100
113
  * @param {Function} [options.onError] - `({ index, error })` when drawing a subplot
101
114
  * throws — on an update or on a resize that draws a deferred one; that
@@ -170,6 +183,11 @@ function renderPlotGrid(container, plotConfig, results, options = {}) {
170
183
  deferred.clear();
171
184
  }
172
185
 
186
+ /** Show the empty message in cell `index`, for its subplot and the current results. */
187
+ function showEmpty(cell, index) {
188
+ showMessage(cell, emptyMessageFor(opts.emptyMessage, config?.subplots?.[index] ?? null, data, index));
189
+ }
190
+
173
191
  function showMessage(cell, text) {
174
192
  purgeCell(cell);
175
193
  const msg = document.createElement('div');
@@ -348,7 +366,7 @@ function renderPlotGrid(container, plotConfig, results, options = {}) {
348
366
 
349
367
  const figure = await buildFigure(sp, index);
350
368
  if (!current()) return false;
351
- if (!figure) { showMessage(cell, opts.emptyMessage); deferred.delete(index); return false; }
369
+ if (!figure) { showEmpty(cell, index); deferred.delete(index); return false; }
352
370
 
353
371
  if (!hasSize(cell)) { deferred.add(index); return false; }
354
372
  deferred.delete(index);
@@ -381,7 +399,7 @@ function renderPlotGrid(container, plotConfig, results, options = {}) {
381
399
  if (disposed) return false;
382
400
  if (cells[index] === cell && tokens[index] === token) {
383
401
  deferred.delete(index);
384
- showMessage(cell, opts.emptyMessage);
402
+ showEmpty(cell, index);
385
403
  }
386
404
  opts.onError?.({ index, error });
387
405
  return false;
@@ -62,6 +62,11 @@ export class PlotPopoutWindow {
62
62
  * updates through `refreshEvent` and explicit calls only.
63
63
  * @param {Function} [options.resolveSeries] - `(series, results, subplot) => { y, x? } | null`
64
64
  * (plot_grid.js); used by the chart and the data table. Default resolveSeriesData.
65
+ * @param {string|Function} [options.emptyMessage='No data yet.'] - What a chart
66
+ * cell with nothing to draw says: a string, or `(subplot, results, index) =>
67
+ * string`, called on every redraw that leaves the cell empty (renderPlotGrid).
68
+ * @param {string} [options.tableEmptyMessage='No data yet.'] - What the Data tab
69
+ * says with no rows.
65
70
  */
66
71
  constructor({ id, title, plotConfig, getResults, services, host, onClose, actuals,
67
72
  configRenderer, seriesTransforms, resolveSeries = resolveSeriesData,
@@ -1010,6 +1015,8 @@ const _openWindows = new Map();
1010
1015
  * @param {Function} options.getResults - Returns { time: number[], series: { [key]: number[] } }
1011
1016
  * @param {Object} options.services - { eventBus, logger }
1012
1017
  * @param {Object} [options.actuals] - ETL overlay data
1018
+ * @param {string|Function} [options.emptyMessage] - As PlotPopoutWindow's: a string
1019
+ * or `(subplot, results, index) => string`. Every option is forwarded.
1013
1020
  * @returns {PlotPopoutWindow}
1014
1021
  */
1015
1022
  export async function openPlotPopoutWindow(options) {
@@ -34,6 +34,17 @@ const MAX_Z_INDEX = 6999;
34
34
  const TOP_BAR_HEIGHT = 35;
35
35
  const BOTTOM_BAR_HEIGHT = 22;
36
36
 
37
+ /** STORY-0119. The space above a window on `document.body`: the top bar, and
38
+ * under it a shell's menu row (`.twm-global-menu-bar`, a sibling of the top
39
+ * bar in body.twm-body's column) when the page has one. The row is measured,
40
+ * not assumed, because its height is its content's. Without a row this is
41
+ * TOP_BAR_HEIGHT, exactly as before. */
42
+ function _topInset() {
43
+ const row = typeof document !== 'undefined'
44
+ ? document.querySelector('body > .twm-global-menu-bar') : null;
45
+ return TOP_BAR_HEIGHT + (row ? row.getBoundingClientRect().height : 0);
46
+ }
47
+
37
48
  /** C11. How close to an edge the POINTER must come for a snap zone to arm.
38
49
  * Small enough that it takes intent, large enough to hit without aiming. */
39
50
  const SNAP_EDGE = 12;
@@ -953,6 +964,10 @@ export class ManagedWindow {
953
964
  * width = window.innerWidth
954
965
  * height = window.innerHeight - TOP_BAR_HEIGHT - BOTTOM_BAR_HEIGHT
955
966
  *
967
+ * (STORY-0119: TOP_BAR_HEIGHT here is `_topInset()`, which is
968
+ * TOP_BAR_HEIGHT on a page without a menu row and adds the row's height
969
+ * on a page with one.)
970
+ *
956
971
  * so, substituting into the clamps below:
957
972
  *
958
973
  * maxWidth = width = window.innerWidth ✓
@@ -1005,11 +1020,12 @@ export class ManagedWindow {
1005
1020
  height: this.container.clientHeight,
1006
1021
  };
1007
1022
  }
1023
+ const top = _topInset();
1008
1024
  return {
1009
1025
  minX: 0,
1010
- minY: TOP_BAR_HEIGHT,
1026
+ minY: top,
1011
1027
  width: window.innerWidth,
1012
- height: window.innerHeight - TOP_BAR_HEIGHT - BOTTOM_BAR_HEIGHT,
1028
+ height: window.innerHeight - top - BOTTOM_BAR_HEIGHT,
1013
1029
  };
1014
1030
  }
1015
1031
 
@@ -1266,7 +1282,8 @@ export class ManagedWindow {
1266
1282
  } else {
1267
1283
  // Center on screen (between top and bottom bars)
1268
1284
  this.x = Math.max(0, (maxWidth - this.width) / 2);
1269
- this.y = Math.max(TOP_BAR_HEIGHT, TOP_BAR_HEIGHT + (maxHeight - this.height) / 2);
1285
+ const top = _topInset();
1286
+ this.y = Math.max(top, top + (maxHeight - this.height) / 2);
1270
1287
  }
1271
1288
  }
1272
1289
 
@@ -1584,7 +1601,8 @@ export class ManagedWindow {
1584
1601
 
1585
1602
  // Calculate max available dimensions
1586
1603
  const maxWidth = window.innerWidth;
1587
- const maxHeight = window.innerHeight - TOP_BAR_HEIGHT - BOTTOM_BAR_HEIGHT;
1604
+ const top = _topInset();
1605
+ const maxHeight = window.innerHeight - top - BOTTOM_BAR_HEIGHT;
1588
1606
 
1589
1607
  let newX = state.startWinX;
1590
1608
  let newY = state.startWinY;
@@ -1604,12 +1622,12 @@ export class ManagedWindow {
1604
1622
 
1605
1623
  // Handle vertical resize
1606
1624
  if (dir.includes('s')) {
1607
- newH = Math.max(this.minHeight, Math.min(state.startHeight + dy, maxHeight - (newY - TOP_BAR_HEIGHT)));
1625
+ newH = Math.max(this.minHeight, Math.min(state.startHeight + dy, maxHeight - (newY - top)));
1608
1626
  }
1609
1627
  if (dir.includes('n')) {
1610
1628
  const maxDy = state.startHeight - this.minHeight;
1611
1629
  const actualDy = Math.min(dy, maxDy);
1612
- newY = Math.max(TOP_BAR_HEIGHT, state.startWinY + actualDy);
1630
+ newY = Math.max(top, state.startWinY + actualDy);
1613
1631
  newH = state.startHeight - (newY - state.startWinY);
1614
1632
  }
1615
1633