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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/esm/AppSession/AppSessionMixin.d.ts +42 -316
  2. package/esm/Assemblies/AssembliesMixin.d.ts +172 -103
  3. package/esm/Assemblies/AssembliesMixin.js +13 -8
  4. package/esm/HistoryManagement/index.d.ts +3 -11
  5. package/esm/HistoryManagement/index.js +24 -28
  6. package/esm/JBrowseConfig/RootConfiguration.d.ts +80 -124
  7. package/esm/JBrowseConfig/RootConfiguration.js +5 -11
  8. package/esm/JBrowseConfig/defaultShareUrl.d.ts +1 -0
  9. package/esm/JBrowseConfig/defaultShareUrl.js +5 -0
  10. package/esm/JBrowseConfig/index.d.ts +89 -83
  11. package/esm/JBrowseConfig/index.js +6 -4
  12. package/esm/JBrowseModel/index.d.ts +93 -96
  13. package/esm/JBrowseModel/index.js +43 -43
  14. package/esm/JbApi/index.d.ts +2 -0
  15. package/esm/JbApi/index.js +1 -0
  16. package/esm/JbApi/jbApi.d.ts +2767 -0
  17. package/esm/JbApi/jbApi.js +1383 -0
  18. package/esm/RootMenu/index.d.ts +2 -11
  19. package/esm/RootMenu/index.js +0 -11
  20. package/esm/SessionSpec/index.d.ts +1 -1
  21. package/esm/SessionSpec/index.js +1 -1
  22. package/esm/SessionSpec/lgvUrlInit.js +5 -2
  23. package/esm/SessionSpec/loadSessionSpec.d.ts +56 -2
  24. package/esm/SessionSpec/loadSessionSpec.js +285 -123
  25. package/esm/SessionSpec/parseSessionSpecUrl.d.ts +2 -8
  26. package/esm/SessionSpec/parseSessionSpecUrl.js +2 -17
  27. package/esm/SessionSpec/types.d.ts +16 -12
  28. package/esm/WorkspaceLayout/WorkspaceContainer.js +4 -2
  29. package/esm/WorkspaceLayout/WorkspaceTab.js +2 -6
  30. package/esm/WorkspaceLayout/model.d.ts +24 -4
  31. package/esm/WorkspaceLayout/model.js +76 -17
  32. package/esm/WorkspaceLayout/spec.d.ts +40 -10
  33. package/esm/WorkspaceLayout/spec.js +81 -8
  34. package/esm/index.d.ts +4 -2
  35. package/esm/index.js +4 -1
  36. package/esm/menus.d.ts +15 -29
  37. package/esm/menus.js +76 -92
  38. package/esm/queryParams.d.ts +1 -1
  39. package/esm/queryParams.js +10 -9
  40. package/esm/ui/App/App.js +18 -11
  41. package/esm/ui/App/AppToolbar.js +23 -8
  42. package/esm/ui/App/ClassicViewsContainer.d.ts +2 -2
  43. package/esm/ui/App/DialogQueue.d.ts +2 -2
  44. package/esm/ui/App/DialogQueue.js +3 -3
  45. package/esm/ui/App/DrawerHeader.js +2 -3
  46. package/esm/ui/App/ThemeModeButton.d.ts +12 -0
  47. package/esm/ui/App/ThemeModeButton.js +34 -0
  48. package/esm/ui/App/ViewButtons.js +2 -2
  49. package/esm/ui/App/ViewContainer.js +1 -17
  50. package/esm/ui/App/ViewContainerTitle.js +15 -12
  51. package/esm/ui/App/ViewLauncher.js +1 -1
  52. package/esm/ui/App/ViewMenu.js +2 -2
  53. package/esm/ui/App/ViewStack.js +7 -1
  54. package/esm/ui/App/ViewsContainer.d.ts +2 -4
  55. package/esm/ui/App/ViewsContainer.js +3 -9
  56. package/esm/ui/App/index.d.ts +1 -1
  57. package/esm/ui/App/index.js +1 -1
  58. package/esm/ui/App/lazyParts.d.ts +32 -0
  59. package/esm/ui/App/lazyParts.js +8 -0
  60. package/esm/ui/App/preloadAppShell.d.ts +14 -0
  61. package/esm/ui/App/preloadAppShell.js +41 -0
  62. package/esm/ui/App/types.d.ts +12 -7
  63. package/esm/ui/App/useViewVisibility.d.ts +9 -8
  64. package/esm/ui/App/useViewVisibility.js +11 -10
  65. package/esm/ui/App/viewTitle.d.ts +10 -3
  66. package/esm/ui/App/viewTitle.js +13 -5
  67. package/esm/ui/index.d.ts +1 -1
  68. package/esm/ui/index.js +1 -1
  69. package/package.json +7 -7
  70. package/esm/ui/App/AppReadyMarker.d.ts +0 -34
  71. package/esm/ui/App/AppReadyMarker.js +0 -56
  72. package/esm/ui/App/DrawerControls.d.ts +0 -1
  73. package/esm/ui/App/DrawerControls.js +0 -1
  74. package/esm/ui/App/copyView.d.ts +0 -1
  75. package/esm/ui/App/copyView.js +0 -29
