@jbrowse/app-core 5.0.0-beta.13 → 5.0.0-beta.14

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 (44) hide show
  1. package/esm/AppSession/AppSessionMixin.d.ts +110 -92
  2. package/esm/Assemblies/AssembliesMixin.d.ts +30 -0
  3. package/esm/HistoryManagement/index.d.ts +2 -0
  4. package/esm/HistoryManagement/index.js +2 -0
  5. package/esm/JBrowseConfig/RootConfiguration.d.ts +3 -2
  6. package/esm/JBrowseConfig/RootConfiguration.js +3 -2
  7. package/esm/JBrowseConfig/index.d.ts +2 -3
  8. package/esm/JBrowseConfig/index.js +2 -3
  9. package/esm/JBrowseModel/index.d.ts +9 -1
  10. package/esm/JBrowseModel/index.js +8 -7
  11. package/esm/JbApi/jbApi.js +5 -2
  12. package/esm/RootMenu/index.d.ts +2 -0
  13. package/esm/RootMenu/index.js +2 -0
  14. package/esm/SessionSpec/index.d.ts +2 -2
  15. package/esm/SessionSpec/index.js +1 -1
  16. package/esm/SessionSpec/parseSessionSpecUrl.d.ts +19 -0
  17. package/esm/SessionSpec/parseSessionSpecUrl.js +29 -0
  18. package/esm/WorkspaceLayout/WorkspaceContainer.js +2 -10
  19. package/esm/WorkspaceLayout/WorkspacePanelActions.js +1 -1
  20. package/esm/WorkspaceLayout/WorkspaceTab.d.ts +2 -3
  21. package/esm/WorkspaceLayout/WorkspaceTab.js +2 -2
  22. package/esm/WorkspaceLayout/model.d.ts +10 -15
  23. package/esm/WorkspaceLayout/model.js +36 -65
  24. package/esm/WorkspaceLayout/tree.d.ts +3 -6
  25. package/esm/WorkspaceLayout/tree.js +8 -12
  26. package/esm/WorkspaceLayout/useLayoutDrag.js +10 -4
  27. package/esm/index.d.ts +5 -3
  28. package/esm/index.js +3 -2
  29. package/esm/queryParams.d.ts +2 -0
  30. package/esm/queryParams.js +16 -5
  31. package/esm/sessionSummary.d.ts +16 -0
  32. package/esm/sessionSummary.js +57 -0
  33. package/esm/ui/App/DrawerHeaderHelpDialog.d.ts +3 -2
  34. package/esm/ui/App/DrawerHeaderHelpDialog.js +4 -2
  35. package/esm/ui/App/JBrowseTabMenu.d.ts +3 -2
  36. package/esm/ui/App/JBrowseTabMenu.js +4 -2
  37. package/esm/ui/App/ThemeModeButton.d.ts +4 -5
  38. package/esm/ui/App/ThemeModeButton.js +18 -11
  39. package/esm/ui/App/ViewContainer.js +7 -1
  40. package/esm/ui/App/ViewMenu.js +34 -25
  41. package/esm/ui/App/ViewWrapper.js +2 -2
  42. package/esm/ui/App/viewTitle.d.ts +6 -1
  43. package/esm/ui/App/viewTitle.js +7 -2
  44. package/package.json +6 -6
@@ -1,20 +1,11 @@
1
1
  import { createElementId } from '@jbrowse/core/util/types/mst';
2
- import { cast, getSnapshot, types } from '@jbrowse/mobx-state-tree';
2
+ import { addDisposer, cast, getSnapshot, types } from '@jbrowse/mobx-state-tree';
3
+ import { autorun } from 'mobx';
3
4
  import { resolveLayoutSpec, tileLayoutSpec, treeFromSpec, viewIdsInSpec, } from './spec.js';
4
- import { activeTabIn, addTab, addViewToTab, findTab, homeViews, moveTabToPanel, normalize, panelContainingView, panels, pruneEmptyPanel, pruneEmptyTabIn, removePanel, removeTab, removeView, renameTab, setActiveTab, setSizes, splitPanel, tabContainingView, tabs, } from './tree.js';
5
+ import { addTab, addViewToTab, findTab, homeViews, moveTabToPanel, normalize, panelContainingView, panels, pruneEmptyPanel, pruneEmptyTab, removePanel, removeTab, removeView, renameTab, setActiveTab, setSizes, splitPanel, tabContainingView, tabs, } from './tree.js';
5
6
  function sessionViewIds(self) {
6
7
  return self.views?.map(v => v.id);
7
8
  }
