@jbrowse/app-core 4.3.0 → 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.
Files changed (165) hide show
  1. package/esm/AppSession/AppRootModel.d.ts +30 -0
  2. package/esm/AppSession/AppRootModel.js +1 -0
  3. package/esm/AppSession/AppSessionMixin.d.ts +1324 -0
  4. package/esm/AppSession/AppSessionMixin.js +62 -0
  5. package/esm/AppSession/index.d.ts +2 -0
  6. package/esm/AppSession/index.js +1 -0
  7. package/esm/Assemblies/AssembliesMixin.d.ts +294 -0
  8. package/esm/Assemblies/AssembliesMixin.js +154 -0
  9. package/esm/Assemblies/index.d.ts +1 -2
  10. package/esm/Assemblies/index.js +1 -2
  11. package/esm/HistoryManagement/index.d.ts +11 -0
  12. package/esm/HistoryManagement/index.js +43 -10
  13. package/esm/JBrowseConfig/RootConfiguration.d.ts +123 -78
  14. package/esm/JBrowseConfig/RootConfiguration.js +79 -7
  15. package/esm/JBrowseConfig/defaultShareUrl.d.ts +1 -0
  16. package/esm/JBrowseConfig/defaultShareUrl.js +5 -0
  17. package/esm/JBrowseConfig/index.d.ts +175 -84
  18. package/esm/JBrowseConfig/index.js +64 -2
  19. package/esm/JBrowseModel/index.d.ts +175 -97
  20. package/esm/JBrowseModel/index.js +132 -31
  21. package/esm/JbApi/index.d.ts +2 -0
  22. package/esm/JbApi/index.js +1 -0
  23. package/esm/JbApi/jbApi.d.ts +2767 -0
  24. package/esm/JbApi/jbApi.js +1383 -0
  25. package/esm/RootMenu/index.d.ts +69 -3
  26. package/esm/RootMenu/index.js +96 -61
  27. package/esm/SessionSpec/index.d.ts +6 -0
  28. package/esm/SessionSpec/index.js +3 -0
  29. package/esm/SessionSpec/lgvUrlInit.d.ts +79 -0
  30. package/esm/SessionSpec/lgvUrlInit.js +161 -0
  31. package/esm/SessionSpec/loadSessionSpec.d.ts +75 -0
  32. package/esm/SessionSpec/loadSessionSpec.js +473 -0
  33. package/esm/SessionSpec/parseSessionSpecUrl.d.ts +7 -0
  34. package/esm/SessionSpec/parseSessionSpecUrl.js +181 -0
  35. package/esm/SessionSpec/types.d.ts +37 -0
  36. package/esm/SessionSpec/types.js +1 -0
  37. package/esm/WorkspaceLayout/LayoutRenderer.d.ts +24 -0
  38. package/esm/WorkspaceLayout/LayoutRenderer.js +185 -0
  39. package/esm/WorkspaceLayout/PanelView.d.ts +13 -0
  40. package/esm/WorkspaceLayout/PanelView.js +99 -0
  41. package/esm/WorkspaceLayout/TabStrip.d.ts +14 -0
  42. package/esm/WorkspaceLayout/TabStrip.js +226 -0
  43. package/esm/WorkspaceLayout/WorkspaceContainer.d.ts +16 -0
  44. package/esm/WorkspaceLayout/WorkspaceContainer.js +107 -0
  45. package/esm/WorkspaceLayout/WorkspacePanelActions.d.ts +24 -0
  46. package/esm/WorkspaceLayout/WorkspacePanelActions.js +120 -0
  47. package/esm/WorkspaceLayout/WorkspaceTab.d.ts +27 -0
  48. package/esm/WorkspaceLayout/WorkspaceTab.js +105 -0
  49. package/esm/WorkspaceLayout/dockviewTheme.d.ts +69 -0
  50. package/esm/WorkspaceLayout/dockviewTheme.js +82 -0
  51. package/esm/WorkspaceLayout/dropZone.d.ts +105 -0
  52. package/esm/WorkspaceLayout/dropZone.js +113 -0
  53. package/esm/WorkspaceLayout/index.d.ts +8 -0
  54. package/esm/WorkspaceLayout/index.js +4 -0
  55. package/esm/WorkspaceLayout/model.d.ts +260 -0
  56. package/esm/WorkspaceLayout/model.js +586 -0
  57. package/esm/WorkspaceLayout/panelChrome.d.ts +19 -0
  58. package/esm/WorkspaceLayout/panelChrome.js +4 -0
  59. package/esm/WorkspaceLayout/spec.d.ts +88 -0
  60. package/esm/WorkspaceLayout/spec.js +226 -0
  61. package/esm/WorkspaceLayout/splitter.d.ts +25 -0
  62. package/esm/WorkspaceLayout/splitter.js +37 -0
  63. package/esm/WorkspaceLayout/tree.d.ts +158 -0
  64. package/esm/WorkspaceLayout/tree.js +379 -0
  65. package/esm/WorkspaceLayout/useLayoutDrag.d.ts +34 -0
  66. package/esm/WorkspaceLayout/useLayoutDrag.js +232 -0
  67. package/esm/index.d.ts +18 -9
  68. package/esm/index.js +16 -9
  69. package/esm/menus.d.ts +55 -68
  70. package/esm/menus.js +117 -86
  71. package/esm/queryParams.d.ts +9 -0
  72. package/esm/queryParams.js +93 -0
  73. package/esm/sessionModelFactory.d.ts +18 -0
  74. package/esm/sessionModelFactory.js +1 -0
  75. package/esm/ui/App/App.d.ts +3 -14
  76. package/esm/ui/App/App.js +39 -19
  77. package/esm/ui/App/AppFab.d.ts +12 -3
  78. package/esm/ui/App/AppFab.js +15 -8
  79. package/esm/ui/App/AppToolbar.d.ts +2 -14
  80. package/esm/ui/App/AppToolbar.js +45 -31
  81. package/esm/ui/App/ClassicViewsContainer.d.ts +4 -10
  82. package/esm/ui/App/ClassicViewsContainer.js +9 -8
  83. package/esm/ui/App/DialogQueue.d.ts +17 -1
  84. package/esm/ui/App/DialogQueue.js +30 -1
  85. package/esm/ui/App/Drawer.d.ts +1 -1
  86. package/esm/ui/App/Drawer.js +10 -39
  87. package/esm/ui/App/DrawerHeader.d.ts +2 -3
  88. package/esm/ui/App/DrawerHeader.js +24 -11
  89. package/esm/ui/App/DrawerHeaderHelpButton.d.ts +1 -1
  90. package/esm/ui/App/DrawerHeaderHelpButton.js +3 -3
  91. package/esm/ui/App/DrawerHeaderHelpDialog.d.ts +2 -2
  92. package/esm/ui/App/DrawerHeaderHelpDialog.js +3 -4
  93. package/esm/ui/App/DrawerWidget.d.ts +1 -1
  94. package/esm/ui/App/DrawerWidget.js +9 -21
  95. package/esm/ui/App/JBrowseTabMenu.d.ts +2 -3
  96. package/esm/ui/App/JBrowseTabMenu.js +24 -5
  97. package/esm/ui/App/ThemeModeButton.d.ts +12 -0
  98. package/esm/ui/App/ThemeModeButton.js +34 -0
  99. package/esm/ui/App/ViewButtons.d.ts +2 -4
  100. package/esm/ui/App/ViewButtons.js +15 -3
  101. package/esm/ui/App/ViewContainer.d.ts +3 -2
  102. package/esm/ui/App/ViewContainer.js +128 -24
  103. package/esm/ui/App/ViewContainerTitle.d.ts +1 -1
  104. package/esm/ui/App/ViewContainerTitle.js +21 -11
  105. package/esm/ui/App/ViewErrorFallback.d.ts +18 -0
  106. package/esm/ui/App/ViewErrorFallback.js +42 -0
  107. package/esm/ui/App/ViewHeader.d.ts +3 -4
  108. package/esm/ui/App/ViewHeader.js +28 -17
  109. package/esm/ui/App/ViewLauncher.d.ts +2 -8
  110. package/esm/ui/App/ViewLauncher.js +7 -5
  111. package/esm/ui/App/ViewMenu.d.ts +3 -5
  112. package/esm/ui/App/ViewMenu.js +87 -92
  113. package/esm/ui/App/ViewStack.d.ts +7 -0
  114. package/esm/ui/App/ViewStack.js +48 -0
  115. package/esm/ui/App/ViewWrapper.d.ts +15 -1
  116. package/esm/ui/App/ViewWrapper.js +26 -10
  117. package/esm/ui/App/ViewsContainer.d.ts +4 -11
  118. package/esm/ui/App/ViewsContainer.js +6 -9
  119. package/esm/ui/App/index.d.ts +2 -1
  120. package/esm/ui/App/index.js +3 -2
  121. package/esm/ui/App/lazyChunk.d.ts +13 -0
  122. package/esm/ui/App/lazyChunk.js +91 -0
  123. package/esm/ui/App/lazyParts.d.ts +32 -0
  124. package/esm/ui/App/lazyParts.js +8 -0
  125. package/esm/ui/App/preloadAppShell.d.ts +14 -0
  126. package/esm/ui/App/preloadAppShell.js +41 -0
  127. package/esm/ui/App/types.d.ts +17 -3
  128. package/esm/ui/App/types.js +1 -1
  129. package/esm/ui/App/useViewVisibility.d.ts +35 -0
  130. package/esm/ui/App/useViewVisibility.js +88 -0
  131. package/esm/ui/App/viewTitle.d.ts +12 -0
  132. package/esm/ui/App/viewTitle.js +17 -0
  133. package/esm/ui/index.d.ts +1 -1
  134. package/esm/ui/index.js +1 -1
  135. package/package.json +12 -17
  136. package/esm/AppFocus/index.d.ts +0 -5
  137. package/esm/AppFocus/index.js +0 -12
  138. package/esm/Assemblies/SessionAssembliesMixin.d.ts +0 -92
  139. package/esm/Assemblies/SessionAssembliesMixin.js +0 -53
  140. package/esm/Assemblies/TemporaryAssembliesMixin.d.ts +0 -10
  141. package/esm/Assemblies/TemporaryAssembliesMixin.js +0 -37
  142. package/esm/DockviewLayout/index.d.ts +0 -35
  143. package/esm/DockviewLayout/index.js +0 -111
  144. package/esm/ui/App/DockviewContext.d.ts +0 -22
  145. package/esm/ui/App/DockviewContext.js +0 -29
  146. package/esm/ui/App/DockviewLeftHeaderActions.d.ts +0 -2
  147. package/esm/ui/App/DockviewLeftHeaderActions.js +0 -58
  148. package/esm/ui/App/DockviewRightHeaderActions.d.ts +0 -2
  149. package/esm/ui/App/DockviewRightHeaderActions.js +0 -100
  150. package/esm/ui/App/DrawerControls.d.ts +0 -5
  151. package/esm/ui/App/DrawerControls.js +0 -25
  152. package/esm/ui/App/DrawerWidgetSelector.d.ts +0 -5
  153. package/esm/ui/App/DrawerWidgetSelector.js +0 -47
  154. package/esm/ui/App/JBrowseViewPanel.d.ts +0 -8
  155. package/esm/ui/App/JBrowseViewPanel.js +0 -40
  156. package/esm/ui/App/JBrowseViewTab.d.ts +0 -8
  157. package/esm/ui/App/JBrowseViewTab.js +0 -97
  158. package/esm/ui/App/ModalWidget.d.ts +0 -6
  159. package/esm/ui/App/ModalWidget.js +0 -48
  160. package/esm/ui/App/TiledViewsContainer.d.ts +0 -6
  161. package/esm/ui/App/TiledViewsContainer.js +0 -366
  162. package/esm/ui/App/copyView.d.ts +0 -1
  163. package/esm/ui/App/copyView.js +0 -29
  164. package/esm/ui/App/dockviewUtils.d.ts +0 -46
  165. package/esm/ui/App/dockviewUtils.js +0 -55
