@jbrowse/app-core 4.3.0 → 5.0.0-beta.1

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 (154) 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 +1598 -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 +225 -0
  8. package/esm/Assemblies/AssembliesMixin.js +149 -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 +19 -0
  12. package/esm/HistoryManagement/index.js +42 -5
  13. package/esm/JBrowseConfig/RootConfiguration.d.ts +167 -78
  14. package/esm/JBrowseConfig/RootConfiguration.js +85 -7
  15. package/esm/JBrowseConfig/index.d.ts +169 -84
  16. package/esm/JBrowseConfig/index.js +62 -2
  17. package/esm/JBrowseModel/index.d.ts +176 -95
  18. package/esm/JBrowseModel/index.js +122 -21
  19. package/esm/RootMenu/index.d.ts +77 -2
  20. package/esm/RootMenu/index.js +107 -61
  21. package/esm/SessionSpec/index.d.ts +6 -0
  22. package/esm/SessionSpec/index.js +3 -0
  23. package/esm/SessionSpec/lgvUrlInit.d.ts +79 -0
  24. package/esm/SessionSpec/lgvUrlInit.js +158 -0
  25. package/esm/SessionSpec/loadSessionSpec.d.ts +21 -0
  26. package/esm/SessionSpec/loadSessionSpec.js +311 -0
  27. package/esm/SessionSpec/parseSessionSpecUrl.d.ts +13 -0
  28. package/esm/SessionSpec/parseSessionSpecUrl.js +196 -0
  29. package/esm/SessionSpec/types.d.ts +33 -0
  30. package/esm/SessionSpec/types.js +1 -0
  31. package/esm/WorkspaceLayout/LayoutRenderer.d.ts +24 -0
  32. package/esm/WorkspaceLayout/LayoutRenderer.js +185 -0
  33. package/esm/WorkspaceLayout/PanelView.d.ts +13 -0
  34. package/esm/WorkspaceLayout/PanelView.js +99 -0
  35. package/esm/WorkspaceLayout/TabStrip.d.ts +14 -0
  36. package/esm/WorkspaceLayout/TabStrip.js +226 -0
  37. package/esm/WorkspaceLayout/WorkspaceContainer.d.ts +16 -0
  38. package/esm/WorkspaceLayout/WorkspaceContainer.js +105 -0
  39. package/esm/WorkspaceLayout/WorkspacePanelActions.d.ts +24 -0
  40. package/esm/WorkspaceLayout/WorkspacePanelActions.js +120 -0
  41. package/esm/WorkspaceLayout/WorkspaceTab.d.ts +27 -0
  42. package/esm/WorkspaceLayout/WorkspaceTab.js +109 -0
  43. package/esm/WorkspaceLayout/dockviewTheme.d.ts +69 -0
  44. package/esm/WorkspaceLayout/dockviewTheme.js +82 -0
  45. package/esm/WorkspaceLayout/dropZone.d.ts +105 -0
  46. package/esm/WorkspaceLayout/dropZone.js +113 -0
  47. package/esm/WorkspaceLayout/index.d.ts +8 -0
  48. package/esm/WorkspaceLayout/index.js +4 -0
  49. package/esm/WorkspaceLayout/model.d.ts +240 -0
  50. package/esm/WorkspaceLayout/model.js +527 -0
  51. package/esm/WorkspaceLayout/panelChrome.d.ts +19 -0
  52. package/esm/WorkspaceLayout/panelChrome.js +4 -0
  53. package/esm/WorkspaceLayout/spec.d.ts +58 -0
  54. package/esm/WorkspaceLayout/spec.js +153 -0
  55. package/esm/WorkspaceLayout/splitter.d.ts +25 -0
  56. package/esm/WorkspaceLayout/splitter.js +37 -0
  57. package/esm/WorkspaceLayout/tree.d.ts +158 -0
  58. package/esm/WorkspaceLayout/tree.js +379 -0
  59. package/esm/WorkspaceLayout/useLayoutDrag.d.ts +34 -0
  60. package/esm/WorkspaceLayout/useLayoutDrag.js +232 -0
  61. package/esm/index.d.ts +16 -9
  62. package/esm/index.js +13 -9
  63. package/esm/menus.d.ts +63 -62
  64. package/esm/menus.js +131 -84
  65. package/esm/queryParams.d.ts +9 -0
  66. package/esm/queryParams.js +92 -0
  67. package/esm/sessionModelFactory.d.ts +18 -0
  68. package/esm/sessionModelFactory.js +1 -0
  69. package/esm/ui/App/App.d.ts +3 -14
  70. package/esm/ui/App/App.js +30 -17
  71. package/esm/ui/App/AppFab.d.ts +12 -3
  72. package/esm/ui/App/AppFab.js +15 -8
  73. package/esm/ui/App/AppReadyMarker.d.ts +34 -0
  74. package/esm/ui/App/AppReadyMarker.js +56 -0
  75. package/esm/ui/App/AppToolbar.d.ts +2 -14
  76. package/esm/ui/App/AppToolbar.js +23 -24
  77. package/esm/ui/App/ClassicViewsContainer.d.ts +3 -9
  78. package/esm/ui/App/ClassicViewsContainer.js +9 -8
  79. package/esm/ui/App/DialogQueue.d.ts +17 -1
  80. package/esm/ui/App/DialogQueue.js +30 -1
  81. package/esm/ui/App/Drawer.d.ts +1 -1
  82. package/esm/ui/App/Drawer.js +10 -39
  83. package/esm/ui/App/DrawerControls.d.ts +1 -5
  84. package/esm/ui/App/DrawerControls.js +1 -25
  85. package/esm/ui/App/DrawerHeader.d.ts +2 -3
  86. package/esm/ui/App/DrawerHeader.js +24 -10
  87. package/esm/ui/App/DrawerHeaderHelpButton.d.ts +1 -1
  88. package/esm/ui/App/DrawerHeaderHelpButton.js +3 -3
  89. package/esm/ui/App/DrawerHeaderHelpDialog.d.ts +2 -2
  90. package/esm/ui/App/DrawerHeaderHelpDialog.js +3 -4
  91. package/esm/ui/App/DrawerWidget.d.ts +1 -1
  92. package/esm/ui/App/DrawerWidget.js +9 -21
  93. package/esm/ui/App/JBrowseTabMenu.d.ts +2 -3
  94. package/esm/ui/App/JBrowseTabMenu.js +24 -5
  95. package/esm/ui/App/ViewButtons.d.ts +2 -4
  96. package/esm/ui/App/ViewButtons.js +15 -3
  97. package/esm/ui/App/ViewContainer.d.ts +3 -2
  98. package/esm/ui/App/ViewContainer.js +144 -24
  99. package/esm/ui/App/ViewContainerTitle.d.ts +1 -1
  100. package/esm/ui/App/ViewContainerTitle.js +9 -2
  101. package/esm/ui/App/ViewErrorFallback.d.ts +18 -0
  102. package/esm/ui/App/ViewErrorFallback.js +42 -0
  103. package/esm/ui/App/ViewHeader.d.ts +3 -4
  104. package/esm/ui/App/ViewHeader.js +28 -17
  105. package/esm/ui/App/ViewLauncher.d.ts +2 -8
  106. package/esm/ui/App/ViewLauncher.js +7 -5
  107. package/esm/ui/App/ViewMenu.d.ts +3 -5
  108. package/esm/ui/App/ViewMenu.js +87 -92
  109. package/esm/ui/App/ViewStack.d.ts +7 -0
  110. package/esm/ui/App/ViewStack.js +42 -0
  111. package/esm/ui/App/ViewWrapper.d.ts +15 -1
  112. package/esm/ui/App/ViewWrapper.js +26 -10
  113. package/esm/ui/App/ViewsContainer.d.ts +3 -8
  114. package/esm/ui/App/ViewsContainer.js +9 -6
  115. package/esm/ui/App/copyView.js +1 -1
  116. package/esm/ui/App/index.d.ts +2 -1
  117. package/esm/ui/App/index.js +3 -2
  118. package/esm/ui/App/lazyChunk.d.ts +13 -0
  119. package/esm/ui/App/lazyChunk.js +91 -0
  120. package/esm/ui/App/types.d.ts +12 -3
  121. package/esm/ui/App/types.js +1 -1
  122. package/esm/ui/App/useViewVisibility.d.ts +34 -0
  123. package/esm/ui/App/useViewVisibility.js +87 -0
  124. package/esm/ui/App/viewTitle.d.ts +5 -0
  125. package/esm/ui/App/viewTitle.js +9 -0
  126. package/esm/ui/index.d.ts +1 -1
  127. package/esm/ui/index.js +1 -1
  128. package/package.json +12 -17
  129. package/esm/AppFocus/index.d.ts +0 -5
  130. package/esm/AppFocus/index.js +0 -12
  131. package/esm/Assemblies/SessionAssembliesMixin.d.ts +0 -92
  132. package/esm/Assemblies/SessionAssembliesMixin.js +0 -53
  133. package/esm/Assemblies/TemporaryAssembliesMixin.d.ts +0 -10
  134. package/esm/Assemblies/TemporaryAssembliesMixin.js +0 -37
  135. package/esm/DockviewLayout/index.d.ts +0 -35
  136. package/esm/DockviewLayout/index.js +0 -111
  137. package/esm/ui/App/DockviewContext.d.ts +0 -22
  138. package/esm/ui/App/DockviewContext.js +0 -29
  139. package/esm/ui/App/DockviewLeftHeaderActions.d.ts +0 -2
  140. package/esm/ui/App/DockviewLeftHeaderActions.js +0 -58
  141. package/esm/ui/App/DockviewRightHeaderActions.d.ts +0 -2
  142. package/esm/ui/App/DockviewRightHeaderActions.js +0 -100
  143. package/esm/ui/App/DrawerWidgetSelector.d.ts +0 -5
  144. package/esm/ui/App/DrawerWidgetSelector.js +0 -47
  145. package/esm/ui/App/JBrowseViewPanel.d.ts +0 -8
  146. package/esm/ui/App/JBrowseViewPanel.js +0 -40
  147. package/esm/ui/App/JBrowseViewTab.d.ts +0 -8
  148. package/esm/ui/App/JBrowseViewTab.js +0 -97
  149. package/esm/ui/App/ModalWidget.d.ts +0 -6
  150. package/esm/ui/App/ModalWidget.js +0 -48
  151. package/esm/ui/App/TiledViewsContainer.d.ts +0 -6
  152. package/esm/ui/App/TiledViewsContainer.js +0 -366
  153. package/esm/ui/App/dockviewUtils.d.ts +0 -46
  154. package/esm/ui/App/dockviewUtils.js +0 -55