@@ -1,10 +1,10 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import EditableTypography from '@jbrowse/core/ui/EditableTypography';
3
3
  import { getSession } from '@jbrowse/core/util';
4
4
  import { makeStyles } from '@jbrowse/core/util/tss-react';
5
- import { Tooltip } from '@mui/material';
5
+ import { Tooltip, Typography } from '@mui/material';
6
6
  import { observer } from 'mobx-react';
7
- import { viewTitle } from './viewTitle.js';
7
+ import { viewName, viewTitle } from './viewTitle.js';
8
8
  const useStyles = makeStyles()(theme => ({
9
9
  input: {
10
10
  paddingBottom: 0,
@@ -28,18 +28,21 @@ const useStyles = makeStyles()(theme => ({
28
28
  borderColor: theme.palette.primary.main,
29
29
  backgroundColor: theme.palette.secondary.light,
30
30
  },
31
+ minimized: {
32
+ color: theme.palette.secondary.contrastText,
33
+ },
31
34
  }));
32
35
  const ViewContainerTitle = observer(function ViewContainerTitle({ view, }) {
33
36
  const { classes } = useStyles();
34
37
  const { assemblyManager } = getSession(view);
35
- const title = viewTitle(view, r => assemblyManager.getDisplayName(r));
36
- return (_jsx(Tooltip, { title: `${title} (click to rename)`, arrow: true, children: _jsx(EditableTypography, { value: title, setValue: val => {
37
- view.setDisplayName(val);
38
- }, variant: "body2", classes: {
39
- input: classes.input,
40
- inputBase: classes.inputBase,
41
- inputRoot: classes.inputRoot,
42
- inputFocused: classes.inputFocused,
43
- } }) }));
38
+ const getDisplayName = (r) => assemblyManager.getDisplayName(r);
39
+ return (_jsxs(_Fragment, { children: [_jsx(Tooltip, { title: `${viewTitle(view, getDisplayName)} (click to rename)`, arrow: true, children: _jsx(EditableTypography, { value: viewName(view, getDisplayName), setValue: val => {
40
+ view.setDisplayName(val);
41
+ }, variant: "body2", classes: {
42
+ input: classes.input,
43
+ inputBase: classes.inputBase,
44
+ inputRoot: classes.inputRoot,
45
+ inputFocused: classes.inputFocused,
46
+ } }) }), view.minimized ? (_jsx(Typography, { variant: "body2", className: classes.minimized, children: "(minimized)" })) : null] }));
44
47
  });
45
48
  export default ViewContainerTitle;
@@ -22,7 +22,7 @@ const ViewLauncher = observer(function ViewLauncher({ session, }) {
22
22
  return (_jsxs(Paper, { className: classes.selectPaper, children: [_jsx(Typography, { children: "Select a view to launch" }), _jsx(FormControl, { className: classes.m2, children: _jsx(Select, { value: value, onChange: event => {
23
23
  setValue(event.target.value);
24
24
  }, children: viewTypes.map(({ displayName, name }) => (_jsx(MenuItem, { value: name, children: displayName }, name))) }) }), _jsx(FormControl, { className: classes.m2, children: _jsx(Button, { onClick: () => {
25
- session.addView(value, {});
25
+ void session.launchView(value, {});
26
26
  }, variant: "contained", color: "primary", children: "Launch view" }) })] }));
27
27
  });
28
28
  export default ViewLauncher;
@@ -1,6 +1,7 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { CascadingMenuButton } from '@jbrowse/core/ui';
3
3
  import { getSession } from '@jbrowse/core/util';
4
+ import { renameIds } from '@jbrowse/core/util/types/mst';
4
5
  import { getSnapshot } from '@jbrowse/mobx-state-tree';
5
6
  import ContentCopyIcon from '@mui/icons-material/ContentCopy';
6
7
  import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
@@ -11,7 +12,6 @@ import MenuIcon from '@mui/icons-material/Menu';
11
12
  import OpenInNewIcon from '@mui/icons-material/OpenInNew';
12
13
  import VerticalSplitIcon from '@mui/icons-material/VerticalSplit';
13
14
  import { observer } from 'mobx-react';
14
- import { renameIds } from './copyView.js';
15
15
  // takes the icon's class, not an SvgIconProps object: the object was built
16
16
  // inline by ViewHeader and so was new on each of its renders, which defeated
17
17
  // mobx-react's memo for this whole menu (a Tooltip and an IconButton) every
@@ -63,7 +63,7 @@ const ViewMenu = observer(function ViewMenu({ model, className, }) {
63
63
  label: 'Copy view',
64
64
  icon: ContentCopyIcon,
65
65
  onClick: () => {
66
- session.addView(model.type, renameIds(structuredClone(getSnapshot(model))));
66
+ session.addView(model.type, renameIds(getSnapshot(model)));
67
67
  },
68
68
  },
69
69
  {
@@ -10,6 +10,12 @@ import ViewContainer from './ViewContainer.js';
10
10
  // the amount that makes that gesture reach, and any more is room to scroll the
11
11
  // content off its own surface.
12
12
  //
13
+ // It is scrollable room rather than content, so anything measuring how tall the
14
+ // session is discounts it by its `view-stack-overscroll` testid — `overflow` in
15
+ // jbApi does, and a filmed take paid for that: counted as content, it told
16
+ // `jb.fitToWindow` the session was 300px taller than it was, and the dotplot
17
+ // absorbing the cut came out a third of the height that fit.
18
+ //
13
19
  // Capped, because half a port stops being a courtesy on a tall screen. 300px is
14
20
  // the cap the classic stack has always had and it stays the answer there — a
15
21
  // full window is comfortably past twice it. What the cap was hiding is the
@@ -37,6 +43,6 @@ const ViewStack = observer(function ViewStack({ views, session, className, }) {
37
43
  // restore / initial load), so they must not steal scroll when they mount. A
38
44
  // view whose id isn't in this set was added later and auto-scrolls into view.
39
45
  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] }));
46
+ 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, "data-testid": "view-stack-overscroll" })) : null] }));
41
47
  });