@@ -1,26 +1,33 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { FAB_Z_INDEX } from '@jbrowse/core/ui/zIndexes';
2
3
  import { makeStyles } from '@jbrowse/core/util/tss-react';
3
4
  import LaunchIcon from '@mui/icons-material/Launch';
4
5
  import { Fab, Tooltip } from '@mui/material';
5
6
  import { observer } from 'mobx-react';
7
+ // `absolute`, against the app root, rather than the `fixed` this used to
8
+ // carry. Fixed measures against the window, and the app root is only the
9
+ // window in a full-window app: under `--jbrowse-app-height` the app is a box
10
+ // somewhere on a host's page (see react-app's fit-to-container example), and
11
+ // this button was pinned to the page's corner instead of the app's. It is the
12
+ // only way back from a minimized drawer, so it cannot be somewhere else on the
13
+ // page.
6
14
  const useStyles = makeStyles()(theme => ({
7
- left: {
8
- zIndex: 10000,
9
- position: 'fixed',
15
+ fab: {
16
+ zIndex: FAB_Z_INDEX,
17
+ position: 'absolute',
10
18
  bottom: theme.spacing(2),
19
+ },
20
+ left: {
11
21
  left: theme.spacing(2),
12
22
  },
13
23
  right: {
14
- zIndex: 10000,
15
- position: 'fixed',
16
- bottom: theme.spacing(2),
17
24
  right: theme.spacing(2),
18
25
  },
19
26
  }));
20
27
  const AppFab = observer(function AppFab({ session, }) {
21
28
  const { minimized, activeWidgets, drawerPosition } = session;
22
- const { classes } = useStyles();
23
- return activeWidgets.size > 0 && minimized ? (_jsx(Tooltip, { title: "Open drawer widget", children: _jsx(Fab, { className: drawerPosition === 'right' ? classes.right : classes.left, color: "primary", "data-testid": "drawer-maximize", onClick: () => {
29
+ const { classes, cx } = useStyles();
30
+ return activeWidgets.size > 0 && minimized ? (_jsx(Tooltip, { title: "Open drawer widget", children: _jsx(Fab, { className: cx(classes.fab, drawerPosition === 'right' ? classes.right : classes.left), color: "primary", "data-testid": "drawer-maximize", onClick: () => {
24
31
  session.showWidgetDrawer();
25
32
  }, children: _jsx(LaunchIcon, {}) }) })) : null;
26
33
  });
@@ -1,18 +1,6 @@
1
- import type { MenuItem as JBMenuItem } from '@jbrowse/core/ui/Menu';
2
- import type { SnackbarMessage } from '@jbrowse/core/ui/SnackbarModel';
3
- import type { SessionWithDrawerWidgets } from '@jbrowse/core/util';
4
- interface Menu {
5
- label: string;
6
- menuItems: JBMenuItem[] | (() => JBMenuItem[]);
7
- }
8
- type AppSession = SessionWithDrawerWidgets & {
9
- menus: () => Menu[];
10
- snackbarMessages: SnackbarMessage[];
11
- renameCurrentSession: (arg: string) => void;
12
- popSnackbarMessage: () => unknown;
13
- };
1
+ import type { AppSession } from './types.ts';
14
2
  declare const AppToolbar: ({ session, HeaderButtons, }: {
15
3
  HeaderButtons?: React.ReactElement;
16
4
  session: AppSession;
17
- }) => import("react/jsx-runtime").JSX.Element;
5
+ }) => import("react").JSX.Element;
18
6
  export default AppToolbar;
@@ -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': {
@@ -21,39 +31,43 @@ const useStyles = makeStyles()(theme => ({
21
31
  borderColor: theme.palette.secondary.main,
22
32
  backgroundColor: theme.palette.primary.light,
23
33
  },
34
+ logo: {
35
+ // stretch to the toolbar height so the logo's height follows the bar
36
+ // rather than a hardcoded pixel value
37
+ alignSelf: 'stretch',
38
+ display: 'flex',
39
+ alignItems: 'center',
40
+ // autofit any logo (custom <img> or the default svg): full toolbar height
41
+ // with auto width preserves aspect ratio, maxWidth caps horizontal room
42
+ '& img, & svg': {
43
+ height: '100%',
44
+ width: 'auto',
45
+ maxWidth: 150,
46
+ objectFit: 'contain',
47
+ },
48
+ },
24
49
  }));
25
- function wrapMenuItems(items, session) {
26
- return items.map(item => ({
27
- ...item,
28
- ...('onClick' in item
29
- ? {
30
- onClick: () => {
31
- item.onClick(session);
32
- },
33
- }
34
- : {}),
35
- ...('subMenu' in item
36
- ? { subMenu: wrapMenuItems(item.subMenu, session) }
37
- : {}),
38
- }));
39
- }
40
50
  const AppToolbar = observer(function AppToolbar({ session, HeaderButtons = _jsx("div", {}), }) {
41
51
  const { classes } = useStyles();
42
52
  const { name, menus } = session;
43
- return (_jsxs(Toolbar, { children: [menus().map(menu => (_jsx(DropDownMenu, { menuTitle: menu.label, menuItems: () => {
44
- const items = typeof menu.menuItems === 'function'
45
- ? menu.menuItems()
46
- : menu.menuItems;
47
- return wrapMenuItems(items, session);
48
- } }, menu.label))), _jsx("div", { className: classes.grow }), _jsx(Tooltip, { title: "Rename session", arrow: true, children: _jsx(EditableTypography, { value: name, variant: "body1", classes: {
49
- inputBase: classes.inputBase,
50
- inputRoot: classes.inputRoot,
51
- inputFocused: classes.inputFocused,
52
- }, setValue: newName => {
53
- session.renameCurrentSession(newName);
54
- } }) }), HeaderButtons, _jsx("div", { className: classes.grow }), _jsx("div", { style: {
55
- width: 150,
56
- maxHeight: 48,
57
- }, children: _jsx(AppLogo, { session: 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
+ });
57
+ return (_jsxs(Toolbar, { children: [menus().map(menu => (_jsx(DropDownMenu, { menuTitle: menu.label, menuItems: menu.menuItems,
58
+ // a root model's menu items take the session as their argument, and
59
+ // the renderer decides how to invoke onClick — so this is the whole
60
+ // binding. It used to be a recursive rewrite of every row of every
61
+ // menu, rebuilt on each open, to arrive at the same call
62
+ onMenuItemClick: callback => {
63
+ callback(session);
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 }) }))] }));
58
72
  });
59
73
  export default AppToolbar;
@@ -1,12 +1,6 @@
1
- import type { SnackbarMessage } from '@jbrowse/core/ui/SnackbarModel';
2
- import type { AbstractViewContainer, SessionWithFocusedViewAndDrawerWidgets } from '@jbrowse/core/util';
3
- type SessionType = SessionWithFocusedViewAndDrawerWidgets & AbstractViewContainer & {
4
- renameCurrentSession: (arg: string) => void;
5
- snackbarMessages: SnackbarMessage[];
6
- popSnackbarMessage: () => unknown;
7
- };
8
- interface Props {
9
- session: SessionType;
1
+ import type { SessionWithFocusedViewAndDrawerWidgets } from '@jbrowse/core/util';
2
+ export interface ClassicViewsContainerProps {
3
+ session: SessionWithFocusedViewAndDrawerWidgets;
10
4
  }
11
- declare const ClassicViewsContainer: ({ session, }: Props) => import("react/jsx-runtime").JSX.Element;
5
+ declare const ClassicViewsContainer: ({ session, }: ClassicViewsContainerProps) => import("react").JSX.Element;
12
6
  export default ClassicViewsContainer;
@@ -1,21 +1,22 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { persistentScrollbarStyle } from '@jbrowse/core/ui/persistentScrollbarStyle';
3
+ import { useScrollPortHeightVar } from '@jbrowse/core/util/hooks';
2
4
  import { makeStyles } from '@jbrowse/core/util/tss-react';
3
5
  import { observer } from 'mobx-react';
4
- import ViewContainer from "./ViewContainer.js";
5
- const useStyles = makeStyles()({
6
+ import ViewStack from './ViewStack.js';
7
+ const useStyles = makeStyles()(theme => ({
6
8
  container: {
7
9
  height: '100%',
8
10
  width: '100%',
9
11
  gridRow: 'components',
10
12
  overflowY: 'auto',
13
+ ...persistentScrollbarStyle(theme),
11
14
  },
12
- spacer: {
13
- height: 300,
14
- },
15
- });
15
+ }));
16
16
  const ClassicViewsContainer = observer(function ClassicViewsContainer({ session, }) {
17
17
  const { classes } = useStyles();
18
18
  const { views } = session;
19
- return (_jsxs("div", { className: classes.container, children: [views.map(view => (_jsx(ViewContainer, { view: view, session: session }, view.id))), _jsx("div", { className: classes.spacer })] }));
19
+ const ref = useScrollPortHeightVar();
20
+ return (_jsx("div", { ref: ref, className: classes.container, children: _jsx(ViewStack, { views: views, session: session }) }));
20
21
  });
21
22
  export default ClassicViewsContainer;
@@ -1,5 +1,21 @@
1
1
  import type { AbstractSessionModel } from '@jbrowse/core/util';
2
+ /**
3
+ * The session's dialog, and the boundary that keeps a dialog that throws from
4
+ * being an application-wide failure.
5
+ *
6
+ * `core/ui/Dialog` already wraps its own CHILDREN, which covers a dialog body
7
+ * that throws. It cannot cover the dialog component's own render, and that is
8
+ * where the reachable case is: a launch dialog resolves its source with
9
+ * `getSession` / `getContainingView` off a track, and "Replace current view"
10
+ * destroys the view that track lives in. This queue renders under `App`, above
11
+ * every per-view boundary, so without this the next one up is the product's —
12
+ * jbrowse-web's fatal-error dialog, which takes every view the user had open.
13
+ * ADR-069, and `cancer_sv/multihop_split_view` is the figure it took.
14
+ *
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
+ */
2
18
  declare const DialogQueue: ({ session, }: {
3
19
  session: AbstractSessionModel;
4
- }) => import("react/jsx-runtime").JSX.Element | null;
20
+ }) => import("react").JSX.Element | null;
5
21
  export default DialogQueue;
@@ -1,8 +1,37 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { Suspense } from 'react';
3
+ import { Dialog, ErrorMessage } from '@jbrowse/core/ui';
4
+ import { ErrorBoundary } from '@jbrowse/core/ui/ErrorBoundary';
5
+ import { DialogContent } from '@mui/material';
3
6
  import { observer } from 'mobx-react';
7
+ // `removeActiveDialog` is BaseSession's, and every product session composes it,
8
+ // but `AbstractSessionModel` does not declare it — so it is asked for rather
9
+ // than assumed. A session without it leaves the fallback's close button doing
10
+ // nothing, which is still better than the page it replaces.
11
+ function dismissActiveDialog(session) {
12
+ ;
13
+ session.removeActiveDialog?.();
14
+ }
15
+ /**
16
+ * The session's dialog, and the boundary that keeps a dialog that throws from
17
+ * being an application-wide failure.
18
+ *
19
+ * `core/ui/Dialog` already wraps its own CHILDREN, which covers a dialog body
20
+ * that throws. It cannot cover the dialog component's own render, and that is
21
+ * where the reachable case is: a launch dialog resolves its source with
22
+ * `getSession` / `getContainingView` off a track, and "Replace current view"
23
+ * destroys the view that track lives in. This queue renders under `App`, above
24
+ * every per-view boundary, so without this the next one up is the product's —
25
+ * jbrowse-web's fatal-error dialog, which takes every view the user had open.
26
+ * ADR-069, and `cancer_sv/multihop_split_view` is the figure it took.
27
+ *
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
+ */
4
31
  const DialogQueue = observer(function DialogQueue({ session, }) {
5
32
  const { DialogComponent, DialogProps } = session;
6
- return DialogComponent ? (_jsx(Suspense, { fallback: null, children: _jsx(DialogComponent, { ...DialogProps }) })) : null;
33
+ return DialogComponent ? (_jsx(ErrorBoundary, { resetKeys: [DialogComponent, DialogProps], FallbackComponent: ({ error }) => (_jsx(Dialog, { open: true, title: "Error", onClose: () => {
34
+ dismissActiveDialog(session);
35
+ }, children: _jsx(DialogContent, { children: _jsx(ErrorMessage, { error: error }) }) })), children: _jsx(Suspense, { fallback: null, children: _jsx(DialogComponent, { ...DialogProps }) }) })) : null;
7
36
  });
8
37
  export default DialogQueue;
@@ -2,5 +2,5 @@ import type { SessionWithFocusedViewAndDrawerWidgets } from '@jbrowse/core/util/
2
2
  declare const Drawer: ({ children, session, }: {
3
3
  children: React.ReactNode;
4
4
  session: SessionWithFocusedViewAndDrawerWidgets;
5
- }) => import("react/jsx-runtime").JSX.Element;
5
+ }) => import("react").JSX.Element;
6
6
  export default Drawer;
@@ -1,45 +1,16 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useEffect, useRef } from 'react';
3
- import ResizeHandle from '@jbrowse/core/ui/ResizeHandle';
4
- import { makeStyles } from '@jbrowse/core/util/tss-react';
5
- import { Paper } from '@mui/material';
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useRef } from 'react';
3
+ import { useFocusOnInteraction } from '@jbrowse/core/util/hooks';
4
+ import { Drawer as DrawerShell } from '@jbrowse/product-core';
6
5
  import { observer } from 'mobx-react';
7
- const useStyles = makeStyles()(theme => ({
8
- paper: {
9
- overflowY: 'auto',
10
- height: '100%',
11
- position: 'relative',
12
- zIndex: theme.zIndex.drawer,
13
- outline: 'none',
14
- background: theme.palette.background.default,
15
- },
16
- resizeHandle: {
17
- width: 4,
18
- position: 'fixed',
19
- top: 0,
20
- zIndex: theme.zIndex.drawer + 1,
21
- },
22
- }));
23
6
  const Drawer = observer(function Drawer({ children, session, }) {
24
- const { drawerPosition, drawerWidth } = session;
25
- const { classes } = useStyles();
26
7
  const ref = useRef(null);
27
- useEffect(() => {
28
- function handleSelectView(e) {
29
- if (e.target instanceof Element && ref.current?.contains(e.target)) {
30
- const visibleWidgetId = session.visibleWidget?.view?.id;
31
- if (visibleWidgetId) {
32
- session.setFocusedViewId(visibleWidgetId);
33
- }
34
- }
8
+ useFocusOnInteraction(ref, () => {
9
+ const visibleWidgetId = session.visibleWidget?.view?.id;
10
+ if (visibleWidgetId) {
11
+ session.setFocusedViewId(visibleWidgetId);
35
12
  }
36
- document.addEventListener('mousedown', handleSelectView);
37
- document.addEventListener('keydown', handleSelectView);
38
- return () => {
39
- document.removeEventListener('mousedown', handleSelectView);
40
- document.removeEventListener('keydown', handleSelectView);
41
- };
42
- }, [session]);
43
- return (_jsxs(Paper, { ref: ref, className: classes.paper, elevation: 16, square: true, children: [drawerPosition === 'right' ? (_jsx(ResizeHandle, { onDrag: session.resizeDrawer, className: classes.resizeHandle, vertical: true })) : null, children, drawerPosition === 'left' ? (_jsx(ResizeHandle, { onDrag: session.resizeDrawer, className: classes.resizeHandle, style: { left: drawerWidth }, vertical: true })) : null] }));
13
+ });
14
+ return (_jsx(DrawerShell, { session: session, ref: ref, children: children }));
44
15
  });
45
16
  export default Drawer;
@@ -1,7 +1,6 @@
1
1
  import type { SessionWithFocusedViewAndDrawerWidgets } from '@jbrowse/core/util/types';
2
- declare const DrawerHeader: ({ session, setToolbarHeight, onPopoutDrawer, }: {
2
+ declare const DrawerHeader: ({ session, setToolbarHeight, }: {
3
3
  session: SessionWithFocusedViewAndDrawerWidgets;
4
4
  setToolbarHeight: (arg: number) => void;
5
- onPopoutDrawer: () => void;
6
- }) => import("react/jsx-runtime").JSX.Element;
5
+ }) => import("react").JSX.Element;
7
6
  export default DrawerHeader;
@@ -1,13 +1,12 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Suspense, lazy } from 'react';
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 { DrawerControls, DrawerWidgetSelector } from '@jbrowse/product-core';
5
6
  import LaunchIcon from '@mui/icons-material/Launch';
6
7
  import { AppBar, IconButton, Toolbar, Tooltip } from '@mui/material';
7
8
  import { observer } from 'mobx-react';
8
- import DrawerControls from "./DrawerControls.js";
9
- import DrawerWidgetSelector from "./DrawerWidgetSelector.js";
10
- const DrawerHeaderHelpButton = lazy(() => import("./DrawerHeaderHelpButton.js"));
9
+ const DrawerHeaderHelpButton = lazy(() => import('./DrawerHeaderHelpButton.js'));
11
10
  const useStyles = makeStyles()(theme => ({
12
11
  spacer: {
13
12
  flexGrow: 1,
@@ -19,7 +18,7 @@ const useStyles = makeStyles()(theme => ({
19
18
  background: theme.palette.secondary.dark,
20
19
  },
21
20
  }));
22
- const DrawerHeader = observer(function DrawerHeader({ session, setToolbarHeight, onPopoutDrawer, }) {
21
+ const DrawerHeader = observer(function DrawerHeader({ session, setToolbarHeight, }) {
23
22
  const { classes } = useStyles();
24
23
  const focusedViewId = session.focusedViewId;
25
24
  const { visibleWidget } = session;
@@ -28,13 +27,27 @@ const DrawerHeader = observer(function DrawerHeader({ session, setToolbarHeight,
28
27
  const widgetType = visibleWidget
29
28
  ? pluginManager.getWidgetType(visibleWidget.type)
30
29
  : undefined;
31
- const { helpText } = widgetType || {};
32
- return (_jsx(AppBar, { position: "sticky", className: focusedViewId === viewWidgetId
30
+ const { helpText } = widgetType ?? {};
31
+ // Measured, and re-measured: the toolbar's height is the room a virtualized
32
+ // widget subtracts from the box it is given, and it changes without this
33
+ // component re-rendering -- MUI's Toolbar is 56px under the `sm` breakpoint
34
+ // and 64 above it, and grows again with the root font size. Read once at
35
+ // mount, a widget's list was that much too tall for the rest of the session.
36
+ const appBarRef = useCallback((node) => {
37
+ const publish = () => {
38
+ setToolbarHeight(node.getBoundingClientRect().height);
39
+ };
40
+ publish();
41
+ const observer = new ResizeObserver(publish);
42
+ observer.observe(node);
43
+ return () => {
44
+ observer.disconnect();
45
+ };
46
+ }, [setToolbarHeight]);
47
+ return (_jsx(AppBar, { position: "sticky", className: viewWidgetId !== undefined && focusedViewId === viewWidgetId
33
48
  ? classes.headerFocused
34
- : classes.headerUnfocused, ref: ref => {
35
- setToolbarHeight(ref?.getBoundingClientRect().height || 0);
36
- }, children: _jsxs(Toolbar, { disableGutters: true, children: [_jsx(DrawerWidgetSelector, { session: session }), _jsx(Tooltip, { title: "Open drawer in dialog", children: _jsx(IconButton, { color: "inherit", onClick: () => {
37
- onPopoutDrawer();
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: () => {
50
+ session.popoutWidget();
38
51
  }, children: _jsx(LaunchIcon, {}) }) }), helpText ? (_jsx(Suspense, { fallback: null, children: _jsx(DrawerHeaderHelpButton, { helpText: helpText, session: session }) })) : null, _jsx("div", { className: classes.spacer }), _jsx(DrawerControls, { session: session })] }) }));
39
52
  });
40
53
  export default DrawerHeader;
@@ -2,5 +2,5 @@ import type { SessionWithFocusedViewAndDrawerWidgets } from '@jbrowse/core/util/
2
2
  declare const DrawerHeaderHelpButton: ({ session, helpText, }: {
3
3
  session: SessionWithFocusedViewAndDrawerWidgets;
4
4
  helpText: React.ReactNode;
5
- }) => import("react/jsx-runtime").JSX.Element;
5
+ }) => import("react").JSX.Element;
6
6
  export default DrawerHeaderHelpButton;
@@ -1,9 +1,9 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { lazy } from 'react';
3
- import HelpOutline from '@mui/icons-material/HelpOutline';
3
+ import HelpOutlined from '@mui/icons-material/HelpOutlined';
4
4
  import { IconButton, Tooltip } from '@mui/material';
5
5
  import { observer } from 'mobx-react';
6
- const DrawerHeaderHelpDialog = lazy(() => import("./DrawerHeaderHelpDialog.js"));
6
+ const DrawerHeaderHelpDialog = lazy(() => import('./DrawerHeaderHelpDialog.js'));
7
7
  const DrawerHeaderHelpButton = observer(function DrawerHeaderHelpButton({ session, helpText, }) {
8
8
  return (_jsx(Tooltip, { title: "Help", children: _jsx(IconButton, { color: "inherit", onClick: () => {
9
9
  session.queueDialog(handleClose => [
@@ -13,6 +13,6 @@ const DrawerHeaderHelpButton = observer(function DrawerHeaderHelpButton({ sessio
13
13
  helpText,
14
14
  },
15
15
  ]);
16
- }, children: _jsx(HelpOutline, {}) }) }));
16
+ }, children: _jsx(HelpOutlined, {}) }) }));
17
17
  });
18
18
  export default DrawerHeaderHelpButton;
@@ -1,4 +1,4 @@
1
1
  export default function DrawerHeaderHelpDialog({ onClose, helpText, }: {
2
- onClose: (event: React.MouseEvent | React.KeyboardEvent) => void;
2
+ onClose: () => void;
3
3
  helpText: React.ReactNode;
4
- }): import("react/jsx-runtime").JSX.Element;
4
+ }): import("react").JSX.Element;
@@ -1,7 +1,6 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import Dialog from '@jbrowse/core/ui/Dialog';
2
+ import InfoDialog from '@jbrowse/core/ui/InfoDialog';
3
3
  import { makeStyles } from '@jbrowse/core/util/tss-react';
4
- import { DialogContent } from '@mui/material';
5
4
  const useStyles = makeStyles()({
6
5
  max: {
7
6
  minWidth: '40em',
@@ -9,7 +8,7 @@ const useStyles = makeStyles()({
9
8
  });
10
9
  export default function DrawerHeaderHelpDialog({ onClose, helpText, }) {
11
10
  const { classes } = useStyles();
12
- return (_jsx(Dialog, { open: true, onClose: onClose, title: "Help", maxWidth: "xl", onClick: e => {
11
+ return (_jsx(InfoDialog, { open: true, onClose: onClose, title: "Help", maxWidth: "xl", onClick: e => {
13
12
  e.stopPropagation();
14
- }, children: _jsx(DialogContent, { className: classes.max, children: helpText }) }));
13
+ }, children: _jsx("div", { className: classes.max, children: helpText }) }));
15
14
  }
@@ -1,5 +1,5 @@
1
1
  import type { SessionWithFocusedViewAndDrawerWidgets } from '@jbrowse/core/util/types';
2
2
  declare const DrawerWidget: ({ session, }: {
3
3
  session: SessionWithFocusedViewAndDrawerWidgets;
4
- }) => import("react/jsx-runtime").JSX.Element;
4
+ }) => import("react").JSX.Element;
5
5
  export default DrawerWidget;
@@ -1,27 +1,15 @@
1
- import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Suspense, lazy, useState } from 'react';
3
- import { ErrorMessage, LoadingEllipses } from '@jbrowse/core/ui';
4
- import { ErrorBoundary } from '@jbrowse/core/ui/ErrorBoundary';
5
- import { getEnv } from '@jbrowse/core/util';
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState } from 'react';
3
+ import { WidgetBody } from '@jbrowse/product-core';
6
4
  import { observer } from 'mobx-react';
7
- import Drawer from "./Drawer.js";
8
- import DrawerHeader from "./DrawerHeader.js";
9
- const ModalWidget = lazy(() => import("./ModalWidget.js"));
5
+ import Drawer from './Drawer.js';
6
+ import DrawerHeader from './DrawerHeader.js';
10
7
  const DrawerWidget = observer(function DrawerWidget({ session, }) {
11
8
  const { visibleWidget } = session;
12
- const { pluginManager } = getEnv(session);
13
- const DrawerComponent = visibleWidget
14
- ? pluginManager.evaluateExtensionPoint('Core-replaceWidget', pluginManager.getWidgetType(visibleWidget.type).ReactComponent, {
15
- session,
16
- model: visibleWidget,
17
- })
18
- : null;
9
+ // we track the toolbar height because components that use virtualized
10
+ // height want to be able to fill the contained, minus the toolbar height
11
+ // (the position static/sticky is included in AutoSizer estimates)
19
12
  const [toolbarHeight, setToolbarHeight] = useState(0);
20
- const [popoutDrawer, setPopoutDrawer] = useState(false);
21
- return (_jsxs(Drawer, { session: session, children: [_jsx(DrawerHeader, { onPopoutDrawer: () => {
22
- setPopoutDrawer(true);
23
- }, session: session, setToolbarHeight: setToolbarHeight }), _jsx(Suspense, { fallback: _jsx(LoadingEllipses, {}), children: _jsx(ErrorBoundary, { FallbackComponent: ({ error }) => _jsx(ErrorMessage, { error: error }), children: DrawerComponent ? (popoutDrawer ? (_jsxs(_Fragment, { children: [_jsx("div", { children: "Opened in dialog..." }), _jsx(ModalWidget, { session: session, onClose: () => {
24
- setPopoutDrawer(false);
25
- } })] })) : (_jsxs(_Fragment, { children: [_jsx(DrawerComponent, { model: visibleWidget, session: session, toolbarHeight: toolbarHeight }), _jsx("div", { style: { height: 300 } })] }))) : null }) })] }));
13
+ return (_jsxs(Drawer, { session: session, children: [_jsx(DrawerHeader, { session: session, setToolbarHeight: setToolbarHeight }), visibleWidget ? (_jsx(WidgetBody, { session: session, widget: visibleWidget, toolbarHeight: toolbarHeight })) : null] }));
26
14
  });
27
15
  export default DrawerWidget;
@@ -1,5 +1,4 @@
1
- export default function JBrowseTabMenu({ isHovered, onRename, onClose, }: {
2
- isHovered: boolean;
1
+ export default function JBrowseTabMenu({ onRename, onClose, }: {
3
2
  onRename: () => void;
4
3
  onClose: () => void;
5
- }): import("react/jsx-runtime").JSX.Element;
4
+ }): import("react").JSX.Element;
@@ -1,16 +1,24 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useState } from 'react';
2
3
  import CascadingMenuButton from '@jbrowse/core/ui/CascadingMenuButton';
3
4
  import { makeStyles } from '@jbrowse/core/util/tss-react';
4
5
  import CloseIcon from '@mui/icons-material/Close';
5
6
  import EditIcon from '@mui/icons-material/Edit';
6
7
  import MoreVertIcon from '@mui/icons-material/MoreVert';
7
8
  const useStyles = makeStyles()({
9
+ // hidden until the tab is hovered or focused, by a rule in `TabStrip` keyed
10
+ // on the `jbrowse-tab-menu` class — hence the plain class name beside the
11
+ // generated one. `visibility` rather than `display`, so the tab does not
12
+ // change width under the pointer as it arrives.
8
13
  tabIcons: {
9
14
  display: 'flex',
10
15
  alignItems: 'center',
11
16
  visibility: 'hidden',
12
17
  },
13
- tabIconsVisible: {
18
+ // the pointer is over the menu, not the tab, for as long as the menu is open,
19
+ // so the hover rule stops applying the moment it opens — leaving the popover
20
+ // anchored to an invisible button
21
+ open: {
14
22
  visibility: 'visible',
15
23
  },
16
24
  tabIcon: {
@@ -22,9 +30,20 @@ const useStyles = makeStyles()({
22
30
  fontSize: 14,
23
31
  },
24
32
  });
25
- export default function JBrowseTabMenu({ isHovered, onRename, onClose, }) {
26
- const { classes } = useStyles();
27
- return (_jsx("div", { className: `${classes.tabIcons} ${isHovered ? classes.tabIconsVisible : ''}`, children: _jsx(CascadingMenuButton, { menuItems: [
33
+ export default function JBrowseTabMenu({ onRename, onClose, }) {
34
+ const { classes, cx } = useStyles();
35
+ const [open, setOpen] = useState(false);
36
+ return (_jsx("div", { className: cx('jbrowse-tab-menu', classes.tabIcons, open && classes.open),
37
+ // The tab strip starts a tab drag on pointerdown and takes POINTER
38
+ // CAPTURE for it, and a captured pointer retargets the compatibility
39
+ // mouse events with it — so `click` is delivered to the tab rather than
40
+ // to this button, and the menu silently never opens. Stopping the
41
+ // pointerdown here means a press that starts on the menu is not a drag
42
+ // and never captures, which is the same reason the rename input in
43
+ // `WorkspaceTab` stops it.
44
+ onPointerDown: event => {
45
+ event.stopPropagation();
46
+ }, children: _jsx(CascadingMenuButton, { menuItems: [
28
47
  {
29
48
  label: 'Rename tab',
30
49
  icon: EditIcon,
@@ -35,5 +54,5 @@ export default function JBrowseTabMenu({ isHovered, onRename, onClose, }) {
35
54
  icon: CloseIcon,
36
55
  onClick: onClose,
37
56
  },
38
- ], size: "small", className: classes.tabIcon, stopPropagation: true, children: _jsx(MoreVertIcon, { className: classes.smallIcon }) }) }));
57
+ ], size: "small", className: classes.tabIcon, setOpen: setOpen, stopPropagation: true, children: _jsx(MoreVertIcon, { className: classes.smallIcon }) }) }));
39
58
  }
@@ -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;