@jbrowse/display-ui 5.0.0-beta.11 → 5.0.0-beta.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/README.md +70 -76
  2. package/esm/AxisCaption.d.ts +2 -2
  3. package/esm/AxisCaption.js +6 -4
  4. package/esm/AxisGutter.d.ts +2 -2
  5. package/esm/AxisGutter.js +5 -4
  6. package/esm/CrossHatches.d.ts +3 -2
  7. package/esm/CrossHatches.js +5 -2
  8. package/esm/DisplayUIProvider.d.ts +3 -2
  9. package/esm/DisplayUIProvider.js +4 -2
  10. package/esm/FloatingLegend.js +7 -3
  11. package/esm/FloatingText.d.ts +9 -8
  12. package/esm/FloatingText.js +9 -7
  13. package/esm/ScoreDomainCaption.d.ts +2 -2
  14. package/esm/ScoreDomainCaption.js +3 -2
  15. package/esm/ScoreRules.d.ts +3 -2
  16. package/esm/ScoreRules.js +5 -2
  17. package/esm/YScaleBar.js +1 -8
  18. package/esm/YScaleBarOverlay.d.ts +3 -2
  19. package/esm/YScaleBarOverlay.js +4 -2
  20. package/esm/axisPlacement.d.ts +16 -9
  21. package/esm/axisPlacement.js +22 -14
  22. package/esm/bottomRightCorner.d.ts +19 -47
  23. package/esm/bottomRightCorner.js +19 -47
  24. package/esm/chromeOverlayContext.js +10 -25
  25. package/esm/chromeOverlays.d.ts +73 -67
  26. package/esm/embed/engineBoundary.d.ts +34 -0
  27. package/esm/embed/engineBoundary.js +29 -0
  28. package/esm/embed/index.d.ts +6 -0
  29. package/esm/embed/index.js +18 -3
  30. package/esm/embed/legend.js +2 -2
  31. package/esm/embed/location.d.ts +2 -1
  32. package/esm/embed/location.js +18 -17
  33. package/esm/embed/notifications.d.ts +29 -0
  34. package/esm/embed/notifications.js +38 -0
  35. package/esm/embed/provider.d.ts +5 -3
  36. package/esm/embed/provider.js +6 -4
  37. package/esm/embed/regions.d.ts +3 -2
  38. package/esm/embed/regions.js +9 -7
  39. package/esm/embed/searchPicker.d.ts +24 -0
  40. package/esm/embed/searchPicker.js +14 -0
  41. package/esm/embed/toolbar.d.ts +16 -0
  42. package/esm/embed/toolbar.js +22 -0
  43. package/esm/index.d.ts +1 -1
  44. package/esm/isLiveModel.d.ts +10 -15
  45. package/esm/isLiveModel.js +10 -15
  46. package/esm/plainChromeOverlays.d.ts +2 -2
  47. package/esm/plainChromeOverlays.js +17 -39
  48. package/esm/tooltip/Tooltip.d.ts +3 -3
  49. package/esm/tooltip/Tooltip.js +4 -2
  50. package/esm/trackControl/plainTrackControl.d.ts +2 -1
  51. package/esm/trackControl/plainTrackControl.js +8 -6
  52. package/esm/trackOverlay/TrackOverlaySlot.d.ts +3 -3
  53. package/esm/trackOverlay/TrackOverlaySlot.js +4 -3
  54. package/esm/valueScale.d.ts +5 -5
  55. package/package.json +4 -4
@@ -2,10 +2,15 @@ import type { PanZoomView } from '@jbrowse/core/util/usePanZoom';
2
2
  import type { ViewStatus as ViewStatusValue } from '@jbrowse/core/util/viewStatus';
3
3
  import type { IStateTreeNode } from '@jbrowse/mobx-state-tree';
4
4
  import type React from 'react';
5
+ export { EngineBoundary } from './engineBoundary.tsx';
6
+ export type { EngineFallbackProps } from './engineBoundary.tsx';
5
7
  export { Legend } from './legend.tsx';
6
8
  export { LocationBox, useLocationBox } from './location.tsx';
9
+ export { Notifications } from './notifications.tsx';
7
10
  export { EmbedProvider } from './provider.tsx';
8
11
  export { Highlights, RegionSeams, Scalebar } from './regions.tsx';