42
48
  export default ViewStack;
@@ -1,7 +1,5 @@
1
1
  import type { AppSession } from './types.ts';
2
- interface Props {
3
- HeaderButtons?: React.ReactElement;
2
+ declare const ViewsContainer: ({ session, }: {
4
3
  session: AppSession;
5
- }
6
- declare const ViewsContainer: (props: Props) => import("react").JSX.Element;
4
+ }) => import("react").JSX.Element;
7
5
  export default ViewsContainer;
@@ -1,22 +1,16 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { Suspense, lazy } from 'react';
2
+ import { Suspense } from 'react';
3
3
  import { makeStyles } from '@jbrowse/core/util/tss-react';
4
4
  import { observer } from 'mobx-react';
5
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')));
6
+ import { ClassicViewsContainer, ViewLauncher, WorkspaceContainer, } from './lazyParts.js';
12
7
  const useStyles = makeStyles()({
13
8
  viewsContainer: {
14
9
  gridRow: 'components',
15
10
  overflow: 'hidden',
16
11
  },
17
12
  });
18
- const ViewsContainer = observer(function ViewsContainer(props) {
19
- const { session } = props;
13
+ const ViewsContainer = observer(function ViewsContainer({ session, }) {
20
14
  const { views, effectiveUseWorkspaces } = session;
21
15
  const { classes } = useStyles();
22
16
  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 })) }) }));
@@ -1,3 +1,3 @@
1
1
  export { App } from './App.tsx';
2
- export { default as AppReadyMarker } from './AppReadyMarker.tsx';
3
2
  export { default as DialogQueue } from './DialogQueue.tsx';
3
+ export { preloadAppFrame, preloadAppShell } from './preloadAppShell.ts';
@@ -1,3 +1,3 @@
1
1
  export { App } from './App.js';