8
- /**
9
- * A caller's view list narrowed to the session's views, deduplicated. The list
10
- * may be stale, and `applyLayoutSpec` throws on an unknown or repeated id.
11
- */
12
- function liveViewIds(self, allViewIds) {
13
- const live = sessionViewIds(self);
14
- return [
15
- ...new Set(live ? allViewIds.filter(id => live.includes(id)) : allViewIds),
16
- ];
17
- }
18
9
  /**
19
10
  * #stateModel WorkspaceLayoutMixin
20
11
  *
@@ -104,16 +95,6 @@ export function WorkspaceLayoutMixin() {
104
95
  panelContainingView(viewId) {
105
96
  return panelContainingView(self.tree, viewId);
106
97
  },
107
- /** The views a tab renders: its members, in `session.views` order. */
108
- viewIdsForTab(tabId, order) {
109
- const members = new Set(findTab(self.tree, tabId)?.tab.viewIds ?? []);
110
- return order.filter(id => members.has(id));
111
- },
112
- /** The tab a panel is showing, or its first. */
113
- activeTabOf(panelId) {
114
- const panel = panels(self.tree).find(p => p.id === panelId);
115
- return panel ? activeTabIn(panel) : undefined;
116
- },
117
98
  /**
118
99
  * The maximized cell alone, or the whole tree. The cell is resized to 1
119
100
  * because CSS hands out free space by grow factor only up to a total of
@@ -161,33 +142,21 @@ export function WorkspaceLayoutMixin() {
161
142
  function home(tree, viewIds) {
162
143
  return homeViews(tree, viewIds, self.activePanelId, () => nextId('tab'));
163
144
  }
164
- function everyViewId(action, allViewIds) {
165
- const ids = allViewIds ?? sessionViewIds(self);
166
- if (!ids) {
167
- throw new Error(`${action}(viewId, allViewIds): allViewIds is every view id in the session, and this session has no view list to default it from`);
168
- }
169
- return ids;
170
- }
171
145
  /**
172
- * Homes every view first, since from the classic stack none has a tab
173
- * yet. Returns where the view came from, or `undefined` if it is not in
174
- * the session, in which case nothing is applied.
146
+ * Homes every view first: the homing autorun waits for the outermost
147
+ * action, so a view added in the same action has no tab yet. Returns
148
+ * where the view came from, or `undefined` if it is not in the
149
+ * session, in which case nothing is applied.
175
150
  */
