@jbrowse/app-core 5.0.0-beta.11 → 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.
- package/esm/AppSession/AppSessionMixin.d.ts +110 -92
- package/esm/Assemblies/AssembliesMixin.d.ts +30 -0
- package/esm/HistoryManagement/index.d.ts +2 -0
- package/esm/HistoryManagement/index.js +2 -0
- package/esm/JBrowseConfig/RootConfiguration.d.ts +4 -10
- package/esm/JBrowseConfig/RootConfiguration.js +5 -8
- package/esm/JBrowseConfig/index.d.ts +3 -11
- package/esm/JBrowseConfig/index.js +2 -3
- package/esm/JBrowseModel/index.d.ts +10 -9
- package/esm/JBrowseModel/index.js +8 -7
- package/esm/JbApi/jbApi.d.ts +2 -2
- package/esm/JbApi/jbApi.js +16 -11
- package/esm/RootMenu/index.d.ts +2 -0
- package/esm/RootMenu/index.js +2 -0
- package/esm/SessionSpec/index.d.ts +2 -2
- package/esm/SessionSpec/index.js +1 -1
- package/esm/SessionSpec/loadSessionSpec.js +2 -12
- package/esm/SessionSpec/parseSessionSpecUrl.d.ts +19 -0
- package/esm/SessionSpec/parseSessionSpecUrl.js +29 -0
- package/esm/WorkspaceLayout/LayoutRenderer.d.ts +0 -10
- package/esm/WorkspaceLayout/LayoutRenderer.js +34 -101
- package/esm/WorkspaceLayout/PanelView.js +19 -45
- package/esm/WorkspaceLayout/TabStrip.d.ts +2 -2
- package/esm/WorkspaceLayout/TabStrip.js +36 -104
- package/esm/WorkspaceLayout/WorkspaceContainer.d.ts +0 -9
- package/esm/WorkspaceLayout/WorkspaceContainer.js +9 -42
- package/esm/WorkspaceLayout/WorkspacePanelActions.d.ts +1 -16
- package/esm/WorkspaceLayout/WorkspacePanelActions.js +46 -51
- package/esm/WorkspaceLayout/WorkspaceTab.d.ts +3 -19
- package/esm/WorkspaceLayout/WorkspaceTab.js +12 -48
- package/esm/WorkspaceLayout/dropZone.d.ts +7 -37
- package/esm/WorkspaceLayout/dropZone.js +7 -44
- package/esm/WorkspaceLayout/model.d.ts +45 -120
- package/esm/WorkspaceLayout/model.js +145 -303
- package/esm/WorkspaceLayout/panelChrome.d.ts +4 -7
- package/esm/WorkspaceLayout/panelChrome.js +0 -2
- package/esm/WorkspaceLayout/spec.d.ts +10 -51
- package/esm/WorkspaceLayout/spec.js +12 -80
- package/esm/WorkspaceLayout/splitter.d.ts +3 -19
- package/esm/WorkspaceLayout/splitter.js +5 -21
- package/esm/WorkspaceLayout/tabName.d.ts +8 -0
- package/esm/WorkspaceLayout/tabName.js +14 -0
- package/esm/WorkspaceLayout/tree.d.ts +19 -79
- package/esm/WorkspaceLayout/tree.js +34 -131
- package/esm/WorkspaceLayout/useLayoutDrag.d.ts +4 -22
- package/esm/WorkspaceLayout/useLayoutDrag.js +55 -177
- package/esm/WorkspaceLayout/usePointerGesture.d.ts +24 -0
- package/esm/WorkspaceLayout/usePointerGesture.js +83 -0
- package/esm/WorkspaceLayout/workspaceTheme.d.ts +27 -0
- package/esm/WorkspaceLayout/workspaceTheme.js +37 -0
- package/esm/index.d.ts +5 -3
- package/esm/index.js +3 -2
- package/esm/queryParams.d.ts +2 -0
- package/esm/queryParams.js +16 -5
- package/esm/sessionSummary.d.ts +16 -0
- package/esm/sessionSummary.js +57 -0
- package/esm/ui/App/App.js +3 -0
- package/esm/ui/App/DrawerHeaderHelpDialog.d.ts +3 -2
- package/esm/ui/App/DrawerHeaderHelpDialog.js +4 -2
- package/esm/ui/App/JBrowseTabMenu.d.ts +3 -2
- package/esm/ui/App/JBrowseTabMenu.js +5 -3
- package/esm/ui/App/ThemeModeButton.d.ts +4 -5
- package/esm/ui/App/ThemeModeButton.js +22 -14
- package/esm/ui/App/ViewContainer.js +7 -1
- package/esm/ui/App/ViewMenu.js +69 -42
- package/esm/ui/App/ViewWrapper.js +2 -2
- package/esm/ui/App/ViewsContainer.js +3 -4
- package/esm/ui/App/lazyParts.d.ts +0 -5
- package/esm/ui/App/lazyParts.js +0 -1
- package/esm/ui/App/preloadAppShell.js +3 -6
- package/esm/ui/App/types.d.ts +3 -2
- package/esm/ui/App/viewTitle.d.ts +6 -1
- package/esm/ui/App/viewTitle.js +7 -2
- package/package.json +7 -7
- package/esm/WorkspaceLayout/dockviewTheme.d.ts +0 -69
- package/esm/WorkspaceLayout/dockviewTheme.js +0 -82
- package/esm/ui/App/ClassicViewsContainer.d.ts +0 -6
- package/esm/ui/App/ClassicViewsContainer.js +0 -22
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { useEffect, useLayoutEffect, useMemo, useRef } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* One pointer drag, owning the rules every gesture here needs: the primary
|
|
4
|
+
* button of the primary pointer starts it, one `pointerId` steers it, capture
|
|
5
|
+
* routes it, and a cancel, a lost capture or (opted in) Escape abandons it.
|
|
6
|
+
* The handlers are identity-stable, so they can sit in memoised chrome.
|
|
7
|
+
*/
|
|
8
|
+
export function usePointerGesture(gesture) {
|
|
9
|
+
const gestureRef = useRef(gesture);
|
|
10
|
+
useLayoutEffect(() => {
|
|
11
|
+
gestureRef.current = gesture;
|
|
12
|
+
});
|
|
13
|
+
const activeRef = useRef(undefined);
|
|
14
|
+
const handlers = useMemo(() => {
|
|
15
|
+
function finish(completed) {
|
|
16
|
+
const active = activeRef.current;
|
|
17
|
+
if (!active) {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
activeRef.current = undefined;
|
|
21
|
+
window.removeEventListener('keydown', onKeyDown);
|
|
22
|
+
if (active.element.hasPointerCapture(active.pointerId)) {
|
|
23
|
+
active.element.releasePointerCapture(active.pointerId);
|
|
24
|
+
}
|
|
25
|
+
gestureRef.current.end?.(active.state, completed);
|
|
26
|
+
}
|
|
27
|
+
function onKeyDown(event) {
|
|
28
|
+
if (event.key === 'Escape') {
|
|
29
|
+
finish(false);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function owns(event) {
|
|
33
|
+
return activeRef.current?.pointerId === event.pointerId;
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
onPointerDown(event) {
|
|
37
|
+
if (event.button !== 0 || !event.isPrimary) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
finish(false);
|
|
41
|
+
const state = gestureRef.current.start(event);
|
|
42
|
+
if (state === undefined) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
activeRef.current = {
|
|
46
|
+
pointerId: event.pointerId,
|
|
47
|
+
element: event.currentTarget,
|
|
48
|
+
state,
|
|
49
|
+
};
|
|
50
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
51
|
+
if (gestureRef.current.escapeCancels) {
|
|
52
|
+
window.addEventListener('keydown', onKeyDown);
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
onPointerMove(event) {
|
|
56
|
+
const active = activeRef.current;
|
|
57
|
+
if (active && owns(event)) {
|
|
58
|
+
gestureRef.current.move(active.state, event);
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
onPointerUp(event) {
|
|
62
|
+
if (owns(event)) {
|
|
63
|
+
finish(true);
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
onPointerCancel(event) {
|
|
67
|
+
if (owns(event)) {
|
|
68
|
+
finish(false);
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
onLostPointerCapture(event) {
|
|
72
|
+
if (owns(event)) {
|
|
73
|
+
finish(false);
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
cancel() {
|
|
77
|
+
finish(false);
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
}, []);
|
|
81
|
+
useEffect(() => handlers.cancel, [handlers]);
|
|
82
|
+
return handlers;
|
|
83
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The workspace chrome: tab strips, splitters and drop indicators.
|
|
3
|
+
*
|
|
4
|
+
* Fixed and dark in every JBrowse theme, the way the app bar is, so the frame
|
|
5
|
+
* never reads as content. The cell body is not chrome and follows
|
|
6
|
+
* `theme.palette.background.default` in `PanelView`.
|
|
7
|
+
*/
|
|
8
|
+
export declare const workspaceTheme: {
|
|
9
|
+
readonly stripBackground: '#252526';
|
|
10
|
+
readonly stripHeight: 35;
|
|
11
|
+
readonly stripFontSize: 13;
|
|
12
|
+
readonly tabDivider: '#1e1e1e';
|
|
13
|
+
readonly selectedTabBackground: '#1e1e1e';
|
|
14
|
+
readonly tabBackground: '#2d2d2d';
|
|
15
|
+
readonly iconHoverBackground: 'rgba(90, 93, 94, 0.31)';
|
|
16
|
+
readonly splitterLine: 'rgb(68, 68, 68)';
|
|
17
|
+
/** grab area; the line drawn inside it is 1px */
|
|
18
|
+
readonly splitterSize: 4;
|
|
19
|
+
readonly dropWash: 'rgba(83, 89, 93, 0.5)';
|
|
20
|
+
readonly accent: 'rgba(56, 139, 253, 0.9)';
|
|
21
|
+
};
|
|
22
|
+
export declare function tabColors(cellActive: boolean, selected: boolean): {
|
|
23
|
+
background: "#1e1e1e" | "#2d2d2d";
|
|
24
|
+
color: string;
|
|
25
|
+
};
|
|
26
|
+
/** the panel actions' icons, which sit on the strip beside unselected tabs */
|
|
27
|
+
export declare const stripIconColor: string;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The workspace chrome: tab strips, splitters and drop indicators.
|
|
3
|
+
*
|
|
4
|
+
* Fixed and dark in every JBrowse theme, the way the app bar is, so the frame
|
|
5
|
+
* never reads as content. The cell body is not chrome and follows
|
|
6
|
+
* `theme.palette.background.default` in `PanelView`.
|
|
7
|
+
*/
|
|
8
|
+
export const workspaceTheme = {
|
|
9
|
+
stripBackground: '#252526',
|
|
10
|
+
stripHeight: 35,
|
|
11
|
+
stripFontSize: 13,
|
|
12
|
+
tabDivider: '#1e1e1e',
|
|
13
|
+
selectedTabBackground: '#1e1e1e',
|
|
14
|
+
tabBackground: '#2d2d2d',
|
|
15
|
+
iconHoverBackground: 'rgba(90, 93, 94, 0.31)',
|
|
16
|
+
splitterLine: 'rgb(68, 68, 68)',
|
|
17
|
+
/** grab area; the line drawn inside it is 1px */
|
|
18
|
+
splitterSize: 4,
|
|
19
|
+
dropWash: 'rgba(83, 89, 93, 0.5)',
|
|
20
|
+
accent: 'rgba(56, 139, 253, 0.9)',
|
|
21
|
+
};
|
|
22
|
+
// the active cell's selected tab is the one fully white label on screen
|
|
23
|
+
const tabTextColors = {
|
|
24
|
+
activeCell: { selected: 'white', other: '#969696' },
|
|
25
|
+
otherCell: { selected: '#8f8f8f', other: '#626262' },
|
|
26
|
+
};
|
|
27
|
+
export function tabColors(cellActive, selected) {
|
|
28
|
+
const text = cellActive ? tabTextColors.activeCell : tabTextColors.otherCell;
|
|
29
|
+
return {
|
|
30
|
+
background: selected
|
|
31
|
+
? workspaceTheme.selectedTabBackground
|
|
32
|
+
: workspaceTheme.tabBackground,
|
|
33
|
+
color: selected ? text.selected : text.other,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/** the panel actions' icons, which sit on the strip beside unselected tabs */
|
|
37
|
+
export const stripIconColor = tabTextColors.activeCell.other;
|
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';
|
package/esm/queryParams.d.ts
CHANGED
|
@@ -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.
|
package/esm/queryParams.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|
package/esm/ui/App/App.js
CHANGED
|
@@ -38,6 +38,9 @@ const useStyles = makeStyles()(theme => ({
|
|
|
38
38
|
appBar: {
|
|
39
39
|
flexGrow: 1,
|
|
40
40
|
gridRow: 'menubar',
|
|
41
|
+
// above the workspace, whose cells paint a background its shadow falls on
|
|
42
|
+
position: 'relative',
|
|
43
|
+
zIndex: 1,
|
|
41
44
|
},
|
|
42
45
|
}));
|
|
43
46
|
const App = observer(function App(props) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
declare const DrawerHeaderHelpDialog: ({ onClose, helpText, }: {
|
|
2
2
|
onClose: () => void;
|
|
3
3
|
helpText: React.ReactNode;
|
|
4
|
-
})
|
|
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
|
-
|
|
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
|
-
|
|
1
|
+
declare const JBrowseTabMenu: ({ onRename, onClose, }: {
|
|
2
2
|
onRename: () => void;
|
|
3
3
|
onClose: () => void;
|
|
4
|
-
})
|
|
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
|
-
|
|
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),
|
|
@@ -54,5 +55,6 @@ export default function JBrowseTabMenu({ onRename, onClose, }) {
|
|
|
54
55
|
icon: CloseIcon,
|
|
55
56
|
onClick: onClose,
|
|
56
57
|
},
|
|
57
|
-
], size: "small", className: classes.tabIcon, setOpen: setOpen, stopPropagation: true, children: _jsx(MoreVertIcon, { className: classes.smallIcon }) }) }));
|
|
58
|
-
}
|
|
58
|
+
], tooltip: "Tab menu", size: "small", className: classes.tabIcon, setOpen: setOpen, stopPropagation: true, children: _jsx(MoreVertIcon, { className: classes.smallIcon }) }) }));
|
|
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
|
|
4
|
-
* drawn. A click holds the other mode, a second click
|
|
5
|
-
* session on an explicit light
|
|
6
|
-
*
|
|
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,29 +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
|
|
8
|
-
* drawn. A click holds the other mode, a second click
|
|
9
|
-
* session on an explicit light
|
|
10
|
-
*
|
|
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
|
-
// A palette pinned to its own mode draws dark whatever the
|
|
17
|
-
//
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
15
|
+
// A palette pinned to its own mode, or Dark Reader, draws dark whatever the
|
|
16
|
+
// OS says, so a click here would change nothing.
|
|
17
|
+
const switchable = session.themeMode !== 'light' &&
|
|
18
|
+
!session.darkReaderDark &&
|
|
19
|
+
drawnMode === session.effectiveThemeMode;
|
|
20
|
+
if (!switchable) {
|
|
21
21
|
return null;
|
|
22
22
|
}
|
|
23
23
|
const held = session.systemThemeOverride;
|
|
24
24
|
const otherMode = dark ? 'light' : 'dark';
|
|
25
25
|
const Icon = dark ? DarkModeIcon : LightModeIcon;
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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}`;
|
|
30
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: () => {
|
|
31
39
|
session.setSystemThemeOverride(held ? undefined : otherMode);
|
|
32
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;
|
package/esm/ui/App/ViewMenu.js
CHANGED
|
@@ -4,18 +4,19 @@ import { getSession } from '@jbrowse/core/util';
|
|
|
4
4
|
import { renameIds } from '@jbrowse/core/util/types/mst';
|
|
5
5
|
import { getSnapshot } from '@jbrowse/mobx-state-tree';
|
|
6
6
|
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
|
|
7
|
+
import HorizontalSplitIcon from '@mui/icons-material/HorizontalSplit';
|
|
7
8
|
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
|
|
8
9
|
import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp';
|
|
9
10
|
import KeyboardDoubleArrowDownIcon from '@mui/icons-material/KeyboardDoubleArrowDown';
|
|
10
11
|
import KeyboardDoubleArrowUpIcon from '@mui/icons-material/KeyboardDoubleArrowUp';
|
|
11
12
|
import MenuIcon from '@mui/icons-material/Menu';
|
|
12
13
|
import OpenInNewIcon from '@mui/icons-material/OpenInNew';
|
|
14
|
+
import TabIcon from '@mui/icons-material/Tab';
|
|
13
15
|
import VerticalSplitIcon from '@mui/icons-material/VerticalSplit';
|
|
14
16
|
import { observer } from 'mobx-react';
|
|
15
|
-
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
// time anything re-rendered the header
|
|
17
|
+
import { tabDisplayName } from '../../WorkspaceLayout/tabName.js';
|
|
18
|
+
// takes a class string rather than an SvgIconProps object, which a caller
|
|
19
|
+
// builds inline and so defeats this observer's memo
|
|
19
20
|
const ViewMenu = observer(function ViewMenu({ model, className, }) {
|
|
20
21
|
const session = getSession(model);
|
|
21
22
|
const moves = {
|
|
@@ -24,36 +25,32 @@ const ViewMenu = observer(function ViewMenu({ model, className, }) {
|
|
|
24
25
|
down: session.moveViewDown,
|
|
25
26
|
bottom: session.moveViewToBottom,
|
|
26
27
|
};
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
// no second implementation for "the panels do not exist yet". That fork is
|
|
31
|
-
// the reason `init` existed.
|
|
32
|
-
const moveViewOut = (direction) => {
|
|
33
|
-
const allViewIds = session.views.map(v => v.id);
|
|
34
|
-
if (direction === 'tabs') {
|
|
35
|
-
session.moveViewToNewTab(model.id, allViewIds);
|
|
28
|
+
const moveViewOut = (to) => {
|
|
29
|
+
if (to === 'tab') {
|
|
30
|
+
session.moveViewToNewTab(model.id);
|
|
36
31
|
}
|
|
37
32
|
else {
|
|
38
|
-
session.
|
|
33
|
+
session.moveViewToSplit(model.id, to);
|
|
39
34
|
}
|
|
40
|
-
|
|
35
|
+
};
|
|
36
|
+
// views render in `session.views` order within a tab, so a view joining one
|
|
37
|
+
// goes to its bottom rather than wherever its old slot falls
|
|
38
|
+
const moveViewToTab = (tabId) => {
|
|
39
|
+
const members = session.findTab(tabId)?.tab.viewIds ?? [];
|
|
40
|
+
session.moveViewToTab(model.id, tabId);
|
|
41
|
+
session.moveViewToBottom(model.id, [...members, model.id]);
|
|
41
42
|
};
|
|
42
43
|
return (_jsx(CascadingMenuButton, { "data-testid": "view_menu_icon", tooltip: "View menu", menuItems: () => {
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
// render would also subscribe this menu to them, so a view moving
|
|
51
|
-
// between panels anywhere re-rendered every view's menu. None of it is
|
|
52
|
-
// needed until the menu opens.
|
|
53
|
-
const scopeIds = session.effectiveUseWorkspaces
|
|
54
|
-
? session.tabContainingView(model.id)?.tab.viewIds.slice()
|
|
55
|
-
: undefined;
|
|
44
|
+
// A move's scope is this view's tab. Read on open rather than in
|
|
45
|
+
// render, so the menu does not observe every tab's membership.
|
|
46
|
+
const home = session.tabContainingView(model.id);
|
|
47
|
+
const scopeIds = home?.tab.viewIds.slice();
|
|
48
|
+
const otherTabs = home
|
|
49
|
+
? session.tabs.filter(t => t.id !== home.tab.id)
|
|
50
|
+
: [];
|
|
56
51
|
const viewCount = scopeIds?.length ?? session.views.length;
|
|
52
|
+
const aloneInTab = viewCount <= 1;
|
|
53
|
+
const aloneInCell = aloneInTab && (home?.panel.tabs.length ?? 1) <= 1;
|
|
57
54
|
return [
|
|
58
55
|
{
|
|
59
56
|
label: 'View options',
|
|
@@ -66,20 +63,50 @@ const ViewMenu = observer(function ViewMenu({ model, className, }) {
|
|
|
66
63
|
session.addView(model.type, renameIds(getSnapshot(model)));
|
|
67
64
|
},
|
|
68
65
|
},
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
66
|
+
...(aloneInTab
|
|
67
|
+
? []
|
|
68
|
+
: [
|
|
69
|
+
{
|
|
70
|
+
label: 'Move to new tab',
|
|
71
|
+
icon: OpenInNewIcon,
|
|
72
|
+
onClick: () => {
|
|
73
|
+
moveViewOut('tab');
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
]),
|
|
77
|
+
...(otherTabs.length > 0
|
|
78
|
+
? [
|
|
79
|
+
{
|
|
80
|
+
label: 'Move to tab',
|
|
81
|
+
icon: TabIcon,
|
|
82
|
+
type: 'subMenu',
|
|
83
|
+
subMenu: otherTabs.map(tab => ({
|
|
84
|
+
label: tabDisplayName(tab, session.views.filter(v => tab.viewIds.includes(v.id)), session),
|
|
85
|
+
onClick: () => {
|
|
86
|
+
moveViewToTab(tab.id);
|
|
87
|
+
},
|
|
88
|
+
})),
|
|
89
|
+
},
|
|
90
|
+
]
|
|
91
|
+
: []),
|
|
92
|
+
...(aloneInCell
|
|
93
|
+
? []
|
|
94
|
+
: [
|
|
95
|
+
{
|
|
96
|
+
label: 'Move to split view (right)',
|
|
97
|
+
icon: VerticalSplitIcon,
|
|
98
|
+
onClick: () => {
|
|
99
|
+
moveViewOut('row');
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
label: 'Move to split view (below)',
|
|
104
|
+
icon: HorizontalSplitIcon,
|
|
105
|
+
onClick: () => {
|
|
106
|
+
moveViewOut('column');
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
]),
|
|
83
110
|
// 'top'/'bottom' only mean something with a view above *and* below
|
|
84
111
|
...[
|
|
85
112
|
['top', 'Move view to top', KeyboardDoubleArrowUpIcon, 2],
|