2
- export { default as AppReadyMarker } from './AppReadyMarker.js';
3
2
  export { default as DialogQueue } from './DialogQueue.js';
3
+ export { preloadAppFrame, preloadAppShell } from './preloadAppShell.js';
@@ -0,0 +1,32 @@
1
+ export declare const DrawerWidget: import("react").LazyExoticComponent<({ session, }: {
2
+ session: import("@jbrowse/core/util").SessionWithFocusedViewAndDrawerWidgets;
3
+ }) => import("react").JSX.Element> & {
4
+ preload: () => Promise<{
5
+ default: ({ session, }: {
6
+ session: import("@jbrowse/core/util").SessionWithFocusedViewAndDrawerWidgets;
7
+ }) => import("react").JSX.Element;
8
+ }>;
9
+ };
10
+ export declare const ClassicViewsContainer: import("react").LazyExoticComponent<({ session, }: import("./ClassicViewsContainer.tsx").ClassicViewsContainerProps) => import("react").JSX.Element> & {
11
+ preload: () => Promise<{
12
+ default: ({ session, }: import("./ClassicViewsContainer.tsx").ClassicViewsContainerProps) => import("react").JSX.Element;
13
+ }>;
14
+ };
15
+ export declare const WorkspaceContainer: import("react").LazyExoticComponent<({ session, }: {
16
+ session: import("@jbrowse/core/util").SessionWithFocusedViewAndDrawerWidgets & import("../../index.ts").WorkspaceLayout;
17
+ }) => import("react").JSX.Element> & {
18
+ preload: () => Promise<{
19
+ default: ({ session, }: {
20
+ session: import("@jbrowse/core/util").SessionWithFocusedViewAndDrawerWidgets & import("../../index.ts").WorkspaceLayout;
21
+ }) => import("react").JSX.Element;
22
+ }>;
23
+ };
24
+ export declare const ViewLauncher: import("react").LazyExoticComponent<({ session, }: {
25
+ session: import("@jbrowse/core/util").SessionWithDrawerWidgets;
26
+ }) => import("react").JSX.Element> & {
27
+ preload: () => Promise<{
28
+ default: ({ session, }: {
29
+ session: import("@jbrowse/core/util").SessionWithDrawerWidgets;
30
+ }) => import("react").JSX.Element;
31
+ }>;
32
+ };
@@ -0,0 +1,8 @@
1
+ import { lazyWithPreload } from '@jbrowse/core/util/lazyWithPreload';
2
+ import { lazyChunk } from './lazyChunk.js';
3
+ export const DrawerWidget = lazyWithPreload(lazyChunk('DrawerWidget', () => import('./DrawerWidget.js')));
4
+ export const ClassicViewsContainer = lazyWithPreload(lazyChunk('ClassicViewsContainer', () => import('./ClassicViewsContainer.js')));
5
+ export const WorkspaceContainer = lazyWithPreload(lazyChunk('WorkspaceContainer', () => import('../../WorkspaceLayout/WorkspaceContainer.js').then(m => ({
6
+ default: m.WorkspaceContainer,
7
+ }))));
8
+ export const ViewLauncher = lazyWithPreload(lazyChunk('ViewLauncher', () => import('./ViewLauncher.js')));
@@ -0,0 +1,14 @@
1
+ import type { AppSession } from './types.ts';
2
+ /**
3
+ * Load the views container and the drawer while the session's own view code
4
+ * downloads, settling when both have. An app that renders after this renders
5
+ * its frame in one pass; otherwise the first render suspends on them and React
6
+ * holds the retry for 300 ms.
7
+ */
8
+ export declare function preloadAppFrame(): Promise<void>;
9
+ /**
10
+ * Request the app frame's lazy parts that `session` will show (its views
11
+ * container, the drawer and the widget open in it) as soon as the session
12
+ * exists. Rendering asks for each only when it reaches it, a round trip apiece.
13
+ */
14
+ export declare function preloadAppShell(session: AppSession): void;
@@ -0,0 +1,41 @@
1
+ import { preloadComponent } from '@jbrowse/core/util/preloadComponent';
2
+ import { getEnv } from '@jbrowse/mobx-state-tree';
3
+ import { isSessionWithWorkspaceLayout } from '../../WorkspaceLayout/model.js';
4
+ import { ClassicViewsContainer, DrawerWidget, WorkspaceContainer, } from './lazyParts.js';
5
+ /**
6
+ * Load the views container and the drawer while the session's own view code
7
+ * downloads, settling when both have. An app that renders after this renders
8
+ * its frame in one pass; otherwise the first render suspends on them and React
9
+ * holds the retry for 300 ms.
10
+ */
11
+ export async function preloadAppFrame() {
12
+ await Promise.allSettled([
13
+ ClassicViewsContainer.preload(),
14
+ DrawerWidget.preload(),
15
+ ]);
16
+ }
17
+ /**
18
+ * Request the app frame's lazy parts that `session` will show (its views
19
+ * container, the drawer and the widget open in it) as soon as the session
20
+ * exists. Rendering asks for each only when it reaches it, a round trip apiece.
21
+ */
22
+ export function preloadAppShell(session) {
23
+ if (session.views.length > 0) {
24
+ preloadComponent(session.effectiveUseWorkspaces && isSessionWithWorkspaceLayout(session)
25
+ ? WorkspaceContainer
26
+ : ClassicViewsContainer);
27
+ }
28
+ const { drawerVisible, visibleWidget } = session;
29
+ if (drawerVisible) {
30
+ preloadComponent(DrawerWidget);
31
+ }
32
+ if (drawerVisible && visibleWidget) {
33
+ const { pluginManager } = getEnv(session);
34
+ try {
35
+ preloadComponent(pluginManager.getWidgetType(visibleWidget.type).ReactComponent);
36
+ }
37
+ catch {
38
+ // an unknown widget type is reported when the drawer renders it
39
+ }
40
+ }
41
+ }
@@ -2,15 +2,20 @@ import type { Menu } from '../../menus.ts';
2
2
  import type { ErrorDialogState, SnackbarMessage } from '@jbrowse/core/ui/SnackbarModel';
