@jbrowse/display-ui 5.0.0-beta.13 → 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 +2 -2
  11. package/esm/FloatingText.d.ts +5 -5
  12. package/esm/FloatingText.js +6 -5
  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 +11 -5
  21. package/esm/axisPlacement.js +15 -8
  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 -44
  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 +3 -3
@@ -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
@@ -1,12 +1,9 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { isRegionTooLargeError } from '@jbrowse/core/rpc/byteBudget';
3
2
  // the subpath, not the `@jbrowse/core/ui` barrel: this file is the MUI-free
4
3
  // chrome, and gpuFallback.ts is React-free for exactly that reason
5
4
  import { GPU_FALLBACK_LABEL, GPU_FALLBACK_TOOLTIP, disableGpuRendering, shouldOfferGpuFallback, } from '@jbrowse/core/ui/gpuFallback';
6
5
  import { progressLabel } from '@jbrowse/core/util/progress';
7
6
  import { observer } from 'mobx-react';
8
- import { isLiveModel } from './isLiveModel.js';
9
- import { tooLargeBannerText } from './tooLargeBannerText.js';
10
7
  import Tooltip from './tooltip/Tooltip.js';
11
8
  // A dependency-free `DisplayChromeOverlays` for embedders who don't want MUI:
12
9
  // no theme provider to mount, no emotion injected into the host page, and
@@ -38,9 +35,9 @@ const overlayBox = {
38
35
  pointerEvents: 'none',
39
36
  zIndex: 1,
40
37
  };
41
- // 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
42
39
  // here, and a translucent chip over dense data was measurably hard to read.
43
- // `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
44
41
  // light/dark mode without needing a theme object.
45
42
  const chip = {
46
43
  pointerEvents: 'auto',
@@ -99,7 +96,7 @@ function errorText(error) {
99
96
  // toolkit-free, but it paints the palette's `accent`; every rule in this
100
97
  // file reads the host's own cascade instead, so the bar is drawn here rather
101
98
  // than imported.
102
- function ProgressBar({ fraction }) {
99
+ const ProgressBar = observer(function ProgressBar({ fraction, }) {
103
100
  const filled = Math.min(1, Math.max(0, fraction));
104
101
  return (_jsx("div", { role: "progressbar", "aria-valuemin": 0, "aria-valuemax": 100, "aria-valuenow": Math.round(filled * 100), style: {
105
102
  overflow: 'hidden',
@@ -117,57 +114,33 @@ function ProgressBar({ fraction }) {
117
114
  transform: `scaleX(${filled})`,
118
115
  transition: 'transform 200ms linear',
119
116
  } }) }));
120
- }
117
+ });
121
118
  const PlainRenderError = observer(function PlainRenderError({ error, onRetry, height, }) {
122
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: () => {
123
120
  disableGpuRendering();
124
121
  onRetry();
125
122
  }, children: GPU_FALLBACK_LABEL }) })) : null] }));
126
123
  });
127
- const PlainTooLarge = observer(function PlainTooLarge({ model, }) {
128
- return (_jsxs("div", { style: banner(), role: "status", children: [_jsx("span", { style: { wordBreak: 'break-word' }, children: tooLargeBannerText(model.regionTooLargeReason, {
129
- zoomCanRelease: model.zoomCanReleaseGate,
130
- }) }), _jsx("button", { type: "button", style: button, onClick: () => {
131
- // the banner unmounts the canvas, so a click landing after the track
132
- // was closed would otherwise call an action on a dead node. Not bare
133
- // `isAlive` — this model may be the plain object the contract invites,
134
- // which that throws on. See isLiveModel.ts.
135
- if (isLiveModel(model)) {
136
- model.forceLoad();
137
- }
138
- }, 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" })] }));
139
126
  });
140
- const PlainErrorBar = observer(function PlainErrorBar({ model, visible, }) {
141
- const { error } = model;
142
- if (!visible || !error) {
143
- return null;
144
- }
145
- return isRegionTooLargeError(error) ? (_jsx("div", { style: overlayBox, role: "status", children: _jsx("div", { style: chip, children: _jsx("span", { style: { wordBreak: 'break-word' }, children: error.message }) }) })) : (_jsx("div", { style: overlayBox, role: "alert", children: _jsxs("div", { style: chip, children: [_jsx("span", { style: { wordBreak: 'break-word' }, children: errorText(error) }), _jsx("button", { type: "button", style: button, "data-testid": "reload_button", onClick: () => {
146
- model.reload();
147
- }, children: "Retry" })] }) }));
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] }) }));
148
129
  });
149
130
  // No anti-flash delay here, unlike the MUI set: that timing is a product