@@ -11,106 +11,101 @@ import MenuIcon from '@mui/icons-material/Menu';
11
11
  import OpenInNewIcon from '@mui/icons-material/OpenInNew';
12
12
  import VerticalSplitIcon from '@mui/icons-material/VerticalSplit';
13
13
  import { observer } from 'mobx-react';
14
- import { useDockview } from "./DockviewContext.js";
15
- import { renameIds } from "./copyView.js";
16
- import { isSessionWithDockviewLayout } from "../../DockviewLayout/index.js";
17
- function getPanelViewCount(session, viewId) {
18
- if (!isSessionWithDockviewLayout(session)) {
19
- return 0;
20
- }
21
- for (const viewIds of session.panelViewAssignments.values()) {
22
- if (viewIds.includes(viewId)) {
23
- return viewIds.length;
24
- }
25
- }
26
- return 0;
27
- }
28
- const ViewMenu = observer(function ViewMenu({ model, IconProps, }) {
14
+ import { renameIds } from './copyView.js';
15
+ // takes the icon's class, not an SvgIconProps object: the object was built
16
+ // inline by ViewHeader and so was new on each of its renders, which defeated
17
+ // mobx-react's memo for this whole menu (a Tooltip and an IconButton) every
18
+ // time anything re-rendered the header
19
+ const ViewMenu = observer(function ViewMenu({ model, className, }) {
29
20
  const session = getSession(model);
30
- const { moveViewToNewTab, moveViewToSplitRight } = useDockview();
31
- const usePanel = session.useWorkspaces && isSessionWithDockviewLayout(session);
32
- const viewCount = usePanel
33
- ? getPanelViewCount(session, model.id)
34
- : session.views.length;
35
- const moveView = (panelFn, sessionFn) => {
36
- if (usePanel) {
37
- panelFn(model.id);
21
+ const moves = {
22
+ top: session.moveViewToTop,
23
+ up: session.moveViewUp,
24
+ down: session.moveViewDown,
25
+ bottom: session.moveViewToBottom,
26
+ };
27
+ // Give this view a home of its own: its own tab beside the rest, or its own
28
+ // split to their right. One path, whether or not the workspace is already up
29
+ // — the layout is session state either way, so there is nothing to defer and
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);
38
36
  }
39
37
  else {
40
- sessionFn(model.id);
38
+ session.moveViewToSplitRight(model.id, allViewIds);
41
39
  }
40
+ session.setUseWorkspaces(true);
42
41
  };
43
- return (_jsx(CascadingMenuButton, { "data-testid": "view_menu_icon", menuItems: () => [
44
- {
45
- label: 'View options',
46
- type: 'subMenu',
47
- subMenu: [
48
- {
49
- label: 'Copy view',
50
- icon: ContentCopyIcon,
51
- onClick: () => {
52
- session.addView(model.type, renameIds(structuredClone(getSnapshot(model))));
53
- },
54
- },
55
- {
56
- label: 'Move to new tab',
57
- icon: OpenInNewIcon,
58
- onClick: () => {
59
- moveViewToNewTab(model.id);
60
- session.setUseWorkspaces(true);
61
- },
62
- },
63
- {
64
- label: 'Move to split view (right side of screen)',
65
- icon: VerticalSplitIcon,
66
- onClick: () => {
67
- moveViewToSplitRight(model.id);
68
- session.setUseWorkspaces(true);
69
- },
70
- },
71
- ...(viewCount > 2
72
- ? [
73
- {
74
- label: 'Move view to top',
75
- icon: KeyboardDoubleArrowUpIcon,
76
- onClick: () => {
77
- moveView(session.moveViewToTopInPanel, session.moveViewToTop);
78
- },
42
+ return (_jsx(CascadingMenuButton, { "data-testid": "view_menu_icon", tooltip: "View menu", menuItems: () => {
43
+ // The views this move is relative to: in a workspace, the ones sharing
44
+ // this view's panel; in the classic stack, all of them. `session.views`
45
+ // is the order either way, so the mode decides the SCOPE of a move and
46
+ // nothing else. There is one implementation of "move a view" again.
47
+ //
48
+ // Resolved here rather than during render. It scans every panel's
49
+ // assignment list and copies one of them, and reading those lists in
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;
56
+ const viewCount = scopeIds?.length ?? session.views.length;
57
+ return [
58
+ {
59
+ label: 'View options',
60
+ type: 'subMenu',
61
+ subMenu: [
62
+ {
63
+ label: 'Copy view',
64
+ icon: ContentCopyIcon,
65
+ onClick: () => {
66
+ session.addView(model.type, renameIds(structuredClone(getSnapshot(model))));
79
67
  },
80
- ]
81
- : []),
82
- ...(viewCount > 1
83
- ? [
84
- {
85
- label: 'Move view up',
86
- icon: KeyboardArrowUpIcon,
87
- onClick: () => {
88
- moveView(session.moveViewUpInPanel, session.moveViewUp);
89
- },
68
+ },
69
+ {
70
+ label: 'Move to new tab',
71
+ icon: OpenInNewIcon,
72
+ onClick: () => {
73
+ moveViewOut('tabs');
90
74
  },
91
- {
92
- label: 'Move view down',
93
- icon: KeyboardArrowDownIcon,
94
- onClick: () => {
95
- moveView(session.moveViewDownInPanel, session.moveViewDown);
96
- },
75
+ },
76
+ {
77
+ label: 'Move to split view (right side of screen)',
78
+ icon: VerticalSplitIcon,
79
+ onClick: () => {
80
+ moveViewOut('horizontal');
97
81
  },
98
- ]
99
- : []),
100
- ...(viewCount > 2
101
- ? [
102
- {
103
- label: 'Move view to bottom',
104
- icon: KeyboardDoubleArrowDownIcon,
105
- onClick: () => {
106
- moveView(session.moveViewToBottomInPanel, session.moveViewToBottom);
82
+ },
83
+ // 'top'/'bottom' only mean something with a view above *and* below
84
+ ...[
85
+ ['top', 'Move view to top', KeyboardDoubleArrowUpIcon, 2],
86
+ ['up', 'Move view up', KeyboardArrowUpIcon, 1],
87
+ ['down', 'Move view down', KeyboardArrowDownIcon, 1],
88
+ [
89
+ 'bottom',
90
+ 'Move view to bottom',
91
+ KeyboardDoubleArrowDownIcon,
92
+ 2,
93
+ ],
94
+ ].flatMap(([direction, label, icon, minViews]) => viewCount > minViews
95
+ ? [
96
+ {
97
+ label,
98
+ icon,
99
+ onClick: () => {
100
+ moves[direction](model.id, scopeIds);
101
+ },
107
102
  },
108
- },
109
- ]
110
- : []),
111
- ],
112
- },
113
- ...model.menuItems(),
114
- ], children: _jsx(MenuIcon, { ...IconProps, fontSize: "small" }) }));
103
+ ]
104
+ : []),
105
+ ],
106
+ },
107
+ ...model.menuItems(),
108
+ ];
109
+ }, children: _jsx(MenuIcon, { className: className, fontSize: "small" }) }));
115
110
  });
116
111
  export default ViewMenu;
@@ -0,0 +1,7 @@
1
+ import type { AbstractViewModel, SessionWithFocusedViewAndDrawerWidgets } from '@jbrowse/core/util';
2
+ declare const ViewStack: ({ views, session, className, }: {
3
+ views: AbstractViewModel[];
4
+ session: SessionWithFocusedViewAndDrawerWidgets;
5
+ className?: string;
6
+ }) => import("react").JSX.Element;
7
+ export default ViewStack;
@@ -0,0 +1,42 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useRef, useState } from 'react';
3
+ import { SCROLL_PORT_HEIGHT_VAR, useScrollPortOverflow, } from '@jbrowse/core/util/hooks';
4
+ import { makeStyles } from '@jbrowse/core/util/tss-react';
5
+ import { observer } from 'mobx-react';
6
+ import ViewContainer from './ViewContainer.js';
7
+ // What the trailing space is *for*, which is what sets it: `ViewHeader` brings a
8
+ // newly-added view in with `scrollIntoView({ block: 'center' })`, and centering
9
+ // the last view needs half a port of room below it. Half the port is therefore
10
+ // the amount that makes that gesture reach, and any more is room to scroll the
11
+ // content off its own surface.
12
+ //
13
+ // Capped, because half a port stops being a courtesy on a tall screen. 300px is
14
+ // the cap the classic stack has always had and it stays the answer there — a
15
+ // full window is comfortably past twice it. What the cap was hiding is the
16
+ // workspace panel, where a 2x2 grid's cell can be shorter than 600px and a flat
17
+ // 300px let a view be dragged nearly clear of its own cell.
18
+ const MAX_OVERSCROLL = 300;
19
+ const useStyles = makeStyles()({
20
+ // trailing space so the last view can scroll up past the bottom of a
21
+ // container that would otherwise clip its lower edge.
22
+ //
23
+ // Rendered only once the views themselves overflow, and that is what makes
24
+ // the port's scrollbar honest: kept unconditionally, this alone overflowed
25
+ // the port, so every session — a single short view included — scrolled into
26
+ // nothing and drew a scrollbar saying so. It is outside the measured element
27
+ // for the same reason (see `useScrollPortOverflow`).
28
+ spacer: {
29
+ height: `min(${MAX_OVERSCROLL}px, calc(var(${SCROLL_PORT_HEIGHT_VAR}, 100vh) / 2))`,
30
+ },
31
+ });
32
+ const ViewStack = observer(function ViewStack({ views, session, className, }) {
33
+ const { classes } = useStyles();
34
+ const viewsRef = useRef(null);
35
+ const scrollable = useScrollPortOverflow(viewsRef);
36
+ // View ids present at first render: these arrived with the page (session
37
+ // restore / initial load), so they must not steal scroll when they mount. A
38
+ // view whose id isn't in this set was added later and auto-scrolls into view.
39
+ const [initialViewIds] = useState(() => new Set(views.map(v => v.id)));
40
+ return (_jsxs(_Fragment, { children: [_jsx("div", { ref: viewsRef, className: className, children: views.map(view => (_jsx(ViewContainer, { view: view, session: session, scrollOnMount: !initialViewIds.has(view.id) }, view.id))) }), scrollable ? _jsx("div", { className: classes.spacer }) : null] }));
41
+ });
42
+ export default ViewStack;
@@ -1,6 +1,20 @@
1
1
  import type { AbstractViewModel, SessionWithFocusedViewAndDrawerWidgets } from '@jbrowse/core/util';
2
+ /**
3
+ * A view's body, and the boundary that keeps a view that throws from being an
4
+ * application-wide failure.
5
+ *
6
+ * Without it the next boundary up is the product's own — jbrowse-web's is in
7
+ * `Loader.tsx` and its fallback is `FatalErrorDialog`, so one bad view replaced
8
+ * every other view the user had open. The reachable case is not hypothetical: a
9
+ * display's reads walk to `getContainingView` and throw when the walk finds no
10
+ * view, which is what ADR-069 is about.
11
+ *
12
+ * No reset keys: a view body unmounts on its own when the view is minimized or
13
+ * scrolls out of the mount band, so remounting is already how a stale banner
14
+ * clears. What the fallback adds is the deliberate ways out — retry, and close.
15
+ */
2
16
  declare const ViewWrapper: ({ view, session, }: {
3
17
  view: AbstractViewModel;
4
18
  session: SessionWithFocusedViewAndDrawerWidgets;
5
- }) => import("react/jsx-runtime").JSX.Element | null;
19
+ }) => import("react").JSX.Element;
6
20
  export default ViewWrapper;
@@ -1,18 +1,34 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { Suspense } from 'react';
3
+ import { ErrorBoundary } from '@jbrowse/core/ui/ErrorBoundary';
3
4
  import LoadingEllipses from '@jbrowse/core/ui/LoadingEllipses';
4
5
  import { getEnv } from '@jbrowse/core/util';
5
6
  import { observer } from 'mobx-react';
6
- const ViewWrapper = observer(function ViewWrapper({ view, session, }) {
7
+ import ViewErrorFallback from './ViewErrorFallback.js';
8
+ // Separate from the boundary below on purpose: `getViewType` throws when no
9
+ // plugin registered the type, and a lookup done in the boundary's own render is
10
+ // a lookup the boundary cannot catch. This is the one thing in a view's render
11
+ // path that fails without any of the view's own code running.
12
+ const ViewComponent = observer(function ViewComponent({ view, session, }) {
7
13
  const { pluginManager } = getEnv(session);
8
- const viewType = pluginManager.getViewType(view.type);
9
- if (!viewType) {
10
- throw new Error(`unknown view type ${view.type}`);
11
- }
12
- const { ReactComponent } = viewType;
13
- if (view.minimized) {
14
- return null;
15
- }
16
- return (_jsx(Suspense, { fallback: _jsx(LoadingEllipses, { variant: "h6" }), children: _jsx(ReactComponent, { model: view, session: session }) }));
14
+ const { ReactComponent } = pluginManager.getViewType(view.type);
15
+ return (_jsx(Suspense, { fallback: _jsx(LoadingEllipses, { variant: "h6", "data-view-component-pending": "true" }), children: _jsx(ReactComponent, { model: view, session: session }) }));
16
+ });
17
+ /**
18
+ * A view's body, and the boundary that keeps a view that throws from being an
19
+ * application-wide failure.
20
+ *
21
+ * Without it the next boundary up is the product's own — jbrowse-web's is in
22
+ * `Loader.tsx` and its fallback is `FatalErrorDialog`, so one bad view replaced
23
+ * every other view the user had open. The reachable case is not hypothetical: a
24
+ * display's reads walk to `getContainingView` and throw when the walk finds no
25
+ * view, which is what ADR-069 is about.
26
+ *
27
+ * No reset keys: a view body unmounts on its own when the view is minimized or
28
+ * scrolls out of the mount band, so remounting is already how a stale banner
29
+ * clears. What the fallback adds is the deliberate ways out — retry, and close.
30
+ */
31
+ const ViewWrapper = observer(function ViewWrapper({ view, session, }) {
32
+ return (_jsx(ErrorBoundary, { FallbackComponent: ({ error, resetErrorBoundary }) => (_jsx(ViewErrorFallback, { view: view, session: session, error: error, onRetry: resetErrorBoundary })), children: _jsx(ViewComponent, { view: view, session: session }) }));
17
33
  });
18
34
  export default ViewWrapper;
@@ -1,12 +1,7 @@
1
- import type { SnackbarMessage } from '@jbrowse/core/ui/SnackbarModel';
2
- import type { AbstractViewContainer, SessionWithFocusedViewAndDrawerWidgets } from '@jbrowse/core/util';
1
+ import type { AppSession } from './types.ts';
3
2
  interface Props {
4
3
  HeaderButtons?: React.ReactElement;
5
- session: SessionWithFocusedViewAndDrawerWidgets & AbstractViewContainer & {
6
- renameCurrentSession: (arg: string) => void;
7
- snackbarMessages: SnackbarMessage[];
8
- popSnackbarMessage: () => unknown;
9
- };
4
+ session: AppSession;
10
5
  }
11
- declare const ViewsContainer: (props: Props) => import("react/jsx-runtime").JSX.Element;
6
+ declare const ViewsContainer: (props: Props) => import("react").JSX.Element;
12
7
  export default ViewsContainer;
@@ -2,9 +2,13 @@ import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { Suspense, lazy } from 'react';
3
3
  import { makeStyles } from '@jbrowse/core/util/tss-react';
4
4
  import { observer } from 'mobx-react';
5
- const ClassicViewsContainer = lazy(() => import("./ClassicViewsContainer.js"));
6
- const TiledViewsContainer = lazy(() => import("./TiledViewsContainer.js"));
7
- const ViewLauncher = lazy(() => import("./ViewLauncher.js"));
5
+ import { isSessionWithWorkspaceLayout } from '../../WorkspaceLayout/model.js';
6
+ import { lazyChunk } from './lazyChunk.js';
7
+ const ClassicViewsContainer = lazy(lazyChunk('ClassicViewsContainer', () => import('./ClassicViewsContainer.js')));
8
+ const WorkspaceContainer = lazy(lazyChunk('WorkspaceContainer', () => import('../../WorkspaceLayout/WorkspaceContainer.js').then(m => ({
9
+ default: m.WorkspaceContainer,
10
+ }))));
11
+ const ViewLauncher = lazy(lazyChunk('ViewLauncher', () => import('./ViewLauncher.js')));
8
12
  const useStyles = makeStyles()({
9
13
  viewsContainer: {
10
14
  gridRow: 'components',
@@ -13,9 +17,8 @@ const useStyles = makeStyles()({
13
17
  });
14
18
  const ViewsContainer = observer(function ViewsContainer(props) {
15
19
  const { session } = props;
16
- const { views } = session;
20
+ const { views, effectiveUseWorkspaces } = session;
17
21
  const { classes } = useStyles();
18
- const useWorkspaces = 'useWorkspaces' in session ? session.useWorkspaces : true;
19
- return (_jsx("div", { className: classes.viewsContainer, children: views.length > 0 ? (_jsx(Suspense, { fallback: null, children: useWorkspaces ? (_jsx(TiledViewsContainer, { session: session })) : (_jsx(ClassicViewsContainer, { session: session })) })) : (_jsx(Suspense, { fallback: null, children: _jsx(ViewLauncher, { ...props }) })) }));
22
+ return (_jsx("div", { className: classes.viewsContainer, children: _jsx(Suspense, { fallback: null, children: views.length > 0 ? (effectiveUseWorkspaces && isSessionWithWorkspaceLayout(session) ? (_jsx(WorkspaceContainer, { session: session })) : (_jsx(ClassicViewsContainer, { session: session }))) : (_jsx(ViewLauncher, { session: session })) }) }));
20
23
  });
21
24
  export default ViewsContainer;
@@ -15,7 +15,7 @@ export function renameIds(obj) {
15
15
  if (!idMap.has(val)) {
16
16
  idMap.set(val, createElementId());
17
17
  }
18
- result[key] = `${val}-${idMap.get(val)}`;
18
+ result[key] = idMap.get(val);
19
19
  }
20
20
  else {
21
21
  result[key] = transformIds(val);
@@ -1,2 +1,3 @@
1
- export * from './App.tsx';
1
+ export { App } from './App.tsx';
2
+ export { default as AppReadyMarker } from './AppReadyMarker.tsx';
2
3
  export { default as DialogQueue } from './DialogQueue.tsx';
@@ -1,2 +1,3 @@
1
- export * from "./App.js";
2
- export { default as DialogQueue } from "./DialogQueue.js";
1
+ export { App } from './App.js';
2
+ export { default as AppReadyMarker } from './AppReadyMarker.js';
3
+ export { default as DialogQueue } from './DialogQueue.js';
@@ -0,0 +1,13 @@
1
+ /**
2
+ * A dynamic import that survives one bad response for its chunk.
3
+ *
4
+ * The retry is not a hopeful second roll: `probeChunk` refetches with
5
+ * `cache: 'reload'`, which both reports what the server really says and
6
+ * replaces whatever the HTTP cache was holding, and webpack clears its
7
+ * `installedChunks` entry on failure — so the second `import()` is a real
8
+ * request against a repaired cache entry. React.lazy cannot do this itself: it
9
+ * stores the rejection and rethrows it for the life of the page without ever
10
+ * calling the factory again, so the recovery has to happen here, before the
11
+ * promise settles.
12
+ */
13
+ export declare function lazyChunk<T>(name: string, load: () => Promise<T>): () => Promise<T>;
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Wraps a dynamic import so a failed chunk fetch says what actually happened.
3
+ *
4
+ * Webpack flattens every network-level failure into the same string —
5
+ * `Loading chunk X failed.\n(error: <url>)` — where `error` only means the
6
+ * script element fired `error` rather than `load` (a 404, a refused
7
+ * connection and a dropped socket are indistinguishable). That string is what
8
+ * jbrowse-web records via `markCrashedSession`, so it is also all a later boot
9
+ * can show. The immediate refetch below is the part that discriminates:
10
+ * webpack clears its `installedChunks` entry on failure, so the asset is
11
+ * really re-requested, and a 200 here means the asset was always fine and the
12
+ * one request was not.
13
+ */
14
+ async function probeChunk(url) {
15
+ try {
16
+ const response = await fetch(url, { cache: 'reload' });
17
+ const body = await response.text();
18
+ return `refetch: HTTP ${response.status} ${response.statusText}, ${response.headers.get('content-type')}, ${body.length} bytes`;
19
+ }
20
+ catch (e) {
21
+ return `refetch also failed: ${e}`;
22
+ }
23
+ }
24
+ /**
25
+ * What the browser recorded for the request that failed — the status it
26
+ * actually saw and whether any bytes crossed the network. A `transferSize` of
27
+ * 0 alongside a body size says the response came from the cache rather than
28
+ * the server, which is the one thing `curl` can never show.
29
+ */
30
+ function resourceTiming(url) {
31
+ const entry = performance
32
+ .getEntriesByName(url)
33
+ .find(e => e instanceof PerformanceResourceTiming);
34
+ return entry
35
+ ? {
36
+ responseStatus: entry.responseStatus,
37
+ transferSize: entry.transferSize,
38
+ encodedBodySize: entry.encodedBodySize,
39
+ duration: Math.round(entry.duration),
40
+ }
41
+ : 'no resource timing entry';
42
+ }
43
+ /**
44
+ * The two fields webpack hangs off a ChunkLoadError: the URL it asked for, and
45
+ * whether the script element reported `error` (the request failed), `missing`
46
+ * (it loaded but registered nothing) or `timeout`.
47
+ */
48
+ function chunkErrorDetail(error) {
49
+ return error instanceof Error &&
50
+ error.name === 'ChunkLoadError' &&
51
+ 'request' in error &&
52
+ typeof error.request === 'string'
53
+ ? {
54
+ url: error.request,
55
+ type: 'type' in error ? String(error.type) : 'unknown',
56
+ }
57
+ : undefined;
58
+ }
59
+ /**
60
+ * A dynamic import that survives one bad response for its chunk.
61
+ *
62
+ * The retry is not a hopeful second roll: `probeChunk` refetches with
63
+ * `cache: 'reload'`, which both reports what the server really says and
64
+ * replaces whatever the HTTP cache was holding, and webpack clears its
65
+ * `installedChunks` entry on failure — so the second `import()` is a real
66
+ * request against a repaired cache entry. React.lazy cannot do this itself: it
67
+ * stores the rejection and rethrows it for the life of the page without ever
68
+ * calling the factory again, so the recovery has to happen here, before the
69
+ * promise settles.
70
+ */
71
+ export function lazyChunk(name, load) {
72
+ return () => load().catch(async (error) => {
73
+ const detail = chunkErrorDetail(error);
74
+ if (detail) {
75
+ console.error(`chunk load failed: ${name}`, JSON.stringify({
76
+ ...detail,
77
+ online: navigator.onLine,
78
+ at: new Date().toISOString(),
79
+ timing: resourceTiming(detail.url),
80
+ probe: await probeChunk(detail.url),
81
+ }, undefined, 2));
82
+ const retried = await load().catch((retryError) => {
83
+ console.error(`chunk retry failed: ${name}`, retryError);
84
+ throw error;
85
+ });
86
+ console.error(`chunk retry succeeded: ${name}`);
87
+ return retried;
88
+ }
89
+ throw error;
90
+ });
91
+ }
@@ -1,7 +1,16 @@
1
- import type { SnackbarMessage } from '@jbrowse/core/ui/SnackbarModel';
2
- import type { AbstractViewContainer, SessionWithFocusedViewAndDrawerWidgets } from '@jbrowse/core/util';
3
- export type DockviewSessionType = SessionWithFocusedViewAndDrawerWidgets & AbstractViewContainer & {
1
+ import type { Menu } from '../../menus.ts';
2
+ import type { ErrorDialogState, SnackbarMessage } from '@jbrowse/core/ui/SnackbarModel';
3
+ import type { SessionWithFocusedViewAndDrawerWidgets } from '@jbrowse/core/util';
4
+ export { type Menu } from '../../menus.ts';
5
+ export type WorkspaceSessionType = SessionWithFocusedViewAndDrawerWidgets & {
4
6
  renameCurrentSession: (arg: string) => void;
5
7
  snackbarMessages: SnackbarMessage[];
6
8
  popSnackbarMessage: () => unknown;
9
+ orderViews: (ids: string[]) => void;
10
+ };
11
+ export type AppSession = WorkspaceSessionType & {
12
+ menus: () => Menu[];
13
+ errorDialog: ErrorDialogState | undefined;
14
+ setErrorDialog: (state: ErrorDialogState | undefined) => void;
15
+ effectiveUseWorkspaces: boolean;
7
16
  };
@@ -1 +1 @@
1
- export {};
1
+ export {} from '../../menus.js';
@@ -0,0 +1,34 @@
1
+ /**
2
+ * View-level lazy mounting. A view only mounts its (GPU-heavy) body when it is
3
+ * within `rootMargin` of the viewport, and collapses to a height-preserving
4
+ * spacer once scrolled far away. This bounds the number of simultaneously-live
5
+ * GPU canvases/contexts — the root cause of the "workspaces freeze" with many
6
+ * stacked views (one WebGL context per display canvas blows past the browser's
7
+ * per-page cap; see agent-docs/reference/ARCHITECTURAL_LIMITS.md, "One WebGL2
8
+ * context per display canvas").
9
+ *
10
+ * `root: null` (viewport) is container-agnostic: it reports on-screen-ness the
11
+ * same way whether the views scroll inside the classic container or a workspace
12
+ * panel, so neither container needs to know about windowing. It also means
13
+ * **`rootMargin` has no effect** — an observer clips the target against each
14
+ * scrolling ancestor before intersecting with the root box that the margin
15
+ * expands, and both containers are `overflow-y: auto`. So this is a hard
16
+ * window with no hysteresis: a view is torn down the moment it leaves its
17
+ * container and rebuilt when it returns, which on a GPU backend costs a fresh
18
+ * WebGL2 context and a full shader recompile per display.
19
+ *
20
+ * That is deliberate as of 2026-08-05, not an oversight. Rooting the observer at
21
+ * the scroll port restores the band and measures as a wash on scroll cost while
22
+ * roughly doubling live contexts — and the ceiling is 16 live contexts, so the
23
+ * cap bites before the rebuild saving pays for itself.
24
+ * agent-docs/reference/GPU_CONTEXT_BUDGET.md has the numbers and the three
25
+ * other fixes that were measured and eliminated.
26
+ *
27
+ * Starts hidden so a cold load with N crammed views doesn't mount them all at
28
+ * once; the observer's first callback mounts only what's near the viewport.
29
+ */
30
+ export declare function useViewVisibility(rootMargin: string): {
31
+ ref: import("react").RefObject<HTMLDivElement | null>;
32
+ visible: boolean;
33
+ measuredHeight: number | undefined;
34
+ };
@@ -0,0 +1,87 @@
1
+ import { useEffect, useRef, useState } from 'react';
2
+ // No IntersectionObserver (jsdom/SSR): start (and stay) visible, the
3
+ // pre-lazy-mount behavior, so tests and non-browser environments are unchanged.
4
+ function intersectionObserverAvailable() {
5
+ return typeof window !== 'undefined' && 'IntersectionObserver' in window;
6
+ }
7
+ /**
8
+ * View-level lazy mounting. A view only mounts its (GPU-heavy) body when it is
9
+ * within `rootMargin` of the viewport, and collapses to a height-preserving
10
+ * spacer once scrolled far away. This bounds the number of simultaneously-live
11
+ * GPU canvases/contexts — the root cause of the "workspaces freeze" with many
12
+ * stacked views (one WebGL context per display canvas blows past the browser's
13
+ * per-page cap; see agent-docs/reference/ARCHITECTURAL_LIMITS.md, "One WebGL2
14
+ * context per display canvas").
15
+ *
16
+ * `root: null` (viewport) is container-agnostic: it reports on-screen-ness the
17
+ * same way whether the views scroll inside the classic container or a workspace
18
+ * panel, so neither container needs to know about windowing. It also means
19
+ * **`rootMargin` has no effect** — an observer clips the target against each
20
+ * scrolling ancestor before intersecting with the root box that the margin
21
+ * expands, and both containers are `overflow-y: auto`. So this is a hard
22
+ * window with no hysteresis: a view is torn down the moment it leaves its
23
+ * container and rebuilt when it returns, which on a GPU backend costs a fresh
24
+ * WebGL2 context and a full shader recompile per display.
25
+ *
26
+ * That is deliberate as of 2026-08-05, not an oversight. Rooting the observer at
27
+ * the scroll port restores the band and measures as a wash on scroll cost while
28
+ * roughly doubling live contexts — and the ceiling is 16 live contexts, so the
29
+ * cap bites before the rebuild saving pays for itself.
30
+ * agent-docs/reference/GPU_CONTEXT_BUDGET.md has the numbers and the three
31
+ * other fixes that were measured and eliminated.
32
+ *
33
+ * Starts hidden so a cold load with N crammed views doesn't mount them all at
34
+ * once; the observer's first callback mounts only what's near the viewport.
35
+ */
36
+ export function useViewVisibility(rootMargin) {
37
+ const ref = useRef(null);
38
+ const [state, setState] = useState(() => ({
39
+ visible: !intersectionObserverAvailable(),
40
+ }));
41
+ // Toggle `visible` as the body scrolls in/out of the viewport (root: null),
42
+ // and record the height it had on the way out.
43
+ //
44
+ // The height comes off the entry the observer already handed us. At the
45
+ // moment the body stops intersecting it is still mounted at full height (the
46
+ // collapse is what this callback is about to cause), so the entry's rect is
47
+ // exactly what a ResizeObserver would have reported. Keeping one
48
+ // meant a second observer per view whose every fire re-rendered the whole
49
+ // view chrome, and a view's height moves constantly: every track added,
50
+ // resized, or grown by its own data.
51
+ useEffect(() => {
52
+ const node = ref.current;
53
+ if (node && intersectionObserverAvailable()) {
54
+ const io = new IntersectionObserver(entries => {
55
+ const entry = entries.at(-1);
56
+ if (entry) {
57
+ const { isIntersecting } = entry;
58
+ setState(prev => {
59
+ if (prev.visible === isIntersecting) {
60
+ return prev;
61
+ }
62
+ return {
63
+ visible: isIntersecting,
64
+ // a minimized view measures 0 here (it renders no body at
65
+ // all), which must not become its remembered height
66
+ height: isIntersecting
67
+ ? prev.height
68
+ : entry.boundingClientRect.height || prev.height,
69
+ };
70
+ });
71
+ }
72
+ }, { rootMargin });
73
+ io.observe(node);
74
+ return () => {
75
+ io.disconnect();
76
+ };
77
+ }
78
+ return undefined;
79
+ }, [rootMargin]);
80
+ // `measuredHeight` is undefined until the body has been seen at least once.
81
+ // The caller supplies the estimate, and supplies it lazily. See ViewContainer
82
+ return {
83
+ ref,
84
+ visible: state.visible,
85
+ measuredHeight: state.height,
86
+ };
87
+ }