3
3
  import type { SessionWithFocusedViewAndDrawerWidgets } from '@jbrowse/core/util';
4
4
  export { type Menu } from '../../menus.ts';
5
- export type WorkspaceSessionType = SessionWithFocusedViewAndDrawerWidgets & {
6
- renameCurrentSession: (arg: string) => void;
7
- snackbarMessages: SnackbarMessage[];
8
- popSnackbarMessage: () => unknown;
9
- orderViews: (ids: string[]) => void;
10
- };
11
- export type AppSession = WorkspaceSessionType & {
5
+ export interface ThemeSwitchSession {
6
+ themeMode: 'light' | 'dark' | 'system';
7
+ effectiveThemeMode: 'light' | 'dark';
8
+ themeIsDark: boolean;
9
+ systemThemeOverride?: 'light' | 'dark';
10
+ setSystemThemeOverride: (mode?: 'light' | 'dark') => void;
11
+ }
12
+ export type WorkspaceSessionType = SessionWithFocusedViewAndDrawerWidgets;
13
+ export type AppSession = WorkspaceSessionType & ThemeSwitchSession & {
12
14
  menus: () => Menu[];
13
15
  errorDialog: ErrorDialogState | undefined;
14
16
  setErrorDialog: (state: ErrorDialogState | undefined) => void;
15
17
  effectiveUseWorkspaces: boolean;
18
+ renameCurrentSession: (arg: string) => void;
19
+ snackbarMessages: SnackbarMessage[];
20
+ popSnackbarMessage: () => unknown;
16
21
  };
@@ -1,7 +1,6 @@
1
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
2
+ * View-level lazy mounting. A view only mounts its (GPU-heavy) body while it is
3
+ * on screen, and collapses to a height-preserving spacer once scrolled away. This bounds the number of simultaneously-live
5
4
  * GPU canvases/contexts — the root cause of the "workspaces freeze" with many
6
5
  * stacked views (one WebGL context per display canvas blows past the browser's
7
6
  * per-page cap; see agent-docs/reference/ARCHITECTURAL_LIMITS.md, "One WebGL2
@@ -10,10 +9,12 @@
10
9
  * `root: null` (viewport) is container-agnostic: it reports on-screen-ness the
11
10
  * same way whether the views scroll inside the classic container or a workspace
12
11
  * 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
12
+ * **a `rootMargin` would have no effect** wherever a container scrolls — an
13
+ * observer clips the target against each scrolling ancestor before
14
+ * intersecting with the root box that the margin expands, and both containers
15
+ * are `overflow-y: auto`. Only an embed whose page scrolls instead would feel
16
+ * one, and it gets the same window. So this is a hard window with no
17
+ * hysteresis: a view is torn down the moment it leaves its
17
18
  * container and rebuilt when it returns, which on a GPU backend costs a fresh
18
19
  * WebGL2 context and a full shader recompile per display.
19
20
  *
@@ -27,7 +28,7 @@
27
28
  * Starts hidden so a cold load with N crammed views doesn't mount them all at
28
29
  * once; the observer's first callback mounts only what's near the viewport.
29
30
  */
30
- export declare function useViewVisibility(rootMargin: string): {
31
+ export declare function useViewVisibility(): {
31
32
  ref: import("react").RefObject<HTMLDivElement | null>;
32
33
  visible: boolean;
33
34
  measuredHeight: number | undefined;
@@ -5,9 +5,8 @@ function intersectionObserverAvailable() {
5
5
  return typeof window !== 'undefined' && 'IntersectionObserver' in window;
6
6
  }
7
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
8
+ * View-level lazy mounting. A view only mounts its (GPU-heavy) body while it is
9
+ * on screen, and collapses to a height-preserving spacer once scrolled away. This bounds the number of simultaneously-live
11
10
  * GPU canvases/contexts — the root cause of the "workspaces freeze" with many
12
11
  * stacked views (one WebGL context per display canvas blows past the browser's
13
12
  * per-page cap; see agent-docs/reference/ARCHITECTURAL_LIMITS.md, "One WebGL2
@@ -16,10 +15,12 @@ function intersectionObserverAvailable() {
16
15
  * `root: null` (viewport) is container-agnostic: it reports on-screen-ness the
17
16
  * same way whether the views scroll inside the classic container or a workspace
18
17
  * 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
18
+ * **a `rootMargin` would have no effect** wherever a container scrolls — an
19
+ * observer clips the target against each scrolling ancestor before
20
+ * intersecting with the root box that the margin expands, and both containers
21
+ * are `overflow-y: auto`. Only an embed whose page scrolls instead would feel
22
+ * one, and it gets the same window. So this is a hard window with no
23
+ * hysteresis: a view is torn down the moment it leaves its
23
24
  * container and rebuilt when it returns, which on a GPU backend costs a fresh
24
25
  * WebGL2 context and a full shader recompile per display.
25
26
  *
@@ -33,7 +34,7 @@ function intersectionObserverAvailable() {
33
34
  * Starts hidden so a cold load with N crammed views doesn't mount them all at
34
35
  * once; the observer's first callback mounts only what's near the viewport.
35
36
  */
36
- export function useViewVisibility(rootMargin) {
37
+ export function useViewVisibility() {
37
38
  const ref = useRef(null);
38
39
  const [state, setState] = useState(() => ({
39
40
  visible: !intersectionObserverAvailable(),
@@ -69,14 +70,14 @@ export function useViewVisibility(rootMargin) {
69
70
  };
70
71
  });
71
72
  }
72
- }, { rootMargin });
73
+ });
73
74
  io.observe(node);
74
75
  return () => {
75
76
  io.disconnect();
76
77
  };
77
78
  }
78
79
  return undefined;
79
- }, [rootMargin]);
80
+ }, []);
80
81
  // `measuredHeight` is undefined until the body has been seen at least once.