150
131
  // judgment JBrowse made for its own UI, and an embedder pacing their own
151
132
  // loading affordances shouldn't inherit it invisibly. Wrap this if you want it.
152
- const PlainLoading = observer(function PlainLoading({ model, visible, }) {
133
+ const PlainLoading = observer(function PlainLoading({ visible, message, progress, canceled, onCancel, onRetry, }) {
153
134
  if (!visible) {
154
135
  return null;
155
136
  }
156
- const { statusMessage, statusProgress, fetchCanceled } = model;
157
- 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: () => {
158
- model.reload?.();
159
- }, children: "Retry" })) : null] })) : (_jsxs(_Fragment, { children: [_jsx("span", { children: statusProgress === undefined
160
- ? `${statusMessage || 'Loading'}...`
161
- : 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: () => {
162
- model.cancelFetchByUser?.();
163
- }, 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] })) }) }));
164
140
  });
165
- const PlainBackgroundProgress = observer(function PlainBackgroundProgress({ model, visible, }) {
166
- // The only state that renders no positioned box of its own: the chrome
167
- // anchors the bottom-right corner and shares it with the display's control
168
- // row, so this is laid out there. Claiming `inset: 0` here would collapse
169
- // against that box, and pinning to the corner would sit under the row.
170
- 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 })] }));
171
144
  });
172
145
  /**
173
146
  * #api
@@ -177,8 +150,8 @@ const PlainBackgroundProgress = observer(function PlainBackgroundProgress({ mode
177
150
  *
178
151
  * `DisplayUIProvider` installs this by default, so mount that rather than
179
152
  * naming this — reach for it directly only to wrap a state or to build a
180
- * context value by hand. Colours come from `currentColor` and the CSS system
181
- * 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.
182
155
  *
183
156
  * The `data-testid` values it renders are a contract four of JBrowse's test
184
157
  * systems key on, so a replacement set that keeps them can be driven by those
@@ -41,12 +41,12 @@ interface TriggerProps {
41
41
  * `aria-describedby` and never `aria-label` — see {@link useTooltip}, which is
42
42
  * this without the cloning, for a host writing its own markup.
43
43
  */
44
- export default function Tooltip({ title, placement, children, }: {
44
+ declare const Tooltip: ({ title, placement, children, }: {
45
45
  /** What the bubble says. Nothing renders while this is empty. */
46
46
  title: ReactNode;
47
47
  /** Preferred side. `flip()` overrides it near a viewport edge. */
48
48
  placement?: TooltipPlacement;
49
49
  /** Exactly one element, cloned rather than wrapped. */
50
50
  children: ReactElement<TriggerProps>;
51
- }): import("react").JSX.Element;
52
- export {};
51
+ }) => import("react").JSX.Element;
52
+ export default Tooltip;
@@ -1,5 +1,6 @@
1
1
  import { Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { cloneElement } from 'react';
3
+ import { observer } from 'mobx-react';
3
4
  import { useTooltip } from './useTooltip.js';
4
5
  function compose(theirs, ours) {
5
6
  return (event) => {
@@ -40,7 +41,7 @@ function compose(theirs, ours) {
40
41
  * `aria-describedby` and never `aria-label` — see {@link useTooltip}, which is
41
42
  * this without the cloning, for a host writing its own markup.
42
43
  */
43
- export default function Tooltip({ title, placement, children, }) {
44
+ const Tooltip = observer(function Tooltip({ title, placement, children, }) {
44
45
  const { triggerProps, tooltip } = useTooltip(title, { placement });
45
46
  const props = children.props;
46
47
  // eslint-disable-next-line @eslint-react/no-clone-element -- wrapping instead would move the absolutely-positioned and flex-row controls this exists for
@@ -64,4 +65,5 @@ export default function Tooltip({ title, placement, children, }) {
64
65
  onBlur: compose(props.onBlur, triggerProps.onBlur),
65
66
  });
66
67
  return (_jsxs(_Fragment, { children: [trigger, tooltip] }));
67
- }
68
+ });
69
+ export default Tooltip;
@@ -14,4 +14,5 @@ import type { TrackControlProps } from './types.ts';
14
14
  * `plainChromeOverlays` — the two are a pair, and both are things you hand to a
15
15
  * provider rather than render yourself.
16
16
  */
17
- export default function PlainTrackControl({ icon, tooltip, label, options, onClick, onMenuClose, onDelete, warning, }: TrackControlProps): import("react").JSX.Element;
17
+ declare const PlainTrackControl: ({ icon, tooltip, label, options, onClick, onMenuClose, onDelete, warning, }: TrackControlProps) => import("react").JSX.Element;
18
+ export default PlainTrackControl;