12
+ export { SearchPicker } from './searchPicker.tsx';
13
+ export { NavButton, Toolbar } from './toolbar.tsx';
9
14
  export interface EmbedDisplay {
10
15
  height: number;
11
16
  RenderingComponent: React.ComponentType<{
@@ -30,6 +35,7 @@ export declare const Track: ({ view, trackId, }: {
30
35
  trackId: string;
31
36
  }) => React.JSX.Element | null;
32
37
  export interface ResizableDisplay {
38
+ height: number;
33
39
  setResizing: (resizing: boolean) => void;
34
40
  resizeHeight: (distance: number) => unknown;
35
41
  }
@@ -7,10 +7,14 @@ import { usePanZoom } from '@jbrowse/core/util/usePanZoom';
7
7
  import { useResizeDrag } from '@jbrowse/core/util/useResizeDrag';
8
8
  import { observer } from 'mobx-react';
9
9
  import { TrackOverlaySlot } from '../trackOverlay/TrackOverlaySlot.js';
10
+ export { EngineBoundary } from './engineBoundary.js';
10
11
  export { Legend } from './legend.js';
11
12
  export { LocationBox, useLocationBox } from './location.js';
13
+ export { Notifications } from './notifications.js';
12
14
  export { EmbedProvider } from './provider.js';
13
15
  export { Highlights, RegionSeams, Scalebar } from './regions.js';
16
+ export { SearchPicker } from './searchPicker.js';
17
+ export { NavButton, Toolbar } from './toolbar.js';
14
18
  export const Track = observer(function Track({ view, trackId, }) {
15
19
  const display = view.getTrack(trackId)?.activeDisplay;
16
20
  if (!display) {
@@ -19,6 +23,7 @@ export const Track = observer(function Track({ view, trackId, }) {
19
23
  const { RenderingComponent } = display;
20
24
  return (_jsx(TrackOverlaySlot, { zIndex: 3, style: { height: display.height }, children: _jsx("div", { style: { position: 'absolute', inset: 0, contain: 'strict' }, children: _jsx(Suspense, { fallback: null, children: _jsx(RenderingComponent, { model: display, onHorizontalScroll: view.horizontalScroll }) }) }) }));
21
25
  });
26
+ const KEYBOARD_RESIZE_STEP = 10;
22
27
  export const ResizeHandle = observer(function ResizeHandle({ view, trackId, style, }) {
23
28
  const display = view.getTrack(trackId)?.activeDisplay;
24
29
  const props = useResizeDrag({
@@ -32,7 +37,17 @@ export const ResizeHandle = observer(function ResizeHandle({ view, trackId, styl
32
37
  display?.setResizing(false);
33
38
  },
34
39
  });
35
- return display ? (_jsx("div", { ...props, "data-gesture-owner": "true", "aria-label": `Resize ${trackId}`, style: {
40
+ return display ? (_jsx("div", { ...props, "data-gesture-owner": "true", role: "separator", tabIndex: 0, "aria-orientation": "horizontal", "aria-label": `Resize ${trackId}`, "aria-valuenow": Math.round(display.height), onKeyDown: event => {
41
+ const step = event.key === 'ArrowDown'
42
+ ? KEYBOARD_RESIZE_STEP
43
+ : event.key === 'ArrowUp'
44
+ ? -KEYBOARD_RESIZE_STEP
45
+ : 0;
46
+ if (step) {
47
+ event.preventDefault();
48
+ display.resizeHeight(step);
49
+ }
50
+ }, style: {
36
51
  height: 4,
37
52
  cursor: 'row-resize',
38
53
  touchAction: 'none',
@@ -45,11 +60,11 @@ export const TrackToggle = observer(function TrackToggle({ view, trackId, style,
45
60
  void view.launchToggleTrack(trackId);
46
61
  } }), children] }));
47
62
  });
48
- function ViewLoading({ message, progress, source }) {
63
+ const ViewLoading = observer(function ViewLoading({ message, progress, source, }) {
49
64
  const slow = useSlowLoad();
50
65
  const stalled = useStalled(`${message}|${progress}|${source}`);
51
66
  return (_jsxs(_Fragment, { children: [slow ? message : 'Loading', !slow || progress === undefined ? null : (_jsx("progress", { value: progress, max: 1, style: { display: 'block', width: 300, maxWidth: '100%' } })), stalled && source ? (_jsxs("div", { style: { overflowWrap: 'anywhere' }, children: ["still waiting on ", source] })) : null] }));
52
- }
67
+ });
53
68
  export const ViewStatus = observer(function ViewStatus({ view, style, }) {
54
69
  const { status } = view;
55
70
  return status.type === 'ready' ? null : (_jsx("div", { role: status.type === 'error' ? 'alert' : 'status', style: {
@@ -8,7 +8,7 @@ function gradientCss({ stops }) {
8
8
  .map(({ color, opacity = 1, offset }) => `color-mix(in srgb, ${color} ${Math.round(opacity * 100)}%, transparent) ${offset * 100}%`)
9
9
  .join(', ')})`;
10
10
  }
11
- function Row({ item }) {
11
+ const Row = observer(function Row({ item }) {
12
12
  const { gradient, label, hidden } = item;
13
13
  return gradient ? (_jsxs("span", { style: { display: 'inline-flex', alignItems: 'center', gap: 4 }, children: [label, _jsx("span", { children: gradient.minLabel }), _jsx("span", { style: {
14
14
  width: 80,
@@ -21,7 +21,7 @@ function Row({ item }) {
21
21
  opacity: hidden ? 0.5 : undefined,
22
22
  textDecoration: hidden ? 'line-through' : undefined,
23
23
  }, children: [legendSwatches(item).map(swatch => (_jsx("svg", { "aria-hidden": true, width: SWATCH, height: SWATCH, children: _jsx(LegendSwatchGlyph, { swatch: swatch, size: SWATCH }) }, swatch.color))), label] }));
24
- }
24
+ });
25
25
  function legendSpecIn(display) {
26
26
  return display && typeof display === 'object' && 'legendSpec' in display
27
27
  ? display.legendSpec
@@ -1,3 +1,4 @@
1
+ import type { SearchPickerView } from './searchPicker.tsx';
1
2
  import type React from 'react';
2
3
  export interface LocationView {
3
4
  coarseVisibleLocStrings: string;
@@ -12,6 +13,6 @@ export declare function useLocationBox(view: LocationView): {
12
13
  go(input?: string): void;
13
14
  };
14
15
  export declare const LocationBox: ({ view, style, }: {
15
- view: LocationView;
16
+ view: LocationView & SearchPickerView;
16
17
  style?: React.CSSProperties;
17
18
  }) => React.JSX.Element;
@@ -1,6 +1,7 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useRef, useState } from 'react';
3
3
  import { observer } from 'mobx-react';
4
+ import { SearchPicker } from './searchPicker.js';
4
5
  export function useLocationBox(view) {
5
6
  const [draft, setDraft] = useState();
6
7
  const [pending, setPending] = useState(false);
@@ -46,20 +47,20 @@ export function useLocationBox(view) {
46
47
  }
47
48
  export const LocationBox = observer(function LocationBox({ view, style, }) {
48
49
  const box = useLocationBox(view);
49
- return (_jsxs("form", { style: {
50
- display: 'flex',
51
- flexWrap: 'wrap',
52
- alignItems: 'center',
53
- gap: 4,
54
- ...style,
55
- }, onSubmit: event => {
56
- event.preventDefault();
57
- box.go();
58
- }, children: [_jsx("input", { "aria-label": "Location", value: box.value, size: 38, style: { font: 'inherit', padding: '2px 4px' }, onChange: event => {
59
- box.edit(event.target.value);
60
- }, onKeyDown: event => {
61
- if (event.key === 'Escape') {
62
- box.reset();
63
- }
64
- } }), _jsx("button", { type: "submit", disabled: box.pending, style: { font: 'inherit' }, children: "Go" }), box.error ? (_jsx("span", { role: "alert", children: box.error instanceof Error ? box.error.message : String(box.error) })) : null] }));
50
+ return (_jsxs(_Fragment, { children: [_jsxs("form", { style: {
51
+ display: 'flex',
52
+ flexWrap: 'wrap',
53
+ alignItems: 'center',
54
+ gap: 4,
55
+ ...style,
56
+ }, onSubmit: event => {
57
+ event.preventDefault();
58
+ box.go();
59
+ }, children: [_jsx("input", { "aria-label": "Location", value: box.value, size: 38, style: { font: 'inherit', padding: '2px 4px' }, onChange: event => {
60
+ box.edit(event.target.value);
61
+ }, onKeyDown: event => {
62
+ if (event.key === 'Escape') {
63
+ box.reset();
64
+ }
65
+ } }), _jsx("button", { type: "submit", disabled: box.pending, style: { font: 'inherit' }, children: "Go" }), box.error ? (_jsx("span", { role: "alert", children: box.error instanceof Error ? box.error.message : String(box.error) })) : null] }), _jsx(SearchPicker, { view: view })] }));
65
66
  });
@@ -0,0 +1,29 @@
1
+ import type React from 'react';
2
+ export interface NotificationsSession {
3
+ snackbarMessages: readonly {
4
+ message: string;
5
+ level?: string;
6
+ actions?: readonly {
7
+ name: React.ReactNode;
8
+ onClick: () => void;
9
+ }[];
10
+ }[];
11
+ popSnackbarMessage: () => unknown;
12
+ queueOfDialogs: readonly unknown[];
13
+ removeActiveDialog: () => void;
14
+ }
15
+ /**
16
+ * Draws the two session channels a host with its own UI otherwise drops: the
17
+ * latest of `session.snackbarMessages`, where a failed navigation, track launch
18
+ * or config lands, and a line for each dialog JBrowse queued, which only a
19
+ * Material host can open. A name search's picker is not one: it is
20
+ * `view.searchPicker`, drawn by `SearchPicker`.
21
+ *
22
+ * `EmbedProvider` mounts one below its children. Pass it
23
+ * `notifications={false}` to place this elsewhere or to draw the two queues
24
+ * yourself.
25
+ */
26
+ export declare const Notifications: ({ session, style, }: {
27
+ session: NotificationsSession;
28
+ style?: React.CSSProperties;
29
+ }) => React.JSX.Element;
@@ -0,0 +1,38 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { usePalette } from '@jbrowse/core/ui/PaletteContext';
3
+ import { observer } from 'mobx-react';
4
+ /**
5
+ * Draws the two session channels a host with its own UI otherwise drops: the
6
+ * latest of `session.snackbarMessages`, where a failed navigation, track launch
7
+ * or config lands, and a line for each dialog JBrowse queued, which only a
8
+ * Material host can open. A name search's picker is not one: it is
9
+ * `view.searchPicker`, drawn by `SearchPicker`.
10
+ *
11
+ * `EmbedProvider` mounts one below its children. Pass it
12
+ * `notifications={false}` to place this elsewhere or to draw the two queues
13
+ * yourself.
14
+ */
15
+ export const Notifications = observer(function Notifications({ session, style, }) {
16
+ const palette = usePalette();
17
+ const latest = session.snackbarMessages.at(-1);
18
+ const queued = session.queueOfDialogs.length;
19
+ const row = {
20
+ display: 'flex',
21
+ alignItems: 'center',
22
+ gap: 8,
23
+ marginTop: 8,
24
+ padding: '4px 8px',
25
+ fontSize: '0.8rem',
26
+ colorScheme: palette.mode,
27
+ color: palette.text.primary,
28
+ background: `color-mix(in srgb, ${palette.text.primary} 8%, ${palette.background.paper})`,
29
+ };
30
+ return (_jsxs(_Fragment, { children: [latest ? (_jsxs("div", { role: latest.level === 'error' ? 'alert' : 'status', "data-testid": "embed-notification", style: { ...row, ...style }, children: [_jsx("span", { style: { flex: 1, overflowWrap: 'anywhere' }, children: latest.message }), latest.actions
31
+ // 'report' opens the stack-trace dialog, which is Material
32
+ ?.filter(action => action.name !== 'report')
33
+ .map((action, i) => (_jsx("button", { type: "button", onClick: action.onClick, children: action.name }, i))), _jsx("button", { type: "button", "aria-label": "Dismiss notification", onClick: () => {
34
+ session.popSnackbarMessage();
35
+ }, children: "\u2715" })] })) : null, queued ? (_jsxs("div", { role: "status", "data-testid": "queued-dialog-notice", style: { ...row, ...style }, children: [_jsxs("span", { style: { flex: 1 }, children: ["JBrowse queued ", queued, " dialog", queued > 1 ? 's' : '', ", and this page draws none."] }), _jsx("button", { type: "button", onClick: () => {
36
+ session.removeActiveDialog();
37
+ }, children: "Dismiss" })] })) : null] }));
38
+ });
@@ -1,11 +1,13 @@
1
1
  import type { DisplayChromeOverlays } from '../chromeOverlays.ts';
2
2
  import type { TrackControlComponent } from '../trackControl/types.ts';
3
+ import type { NotificationsSession } from './notifications.tsx';
3
4
  import type { ThemeModeSession } from '@jbrowse/core/ui/PaletteContext';
4
5
  import type React from 'react';
5
- export declare function EmbedProvider({ session, mode, overlays, trackControl, children, }: {
6
- session: ThemeModeSession;
6
+ export declare const EmbedProvider: ({ session, mode, overlays, trackControl, notifications, children, }: {
7
+ session: ThemeModeSession & NotificationsSession;
7
8
  mode?: 'light' | 'dark';
8
9
  overlays?: Partial<DisplayChromeOverlays>;
9
10
  trackControl?: TrackControlComponent;
11
+ notifications?: boolean;
10
12
  children: React.ReactNode;
11
- }): React.JSX.Element;
13
+ }) => React.JSX.Element;
@@ -1,6 +1,8 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { SessionPaletteProvider } from '@jbrowse/core/ui/PaletteContext';
3
+ import { observer } from 'mobx-react';
3
4
  import DisplayUIProvider from '../DisplayUIProvider.js';
4
- export function EmbedProvider({ session, mode, overlays, trackControl, children, }) {
5
- return (_jsx(SessionPaletteProvider, { session: session, mode: mode, children: _jsx(DisplayUIProvider, { overlays: overlays, trackControl: trackControl, children: children }) }));
6
- }
5
+ import { Notifications } from './notifications.js';
6
+ export const EmbedProvider = observer(function EmbedProvider({ session, mode, overlays, trackControl, notifications = true, children, }) {
7
+ return (_jsx(SessionPaletteProvider, { session: session, mode: mode, children: _jsxs(DisplayUIProvider, { overlays: overlays, trackControl: trackControl, children: [children, notifications ? _jsx(Notifications, { session: session }) : null] }) }));
8
+ });
@@ -1,3 +1,4 @@
1
+ import type { IStateTreeNode } from '@jbrowse/mobx-state-tree';
1
2
  import type React from 'react';
2
3
  export interface RegionSeamsView {
3
4
  staticBlocksTranslateX: number;
@@ -28,7 +29,7 @@ export interface ScalebarView {
28
29
  caption?: string;
29
30
  };
30
31
  }
31
- export interface HighlightsView {
32
+ export interface HighlightsView extends IStateTreeNode {
32
33
  highlights: readonly {
33
34
  assemblyName?: string;
34
35
  refName: string;
@@ -47,7 +48,7 @@ export declare const RegionSeams: ({ view, }: {
47
48
  }) => React.JSX.Element;
48
49
  export declare const Highlights: ({ view, }: {
49
50
  view: HighlightsView;
50
- }) => (React.JSX.Element | null)[];
51
+ }) => (React.JSX.Element | null)[] | null;
51
52
  export declare const Scalebar: ({ view, style, ...props }: {
52
53
  view: ScalebarView;
53
54
  } & React.ComponentProps<'div'>) => React.JSX.Element;
@@ -1,13 +1,11 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { usePalette } from '@jbrowse/core/ui/PaletteContext';
3
3
  import { highlightKey } from '@jbrowse/core/util/highlights';
4
+ import { getSession } from '@jbrowse/core/util/mstUtils';
4
5
  import { observer } from 'mobx-react';
5
- const spanFill = {
6
- seam: 'color-mix(in srgb, CanvasText 45%, Canvas)',
7
- boundary: 'color-mix(in srgb, CanvasText 12%, Canvas)',
8
- elided: 'color-mix(in srgb, CanvasText 30%, Canvas)',
9
- };
6
+ const spanInk = { seam: 45, boundary: 12, elided: 30 };
10
7
  export const RegionSeams = observer(function RegionSeams({ view, }) {
8
+ const { text, background } = usePalette();
11
9
  return (_jsx("div", { "aria-hidden": true, "data-region-seams": true, style: {
12
10
  position: 'absolute',
13
11
  top: 0,
@@ -22,11 +20,15 @@ export const RegionSeams = observer(function RegionSeams({ view, }) {
22
20
  bottom: 0,
23
21
  left: x,
24
22
  width,
25
- background: spanFill[kind],
23
+ background: `color-mix(in srgb, ${text.primary} ${spanInk[kind]}%, ${background.default})`,
26
24
  } }, key))) }));
27
25
  });
28
26
  export const Highlights = observer(function Highlights({ view, }) {
29
27
  const palette = usePalette();
28
+ const { highlightsVisible, highlightLabelsVisible } = getSession(view);
29
+ if (!highlightsVisible) {
30
+ return null;
31
+ }
30
32
  return view.highlights.map((highlight, i) => {
31
33
  const coords = view.getHighlightCoords(highlight);
32
34
  return coords ? (_jsx("div", { "data-testid": "highlight-band", style: {
@@ -40,7 +42,7 @@ export const Highlights = observer(function Highlights({ view, }) {
40
42
  pointerEvents: 'none',
41
43
  background: highlight.color ??
42
44
  `color-mix(in srgb, ${palette.highlight.main} 20%, transparent)`,
43
- }, children: highlight.label ? (_jsx("span", { style: {
45
+ }, children: highlight.label && highlightLabelsVisible ? (_jsx("span", { style: {
44
46
  position: 'absolute',
45
47
  top: 0,
46
48
  left: 3,
@@ -0,0 +1,24 @@
1
+ import type React from 'react';
2
+ export interface SearchPickerView {
3
+ searchPicker?: {
4
+ query: string;
5
+ rows: readonly {
6
+ id: string;
7
+ label: string;
8
+ location: string;
9
+ trackName: string;
10
+ }[];
11
+ pick: (id: string) => Promise<unknown>;
12
+ };
13
+ closeSearchPicker: () => void;
14
+ }
15
+ /**
16
+ * Lists the places a name search points at when it matches several and none
17
+ * exactly, which `view.navToLocString` leaves to the host instead of
18
+ * navigating. Draws nothing until a search sets `view.searchPicker`. A
19
+ * `LocationBox` mounts one under itself.
20
+ */
21
+ export declare const SearchPicker: ({ view, style, }: {
22
+ view: SearchPickerView;
23
+ style?: React.CSSProperties;
24
+ }) => React.JSX.Element | null;
@@ -0,0 +1,14 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { observer } from 'mobx-react';
3
+ /**
4
+ * Lists the places a name search points at when it matches several and none
5
+ * exactly, which `view.navToLocString` leaves to the host instead of
6
+ * navigating. Draws nothing until a search sets `view.searchPicker`. A
7
+ * `LocationBox` mounts one under itself.
8
+ */
9
+ export const SearchPicker = observer(function SearchPicker({ view, style, }) {
10
+ const { searchPicker } = view;
11
+ return searchPicker ? (_jsxs("div", { role: "dialog", "aria-label": "Search results", "data-testid": "search-picker", style: { fontSize: '0.8rem', paddingBottom: 8, ...style }, children: [_jsxs("div", { style: { display: 'flex', gap: 8, alignItems: 'center' }, children: [_jsxs("strong", { style: { flex: 1 }, children: ["\u201C", searchPicker.query, "\u201D matches ", searchPicker.rows.length, " places"] }), _jsx("button", { type: "button", "aria-label": "Close search results", onClick: view.closeSearchPicker, children: "\u2715" })] }), _jsx("ul", { style: { listStyle: 'none', margin: 0, padding: 0 }, children: searchPicker.rows.map(row => (_jsx("li", { children: _jsxs("button", { type: "button", style: { width: '100%', textAlign: 'left' }, onClick: () => {
12
+ void searchPicker.pick(row.id);
13
+ }, children: [_jsx("strong", { children: row.label }), " ", row.location, " ", row.trackName] }) }, row.id))) })] })) : null;
14
+ });
@@ -0,0 +1,16 @@
1
+ import type { IStateTreeNode } from '@jbrowse/mobx-state-tree';
2
+ import type React from 'react';
3
+ export declare const Toolbar: ({ style, children, }: {
4
+ style?: React.CSSProperties;
5
+ children?: React.ReactNode;
6
+ }) => React.JSX.Element;
7
+ export interface NavView extends IStateTreeNode {
8
+ navToLocString: (input: string) => Promise<unknown>;
9
+ }
10
+ export declare const NavButton: ({ view, loc, onClick, style, children, }: {
11
+ view: NavView;
12
+ loc: string;
13
+ onClick?: () => void;
14
+ style?: React.CSSProperties;
15
+ children?: React.ReactNode;
16
+ }) => React.JSX.Element;
@@ -0,0 +1,22 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { getSession } from '@jbrowse/core/util/mstUtils';
3
+ import { observer } from 'mobx-react';
4
+ export const Toolbar = observer(function Toolbar({ style, children, }) {
5
+ return (_jsx("div", { style: {
6
+ display: 'flex',
7
+ flexWrap: 'wrap',
8
+ alignItems: 'center',
9
+ gap: 12,
10
+ paddingBottom: 8,
11
+ fontSize: '0.85rem',
12
+ ...style,
13
+ }, children: children }));
14
+ });
15
+ export const NavButton = observer(function NavButton({ view, loc, onClick, style, children, }) {
16
+ return (_jsx("button", { type: "button", style: style, onClick: () => {
17
+ onClick?.();
18
+ view.navToLocString(loc).catch((e) => {
19
+ getSession(view).notifyError(`${e}`, e);
20
+ });
21
+ }, children: children ?? loc }));
22
+ });
package/esm/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  export { default as DisplayUIProvider, resolveOverlays, } from './DisplayUIProvider.tsx';
2
2
  export { DisplayChromeOverlayProvider, useChromeOverlayOverride, } from './chromeOverlayContext.ts';
3
3
  export { default as plainChromeOverlays } from './plainChromeOverlays.tsx';
4
- export type { DisplayBackgroundProgressModel, DisplayChromeOverlays, DisplayErrorBarModel, DisplayLoadingOverlayModel, TooLargeMessageModel, } from './chromeOverlays.ts';
4
+ export type { BackgroundProgressProps, DisplayChromeOverlays, ErrorBarProps, LoadingProps, RenderErrorProps, TooLargeProps, } from './chromeOverlays.ts';
5
5
  export { TrackControlProvider, useTrackControlOverride, } from './trackControl/trackControlContext.ts';
6
6
  export { default as plainTrackControl } from './trackControl/plainTrackControl.tsx';
7
7
  export { useTrackControlMenu } from './trackControl/useTrackControlMenu.tsx';
@@ -1,21 +1,16 @@
1
1
  /**
2
2
  * #api
3
- * Whether an overlay's model can still be called into.
3
+ * Whether a display model can still be called into.
4
4
  *
5
- * The terminal states unmount the canvas, so a click landing after the track was
6
- * closed would otherwise run an action on a destroyed node — which is why both
7
- * overlay sets guard their one button with `isAlive`.
5
+ * The chrome's overlay slots guard every button with it: the terminal states
6
+ * unmount the canvas, so a click landing after the track was closed would
7
+ * otherwise run an action on a destroyed node.
8
8
  *
9
- * **`isAlive` alone is wrong here, and it throws rather than answering.** The
10
- * four model shapes in `chromeOverlays.ts` are structural on purpose ("A display
11
- * satisfies one by having the fields; no mixin has to be composed"), so a host
12
- * writing their own display over `DisplayChromeBase` may hand these components a
13
- * plain object. `isAlive` runs `assertIsStateTreeNode` and throws on one, inside
14
- * an event handler, where React logs it and moves on — leaving the Force load
15
- * button looking live and doing nothing, which is the exact state
16
- * `DisplayChromeOverlays.TooLarge` documents itself as existing to prevent.
17
- *
18
- * A plain object is never destroyed, so it is always callable. The liveness
19
- * question only exists for an MST node.
9
+ * **`isAlive` alone is wrong here, and it throws rather than answering.** A
10
+ * host writing their own display over `DisplayChromeBase` may hand the chrome a
11
+ * plain object, and `isAlive` runs `assertIsStateTreeNode`, which throws inside
12
+ * the click handler, where React logs it and moves on: a Force load button that
13
+ * looks live and does nothing. A plain object is never destroyed, so it is
14
+ * always callable.
20
15
  */
21
16
  export declare function isLiveModel(model: unknown): boolean;
@@ -1,23 +1,18 @@
1
1
  import { isAlive, isStateTreeNode } from '@jbrowse/mobx-state-tree';
2
2
  /**
3
3
  * #api
4
- * Whether an overlay's model can still be called into.
4
+ * Whether a display model can still be called into.
5
5
  *
6
- * The terminal states unmount the canvas, so a click landing after the track was
7
- * closed would otherwise run an action on a destroyed node — which is why both
8
- * overlay sets guard their one button with `isAlive`.
6
+ * The chrome's overlay slots guard every button with it: the terminal states
7
+ * unmount the canvas, so a click landing after the track was closed would
8
+ * otherwise run an action on a destroyed node.
9
9
  *
10
- * **`isAlive` alone is wrong here, and it throws rather than answering.** The
11
- * four model shapes in `chromeOverlays.ts` are structural on purpose ("A display
12
- * satisfies one by having the fields; no mixin has to be composed"), so a host
13
- * writing their own display over `DisplayChromeBase` may hand these components a
14
- * plain object. `isAlive` runs `assertIsStateTreeNode` and throws on one, inside
15
- * an event handler, where React logs it and moves on — leaving the Force load
16
- * button looking live and doing nothing, which is the exact state
17
- * `DisplayChromeOverlays.TooLarge` documents itself as existing to prevent.
18
- *
19
- * A plain object is never destroyed, so it is always callable. The liveness
20
- * question only exists for an MST node.
10
+ * **`isAlive` alone is wrong here, and it throws rather than answering.** A
11
+ * host writing their own display over `DisplayChromeBase` may hand the chrome a
12
+ * plain object, and `isAlive` runs `assertIsStateTreeNode`, which throws inside
13
+ * the click handler, where React logs it and moves on: a Force load button that
14
+ * looks live and does nothing. A plain object is never destroyed, so it is
15
+ * always callable.
21
16
  */
22
17
  export function isLiveModel(model) {
23
18
  return !isStateTreeNode(model) || isAlive(model);
@@ -7,8 +7,8 @@ import type { DisplayChromeOverlays } from './chromeOverlays.ts';
7
7
  *
8
8
  * `DisplayUIProvider` installs this by default, so mount that rather than
9
9
  * naming this — reach for it directly only to wrap a state or to build a
10
- * context value by hand. Colours come from `currentColor` and the CSS system
11
- * colours, so the host's own cascade drives them in both light and dark.
10
+ * context value by hand. Colors come from `currentColor` and the CSS system
11
+ * colors, so the host's own cascade drives them in both light and dark.
12
12
  *
13
13
  * The `data-testid` values it renders are a contract four of JBrowse's test
14
14
  * systems key on, so a replacement set that keeps them can be driven by those
@@ -4,8 +4,6 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
4
4
  import { GPU_FALLBACK_LABEL, GPU_FALLBACK_TOOLTIP, disableGpuRendering, shouldOfferGpuFallback, } from '@jbrowse/core/ui/gpuFallback';
5
5
  import { progressLabel } from '@jbrowse/core/util/progress';
6
6
  import { observer } from 'mobx-react';
7
- import { isLiveModel } from './isLiveModel.js';
8
- import { tooLargeBannerText } from './tooLargeBannerText.js';
9
7
  import Tooltip from './tooltip/Tooltip.js';
10
8
  // A dependency-free `DisplayChromeOverlays` for embedders who don't want MUI:
11
9
  // no theme provider to mount, no emotion injected into the host page, and
@@ -37,9 +35,9 @@ const overlayBox = {
37
35
  pointerEvents: 'none',
38
36
  zIndex: 1,
39
37
  };
40
- // Nearly opaque on purpose. These sit over a canvas whose colours are not known
38
+ // Nearly opaque on purpose. These sit over a canvas whose colors are not known
41
39
  // here, and a translucent chip over dense data was measurably hard to read.
42
- // `Canvas`/`CanvasText` are the CSS system colours, so this tracks the host's
40
+ // `Canvas`/`CanvasText` are the CSS system colors, so this tracks the host's
43
41
  // light/dark mode without needing a theme object.
44
42
  const chip = {
45
43
  pointerEvents: 'auto',
@@ -98,7 +96,7 @@ function errorText(error) {
98
96
  // toolkit-free, but it paints the palette's `accent`; every rule in this
99
97
  // file reads the host's own cascade instead, so the bar is drawn here rather
100
98
  // than imported.
101
- function ProgressBar({ fraction }) {
99
+ const ProgressBar = observer(function ProgressBar({ fraction, }) {
102
100
  const filled = Math.min(1, Math.max(0, fraction));
103
101
  return (_jsx("div", { role: "progressbar", "aria-valuemin": 0, "aria-valuemax": 100, "aria-valuenow": Math.round(filled * 100), style: {
104
102
  overflow: 'hidden',
@@ -116,53 +114,33 @@ function ProgressBar({ fraction }) {
116
114
  transform: `scaleX(${filled})`,
117
115
  transition: 'transform 200ms linear',
118
116
  } }) }));
119
- }
117
+ });
120
118
  const PlainRenderError = observer(function PlainRenderError({ error, onRetry, height, }) {
121
119
  return (_jsxs("div", { style: banner(height), role: "alert", children: [_jsx("span", { style: { wordBreak: 'break-word' }, children: errorText(error) }), _jsx("button", { type: "button", style: button, "data-testid": "reload_button", onClick: onRetry, children: "Retry" }), shouldOfferGpuFallback(error) ? (_jsx(Tooltip, { title: GPU_FALLBACK_TOOLTIP, children: _jsx("button", { type: "button", style: button, "data-testid": "use_canvas2d_button", onClick: () => {
122
120
  disableGpuRendering();
123
121
  onRetry();
124
122
  }, children: GPU_FALLBACK_LABEL }) })) : null] }));
125
123
  });
126
- const PlainTooLarge = observer(function PlainTooLarge({ model, }) {
127
- return (_jsxs("div", { style: banner(), role: "status", children: [_jsx("span", { style: { wordBreak: 'break-word' }, children: tooLargeBannerText(model.regionTooLargeReason, {
128
- zoomCanRelease: model.zoomCanReleaseGate,
129
- }) }), _jsx("button", { type: "button", style: button, onClick: () => {
130
- // the banner unmounts the canvas, so a click landing after the track
131
- // was closed would otherwise call an action on a dead node. Not bare
132
- // `isAlive` — this model may be the plain object the contract invites,
133
- // which that throws on. See isLiveModel.ts.
134
- if (isLiveModel(model)) {
135
- model.forceLoad();
136
- }
137
- }, children: "Force load" })] }));
124
+ const PlainTooLarge = observer(function PlainTooLarge({ message, onForceLoad, }) {
125
+ return (_jsxs("div", { style: banner(), role: "status", children: [_jsx("span", { style: { wordBreak: 'break-word' }, children: message }), _jsx("button", { type: "button", style: button, onClick: onForceLoad, children: "Force load" })] }));
138
126
  });
139
- const PlainErrorBar = observer(function PlainErrorBar({ model, visible, }) {
140
- return visible && model.error ? (_jsx("div", { style: overlayBox, role: "alert", children: _jsxs("div", { style: chip, children: [_jsx("span", { style: { wordBreak: 'break-word' }, children: errorText(model.error) }), _jsx("button", { type: "button", style: button, "data-testid": "reload_button", onClick: () => {
141
- model.reload();
142
- }, children: "Retry" })] }) })) : null;
127
+ const PlainErrorBar = observer(function PlainErrorBar({ error, onRetry, }) {
128
+ return (_jsx("div", { style: overlayBox, role: onRetry ? 'alert' : 'status', children: _jsxs("div", { style: chip, children: [_jsx("span", { style: { wordBreak: 'break-word' }, children: errorText(error) }), onRetry ? (_jsx("button", { type: "button", style: button, "data-testid": "reload_button", onClick: onRetry, children: "Retry" })) : null] }) }));
143
129
  });
144
130
  // No anti-flash delay here, unlike the MUI set: that timing is a product
145
131
  // judgment JBrowse made for its own UI, and an embedder pacing their own
146
132
  // loading affordances shouldn't inherit it invisibly. Wrap this if you want it.
147
- const PlainLoading = observer(function PlainLoading({ model, visible, }) {
133
+ const PlainLoading = observer(function PlainLoading({ visible, message, progress, canceled, onCancel, onRetry, }) {
148
134
  if (!visible) {
149
135
  return null;
150
136
  }
151
- const { statusMessage, statusProgress, fetchCanceled } = model;
152
- return (_jsx("div", { style: overlayBox, "data-testid": fetchCanceled ? 'loading-overlay-canceled' : 'loading-overlay', children: _jsx("div", { style: { ...chip, display: 'flex', alignItems: 'center' }, role: "status", children: fetchCanceled ? (_jsxs(_Fragment, { children: [_jsx("span", { children: "Loading canceled" }), model.reload ? (_jsx("button", { type: "button", style: button, "data-testid": "loading-overlay-retry", onClick: () => {
153
- model.reload?.();
154
- }, children: "Retry" })) : null] })) : (_jsxs(_Fragment, { children: [_jsx("span", { children: statusProgress === undefined
155
- ? `${statusMessage || 'Loading'}...`
156
- : progressLabel(statusMessage || 'Loading', statusProgress) }), statusProgress === undefined ? null : (_jsx(ProgressBar, { fraction: statusProgress })), model.cancelFetchByUser ? (_jsx("button", { type: "button", style: button, "data-testid": "loading-overlay-cancel", onClick: () => {
157
- model.cancelFetchByUser?.();
158
- }, children: "Cancel" })) : null] })) }) }));
137
+ return (_jsx("div", { style: overlayBox, "data-testid": canceled ? 'loading-overlay-canceled' : 'loading-overlay', children: _jsx("div", { style: { ...chip, display: 'flex', alignItems: 'center' }, role: "status", children: canceled ? (_jsxs(_Fragment, { children: [_jsx("span", { children: "Loading canceled" }), onRetry ? (_jsx("button", { type: "button", style: button, "data-testid": "loading-overlay-retry", onClick: onRetry, children: "Retry" })) : null] })) : (_jsxs(_Fragment, { children: [_jsx("span", { children: progress === undefined
138
+ ? `${message || 'Loading'}...`
139
+ : progressLabel(message || 'Loading', progress) }), progress === undefined ? null : (_jsx(ProgressBar, { fraction: progress })), onCancel ? (_jsx("button", { type: "button", style: button, "data-testid": "loading-overlay-cancel", onClick: onCancel, children: "Cancel" })) : null] })) }) }));
159
140
  });
160
- const PlainBackgroundProgress = observer(function PlainBackgroundProgress({ model, visible, }) {
161
- // The only state that renders no positioned box of its own: the chrome
162
- // anchors the bottom-right corner and shares it with the display's control
163
- // row, so this is laid out there. Claiming `inset: 0` here would collapse
164
- // against that box, and pinning to the corner would sit under the row.
165
- return visible && model.statusMessage ? (_jsxs("div", { style: { ...chip, display: 'flex', alignItems: 'center' }, role: "status", "data-testid": "progress-chip", children: [_jsx("span", { children: progressLabel(model.statusMessage, model.statusProgress) }), model.statusProgress === undefined ? null : (_jsx(ProgressBar, { fraction: model.statusProgress }))] })) : null;
141
+ const PlainBackgroundProgress = observer(function PlainBackgroundProgress({ message, progress, }) {
142
+ // in-flow: the chrome anchors the corner it shares with the control row
143
+ return (_jsxs("div", { style: { ...chip, display: 'flex', alignItems: 'center' }, role: "status", "data-testid": "progress-chip", children: [_jsx("span", { children: progressLabel(message || 'Loading', progress) }), progress === undefined ? null : _jsx(ProgressBar, { fraction: progress })] }));
166
144
  });
167
145
  /**
168
146
  * #api
@@ -172,8 +150,8 @@ const PlainBackgroundProgress = observer(function PlainBackgroundProgress({ mode
172
150
  *
173
151
  * `DisplayUIProvider` installs this by default, so mount that rather than
174
152
  * naming this — reach for it directly only to wrap a state or to build a
175
- * context value by hand. Colours come from `currentColor` and the CSS system
176
- * colours, so the host's own cascade drives them in both light and dark.
153
+ * context value by hand. Colors come from `currentColor` and the CSS system
154
+ * colors, so the host's own cascade drives them in both light and dark.
177
155
  *
178
156
  * The `data-testid` values it renders are a contract four of JBrowse's test
179
157
  * systems key on, so a replacement set that keeps them can be driven by those