81
82
  // The caller supplies the estimate, and supplies it lazily. See ViewContainer
82
83
  return {
@@ -1,5 +1,12 @@
1
- export declare function viewTitle(view: {
1
+ interface TitledView {
2
2
  displayName?: string;
3
3
  assemblyNames?: string[];
4
- minimized: boolean;
5
- }, getDisplayName: (assemblyName: string) => string): string;
4
+ minimized?: boolean;
5
+ }
6
+ /**
7
+ * The view's own name, or its assemblies'. An emptied name counts as unset, so
8
+ * clearing the title in the header puts the derived one back.
9
+ */
10
+ export declare function viewName(view: TitledView, getDisplayName: (assemblyName: string) => string): string;
11
+ export declare function viewTitle(view: TitledView, getDisplayName: (assemblyName: string) => string): string;
12
+ export {};
@@ -1,9 +1,17 @@
1
- export function viewTitle(view, getDisplayName) {
1
+ /**
2
+ * The view's own name, or its assemblies'. An emptied name counts as unset, so
3
+ * clearing the title in the header puts the derived one back.
4
+ */
5
+ export function viewName(view, getDisplayName) {
2
6
  // assemblyNames is [] (not undefined) for a view with no displayed regions
3
7
  // yet, so fall back on emptiness rather than nullishness
4
8
  const assemblyNames = view.assemblyNames ?? [];
5
- return (view.displayName ??
6
- `${assemblyNames.length
7
- ? assemblyNames.map(r => getDisplayName(r)).join(',')
8
- : 'Untitled view'}${view.minimized ? ' (minimized)' : ''}`);
9
+ return (view.displayName ||
10
+ (assemblyNames.length
11
+ ? assemblyNames.map(r => getDisplayName(r)).join(', ')
12
+ : 'Untitled view'));
13
+ }
14
+ export function viewTitle(view, getDisplayName) {
15
+ const name = viewName(view, getDisplayName);
16
+ return view.minimized ? `${name} (minimized)` : name;
9
17
  }
package/esm/ui/index.d.ts CHANGED
@@ -1 +1 @@
1
- export { App, AppReadyMarker, DialogQueue } from './App/index.ts';
1
+ export { App, DialogQueue, preloadAppFrame, preloadAppShell, } from './App/index.ts';
package/esm/ui/index.js CHANGED
@@ -1 +1 @@
1
- export { App, AppReadyMarker, DialogQueue } from './App/index.js';
1
+ export { App, DialogQueue, preloadAppFrame, preloadAppShell, } from './App/index.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jbrowse/app-core",
3
- "version": "5.0.0-beta.1",
3
+ "version": "5.0.0-beta.10",
4
4
  "type": "module",
5
5
  "description": "JBrowse 2 code shared between the 'full featured' apps e.g. jbrowse-web and jbrowse-desktop",
6
6
  "keywords": [
@@ -24,13 +24,13 @@
24
24
  "esm"
25
25
  ],
26
26
  "dependencies": {
27
- "@jbrowse/mobx-state-tree": "^6.5.1",
28
- "@mui/icons-material": "^9.3.1",
29
- "@mui/material": "^9.3.1",
30
- "mobx": "^7.0.3",
27
+ "@jbrowse/mobx-state-tree": "^6.6.1",
28
+ "@mui/icons-material": "^9.4.0",
29
+ "@mui/material": "^9.4.0",
30
+ "mobx": "^7.0.5",
31
31
  "mobx-react": "^10.0.2",
32
- "@jbrowse/product-core": "^5.0.0-beta.1",
33
- "@jbrowse/core": "^5.0.0-beta.1"
32
+ "@jbrowse/product-core": "^5.0.0-beta.10",
33
+ "@jbrowse/core": "^5.0.0-beta.10"
34
34
  },
35
35
  "peerDependencies": {
36
36
  "react": ">=19.0.0",
@@ -1,34 +0,0 @@
1
- import type { AppSession } from './types.ts';
2
- /**
3
- * One element that says whether the whole app has finished.
4
- *
5
- * `[data-app-phase="ready"]` is the entire readiness contract for anything
6
- * driving JBrowse from outside — a screenshot tool, a test, an agent. Wait for
7
- * that selector and you are done; there is no chain to assemble and no
8
- * per-display census to take.
9
- *
10
- * Why it exists when `data-view-phase` and `data-display-phase` already do:
11
- * those are per-element and NEGATIVE. Asking them "is everything finished"
12
- * means asserting that no element carries the loading value, which is equally
13
- * true of an app that has not started — and the gap is real, measured at about
14
- * a second on a two-track session. A positive attribute cannot be satisfied
15
- * early, because something has to render it.
16
- *
17
- * Its own component, and `hidden`, for two reasons: an observer here subscribes
18
- * to every view's phase without re-rendering the app shell around it, and a
19
- * marker that is never drawn cannot be mistaken for UI or shift a layout. It is
20
- * still found by `querySelector`, which is what reads it.
21
- *
22
- * **What it is silent about**: a display in a TERMINAL state. `error` is a
23
- * finished state, so this reads `ready` over a display whose fetch failed —
24
- * correctly, since nothing is still working. A capture wants more than that: it
25
- * wants a picture, and an error banner is not one. `waitForDisplaysDone` after
26
- * this is what draws that line, keying on the `data-display-drawn` the two
27
- * comparative canvases publish from their stricter `settled` gate (see
28
- * `comparativeReadiness` in `@jbrowse/synteny-core`, which holds both answers
29
- * and says why an error separates them). Both harnesses do it in that order.
30
- */
31
- declare const AppReadyMarker: ({ session, }: {
32
- session: AppSession;
33
- }) => import("react").JSX.Element;
34
- export default AppReadyMarker;
@@ -1,56 +0,0 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
2
- import { observer } from 'mobx-react';
3
- function containerLoading(container) {
4
- return (container.tracks ?? []).some(track => (track.displays ?? []).some(d => d.displayPhase === 'loading'));
5
- }
6
- /**
7
- * Is anything in this session still working?
8
- *
9
- * The same two facts the per-element attributes publish — a view still
10
- * resolving its assembly, a display still in its own fetch — reduced to one
11
- * answer for the whole app. Duck-typed rather than imported: every view type
12
- * would otherwise have to be a dependency of the app shell.
13
- */
14
- function anythingLoading(views) {
15
- return views.some(view => view.showLoading === true ||
16
- view.initialized === false ||
17
- containerLoading(view) ||
18
- // A view whose tracks hang off something else — the synteny view's are on
19
- // its levels, one list per band. `view.tracks` is empty there, so without
20
- // this a stack of ribbons still fetching reads as idle.
21
- (view.trackContainers ?? []).some(containerLoading) ||
22
- anythingLoading(view.views ?? []));
23
- }
24
- /**
25
- * One element that says whether the whole app has finished.
26
- *
27
- * `[data-app-phase="ready"]` is the entire readiness contract for anything
28
- * driving JBrowse from outside — a screenshot tool, a test, an agent. Wait for
29
- * that selector and you are done; there is no chain to assemble and no
30
- * per-display census to take.
31
- *
32
- * Why it exists when `data-view-phase` and `data-display-phase` already do:
33
- * those are per-element and NEGATIVE. Asking them "is everything finished"
34
- * means asserting that no element carries the loading value, which is equally
35
- * true of an app that has not started — and the gap is real, measured at about
36
- * a second on a two-track session. A positive attribute cannot be satisfied
37
- * early, because something has to render it.
38
- *
39
- * Its own component, and `hidden`, for two reasons: an observer here subscribes
40
- * to every view's phase without re-rendering the app shell around it, and a
41
- * marker that is never drawn cannot be mistaken for UI or shift a layout. It is
42
- * still found by `querySelector`, which is what reads it.
43
- *
44
- * **What it is silent about**: a display in a TERMINAL state. `error` is a
45
- * finished state, so this reads `ready` over a display whose fetch failed —
46
- * correctly, since nothing is still working. A capture wants more than that: it
47
- * wants a picture, and an error banner is not one. `waitForDisplaysDone` after
48
- * this is what draws that line, keying on the `data-display-drawn` the two
49
- * comparative canvases publish from their stricter `settled` gate (see
50
- * `comparativeReadiness` in `@jbrowse/synteny-core`, which holds both answers
51
- * and says why an error separates them). Both harnesses do it in that order.
52
- */
53
- const AppReadyMarker = observer(function AppReadyMarker({ session, }) {
54
- return (_jsx("span", { hidden: true, "data-testid": "app-ready-marker", "data-app-phase": anythingLoading(session.views) ? 'loading' : 'ready' }));
55
- });
56
- export default AppReadyMarker;
@@ -1 +0,0 @@
1
- export { DrawerControls as default } from '@jbrowse/product-core';
@@ -1 +0,0 @@
1
- export { DrawerControls as default } from '@jbrowse/product-core';
@@ -1 +0,0 @@
1
- export declare function renameIds(obj: Record<string, unknown>): Record<string, unknown>;
@@ -1,29 +0,0 @@
1
- import { createElementId } from '@jbrowse/core/util/types/mst';
2
- export function renameIds(obj) {
3
- const idMap = new Map();
4
- function transformIds(value) {
5
- if (value === null || value === undefined) {
6
- return value;
7
- }
8
- if (Array.isArray(value)) {
9
- return value.map(transformIds);
10
- }
11
- if (typeof value === 'object') {
12
- const result = {};
13
- for (const [key, val] of Object.entries(value)) {
14
- if (key === 'id' && typeof val === 'string') {
15
- if (!idMap.has(val)) {
16
- idMap.set(val, createElementId());
17
- }
18
- result[key] = idMap.get(val);
19
- }
20
- else {
21
- result[key] = transformIds(val);
22
- }
23
- }
24
- return result;
25
- }
26
- return value;
27
- }
28
- return transformIds(obj);
29
- }