176
- function rehomeView(viewId, allViewIds, place) {
177
- const tree = home(self.tree, allViewIds);
151
+ function rehomeView(viewId, place) {
152
+ const ids = sessionViewIds(self);
153
+ const tree = ids ? home(self.tree, ids) : self.tree;
178
154
  const from = tabContainingView(tree, viewId);
179
155
  if (!from) {
180
156
  return undefined;
181
157
  }
182
158
  const placed = place(removeView(tree, viewId), from);
183
- const pruned = pruneEmptyTabIn(placed, from.panel.id, from.tab.id);
184
- const source = panels(pruned).find(p => p.id === from.panel.id);
185
- const emptiedCell = source?.tabs.length === 1 &&
186
- source.tabs[0].id === from.tab.id &&
187
- source.tabs[0].viewIds.length === 0;
188
- apply(emptiedCell
189
- ? pruneEmptyPanel(removeTab(pruned, from.tab.id), from.panel.id)
190
- : pruned);
159
+ apply(pruneEmptyPanel(pruneEmptyTab(placed, from.tab.id), from.panel.id));
191
160
  return from;
192
161
  }
193
162
  /**
@@ -329,12 +298,10 @@ export function WorkspaceLayoutMixin() {
329
298
  },
330
299
  /**
331
300
  * ViewMenu's "move to new tab": the view leaves its tab for a new one.
332
- * `allViewIds` is EVERY view in the session, defaulting to the host's
333
- * own list, since homing drops any view the list omits.
334
301
  */
335
- moveViewToNewTab(viewId, allViewIds) {
302
+ moveViewToNewTab(viewId) {
336
303
  const tab = { id: nextId('tab'), viewIds: [viewId] };
337
- const from = rehomeView(viewId, everyViewId('moveViewToNewTab', allViewIds), (tree, at) => addTab(tree, at.panel.id, tab));
304
+ const from = rehomeView(viewId, (tree, at) => addTab(tree, at.panel.id, tab));
338
305
  if (!from) {
339
306
  return undefined;
340
307
  }
@@ -345,7 +312,7 @@ export function WorkspaceLayoutMixin() {
345
312
  * ViewMenu's "move to split": the view leaves for a new cell to the
346
313
  * right (`row`) or below (`column`) of its own.
347
314
  */
348
- moveViewToSplit(viewId, direction, allViewIds) {
315
+ moveViewToSplit(viewId, direction) {
349
316
  const tabId = nextId('tab');
350
317
  const panel = {
351
318
  id: nextId('panel'),
@@ -353,7 +320,7 @@ export function WorkspaceLayoutMixin() {
353
320
  tabs: [{ id: tabId, viewIds: [viewId] }],
354
321
  activeTabId: tabId,
355
322
  };
356
- const from = rehomeView(viewId, everyViewId('moveViewToSplit', allViewIds), (tree, at) => splitPanel(tree, at.panel.id, direction, panel));
323
+ const from = rehomeView(viewId, (tree, at) => splitPanel(tree, at.panel.id, direction, panel));
357
324
  if (!from) {
358
325
  return undefined;
359
326
  }
@@ -364,12 +331,12 @@ export function WorkspaceLayoutMixin() {
364
331
  * ViewMenu's "move to tab": the view joins an existing tab, which
365
332
  * becomes the one shown.
366
333
  */
367
- moveViewToTab(viewId, tabId, allViewIds) {
334
+ moveViewToTab(viewId, tabId) {
368
335
  const target = findTab(self.tree, tabId);
369
336
  if (!target || target.tab.viewIds.includes(viewId)) {
370
337
  return;
371
338
  }
372
- const from = rehomeView(viewId, everyViewId('moveViewToTab', allViewIds), tree => addViewToTab(tree, tabId, viewId));
339
+ const from = rehomeView(viewId, tree => addViewToTab(tree, tabId, viewId));
373
340
  const landed = findTab(self.tree, tabId);
374
341
  if (from && landed) {
375
342
  apply(setActiveTab(self.tree, landed.panel.id, tabId));
@@ -387,12 +354,11 @@ export function WorkspaceLayoutMixin() {
387
354
  /**
388
355
  * Move one view to a new tab or a split beside its cell, keeping the
389
356
  * rest of the arrangement. PUBLIC API: protein3d and msaview call it
390
- * with the move alone, so `allViewIds` stays optional; it is read only
391
- * on a host with no view list.
357
+ * with the move alone. The second argument is an old plugin signature;
358
+ * the session's own views are read instead.
392
359
  */
393
- setPendingMove(move, allViewIds) {
394
- // the session's own list wins over a plugin's possibly stale copy
395
- const ids = sessionViewIds(self) ?? allViewIds;
360
+ setPendingMove(move, _allViewIds) {
361
+ const ids = sessionViewIds(self);
396
362
  if (!move || !ids?.includes(move.viewId)) {
397
363
  return;
398
364
  }
@@ -400,23 +366,18 @@ export function WorkspaceLayoutMixin() {
400
366
  self.homeUnassignedViews(ids);
401
367
  }
402
368
  else if (move.type === 'newTab') {
403
- self.moveViewToNewTab(move.viewId, ids);
369
+ self.moveViewToNewTab(move.viewId);
404
370
  }
405
371
  else {
406
- self.moveViewToSplit(move.viewId, 'row', ids);
407
- }
408
- const home = tabContainingView(self.tree, move.viewId);
409
- if (home) {
410
- self.setActiveTab(home.panel.id, home.tab.id);
372
+ self.moveViewToSplit(move.viewId, 'row');
411
373
  }
412
374
  },
413
375
  /**
414
376
  * Re-arrange the whole workspace, one view per cell, in one of four
415
- * shapes. Pass `allViewIds` in `session.views` order so there is
416
- * nothing for `orderViews` to apply.
377
+ * shapes, in `session.views` order.
417
378
  */
418
- tileViews(mode, allViewIds) {
419
- self.applyLayoutSpec(tileLayoutSpec(liveViewIds(self, allViewIds), mode));
379
+ tileViews(mode) {
380
+ self.applyLayoutSpec(tileLayoutSpec(sessionViewIds(self) ?? [], mode));
420
381
  },
421
382
  /**
422
383
  * #action
@@ -440,6 +401,16 @@ export function WorkspaceLayoutMixin() {
440
401
  host.orderViews(ids);
441
402
  return ids;
442
403
  },
404
+ }))
405
+ .actions(self => ({
406
+ afterAttach() {
407
+ addDisposer(self, autorun(function homeUnassignedViewsAutorun() {
408
+ const host = self;
409
+ if (host.views) {
410
+ self.homeUnassignedViews(host.views.map(v => v.id));
411
+ }
412
+ }, { name: 'HomeUnassignedViews' }));
413
+ },
443
414
  }))
444
415
  // A session saved with workspaces off showed its views stacked whatever
445
416
  // layout it carried, so it opens as one cell holding them all
@@ -39,6 +39,7 @@ export interface TabHome {
39
39
  export declare function findTab(node: LayoutTree, tabId: string): TabHome | undefined;
40
40
  /** The tab a panel is showing: the one it names, or its first. */
41
41
  export declare function activeTabIn(panel: PanelNode): TabNode | undefined;
42
+ export declare function activeTabOf(root: LayoutTree, panelId: string): TabNode | undefined;
42
43
  export declare function tabContainingView(node: LayoutTree, viewId: string): TabHome | undefined;
43
44
  export declare function panelContainingView(node: LayoutTree, viewId: string): PanelNode | undefined;
44
45
  /**
@@ -93,9 +94,5 @@ export declare function homeViews(root: LayoutTree, viewIds: string[], activePan
93
94
  * normalisation rule: only the gesture that emptied the panel calls it.
94
95
  */
95
96
  export declare function pruneEmptyPanel(root: LayoutTree, panelId: string): LayoutTree;
96
- /**
97
- * Drop a tab a move has left with no views, unless it is the panel's last.
98
- * Closing a tab's last view leaves the tab standing, as a place to open the
99
- * next one; only a move out of it prunes.
100
- */
101
- export declare function pruneEmptyTabIn(root: LayoutTree, panelId: string, tabId: string): LayoutTree;
97
+ /** Remove a tab that holds no views; a tab with views stays. */
98
+ export declare function pruneEmptyTab(root: LayoutTree, tabId: string): LayoutTree;
@@ -31,6 +31,10 @@ export function findTab(node, tabId) {
31
31
  export function activeTabIn(panel) {
32
32
  return panel.tabs.find(t => t.id === panel.activeTabId) ?? panel.tabs[0];
33
33
  }
34
+ export function activeTabOf(root, panelId) {
35
+ const panel = panels(root).find(p => p.id === panelId);
36
+ return panel && activeTabIn(panel);
37
+ }
34
38
  export function tabContainingView(node, viewId) {
35
39
  return homeOf(node, t => t.viewIds.includes(viewId));
36
40
  }
@@ -271,16 +275,8 @@ export function pruneEmptyPanel(root, panelId) {
271
275
  ? removePanel(root, panelId)
272
276
  : root;
273
277
  }
274
- /**
275
- * Drop a tab a move has left with no views, unless it is the panel's last.
276
- * Closing a tab's last view leaves the tab standing, as a place to open the
277
- * next one; only a move out of it prunes.
278
- */
279
- export function pruneEmptyTabIn(root, panelId, tabId) {
280
- const found = findPanel(root, panelId);
281
- if (!found || found.tabs.length <= 1) {
282
- return root;
283
- }
284
- const tab = found.tabs.find(t => t.id === tabId);
285
- return tab?.viewIds.length === 0 ? removeTab(root, tabId) : root;
278
+ /** Remove a tab that holds no views; a tab with views stays. */
279
+ export function pruneEmptyTab(root, tabId) {
280
+ const found = findTab(root, tabId);
281
+ return found?.tab.viewIds.length === 0 ? removeTab(root, tabId) : root;
286
282
  }
@@ -10,12 +10,18 @@ function samePlace(a, b) {
10
10
  a?.strip?.index === b?.strip?.index &&
11
11
  a?.strip?.left === b?.strip?.left);
12
12
  }
13
- // a centre drop on the tab's own cell, which `dropTabInPanel` declines, so it
13
+ // a drop `dropTabInPanel` / `dropTabInNewSplit` would leave as it was, which
14
14
  // paints nothing either
15
15
  function declines(layout, drag) {
16
- return (!drag.strip &&
17
- drag.zone === 'center' &&
18
- layout.findTab(drag.tabId)?.panel.id === drag.panelId);
16
+ const home = layout.findTab(drag.tabId);
17
+ if (home?.panel.id !== drag.panelId) {
18
+ return false;
19
+ }
20
+ if (drag.strip) {
21
+ const from = home.panel.tabs.findIndex(t => t.id === drag.tabId);
22
+ return drag.strip.index === from || drag.strip.index === from + 1;
23
+ }
24
+ return drag.zone === 'center' || home.panel.tabs.length === 1;
19
25
  }
20
26
  /**
21
27
  * Dragging a tab. Pointer events rather than HTML5 dnd, for capture; the
package/esm/index.d.ts CHANGED
@@ -8,11 +8,13 @@ export type { AppRootModel } from './AppSession/index.ts';
8
8
  export { RootAppMenuMixin } from './RootMenu/index.ts';
9
9
  export { WorkspaceLayoutMixin, isSessionWithWorkspaceLayout, } from './WorkspaceLayout/index.ts';
10
10
  export type { WorkspaceLayoutMixinType, WorkspaceLayout, LayoutTree, PanelNode, TabNode, LayoutSpecNode, PendingMove, TileMode, TabHome, } from './WorkspaceLayout/index.ts';
11
- export { addSessionTracks, buildLgvInit, buildLgvInitFromParams, hubConnectionSpec, loadSessionSpec, parseSessionSpecUrl, readHubUrlParam, readNavParam, readTracklistParam, shortHubLabel, splitHighlights, } from './SessionSpec/index.ts';
12
- export type { LayoutNode, LgvUrlInit, ParsedSessionSpec, ViewSpec, } from './SessionSpec/index.ts';
11
+ export { addSessionTracks, buildLgvInit, buildLgvInitFromParams, hubConnectionSpec, loadSessionSpec, parseSessionSnapshotUrl, parseSessionSpecUrl, readHubUrlParam, readNavParam, readTracklistParam, shortHubLabel, splitHighlights, } from './SessionSpec/index.ts';
12
+ export type { LayoutNode, LgvUrlInit, ParsedSessionSnapshotUrl, ParsedSessionSpec, ViewSpec, } from './SessionSpec/index.ts';
13
13
  export type { SessionModelFactory } from './sessionModelFactory.ts';
14
- export { deleteQueryParams, readAllQueryParams, readQueryParams, setQueryParams, useQueryParam, } from './queryParams.ts';
14
+ export { deleteQueryParams, queryParamsHref, readAllQueryParams, readQueryParams, setQueryParams, useQueryParam, } from './queryParams.ts';
15
15
  export { processMutableMenuActions, resolveMenus } from './menus.ts';
16
+ export { formatSessionSummary, summarizeSession } from './sessionSummary.ts';
17
+ export type { SessionSummary } from './sessionSummary.ts';
16
18
  export type { Menu, MenuAction } from './menus.ts';
17
19
  export { createJbApi, ensureReExports, safeJson, sessionOf, undeliveredNotifications, waitReady, } from './JbApi/index.ts';
18
20
  export type { JbApi } from './JbApi/index.ts';
package/esm/index.js CHANGED
@@ -6,11 +6,12 @@ export { AssembliesMixin } from './Assemblies/index.js';
6
6
  export { AppSessionMixin } from './AppSession/index.js';
7
7
  export { RootAppMenuMixin } from './RootMenu/index.js';
8
8
  export { WorkspaceLayoutMixin, isSessionWithWorkspaceLayout, } from './WorkspaceLayout/index.js';
9
- export { addSessionTracks, buildLgvInit, buildLgvInitFromParams, hubConnectionSpec, loadSessionSpec, parseSessionSpecUrl, readHubUrlParam, readNavParam, readTracklistParam, shortHubLabel, splitHighlights, } from './SessionSpec/index.js';
9
+ export { addSessionTracks, buildLgvInit, buildLgvInitFromParams, hubConnectionSpec, loadSessionSpec, parseSessionSnapshotUrl, parseSessionSpecUrl, readHubUrlParam, readNavParam, readTracklistParam, shortHubLabel, splitHighlights, } from './SessionSpec/index.js';
10
10
  // URL params, for the two products that own their page (web, desktop). An
11
11
  // embedded product must not rewrite its host's URL — see queryParams.ts
12
- export { deleteQueryParams, readAllQueryParams, readQueryParams, setQueryParams, useQueryParam, } from './queryParams.js';
12
+ export { deleteQueryParams, queryParamsHref, readAllQueryParams, readQueryParams, setQueryParams, useQueryParam, } from './queryParams.js';
13
13
  export { processMutableMenuActions, resolveMenus } from './menus.js';
14
+ export { formatSessionSummary, summarizeSession } from './sessionSummary.js';
14
15
  // The helper library an agent drives the app through: desktop serves it to
15
16
  // run_javascript over MCP, web hands it to the page as window.jb
16
17
  export { createJbApi, ensureReExports, safeJson, sessionOf, undeliveredNotifications, waitReady, } from './JbApi/index.js';
@@ -2,6 +2,8 @@ export declare function readQueryParams<T extends string>(keys: T[]): Record<T,
2
2
  export declare function readAllQueryParams(url?: URL | Location): URLSearchParams;
3
3
  export declare function deleteQueryParams(keys: readonly string[]): void;
4
4
  export declare function setQueryParams(values: Record<string, string | undefined>): void;
5
+ /** The page's own URL with `values` applied, for a link to another state. */
6
+ export declare function queryParamsHref(values: Record<string, string | undefined>): string;
5
7
  /**
6
8
  * Read one param and keep re-rendering as it changes, including across the
7
9
  * `replaceState` writes above, which fire no event of their own.
@@ -23,7 +23,7 @@ function subscribe(callback) {
23
23
  }
24
24
  // Writes params back into `loc`, leaving the other URL component untouched — the
25
25
  // URL hash/search setters each replace only their own component.
26
- function writeParams(loc, params) {
26
+ function urlWith(loc, params) {
27
27
  const url = new URL(window.location.href);
28
28
  const str = params.toString();
29
29
  if (loc === 'hash') {
@@ -32,7 +32,10 @@ function writeParams(loc, params) {
32
32
  else {
33
33
  url.search = str;
34
34
  }
35
- window.history.replaceState(null, '', url.href);
35
+ return url.href;
36
+ }
37
+ function writeParams(loc, params) {
38
+ window.history.replaceState(null, '', urlWith(loc, params));
36
39
  // Copied, which this note has claimed while the loop read the live set. A
37
40
  // listener is React's store-change callback, and React may flush the re-render
38
41
  // synchronously inside it, so the set can be mutated mid-notify: an unmounted
@@ -67,8 +70,7 @@ export function deleteQueryParams(keys) {
67
70
  }
68
71
  writeParams(loc, params);
69
72
  }
70
- export function setQueryParams(values) {
71
- const loc = paramLocation();
73
+ function paramsWith(loc, values) {
72
74
  const params = readParams(loc);
73
75
  for (const [key, value] of Object.entries(values)) {
74
76
  if (value === undefined) {
@@ -78,7 +80,16 @@ export function setQueryParams(values) {
78
80
  params.set(key, value);
79
81
  }
80
82
  }
81
- writeParams(loc, params);
83
+ return params;
84
+ }
85
+ export function setQueryParams(values) {
86
+ const loc = paramLocation();
87
+ writeParams(loc, paramsWith(loc, values));
88
+ }
89
+ /** The page's own URL with `values` applied, for a link to another state. */
90
+ export function queryParamsHref(values) {
91
+ const loc = paramLocation();
92
+ return urlWith(loc, paramsWith(loc, values));
82
93
  }
83
94
  /**
84
95
  * Read one param and keep re-rendering as it changes, including across the
@@ -0,0 +1,16 @@
1
+ /**
2
+ * What a saved session holds, for telling saved sessions apart in a recent
3
+ * list without opening them. Read off the snapshot rather than a live model,
4
+ * so it can be taken wherever the snapshot is written.
5
+ */
6
+ export interface SessionSummary {
7
+ assemblies: string[];
8
+ views: number;
9
+ tracks: number;
10
+ }
11
+ /**
12
+ * Walks every view, including the subviews a comparative view nests, so a
13
+ * synteny view's assemblies and tracks count the same as two plain views'.
14
+ */
15
+ export declare function summarizeSession(snap: unknown): SessionSummary;
16
+ export declare function formatSessionSummary({ assemblies, views, tracks, }: SessionSummary): string;
@@ -0,0 +1,57 @@
1
+ function isRecord(x) {
2
+ return typeof x === 'object' && x !== null;
3
+ }
4
+ /**
5
+ * Walks every view, including the subviews a comparative view nests, so a
6
+ * synteny view's assemblies and tracks count the same as two plain views'.
7
+ */
8
+ export function summarizeSession(snap) {
9
+ const assemblies = new Set();
10
+ let tracks = 0;
11
+ const visit = (node) => {
12
+ if (Array.isArray(node)) {
13
+ for (const child of node) {
14
+ visit(child);
15
+ }
16
+ }
17
+ else if (isRecord(node)) {
18
+ for (const [key, value] of Object.entries(node)) {
19
+ if (key === 'assemblyName' && typeof value === 'string') {
20
+ assemblies.add(value);
21
+ }
22
+ else if (key === 'assemblyNames' && Array.isArray(value)) {
23
+ for (const name of value) {
24
+ if (typeof name === 'string') {
25
+ assemblies.add(name);
26
+ }
27
+ }
28
+ }
29
+ else if (key === 'tracks' && Array.isArray(value)) {
30
+ tracks += value.length;
31
+ }
32
+ else {
33
+ visit(value);
34
+ }
35
+ }
36
+ }
37
+ };
38
+ const views = isRecord(snap) && Array.isArray(snap.views) ? snap.views : [];
39
+ visit(views);
40
+ return { assemblies: [...assemblies], views: views.length, tracks };
41
+ }
42
+ function plural(n, word) {
43
+ return `${n} ${word}${n === 1 ? '' : 's'}`;
44
+ }
45
+ export function formatSessionSummary({ assemblies, views, tracks, }) {
46
+ const shown = assemblies.slice(0, 2).join(', ');
47
+ const more = assemblies.length - 2;
48
+ return views
49
+ ? [
50
+ more > 0 ? `${shown} +${more} more` : shown,
51
+ plural(views, 'view'),
52
+ tracks ? plural(tracks, 'track') : '',
53
+ ]
54
+ .filter(Boolean)
55
+ .join(' · ')
56
+ : 'No views';
57
+ }
@@ -1,4 +1,5 @@
1
- export default function DrawerHeaderHelpDialog({ onClose, helpText, }: {
1
+ declare const DrawerHeaderHelpDialog: ({ onClose, helpText, }: {
2
2
  onClose: () => void;
3
3
  helpText: React.ReactNode;
4
- }): import("react").JSX.Element;
4
+ }) => import("react").JSX.Element;
5
+ export default DrawerHeaderHelpDialog;
@@ -1,14 +1,16 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import InfoDialog from '@jbrowse/core/ui/InfoDialog';
3
3
  import { makeStyles } from '@jbrowse/core/util/tss-react';
4
+ import { observer } from 'mobx-react';
4
5
  const useStyles = makeStyles()({
5
6
  max: {
6
7
  minWidth: '40em',
7
8
  },
8
9
  });
9
- export default function DrawerHeaderHelpDialog({ onClose, helpText, }) {
10
+ const DrawerHeaderHelpDialog = observer(function DrawerHeaderHelpDialog({ onClose, helpText, }) {
10
11
  const { classes } = useStyles();
11
12
  return (_jsx(InfoDialog, { open: true, onClose: onClose, title: "Help", maxWidth: "xl", onClick: e => {
12
13
  e.stopPropagation();
13
14
  }, children: _jsx("div", { className: classes.max, children: helpText }) }));
14
- }
15
+ });
16
+ export default DrawerHeaderHelpDialog;
@@ -1,4 +1,5 @@
1
- export default function JBrowseTabMenu({ onRename, onClose, }: {
1
+ declare const JBrowseTabMenu: ({ onRename, onClose, }: {
2
2
  onRename: () => void;
3
3
  onClose: () => void;
4
- }): import("react").JSX.Element;
4
+ }) => import("react").JSX.Element;
5
+ export default JBrowseTabMenu;
@@ -5,6 +5,7 @@ import { makeStyles } from '@jbrowse/core/util/tss-react';
5
5
  import CloseIcon from '@mui/icons-material/Close';
6
6
  import EditIcon from '@mui/icons-material/Edit';
7
7
  import MoreVertIcon from '@mui/icons-material/MoreVert';
8
+ import { observer } from 'mobx-react';
8
9
  const useStyles = makeStyles()({
9
10
  // hidden until the tab is hovered or focused, by a rule in `TabStrip` keyed
10
11
  // on the `jbrowse-tab-menu` class — hence the plain class name beside the
@@ -30,7 +31,7 @@ const useStyles = makeStyles()({
30
31
  fontSize: 14,
31
32
  },
32
33
  });
33
- export default function JBrowseTabMenu({ onRename, onClose, }) {
34
+ const JBrowseTabMenu = observer(function JBrowseTabMenu({ onRename, onClose, }) {
34
35
  const { classes, cx } = useStyles();
35
36
  const [open, setOpen] = useState(false);
36
37
  return (_jsx("div", { className: cx('jbrowse-tab-menu', classes.tabIcons, open && classes.open),
@@ -55,4 +56,5 @@ export default function JBrowseTabMenu({ onRename, onClose, }) {
55
56
  onClick: onClose,
56
57
  },
57
58
  ], tooltip: "Tab menu", size: "small", className: classes.tabIcon, setOpen: setOpen, stopPropagation: true, children: _jsx(MoreVertIcon, { className: classes.smallIcon }) }) }));
58
- }
59
+ });
60
+ export default JBrowseTabMenu;
@@ -1,10 +1,9 @@
1
1
  import type { ThemeSwitchSession } from './types.ts';
2
2
  /**
3
- * A light/dark switch for a session that follows the OS, showing the mode
4
- * drawn. A click holds the other mode, a second click follows the OS again. A
5
- * session on an explicit light or dark gets nothing, which is every session
6
- * that has not asked for this in Preferences, so a light reader is offered no
7
- * route into dark.
3
+ * A light/dark switch for a session that follows the OS or sits on dark,
4
+ * showing the mode drawn. A click holds the other mode, a second click returns
5
+ * to the setting. A session on an explicit light gets nothing, so a reader who
6
+ * never asked for dark is offered no route into it.
8
7
  */
9
8
  declare const ThemeModeButton: ({ session, }: {
10
9
  session: ThemeSwitchSession;
@@ -4,30 +4,37 @@ import LightModeIcon from '@mui/icons-material/LightMode';
4
4
  import { IconButton, Tooltip } from '@mui/material';
5
5
  import { observer } from 'mobx-react';
6
6
  /**
7
- * A light/dark switch for a session that follows the OS, showing the mode
8
- * drawn. A click holds the other mode, a second click follows the OS again. A
9
- * session on an explicit light or dark gets nothing, which is every session
10
- * that has not asked for this in Preferences, so a light reader is offered no
11
- * route into dark.
7
+ * A light/dark switch for a session that follows the OS or sits on dark,
8
+ * showing the mode drawn. A click holds the other mode, a second click returns
9
+ * to the setting. A session on an explicit light gets nothing, so a reader who
10
+ * never asked for dark is offered no route into it.
12
11
  */
13
12
  const ThemeModeButton = observer(function ThemeModeButton({ session, }) {
14
13
  const dark = session.themeIsDark;
15
14
  const drawnMode = dark ? 'dark' : 'light';
16
15
  // A palette pinned to its own mode, or Dark Reader, draws dark whatever the
17
16
  // OS says, so a click here would change nothing.
18
- const followsSystem = session.themeMode === 'system' &&
17
+ const switchable = session.themeMode !== 'light' &&
19
18
  !session.darkReaderDark &&
20
19
  drawnMode === session.effectiveThemeMode;
21
- if (!followsSystem) {
20
+ if (!switchable) {
22
21
  return null;
23
22
  }
24
23
  const held = session.systemThemeOverride;
25
24
  const otherMode = dark ? 'light' : 'dark';
26
25
  const Icon = dark ? DarkModeIcon : LightModeIcon;
27
- const label = held
28
- ? `${dark ? 'Dark' : 'Light'} until your system theme changes`
29
- : `Following your system theme (${drawnMode})`;
30
- const action = held ? `to follow it (${otherMode})` : `for ${otherMode}`;
26
+ const system = session.themeMode === 'system';
27
+ const modeName = dark ? 'Dark' : 'Light';
28
+ const label = system
29
+ ? held
30
+ ? `${modeName} until your system theme changes`
31
+ : `Following your system theme (${drawnMode})`
32
+ : held
33
+ ? `${modeName} instead of your dark setting`
34
+ : modeName;
35
+ const action = held
36
+ ? `to ${system ? 'follow it' : 'return'} (${otherMode})`
37
+ : `for ${otherMode}`;
31
38
  return (_jsx(Tooltip, { title: `${label}. Click ${action}.`, arrow: true, children: _jsx(IconButton, { "data-testid": "theme-mode-button", "aria-label": label, color: "inherit", size: "small", onClick: () => {
32
39
  session.setSystemThemeOverride(held ? undefined : otherMode);
33
40
  }, children: _jsx(Icon, { fontSize: "small" }) }) }));
@@ -1,5 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useEffect } from 'react';
3
+ import LoadingEllipses from '@jbrowse/core/ui/LoadingEllipses';
3
4
  import { useFocusOnInteraction, useWidthSetter } from '@jbrowse/core/util/hooks';
4
5
  import { cx, makeStyles } from '@jbrowse/core/util/tss-react';
5
6
  import { isAlive } from '@jbrowse/mobx-state-tree';
@@ -47,6 +48,11 @@ function viewPhase(view) {
47
48
  : 'ready';
48
49
  }
49
50
  const useStyles = makeStyles()(theme => ({
51
+ // ViewLoadingScreen's label and place, which the body shows once mounted
52
+ pending: {
53
+ paddingTop: 20,
54
+ textAlign: 'center',
55
+ },
50
56
  viewContainer: {
51
57
  margin: theme.spacing(0.5),
52
58
  padding: `0 ${theme.spacing(1)} ${theme.spacing(1)}`,
@@ -148,6 +154,6 @@ const ViewContainer = observer(function ViewContainer({ view, session, scrollOnM
148
154
  // landmark list, so "jump to the second view" stops being a Tab count.
149
155
  role: "region", "aria-label": viewTitle(view, r => assemblyManager.getDisplayName(r)), "data-testid": `view-container-${view.id}`, "data-view-phase": viewPhase(view), children: [_jsx(ViewHeader, { view: view, className: backgroundColorClassName, scrollOnMount: scrollOnMount }), _jsx(Paper, { elevation: 0, children: _jsx("div", { ref: bodyRef, style: reserveSpace
150
156
  ? { height: measuredHeight ?? viewHeight(view) }
151
- : undefined, children: showBody ? _jsx(ViewWrapper, { view: view, session: session }) : null }) })] }));
157
+ : undefined, children: showBody ? (_jsx(ViewWrapper, { view: view, session: session })) : reserveSpace && measuredHeight === undefined ? (_jsx(LoadingEllipses, { variant: "h6", busy: false, className: classes.pending })) : null }) })] }));
152
158
  });
153
159
  export default ViewContainer;