@jbrowse/app-core 5.0.0-beta.1 → 5.0.0-beta.10
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.
- package/esm/AppSession/AppSessionMixin.d.ts +42 -316
- package/esm/Assemblies/AssembliesMixin.d.ts +172 -103
- package/esm/Assemblies/AssembliesMixin.js +13 -8
- package/esm/HistoryManagement/index.d.ts +3 -11
- package/esm/HistoryManagement/index.js +24 -28
- package/esm/JBrowseConfig/RootConfiguration.d.ts +80 -124
- package/esm/JBrowseConfig/RootConfiguration.js +5 -11
- package/esm/JBrowseConfig/defaultShareUrl.d.ts +1 -0
- package/esm/JBrowseConfig/defaultShareUrl.js +5 -0
- package/esm/JBrowseConfig/index.d.ts +89 -83
- package/esm/JBrowseConfig/index.js +6 -4
- package/esm/JBrowseModel/index.d.ts +93 -96
- package/esm/JBrowseModel/index.js +43 -43
- package/esm/JbApi/index.d.ts +2 -0
- package/esm/JbApi/index.js +1 -0
- package/esm/JbApi/jbApi.d.ts +2767 -0
- package/esm/JbApi/jbApi.js +1383 -0
- package/esm/RootMenu/index.d.ts +2 -11
- package/esm/RootMenu/index.js +0 -11
- package/esm/SessionSpec/index.d.ts +1 -1
- package/esm/SessionSpec/index.js +1 -1
- package/esm/SessionSpec/lgvUrlInit.js +5 -2
- package/esm/SessionSpec/loadSessionSpec.d.ts +56 -2
- package/esm/SessionSpec/loadSessionSpec.js +285 -123
- package/esm/SessionSpec/parseSessionSpecUrl.d.ts +2 -8
- package/esm/SessionSpec/parseSessionSpecUrl.js +2 -17
- package/esm/SessionSpec/types.d.ts +16 -12
- package/esm/WorkspaceLayout/WorkspaceContainer.js +4 -2
- package/esm/WorkspaceLayout/WorkspaceTab.js +2 -6
- package/esm/WorkspaceLayout/model.d.ts +24 -4
- package/esm/WorkspaceLayout/model.js +76 -17
- package/esm/WorkspaceLayout/spec.d.ts +40 -10
- package/esm/WorkspaceLayout/spec.js +81 -8
- package/esm/index.d.ts +4 -2
- package/esm/index.js +4 -1
- package/esm/menus.d.ts +15 -29
- package/esm/menus.js +76 -92
- package/esm/queryParams.d.ts +1 -1
- package/esm/queryParams.js +10 -9
- package/esm/ui/App/App.js +18 -11
- package/esm/ui/App/AppToolbar.js +23 -8
- package/esm/ui/App/ClassicViewsContainer.d.ts +2 -2
- package/esm/ui/App/DialogQueue.d.ts +2 -2
- package/esm/ui/App/DialogQueue.js +3 -3
- package/esm/ui/App/DrawerHeader.js +2 -3
- package/esm/ui/App/ThemeModeButton.d.ts +12 -0
- package/esm/ui/App/ThemeModeButton.js +34 -0
- package/esm/ui/App/ViewButtons.js +2 -2
- package/esm/ui/App/ViewContainer.js +1 -17
- package/esm/ui/App/ViewContainerTitle.js +15 -12
- package/esm/ui/App/ViewLauncher.js +1 -1
- package/esm/ui/App/ViewMenu.js +2 -2
- package/esm/ui/App/ViewStack.js +7 -1
- package/esm/ui/App/ViewsContainer.d.ts +2 -4
- package/esm/ui/App/ViewsContainer.js +3 -9
- package/esm/ui/App/index.d.ts +1 -1
- package/esm/ui/App/index.js +1 -1
- package/esm/ui/App/lazyParts.d.ts +32 -0
- package/esm/ui/App/lazyParts.js +8 -0
- package/esm/ui/App/preloadAppShell.d.ts +14 -0
- package/esm/ui/App/preloadAppShell.js +41 -0
- package/esm/ui/App/types.d.ts +12 -7
- package/esm/ui/App/useViewVisibility.d.ts +9 -8
- package/esm/ui/App/useViewVisibility.js +11 -10
- package/esm/ui/App/viewTitle.d.ts +10 -3
- package/esm/ui/App/viewTitle.js +13 -5
- package/esm/ui/index.d.ts +1 -1
- package/esm/ui/index.js +1 -1
- package/package.json +7 -7
- package/esm/ui/App/AppReadyMarker.d.ts +0 -34
- package/esm/ui/App/AppReadyMarker.js +0 -56
- package/esm/ui/App/DrawerControls.d.ts +0 -1
- package/esm/ui/App/DrawerControls.js +0 -1
- package/esm/ui/App/copyView.d.ts +0 -1
- package/esm/ui/App/copyView.js +0 -29
package/esm/menus.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { MenuItem
|
|
1
|
+
import type { MenuItem } from '@jbrowse/core/ui/menuItems';
|
|
2
2
|
/**
|
|
3
3
|
* Ensure a top-level menu exists. `position` appends when omitted and counts
|
|
4
4
|
* from the end when negative. A menu whose label is already on the bar is left
|
|
@@ -22,47 +22,33 @@ interface AddItemAction {
|
|
|
22
22
|
menuItem: MenuItem;
|
|
23
23
|
position?: number;
|
|
24
24
|
}
|
|
25
|
-
interface SetMenusAction {
|
|
26
|
-
type: 'setMenus';
|
|
27
|
-
newMenus: MenuDefinition[];
|
|
28
|
-
}
|
|
29
|
-
/**
|
|
30
|
-
* `addMenu`/`setMenus` change which menus exist, so they resolve eagerly — the
|
|
31
|
-
* app bar renders one button per menu before anything is opened. `addItem` only
|
|
32
|
-
* changes a menu's contents, so it resolves when that menu opens, against
|
|
33
|
-
* whatever its items are at that moment.
|
|
34
|
-
*/
|
|
35
|
-
export type MenuAction = AddMenuAction | AddItemAction | SetMenusAction;
|
|
36
25
|
/**
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
26
|
+
* `addMenu` changes which menus exist, so it resolves eagerly — the app bar
|
|
27
|
+
* renders one button per menu before anything is opened. `addItem` only changes
|
|
28
|
+
* a menu's contents, so it resolves when that menu opens, against whatever its
|
|
29
|
+
* items are at that moment.
|
|
40
30
|
*/
|
|
41
|
-
export
|
|
42
|
-
label: string;
|
|
43
|
-
menuItems: MenuItemsGetter;
|
|
44
|
-
}
|
|
31
|
+
export type MenuAction = AddMenuAction | AddItemAction;
|
|
45
32
|
/**
|
|
46
|
-
* A menu
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
33
|
+
* A top-level menu. `menuItems` is a thunk the app bar calls when the menu
|
|
34
|
+
* opens and never before, so a root model's reads stay out of the bar's
|
|
35
|
+
* tracking scope and a plugin's contributions are merged onto each fresh
|
|
36
|
+
* result rather than spliced into a definition.
|
|
50
37
|
*/
|
|
51
38
|
export interface Menu {
|
|
52
39
|
label: string;
|
|
53
40
|
menuItems: () => MenuItem[];
|
|
54
41
|
}
|
|
55
42
|
/**
|
|
56
|
-
* Resolve a root model's
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
* nothing is ever spliced into a definition.
|
|
43
|
+
* Resolve a root model's menus against the contributions plugins have pushed
|
|
44
|
+
* (via `RootAppMenuMixin`). Structural contributions apply now; item
|
|
45
|
+
* contributions are grouped by path and merged when the menu they target
|
|
46
|
+
* opens.
|
|
61
47
|
*
|
|
62
48
|
* Runs on every `menus()` evaluation, so it must not mutate `base` or anything
|
|
63
49
|
* an action carries.
|
|
64
50
|
*/
|
|
65
|
-
export declare function processMutableMenuActions(base:
|
|
51
|
+
export declare function processMutableMenuActions(base: Menu[], actions: MenuAction[]): Menu[];
|
|
66
52
|
/**
|
|
67
53
|
* Flatten resolved menus to plain data by opening every one of them. For tests
|
|
68
54
|
* and other callers that want to assert on menu contents rather than render
|
package/esm/menus.js
CHANGED
|
@@ -1,82 +1,77 @@
|
|
|
1
|
-
// a
|
|
2
|
-
//
|
|
1
|
+
// a conflict is reported once rather than on every open — the menu re-resolves
|
|
2
|
+
// on each open and, while open, on each observer re-render — and the action is
|
|
3
|
+
// what outlives the nodes, which are rebuilt on every menus() evaluation
|
|
3
4
|
const reported = new WeakSet();
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
function cloneMenuItem(item) {
|
|
24
|
-
return 'subMenu' in item
|
|
25
|
-
? { ...item, subMenu: cloneMenuItems(item.subMenu) }
|
|
26
|
-
: item;
|
|
27
|
-
}
|
|
28
|
-
function cloneMenuItems(items) {
|
|
29
|
-
return items.map(item => cloneMenuItem(item));
|
|
30
|
-
}
|
|
31
|
-
function materialize(menuItems) {
|
|
32
|
-
return typeof menuItems === 'function' ? menuItems() : menuItems;
|
|
5
|
+
function register(node, action, depth) {
|
|
6
|
+
const label = action.menuPath[depth];
|
|
7
|
+
if (label === undefined) {
|
|
8
|
+
node.entries.push(action);
|
|
9
|
+
}
|
|
10
|
+
else {
|
|
11
|
+
let entry = node.entries.find((e) => e.type === 'subMenu' && e.label === label);
|
|
12
|
+
if (!entry) {
|
|
13
|
+
entry = {
|
|
14
|
+
type: 'subMenu',
|
|
15
|
+
label,
|
|
16
|
+
path: action.menuPath.slice(0, depth + 1),
|
|
17
|
+
node: { entries: [] },
|
|
18
|
+
first: action,
|
|
19
|
+
};
|
|
20
|
+
node.entries.push(entry);
|
|
21
|
+
}
|
|
22
|
+
register(entry.node, action, depth + 1);
|
|
23
|
+
}
|
|
33
24
|
}
|
|
34
25
|
// splice is already the "counts from the end when negative" rule, and clamps
|
|
35
26
|
// rather than wrapping past the start; the default is what makes it an append
|
|
36
27
|
function insertAt(items, item, position = items.length) {
|
|
37
28
|
items.splice(position, 0, item);
|
|
38
29
|
}
|
|
30
|
+
// the author's form is kept: an array sub-menu resolves now, a thunk sub-menu
|
|
31
|
+
// resolves when its panel opens, with the contributions merged in either way
|
|
32
|
+
function withContributions(item, node) {
|
|
33
|
+
const { subMenu } = item;
|
|
34
|
+
return {
|
|
35
|
+
...item,
|
|
36
|
+
subMenu: typeof subMenu === 'function'
|
|
37
|
+
? () => apply(subMenu(), node)
|
|
38
|
+
: apply(subMenu, node),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
39
41
|
/**
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
* sub-menu
|
|
43
|
-
*
|
|
42
|
+
* Return a copy of `items` with a node's contributions merged in. Nothing is
|
|
43
|
+
* written into `items`, into any sub-menu it holds, or into a contribution's
|
|
44
|
+
* own payload: a sub-menu that gains contributions is replaced by a copy whose
|
|
45
|
+
* `subMenu` resolves them, so the arrays behind a definition or an action are
|
|
46
|
+
* the same on every open.
|
|
47
|
+
*
|
|
48
|
+
* A path segment naming a row that is not a sub-menu is a plugin bug. It
|
|
49
|
+
* costs that subtree of contributions, reported once, and nothing else — this
|
|
50
|
+
* runs from the app bar's click handler, where a throw goes straight through
|
|
51
|
+
* React.
|
|
44
52
|
*/
|
|
45
|
-
function
|
|
46
|
-
|
|
47
|
-
for (const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
if (level.some(mi => 'label' in mi && mi.label === menuName)) {
|
|
51
|
-
const pathSoFar = menuPath.slice(0, idx + 2).join(' > ');
|
|
52
|
-
throw new Error(`"${menuName}" in path "${pathSoFar}" is not a subMenu`);
|
|
53
|
-
}
|
|
54
|
-
sub = { label: menuName, subMenu: [] };
|
|
55
|
-
level.push(sub);
|
|
53
|
+
function apply(items, node) {
|
|
54
|
+
const result = [...items];
|
|
55
|
+
for (const entry of node.entries) {
|
|
56
|
+
if (entry.type === 'addItem') {
|
|
57
|
+
insertAt(result, entry.menuItem, entry.position);
|
|
56
58
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
// cloned rather than inserted by reference — see cloneMenuItem
|
|
70
|
-
insertAt(target, cloneMenuItem(action.menuItem), action.position);
|
|
71
|
-
}
|
|
72
|
-
catch (error) {
|
|
73
|
-
if (!reported.has(action)) {
|
|
74
|
-
reported.add(action);
|
|
75
|
-
console.error(error);
|
|
59
|
+
else {
|
|
60
|
+
const idx = result.findIndex(mi => 'label' in mi && mi.label === entry.label);
|
|
61
|
+
const found = result[idx];
|
|
62
|
+
if (found === undefined) {
|
|
63
|
+
result.push({ label: entry.label, subMenu: apply([], entry.node) });
|
|
64
|
+
}
|
|
65
|
+
else if ('subMenu' in found) {
|
|
66
|
+
result[idx] = withContributions(found, entry.node);
|
|
67
|
+
}
|
|
68
|
+
else if (!reported.has(entry.first)) {
|
|
69
|
+
reported.add(entry.first);
|
|
70
|
+
console.error(new Error(`"${entry.label}" in path "${entry.path.join(' > ')}" is not a subMenu`));
|
|
76
71
|
}
|
|
77
72
|
}
|
|
78
73
|
}
|
|
79
|
-
return
|
|
74
|
+
return result;
|
|
80
75
|
}
|
|
81
76
|
// first menu with this label, or a new empty one — an item action naming a menu
|
|
82
77
|
// that doesn't exist creates it, so a plugin can populate its own menu without
|
|
@@ -86,47 +81,33 @@ function findOrCreateMenu(pending, label, position) {
|
|
|
86
81
|
if (found) {
|
|
87
82
|
return found;
|
|
88
83
|
}
|
|
89
|
-
const menu = {
|
|
90
|
-
insertAt(pending, menu, position);
|
|
91
|
-
return menu;
|
|
92
|
-
}
|
|
93
|
-
function toMenu({ label, base, itemActions }) {
|
|
94
|
-
return {
|
|
84
|
+
const menu = {
|
|
95
85
|
label,
|
|
96
|
-
menuItems: () =>
|
|
86
|
+
menuItems: () => [],
|
|
87
|
+
node: { entries: [] },
|
|
97
88
|
};
|
|
89
|
+
insertAt(pending, menu, position);
|
|
90
|
+
return menu;
|
|
98
91
|
}
|
|
99
92
|
/**
|
|
100
|
-
* Resolve a root model's
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
* nothing is ever spliced into a definition.
|
|
93
|
+
* Resolve a root model's menus against the contributions plugins have pushed
|
|
94
|
+
* (via `RootAppMenuMixin`). Structural contributions apply now; item
|
|
95
|
+
* contributions are grouped by path and merged when the menu they target
|
|
96
|
+
* opens.
|
|
105
97
|
*
|
|
106
98
|
* Runs on every `menus()` evaluation, so it must not mutate `base` or anything
|
|
107
99
|
* an action carries.
|
|
108
100
|
*/
|
|
109
101
|
export function processMutableMenuActions(base, actions) {
|
|
110
|
-
const
|
|
111
|
-
label: m.label,
|
|
112
|
-
base: m.menuItems,
|
|
113
|
-
itemActions: [],
|
|
114
|
-
});
|
|
115
|
-
let pending = base.map(toPending);
|
|
102
|
+
const pending = base.map(m => ({ ...m, node: { entries: [] } }));
|
|
116
103
|
for (const action of actions) {
|
|
117
104
|
switch (action.type) {
|
|
118
|
-
case 'setMenus': {
|
|
119
|
-
// replaces the menu bar wholesale, so item contributions made before it
|
|
120
|
-
// are dropped along with the menus they targeted
|
|
121
|
-
pending = action.newMenus.map(toPending);
|
|
122
|
-
break;
|
|
123
|
-
}
|
|
124
105
|
case 'addMenu': {
|
|
125
106
|
findOrCreateMenu(pending, action.menuName, action.position);
|
|
126
107
|
break;
|
|
127
108
|
}
|
|
128
109
|
case 'addItem': {
|
|
129
|
-
findOrCreateMenu(pending, action.menuPath[0]).
|
|
110
|
+
register(findOrCreateMenu(pending, action.menuPath[0]).node, action, 1);
|
|
130
111
|
break;
|
|
131
112
|
}
|
|
132
113
|
default: {
|
|
@@ -134,7 +115,10 @@ export function processMutableMenuActions(base, actions) {
|
|
|
134
115
|
}
|
|
135
116
|
}
|
|
136
117
|
}
|
|
137
|
-
return pending.map(
|
|
118
|
+
return pending.map(({ label, menuItems, node }) => ({
|
|
119
|
+
label,
|
|
120
|
+
menuItems: () => apply(menuItems(), node),
|
|
121
|
+
}));
|
|
138
122
|
}
|
|
139
123
|
/**
|
|
140
124
|
* Flatten resolved menus to plain data by opening every one of them. For tests
|
package/esm/queryParams.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export declare function readQueryParams<T extends string>(keys: T[]): Record<T, string | undefined>;
|
|
2
|
-
export declare function readAllQueryParams(): URLSearchParams;
|
|
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
5
|
/**
|
package/esm/queryParams.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { useCallback, useSyncExternalStore } from 'react';
|
|
2
2
|
// The single decision of where this URL keeps its params. Computed once per
|
|
3
3
|
// operation and threaded through read+write so the two can't disagree.
|
|
4
|
-
function paramLocation() {
|
|
5
|
-
return
|
|
4
|
+
function paramLocation(url = window.location) {
|
|
5
|
+
return url.hash.includes('=') ? 'hash' : 'search';
|
|
6
6
|
}
|
|
7
|
-
function readParams(loc) {
|
|
8
|
-
return new URLSearchParams(loc === 'hash' ?
|
|
7
|
+
function readParams(loc, url = window.location) {
|
|
8
|
+
return new URLSearchParams(loc === 'hash' ? url.hash.slice(1) : url.search);
|
|
9
9
|
}
|
|
10
10
|
// history.replaceState fires no event, so a write has to notify readers itself.
|
|
11
11
|
// popstate alone covers only back/forward, which nothing here does: desktop's
|
|
@@ -52,11 +52,12 @@ export function readQueryParams(keys) {
|
|
|
52
52
|
}
|
|
53
53
|
return result;
|
|
54
54
|
}
|
|
55
|
-
// Reads the full
|
|
56
|
-
//
|
|
57
|
-
// so the hash/search decision stays in this one
|
|
58
|
-
|
|
59
|
-
|
|
55
|
+
// Reads the full param set from wherever a URL keeps them (hash XOR search),
|
|
56
|
+
// the page's own by default. Producers that need every param at once
|
|
57
|
+
// (buildShareUrl) share this so the hash/search decision stays in this one
|
|
58
|
+
// module.
|
|
59
|
+
export function readAllQueryParams(url = window.location) {
|
|
60
|
+
return readParams(paramLocation(url), url);
|
|
60
61
|
}
|
|
61
62
|
export function deleteQueryParams(keys) {
|
|
62
63
|
const loc = paramLocation();
|
package/esm/ui/App/App.js
CHANGED
|
@@ -1,18 +1,15 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { Suspense,
|
|
2
|
+
import { Suspense, useEffect } from 'react';
|
|
3
3
|
import Snackbar from '@jbrowse/core/ui/Snackbar';
|
|
4
4
|
import { makeStyles } from '@jbrowse/core/util/tss-react';
|
|
5
|
-
import { ModalWidget, drawerGridTemplateColumns } from '@jbrowse/product-core';
|
|
6
|
-
import { AppBar } from '@mui/material';
|
|
5
|
+
import { AppReadyMarker, ModalWidget, drawerGridTemplateColumns, } from '@jbrowse/product-core';
|
|
6
|
+
import { AppBar, useMediaQuery, useTheme } from '@mui/material';
|
|
7
7
|
import { observer } from 'mobx-react';
|
|
8
8
|
import AppFab from './AppFab.js';
|
|
9
|
-
import AppReadyMarker from './AppReadyMarker.js';
|
|
10
9
|
import AppToolbar from './AppToolbar.js';
|
|
11
10
|
import DialogQueue from './DialogQueue.js';
|
|
12
11
|
import ViewsContainer from './ViewsContainer.js';
|
|
13
|
-
import {
|
|
14
|
-
// lazies
|
|
15
|
-
const DrawerWidget = lazy(lazyChunk('DrawerWidget', () => import('./DrawerWidget.js')));
|
|
12
|
+
import { DrawerWidget } from './lazyParts.js';
|
|
16
13
|
const useStyles = makeStyles()(theme => ({
|
|
17
14
|
root: {
|
|
18
15
|
display: 'grid',
|
|
@@ -33,6 +30,9 @@ const useStyles = makeStyles()(theme => ({
|
|
|
33
30
|
gridColumn: 'main',
|
|
34
31
|
display: 'grid',
|
|
35
32
|
gridTemplateRows: '[menubar] min-content [components] minmax(0, 1fr)',
|
|
33
|
+
// an implicit column floors at the toolbar's min-content, which widened
|
|
34
|
+
// the page past a phone's screen rather than ellipsizing the session name
|
|
35
|
+
gridTemplateColumns: 'minmax(0, 1fr)',
|
|
36
36
|
height: '100%',
|
|
37
37
|
},
|
|
38
38
|
appBar: {
|
|
@@ -43,14 +43,21 @@ const useStyles = makeStyles()(theme => ({
|
|
|
43
43
|
const App = observer(function App(props) {
|
|
44
44
|
const { session } = props;
|
|
45
45
|
const { classes } = useStyles();
|
|
46
|
-
const { drawerVisible, drawerWidth, drawerPosition,
|
|
46
|
+
const { drawerVisible, drawerWidth, drawerPosition, modalWidgetVisible } = session;
|
|
47
|
+
// a phone has no room for a drawer column beside the views
|
|
48
|
+
const narrow = useMediaQuery(useTheme().breakpoints.down('sm'), {
|
|
49
|
+
noSsr: true,
|
|
50
|
+
});
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
session.setModalWidgets(narrow);
|
|
53
|
+
}, [session, narrow]);
|
|
47
54
|
const gridTemplateColumns = drawerGridTemplateColumns({
|
|
48
55
|
drawerVisible,
|
|
49
56
|
drawerPosition,
|
|
50
57
|
drawerWidth,
|
|
51
58
|
});
|
|
52
|
-
return (_jsxs("div", { className: classes.root, style: { gridTemplateColumns }, children: [
|
|
53
|
-
session.
|
|
54
|
-
} })) : null, _jsx(DialogQueue, { session: session }), _jsxs("div", { className: classes.appContainer, children: [_jsx(AppBar, { className: classes.appBar, position: "static", "data-testid": "app-bar", children: _jsx(AppToolbar, { ...props }) }), _jsx(ViewsContainer, {
|
|
59
|
+
return (_jsxs("div", { className: classes.root, style: { gridTemplateColumns }, children: [modalWidgetVisible ? (_jsx(ModalWidget, { session: session, fullScreen: narrow, onClose: () => {
|
|
60
|
+
session.closeModalWidget();
|
|
61
|
+
} })) : null, _jsx(DialogQueue, { session: session }), _jsxs("div", { className: classes.appContainer, children: [_jsx(AppBar, { className: classes.appBar, position: "static", "data-testid": "app-bar", children: _jsx(AppToolbar, { ...props }) }), _jsx(ViewsContainer, { session: session })] }), _jsx(AppFab, { session: session }), _jsx(AppReadyMarker, { session: session }), drawerVisible ? (_jsx(Suspense, { fallback: null, children: _jsx(DrawerWidget, { session: session }) })) : null, _jsx(Snackbar, { session: session })] }));
|
|
55
62
|
});
|
|
56
63
|
export { App };
|
package/esm/ui/App/AppToolbar.js
CHANGED
|
@@ -3,14 +3,24 @@ import AppLogo from '@jbrowse/core/ui/AppLogo';
|
|
|
3
3
|
import DropDownMenu from '@jbrowse/core/ui/DropDownMenu';
|
|
4
4
|
import EditableTypography from '@jbrowse/core/ui/EditableTypography';
|
|
5
5
|
import { makeStyles } from '@jbrowse/core/util/tss-react';
|
|
6
|
-
import { Toolbar, Tooltip } from '@mui/material';
|
|
6
|
+
import { Toolbar, Tooltip, useMediaQuery, useTheme } from '@mui/material';
|
|
7
7
|
import { observer } from 'mobx-react';
|
|
8
|
+
import ThemeModeButton from './ThemeModeButton.js';
|
|
8
9
|
const useStyles = makeStyles()(theme => ({
|
|
9
10
|
grow: {
|
|
10
11
|
flexGrow: 1,
|
|
11
12
|
},
|
|
13
|
+
// a long session name ellipsizes rather than pushing Share off a phone
|
|
14
|
+
sessionName: {
|
|
15
|
+
minWidth: 0,
|
|
16
|
+
},
|
|
12
17
|
inputBase: {
|
|
13
18
|
color: theme.palette.primary.contrastText,
|
|
19
|
+
maxWidth: '100%',
|
|
20
|
+
},
|
|
21
|
+
input: {
|
|
22
|
+
maxWidth: '100%',
|
|
23
|
+
textOverflow: 'ellipsis',
|
|
14
24
|
},
|
|
15
25
|
inputRoot: {
|
|
16
26
|
'&:hover': {
|
|
@@ -40,6 +50,10 @@ const useStyles = makeStyles()(theme => ({
|
|
|
40
50
|
const AppToolbar = observer(function AppToolbar({ session, HeaderButtons = _jsx("div", {}), }) {
|
|
41
51
|
const { classes } = useStyles();
|
|
42
52
|
const { name, menus } = session;
|
|
53
|
+
// a phone needs the logo's 150px for the menus and Share
|
|
54
|
+
const narrow = useMediaQuery(useTheme().breakpoints.down('sm'), {
|
|
55
|
+
noSsr: true,
|
|
56
|
+
});
|
|
43
57
|
return (_jsxs(Toolbar, { children: [menus().map(menu => (_jsx(DropDownMenu, { menuTitle: menu.label, menuItems: menu.menuItems,
|
|
44
58
|
// a root model's menu items take the session as their argument, and
|
|
45
59
|
// the renderer decides how to invoke onClick — so this is the whole
|
|
@@ -47,12 +61,13 @@ const AppToolbar = observer(function AppToolbar({ session, HeaderButtons = _jsx(
|
|
|
47
61
|
// menu, rebuilt on each open, to arrive at the same call
|
|
48
62
|
onMenuItemClick: callback => {
|
|
49
63
|
callback(session);
|
|
50
|
-
} }, menu.label))), _jsx("div", { className: classes.grow }), _jsx(Tooltip, { title: "Rename session", arrow: true, children: _jsx(EditableTypography, { value: name, variant: "body1", classes: {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
64
|
+
} }, menu.label))), _jsx("div", { className: classes.grow }), _jsx(Tooltip, { title: "Rename session", arrow: true, children: _jsx("div", { className: classes.sessionName, children: _jsx(EditableTypography, { value: name, variant: "body1", classes: {
|
|
65
|
+
input: classes.input,
|
|
66
|
+
inputBase: classes.inputBase,
|
|
67
|
+
inputRoot: classes.inputRoot,
|
|
68
|
+
inputFocused: classes.inputFocused,
|
|
69
|
+
}, setValue: newName => {
|
|
70
|
+
session.renameCurrentSession(newName);
|
|
71
|
+
} }) }) }), HeaderButtons, _jsx(ThemeModeButton, { session: session }), _jsx("div", { className: classes.grow }), narrow ? null : (_jsx("div", { className: classes.logo, children: _jsx(AppLogo, { session: session }) }))] }));
|
|
57
72
|
});
|
|
58
73
|
export default AppToolbar;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { SessionWithFocusedViewAndDrawerWidgets } from '@jbrowse/core/util';
|
|
2
|
-
interface
|
|
2
|
+
export interface ClassicViewsContainerProps {
|
|
3
3
|
session: SessionWithFocusedViewAndDrawerWidgets;
|
|
4
4
|
}
|
|
5
|
-
declare const ClassicViewsContainer: ({ session, }:
|
|
5
|
+
declare const ClassicViewsContainer: ({ session, }: ClassicViewsContainerProps) => import("react").JSX.Element;
|
|
6
6
|
export default ClassicViewsContainer;
|
|
@@ -12,8 +12,8 @@ import type { AbstractSessionModel } from '@jbrowse/core/util';
|
|
|
12
12
|
* jbrowse-web's fatal-error dialog, which takes every view the user had open.
|
|
13
13
|
* ADR-069, and `cancer_sv/multihop_split_view` is the figure it took.
|
|
14
14
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
15
|
+
* Reset on the queue entry, so the queue advancing clears a banner belonging to
|
|
16
|
+
* the dialog before it, including one of the same component.
|
|
17
17
|
*/
|
|
18
18
|
declare const DialogQueue: ({ session, }: {
|
|
19
19
|
session: AbstractSessionModel;
|
|
@@ -25,12 +25,12 @@ function dismissActiveDialog(session) {
|
|
|
25
25
|
* jbrowse-web's fatal-error dialog, which takes every view the user had open.
|
|
26
26
|
* ADR-069, and `cancer_sv/multihop_split_view` is the figure it took.
|
|
27
27
|
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
28
|
+
* Reset on the queue entry, so the queue advancing clears a banner belonging to
|
|
29
|
+
* the dialog before it, including one of the same component.
|
|
30
30
|
*/
|
|
31
31
|
const DialogQueue = observer(function DialogQueue({ session, }) {
|
|
32
32
|
const { DialogComponent, DialogProps } = session;
|
|
33
|
-
return DialogComponent ? (_jsx(ErrorBoundary, { resetKeys: [DialogComponent], FallbackComponent: ({ error }) => (_jsx(Dialog, { open: true, title: "Error", onClose: () => {
|
|
33
|
+
return DialogComponent ? (_jsx(ErrorBoundary, { resetKeys: [DialogComponent, DialogProps], FallbackComponent: ({ error }) => (_jsx(Dialog, { open: true, title: "Error", onClose: () => {
|
|
34
34
|
dismissActiveDialog(session);
|
|
35
35
|
}, children: _jsx(DialogContent, { children: _jsx(ErrorMessage, { error: error }) }) })), children: _jsx(Suspense, { fallback: null, children: _jsx(DialogComponent, { ...DialogProps }) }) })) : null;
|
|
36
36
|
});
|
|
@@ -2,11 +2,10 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { Suspense, lazy, useCallback } from 'react';
|
|
3
3
|
import { getEnv } from '@jbrowse/core/util';
|
|
4
4
|
import { makeStyles } from '@jbrowse/core/util/tss-react';
|
|
5
|
-
import { DrawerWidgetSelector } from '@jbrowse/product-core';
|
|
5
|
+
import { DrawerControls, DrawerWidgetSelector } from '@jbrowse/product-core';
|
|
6
6
|
import LaunchIcon from '@mui/icons-material/Launch';
|
|
7
7
|
import { AppBar, IconButton, Toolbar, Tooltip } from '@mui/material';
|
|
8
8
|
import { observer } from 'mobx-react';
|
|
9
|
-
import DrawerControls from './DrawerControls.js';
|
|
10
9
|
const DrawerHeaderHelpButton = lazy(() => import('./DrawerHeaderHelpButton.js'));
|
|
11
10
|
const useStyles = makeStyles()(theme => ({
|
|
12
11
|
spacer: {
|
|
@@ -45,7 +44,7 @@ const DrawerHeader = observer(function DrawerHeader({ session, setToolbarHeight,
|
|
|
45
44
|
observer.disconnect();
|
|
46
45
|
};
|
|
47
46
|
}, [setToolbarHeight]);
|
|
48
|
-
return (_jsx(AppBar, { position: "sticky", className: focusedViewId === viewWidgetId
|
|
47
|
+
return (_jsx(AppBar, { position: "sticky", className: viewWidgetId !== undefined && focusedViewId === viewWidgetId
|
|
49
48
|
? classes.headerFocused
|
|
50
49
|
: classes.headerUnfocused, ref: appBarRef, children: _jsxs(Toolbar, { disableGutters: true, children: [_jsx(DrawerWidgetSelector, { session: session }), _jsx(Tooltip, { title: "Open drawer in dialog", children: _jsx(IconButton, { color: "inherit", "data-testid": "drawer-popout", onClick: () => {
|
|
51
50
|
session.popoutWidget();
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ThemeSwitchSession } from './types.ts';
|
|
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.
|
|
8
|
+
*/
|
|
9
|
+
declare const ThemeModeButton: ({ session, }: {
|
|
10
|
+
session: ThemeSwitchSession;
|
|
11
|
+
}) => import("react").JSX.Element | null;
|
|
12
|
+
export default ThemeModeButton;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import DarkModeIcon from '@mui/icons-material/DarkMode';
|
|
3
|
+
import LightModeIcon from '@mui/icons-material/LightMode';
|
|
4
|
+
import { IconButton, Tooltip } from '@mui/material';
|
|
5
|
+
import { observer } from 'mobx-react';
|
|
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.
|
|
12
|
+
*/
|
|
13
|
+
const ThemeModeButton = observer(function ThemeModeButton({ session, }) {
|
|
14
|
+
const dark = session.themeIsDark;
|
|
15
|
+
const drawnMode = dark ? 'dark' : 'light';
|
|
16
|
+
// A palette pinned to its own mode draws dark whatever the OS says, so the
|
|
17
|
+
// two disagree and the system is steering nothing to report. Everything else
|
|
18
|
+
// that hides this control is `themeMode`.
|
|
19
|
+
const followsSystem = session.themeMode === 'system' && drawnMode === session.effectiveThemeMode;
|
|
20
|
+
if (!followsSystem) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
const held = session.systemThemeOverride;
|
|
24
|
+
const otherMode = dark ? 'light' : 'dark';
|
|
25
|
+
const Icon = dark ? DarkModeIcon : LightModeIcon;
|
|
26
|
+
const label = held
|
|
27
|
+
? `${dark ? 'Dark' : 'Light'} until your system theme changes`
|
|
28
|
+
: `Following your system theme (${drawnMode})`;
|
|
29
|
+
const action = held ? `to follow it (${otherMode})` : `for ${otherMode}`;
|
|
30
|
+
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: () => {
|
|
31
|
+
session.setSystemThemeOverride(held ? undefined : otherMode);
|
|
32
|
+
}, children: _jsx(Icon, { fontSize: "small" }) }) }));
|
|
33
|
+
});
|
|
34
|
+
export default ThemeModeButton;
|
|
@@ -20,9 +20,9 @@ const useStyles = makeStyles()(theme => ({
|
|
|
20
20
|
const ViewButtons = observer(function ViewButtons({ view, }) {
|
|
21
21
|
const { classes } = useStyles();
|
|
22
22
|
const session = getSession(view);
|
|
23
|
-
return (_jsxs(_Fragment, { children: [_jsx(IconButton, { "data-testid": "minimize_view", onClick: () => {
|
|
23
|
+
return (_jsxs(_Fragment, { children: [_jsx(IconButton, { "aria-label": view.minimized ? 'Restore view' : 'Minimize view', "data-testid": "minimize_view", onClick: () => {
|
|
24
24
|
view.setMinimized(!view.minimized);
|
|
25
|
-
}, children: view.minimized ? (_jsx(AddIcon, { className: classes.icon, fontSize: "small" })) : (_jsx(MinimizeIcon, { className: classes.icon, fontSize: "small" })) }), _jsx(IconButton, { "data-testid": "close_view", onClick: () => {
|
|
25
|
+
}, children: view.minimized ? (_jsx(AddIcon, { className: classes.icon, fontSize: "small" })) : (_jsx(MinimizeIcon, { className: classes.icon, fontSize: "small" })) }), _jsx(IconButton, { "aria-label": "Close view", "data-testid": "close_view", onClick: () => {
|
|
26
26
|
session.removeView(view);
|
|
27
27
|
}, children: _jsx(CloseIcon, { className: classes.icon, fontSize: "small" }) })] }));
|
|
28
28
|
});
|
|
@@ -9,22 +9,6 @@ import ViewHeader from './ViewHeader.js';
|
|
|
9
9
|
import ViewWrapper from './ViewWrapper.js';
|
|
10
10
|
import { useViewVisibility } from './useViewVisibility.js';
|
|
11
11
|
import { viewTitle } from './viewTitle.js';
|
|
12
|
-
// Intended to keep views mounted within ~1.5 viewport-heights of the visible
|
|
13
|
-
// band. **It does nothing, and making it work was measured as a regression.**
|
|
14
|
-
//
|
|
15
|
-
// An observer clips the target against every scrolling ancestor before applying
|
|
16
|
-
// the margin, which expands only the root's box — and both view containers are
|
|
17
|
-
// `overflow-y: auto`, so through one of them `150% 0px` qualifies exactly the
|
|
18
|
-
// views a `0px` margin would. Rooting the observer at the scroll port instead
|
|
19
|
-
// (`scrollPortOf`) does restore the band, and it came out a wash on scroll cost
|
|
20
|
-
// while holding 9-13 live GPU canvases instead of 6. The mount band trades
|
|
21
|
-
// pipeline rebuilds for live contexts, and with the ceiling at 16 live WebGL2
|
|
22
|
-
// contexts the contexts are the tighter constraint.
|
|
23
|
-
// See agent-docs/reference/GPU_CONTEXT_BUDGET.md.
|
|
24
|
-
//
|
|
25
|
-
// So don't tune this number expecting an effect, and don't make it live without
|
|
26
|
-
// first cutting contexts per display.
|
|
27
|
-
const VIEW_VISIBILITY_ROOT_MARGIN = '150% 0px';
|
|
28
12
|
// Scroll-space reserved for a view that has never been measured yet, so a fresh
|
|
29
13
|
// load spreads views down the page instead of stacking them all in the viewport
|
|
30
14
|
// (which would mark them all visible and defeat the lazy mount).
|
|
@@ -100,7 +84,7 @@ const useStyles = makeStyles()(theme => ({
|
|
|
100
84
|
const ViewContainer = observer(function ViewContainer({ view, session, scrollOnMount, }) {
|
|
101
85
|
const ref = useWidthSetter(view);
|
|
102
86
|
const { classes } = useStyles();
|
|
103
|
-
const { ref: bodyRef, visible, measuredHeight
|
|
87
|
+
const { ref: bodyRef, visible, measuredHeight } = useViewVisibility();
|
|
104
88
|
useFocusOnInteraction(ref, () => {
|
|
105
89
|
session.setFocusedViewId(view.id);
|
|
106
90
|
});
|