@jbrowse/display-ui 4.3.0

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 (41) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +433 -0
  3. package/esm/DisplayUIProvider.d.ts +70 -0
  4. package/esm/DisplayUIProvider.js +96 -0
  5. package/esm/FloatingLegend.d.ts +16 -0
  6. package/esm/FloatingLegend.js +213 -0
  7. package/esm/PointerLayer.d.ts +26 -0
  8. package/esm/PointerLayer.js +27 -0
  9. package/esm/bottomRightCorner.d.ts +58 -0
  10. package/esm/bottomRightCorner.js +59 -0
  11. package/esm/chromeOverlayContext.d.ts +7 -0
  12. package/esm/chromeOverlayContext.js +44 -0
  13. package/esm/chromeOverlays.d.ts +88 -0
  14. package/esm/chromeOverlays.js +1 -0
  15. package/esm/index.d.ts +22 -0
  16. package/esm/index.js +51 -0
  17. package/esm/isLiveModel.d.ts +21 -0
  18. package/esm/isLiveModel.js +24 -0
  19. package/esm/plainChromeOverlays.d.ts +18 -0
  20. package/esm/plainChromeOverlays.js +189 -0
  21. package/esm/tooLargeBannerText.d.ts +18 -0
  22. package/esm/tooLargeBannerText.js +34 -0
  23. package/esm/tooltip/Tooltip.d.ts +52 -0
  24. package/esm/tooltip/Tooltip.js +67 -0
  25. package/esm/tooltip/useTooltip.d.ts +50 -0
  26. package/esm/tooltip/useTooltip.js +116 -0
  27. package/esm/trackControl/plainTrackControl.d.ts +17 -0
  28. package/esm/trackControl/plainTrackControl.js +133 -0
  29. package/esm/trackControl/trackControlContext.d.ts +7 -0
  30. package/esm/trackControl/trackControlContext.js +30 -0
  31. package/esm/trackControl/types.d.ts +58 -0
  32. package/esm/trackControl/types.js +1 -0
  33. package/esm/trackControl/useTrackControlMenu.d.ts +51 -0
  34. package/esm/trackControl/useTrackControlMenu.js +163 -0
  35. package/esm/trackOverlay/TrackOverlayContext.d.ts +1 -0
  36. package/esm/trackOverlay/TrackOverlayContext.js +18 -0
  37. package/esm/trackOverlay/TrackOverlayPortal.d.ts +18 -0
  38. package/esm/trackOverlay/TrackOverlayPortal.js +66 -0
  39. package/esm/trackOverlay/TrackOverlaySlot.d.ts +54 -0
  40. package/esm/trackOverlay/TrackOverlaySlot.js +60 -0
  41. package/package.json +48 -0
@@ -0,0 +1,88 @@
1
+ import type { ComponentType } from 'react';
2
+ /** What `ErrorBar` reads: a failed fetch, and the way to run it again. */
3
+ export interface DisplayErrorBarModel {
4
+ error: unknown;
5
+ reload: () => void;
6
+ }
7
+ /** What `Loading` reads. Everything is optional — a display that reports no
8
+ * progress and offers no cancel still gets a scrim. */
9
+ export interface DisplayLoadingOverlayModel {
10
+ statusMessage?: string;
11
+ statusProgress?: number;
12
+ fetchCanceled?: boolean;
13
+ cancelFetchByUser?: () => void;
14
+ reload?: () => void;
15
+ }
16
+ /** What `BackgroundProgress` reads: the status channel for work with no fetch
17
+ * behind it, while the phase is `ready`. */
18
+ export interface DisplayBackgroundProgressModel {
19
+ statusMessage?: string;
20
+ statusProgress?: number;
21
+ }
22
+ /** What `TooLarge` reads. `forceLoad` is the whole point of the state — see the
23
+ * entry below. */
24
+ export interface TooLargeMessageModel {
25
+ regionTooLargeReason: string;
26
+ zoomCanReleaseGate: boolean;
27
+ forceLoad: () => void;
28
+ }
29
+ export interface DisplayChromeOverlays {
30
+ /**
31
+ * GPU/render-backend failure. A subtree-replacing terminal state: the canvas
32
+ * unmounts and the backend disposes, so this owns the full display area.
33
+ */
34
+ RenderError: ComponentType<{
35
+ error: unknown;
36
+ onRetry: () => void;
37
+ height: number;
38
+ }>;
39
+ /**
40
+ * The byte gate tripped. Also subtree-replacing. Must offer `model.forceLoad`
41
+ * or the region becomes unreachable for the rest of the session.
42
+ */
43
+ TooLarge: ComponentType<{
44
+ model: TooLargeMessageModel;
45
+ }>;
46
+ /**
47
+ * Fetch error, drawn *over* a live canvas rather than replacing it. Mounted
48
+ * unconditionally like the two below, so `visible` (`displayPhase ===
49
+ * 'error'`) is the gate -- don't re-derive it from `model.error`, which is
50
+ * the same subtraction `displayPhase` exists to retire.
51
+ */
52
+ ErrorBar: ComponentType<{
53
+ model: DisplayErrorBarModel;
54
+ visible: boolean;
55
+ }>;
56
+ /**
57
+ * The loading scrim. `visible` is `displayPhase === 'loading'`; `immediate`
58
+ * asks it to skip its anti-flash delay because nothing is painted yet.
59
+ *
60
+ * Mounted unconditionally, so it must handle `visible === false` itself — and
61
+ * that is load-bearing rather than a style choice: the anti-flash delay is
62
+ * component state, so a chrome that mounted this only while loading would
63
+ * restart the timer on every activation and never reach it. Keep any
64
+ * replacement mountable-while-hidden for the same reason.
65
+ */
66
+ Loading: ComponentType<{
67
+ model: DisplayLoadingOverlayModel;
68
+ visible: boolean;
69
+ immediate?: boolean;
70
+ }>;
71
+ /**
72
+ * Status for work with no fetch behind it, while the phase is `ready`.
73
+ * Mounted unconditionally; gates on `visible` itself.
74
+ *
75
+ * **The one state that does not own its own box.** The chrome anchors the
76
+ * bottom-right corner and lays this out there, because that corner is shared
77
+ * with the display's own control row (`BottomRightIndicators`) and two boxes
78
+ * claiming it independently is exactly what they used to do — each pinning
79
+ * itself to `bottom: 2; right: 2` of the same overlay layer, the controls
80
+ * winning on z-index and the status text vanishing underneath. So render an
81
+ * **in-flow** chip: no `position`, no `inset`, no corner offsets. Sizing and
82
+ * colours are yours; placement is not. See `bottomRightCorner.ts`.
83
+ */
84
+ BackgroundProgress: ComponentType<{
85
+ model: DisplayBackgroundProgressModel;
86
+ visible: boolean;
87
+ }>;
88
+ }
@@ -0,0 +1 @@
1
+ export {};
package/esm/index.d.ts ADDED
@@ -0,0 +1,22 @@
1
+ export { default as DisplayUIProvider, resolveOverlays, } from './DisplayUIProvider.tsx';
2
+ export { DisplayChromeOverlayProvider, useChromeOverlayOverride, } from './chromeOverlayContext.ts';
3
+ export { default as plainChromeOverlays } from './plainChromeOverlays.tsx';
4
+ export type { DisplayBackgroundProgressModel, DisplayChromeOverlays, DisplayErrorBarModel, DisplayLoadingOverlayModel, TooLargeMessageModel, } from './chromeOverlays.ts';
5
+ export { TrackControlProvider, useTrackControlOverride, } from './trackControl/trackControlContext.ts';
6
+ export { default as plainTrackControl } from './trackControl/plainTrackControl.tsx';
7
+ export { useTrackControlMenu } from './trackControl/useTrackControlMenu.tsx';
8
+ export type { TrackControlMenu } from './trackControl/useTrackControlMenu.tsx';
9
+ export type { TrackControlComponent, TrackControlIcon, TrackControlOption, TrackControlProps, } from './trackControl/types.ts';
10
+ export { default as FloatingLegend } from './FloatingLegend.tsx';
11
+ export type { LegendItem, LegendSection } from './FloatingLegend.tsx';
12
+ export { default as Tooltip } from './tooltip/Tooltip.tsx';
13
+ export { useTooltip } from './tooltip/useTooltip.tsx';
14
+ export type { TooltipTrigger } from './tooltip/useTooltip.tsx';
15
+ export type { TooltipPlacement } from '@jbrowse/core/ui/BaseTooltip';
16
+ export { PointerLayer } from './PointerLayer.tsx';
17
+ export { TrackOverlayContext } from './trackOverlay/TrackOverlayContext.ts';
18
+ export { TrackOverlayPortal } from './trackOverlay/TrackOverlayPortal.tsx';
19
+ export { TrackOverlaySlot } from './trackOverlay/TrackOverlaySlot.tsx';
20
+ export { tooLargeBannerText } from './tooLargeBannerText.ts';
21
+ export { isLiveModel } from './isLiveModel.ts';
22
+ export { BOTTOM_RIGHT_CONTROLS_ORDER, BottomRightCornerContext, } from './bottomRightCorner.ts';
package/esm/index.js ADDED
@@ -0,0 +1,51 @@
1
+ // The UI a display draws that is not data, and none of it reaches a UI toolkit.
2
+ //
3
+ // This package has no `@mui/*` dependency, which is the guarantee the whole
4
+ // bring-your-own story rests on and the one thing a rendered-element census
5
+ // cannot check. The Material implementations live in
6
+ // `@jbrowse/plugin-linear-genome-view`, which depends on this — never the
7
+ // reverse. See README.md.
8
+ // Both seams at once, defaulting to the plain sets: what an embedder mounts.
9
+ // `resolveOverlays` is the partial-over-plain merge on its own, for code
10
+ // building the context value by hand — it returns one stable object per input,
11
+ // which is what keeps a context value from re-rendering every display.
12
+ export { default as DisplayUIProvider, resolveOverlays, } from './DisplayUIProvider.js';
13
+ // The status states.
14
+ export { DisplayChromeOverlayProvider, useChromeOverlayOverride, } from './chromeOverlayContext.js';
15
+ export { default as plainChromeOverlays } from './plainChromeOverlays.js';
16
+ // The bottom-right controls.
17
+ export { TrackControlProvider, useTrackControlOverride, } from './trackControl/trackControlContext.js';
18
+ export { default as plainTrackControl } from './trackControl/plainTrackControl.js';
19
+ export { useTrackControlMenu } from './trackControl/useTrackControlMenu.js';
20
+ // The one floating legend every display that has colors to explain renders.
21
+ // Here rather than in the LGV plugin because six plugins render it, it reaches
22
+ // no UI toolkit, and it is chrome by the definition this package uses — a thing
23
+ // a display draws that is not its data. `@jbrowse/plugin-linear-genome-view`
24
+ // re-exports it, so its published name is unchanged.
25
+ export { default as FloatingLegend } from './FloatingLegend.js';
26
+ // The hover label every control in here used to delegate to the browser's
27
+ // `title` attribute. The box it draws is `@jbrowse/core`'s `BaseTooltip`, the
28
+ // same one the display tooltips draw, anchored to the control instead of to the
29
+ // cursor — what lives here is the hover, the focus and the dismissal. It stays
30
+ // behind a `lazy()` one module in, which `tooltip/eagerBoundary.test.ts` pins.
31
+ export { default as Tooltip } from './tooltip/Tooltip.js';
32
+ export { useTooltip } from './tooltip/useTooltip.js';
33
+ // What follows the pointer, in a leaf that re-renders alone.
34
+ export { PointerLayer } from './PointerLayer.js';
35
+ // The per-track overlay layer: the node floating chrome escapes into, and the
36
+ // host's half of that portal.
37
+ export { TrackOverlayContext } from './trackOverlay/TrackOverlayContext.js';
38
+ export { TrackOverlayPortal } from './trackOverlay/TrackOverlayPortal.js';
39
+ export { TrackOverlaySlot } from './trackOverlay/TrackOverlaySlot.js';
40
+ // What the byte gate says, so every set that renders the too-large state says
41
+ // the same thing. The measurement behind the gate stays with the mixin that
42
+ // makes it.
43
+ export { tooLargeBannerText } from './tooLargeBannerText.js';
44
+ // The liveness check an overlay's one button needs, for a model that may be an
45
+ // MST node or the plain object the contract invites. Exported because both
46
+ // shipped sets render that button and a bare `isAlive` is wrong in each.
47
+ export { isLiveModel } from './isLiveModel.js';
48
+ // The corner `BackgroundProgress` is laid out in — the half of that state's
49
+ // contract the prop types cannot carry, so it belongs with them rather than
50
+ // with the chrome that happens to mount it.
51
+ export { BOTTOM_RIGHT_CONTROLS_ORDER, BottomRightCornerContext, } from './bottomRightCorner.js';
@@ -0,0 +1,21 @@
1
+ /**
2
+ * #api
3
+ * Whether an overlay's model can still be called into.
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`.
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.
20
+ */
21
+ export declare function isLiveModel(model: unknown): boolean;
@@ -0,0 +1,24 @@
1
+ import { isAlive, isStateTreeNode } from '@jbrowse/mobx-state-tree';
2
+ /**
3
+ * #api
4
+ * Whether an overlay's model can still be called into.
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`.
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.
21
+ */
22
+ export function isLiveModel(model) {
23
+ return !isStateTreeNode(model) || isAlive(model);
24
+ }
@@ -0,0 +1,18 @@
1
+ import type { DisplayChromeOverlays } from './chromeOverlays.ts';
2
+ /**
3
+ * #api
4
+ * The five `displayPhase` states drawn with no UI toolkit: no theme provider to
5
+ * mount, no emotion in the host page, and nothing that reads as a stray Material
6
+ * widget inside someone else's design system.
7
+ *
8
+ * `DisplayUIProvider` installs this by default, so mount that rather than
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.
12
+ *
13
+ * The `data-testid` values it renders are a contract four of JBrowse's test
14
+ * systems key on, so a replacement set that keeps them can be driven by those
15
+ * suites too.
16
+ */
17
+ declare const plainChromeOverlays: DisplayChromeOverlays;
18
+ export default plainChromeOverlays;
@@ -0,0 +1,189 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // the subpath, not the `@jbrowse/core/ui` barrel: this file is the MUI-free
3
+ // chrome, and gpuFallback.ts is React-free for exactly that reason
4
+ import { GPU_FALLBACK_LABEL, GPU_FALLBACK_TOOLTIP, disableGpuRendering, shouldOfferGpuFallback, } from '@jbrowse/core/ui/gpuFallback';
5
+ import { progressLabel } from '@jbrowse/core/util/progress';
6
+ import { observer } from 'mobx-react';
7
+ import { isLiveModel } from './isLiveModel.js';
8
+ import { tooLargeBannerText } from './tooLargeBannerText.js';
9
+ import Tooltip from './tooltip/Tooltip.js';
10
+ // A dependency-free `DisplayChromeOverlays` for embedders who don't want MUI:
11
+ // no theme provider to mount, no emotion injected into the host page, and
12
+ // nothing that reads as a stray Material widget inside someone else's design
13
+ // system. Pair it with `DisplayChromeBase` (NOT `DisplayChrome`, which binds
14
+ // the MUI set and would defeat the point by keeping MUI in the module graph).
15
+ //
16
+ // Deliberately plain rather than pretty: every rule below is either layout that
17
+ // the chrome's contract requires, or a hairline so the state is legible against
18
+ // an unknown background. Colors come from `currentColor` and `color-mix` so the
19
+ // host's own cascade drives them. Restyle by wrapping these, or write your own
20
+ // set against the same interface.
21
+ //
22
+ // The testids are NOT cosmetic. `loading-overlay`, `loading-overlay-cancel`,
23
+ // `loading-overlay-retry`, `progress-chip`, `reload_button` and
24
+ // `use_canvas2d_button` are a contract across four test systems (jest, browser
25
+ // tests, screenshot harness, jbrowse-img), so a replacement set that wants to
26
+ // run the existing suites has to keep them.
27
+ // The scrim and the too-large/error banners all need to sit over the canvas
28
+ // without eating pointer events; only the inner content is interactive. Matches
29
+ // the MUI set's behavior, where leaving an interactive box mounted over every
30
+ // track silently swallowed clicks.
31
+ const overlayBox = {
32
+ position: 'absolute',
33
+ inset: 0,
34
+ display: 'flex',
35
+ justifyContent: 'center',
36
+ alignItems: 'flex-start',
37
+ pointerEvents: 'none',
38
+ zIndex: 1,
39
+ };
40
+ // Nearly opaque on purpose. These sit over a canvas whose colours are not known
41
+ // 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
43
+ // light/dark mode without needing a theme object.
44
+ const chip = {
45
+ pointerEvents: 'auto',
46
+ margin: 4,
47
+ padding: '3px 8px',
48
+ fontSize: '0.8rem',
49
+ lineHeight: 1.4,
50
+ color: 'CanvasText',
51
+ border: '1px solid',
52
+ borderColor: 'color-mix(in srgb, CanvasText 35%, transparent)',
53
+ borderRadius: 4,
54
+ background: 'color-mix(in srgb, Canvas 94%, transparent)',
55
+ };
56
+ const button = {
57
+ font: 'inherit',
58
+ color: 'CanvasText',
59
+ marginLeft: 8,
60
+ padding: '0 6px',
61
+ cursor: 'pointer',
62
+ border: '1px solid',
63
+ borderColor: 'color-mix(in srgb, CanvasText 45%, transparent)',
64
+ borderRadius: 3,
65
+ background: 'transparent',
66
+ };
67
+ // Terminal states replace the whole subtree, so unlike the overlays above they
68
+ // own the display box and are the only thing in it.
69
+ function banner(height) {
70
+ return {
71
+ height,
72
+ boxSizing: 'border-box',
73
+ display: 'flex',
74
+ alignItems: 'center',
75
+ justifyContent: 'center',
76
+ gap: 8,
77
+ padding: 8,
78
+ fontSize: '0.8rem',
79
+ border: '1px solid',
80
+ borderColor: 'color-mix(in srgb, currentColor 25%, transparent)',
81
+ };
82
+ }
83
+ function errorText(error) {
84
+ return error instanceof Error ? error.message : String(error);
85
+ }
86
+ // Announcement, split across two elements on purpose.
87
+ //
88
+ // The chip carrying a status is a `role="status"` polite live region, so a
89
+ // screen reader reads the phase once when it changes ("Loading", "Clustering
90
+ // samples"). The percentage is deliberately NOT in that region: a determinate
91
+ // status ticks about ten times a second, and a live region carrying the number
92
+ // talks over everything else on the page until the fetch ends.
93
+ //
94
+ // The bar below is where the number goes. Screen readers do not announce
95
+ // `aria-valuenow` changes on a `progressbar`, so the value is reachable on
96
+ // demand and silent otherwise — which is the behaviour a progress indicator
97
+ // should have. `StatusProgressBar` in core does the same thing and is
98
+ // toolkit-free, but it paints `theme.palette.primary.main`; every rule in this
99
+ // file reads the host's own cascade instead, so the bar is drawn here rather
100
+ // than imported.
101
+ function ProgressBar({ fraction }) {
102
+ const filled = Math.min(1, Math.max(0, fraction));
103
+ return (_jsx("div", { role: "progressbar", "aria-valuemin": 0, "aria-valuemax": 100, "aria-valuenow": Math.round(filled * 100), style: {
104
+ overflow: 'hidden',
105
+ width: 60,
106
+ height: 3,
107
+ marginLeft: 6,
108
+ borderRadius: 2,
109
+ background: 'color-mix(in srgb, currentColor 26%, transparent)',
110
+ }, children: _jsx("div", { style: {
111
+ width: '100%',
112
+ height: '100%',
113
+ background: 'currentColor',
114
+ // scaled rather than sized, so an update is a compositor transform
115
+ transformOrigin: 'left center',
116
+ transform: `scaleX(${filled})`,
117
+ transition: 'transform 200ms linear',
118
+ } }) }));
119
+ }
120
+ const PlainRenderError = observer(function PlainRenderError({ error, onRetry, height, }) {
121
+ 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
+ disableGpuRendering();
123
+ onRetry();
124
+ }, children: GPU_FALLBACK_LABEL }) })) : null] }));
125
+ });
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" })] }));
138
+ });
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;
143
+ });
144
+ // No anti-flash delay here, unlike the MUI set: that timing is a product
145
+ // judgment JBrowse made for its own UI, and an embedder pacing their own
146
+ // loading affordances shouldn't inherit it invisibly. Wrap this if you want it.
147
+ const PlainLoading = observer(function PlainLoading({ model, visible, }) {
148
+ if (!visible) {
149
+ return null;
150
+ }
151
+ const { statusMessage, statusProgress, fetchCanceled } = model;
152
+ return (_jsx("div", { style: overlayBox, "data-testid": "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] })) }) }));
159
+ });
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;
166
+ });
167
+ /**
168
+ * #api
169
+ * The five `displayPhase` states drawn with no UI toolkit: no theme provider to
170
+ * mount, no emotion in the host page, and nothing that reads as a stray Material
171
+ * widget inside someone else's design system.
172
+ *
173
+ * `DisplayUIProvider` installs this by default, so mount that rather than
174
+ * 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.
177
+ *
178
+ * The `data-testid` values it renders are a contract four of JBrowse's test
179
+ * systems key on, so a replacement set that keeps them can be driven by those
180
+ * suites too.
181
+ */
182
+ const plainChromeOverlays = {
183
+ RenderError: PlainRenderError,
184
+ TooLarge: PlainTooLarge,
185
+ ErrorBar: PlainErrorBar,
186
+ Loading: PlainLoading,
187
+ BackgroundProgress: PlainBackgroundProgress,
188
+ };
189
+ export default plainChromeOverlays;
@@ -0,0 +1,18 @@
1
+ /**
2
+ * #api
3
+ * Which axis tripped (empty when the display gates without a reason), then the
4
+ * way out.
5
+ *
6
+ * `zoomCanRelease` decides whether "zoom in" is offered, and it has to be asked
7
+ * because the advice is not always true. It was, once: the `AUTO_FORCE_LOAD_BP`
8
+ * floor turned the byte gate off below 20kb, so zooming far enough always
9
+ * worked. The byte gate no longer stops at any floor, and an index quotes whole
10
+ * blocks — so for a file whose blocks are large the same bytes come down however
11
+ * far the user goes, and telling them to keep zooming into a fetch whose cost
12
+ * cannot fall is the one thing the banner must not do. `zoomCanReleaseGate`
13
+ * answers it from two consecutive measurements rather than from a threshold; see
14
+ * `ByteEstimate.zoomIneffective`.
15
+ */
16
+ export declare function tooLargeBannerText(regionTooLargeReason: string, { zoomCanRelease }?: {
17
+ zoomCanRelease?: boolean;
18
+ }): string;
@@ -0,0 +1,34 @@
1
+ // What the byte gate says, and the only part of that gate that belongs to the
2
+ // UI. The measurement side — byte estimates, the sub-floor budget factor,
3
+ // `resolveByteLimit` — stays in `RegionTooLargeMixin`'s own module, which
4
+ // re-exports this so a display keeps one import.
5
+ //
6
+ // It is here because *both* overlay sets render it: JBrowse's Material
7
+ // `TooLargeMessage` and the toolkit-free `PlainTooLarge`, plus anyone writing a
8
+ // third. Wording that drifts between them is a difference nobody would see in
9
+ // review and the screenshot harness keys off the literal.
10
+ /**
11
+ * #api
12
+ * Which axis tripped (empty when the display gates without a reason), then the
13
+ * way out.
14
+ *
15
+ * `zoomCanRelease` decides whether "zoom in" is offered, and it has to be asked
16
+ * because the advice is not always true. It was, once: the `AUTO_FORCE_LOAD_BP`
17
+ * floor turned the byte gate off below 20kb, so zooming far enough always
18
+ * worked. The byte gate no longer stops at any floor, and an index quotes whole
19
+ * blocks — so for a file whose blocks are large the same bytes come down however
20
+ * far the user goes, and telling them to keep zooming into a fetch whose cost
21
+ * cannot fall is the one thing the banner must not do. `zoomCanReleaseGate`
22
+ * answers it from two consecutive measurements rather than from a threshold; see
23
+ * `ByteEstimate.zoomIneffective`.
24
+ */
25
+ export function tooLargeBannerText(regionTooLargeReason, { zoomCanRelease = true } = {}) {
26
+ return [
27
+ regionTooLargeReason,
28
+ zoomCanRelease
29
+ ? 'Zoom in to see features, or force load this track for the rest of the session (may be slow)'
30
+ : 'Force load this track for the rest of the session (may be slow)',
31
+ ]
32
+ .filter(f => !!f)
33
+ .join('. ');
34
+ }
@@ -0,0 +1,52 @@
1
+ import type { TooltipPlacement } from '@jbrowse/core/ui/BaseTooltip';
2
+ import type { ReactElement, ReactNode, Ref } from 'react';
3
+ interface TriggerProps {
4
+ ref?: Ref<HTMLElement>;
5
+ onPointerEnter?: (event: React.PointerEvent<HTMLElement>) => void;
6
+ onPointerLeave?: (event: React.PointerEvent<HTMLElement>) => void;
7
+ onPointerDown?: (event: React.PointerEvent<HTMLElement>) => void;
8
+ onFocus?: (event: React.FocusEvent<HTMLElement>) => void;
9
+ onBlur?: (event: React.FocusEvent<HTMLElement>) => void;
10
+ }
11
+ /**
12
+ * #api
13
+ * A hover/focus label for a control, drawn rather than delegated to the
14
+ * browser's `title` attribute — positioned so it clears the display's
15
+ * `contain: strict` box and the window edge, dismissed by Escape, and drawn
16
+ * like every other JBrowse tooltip instead of like whatever the host OS
17
+ * renders.
18
+ *
19
+ * `title` is what this chrome used to use, and it was the wrong tool three
20
+ * ways: it can be neither styled nor positioned, it waits about a second and
21
+ * then disappears on a timer of its own, and on a control that already carries
22
+ * an `aria-label` some screen readers announce both strings. This reaches no UI
23
+ * toolkit, so the package's no-Material-UI guarantee holds.
24
+ *
25
+ * Takes a single element child and clones it rather than wrapping it: the
26
+ * controls that want a tooltip are absolutely positioned inside a legend or sit
27
+ * in a flex row, where an extra `<span>` moves them. The child keeps its own
28
+ * handlers — these compose on top of them. This is the same trigger pattern
29
+ * MUI's own `Tooltip` uses, which is what the rest of JBrowse's chrome is built
30
+ * on; the toolkit is what differs here, not the shape.
31
+ *
32
+ * ```tsx
33
+ * <Tooltip title="Hide legend">
34
+ * <button type="button" aria-label="Hide legend" onClick={onDismiss}>
35
+ * ×
36
+ * </button>
37
+ * </Tooltip>
38
+ * ```
39
+ *
40
+ * **The child still needs its own accessible name**, because this sets
41
+ * `aria-describedby` and never `aria-label` — see {@link useTooltip}, which is
42
+ * this without the cloning, for a host writing its own markup.
43
+ */
44
+ export default function Tooltip({ title, placement, children, }: {
45
+ /** What the bubble says. Nothing renders while this is empty. */
46
+ title: ReactNode;
47
+ /** Preferred side. `flip()` overrides it near a viewport edge. */
48
+ placement?: TooltipPlacement;
49
+ /** Exactly one element, cloned rather than wrapped. */
50
+ children: ReactElement<TriggerProps>;
51
+ }): import("react").JSX.Element;
52
+ export {};
@@ -0,0 +1,67 @@
1
+ import { Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { cloneElement } from 'react';
3
+ import { useTooltip } from './useTooltip.js';
4
+ function compose(theirs, ours) {
5
+ return (event) => {
6
+ theirs?.(event);
7
+ ours(event);
8
+ };
9
+ }
10
+ /**
11
+ * #api
12
+ * A hover/focus label for a control, drawn rather than delegated to the
13
+ * browser's `title` attribute — positioned so it clears the display's
14
+ * `contain: strict` box and the window edge, dismissed by Escape, and drawn
15
+ * like every other JBrowse tooltip instead of like whatever the host OS
16
+ * renders.
17
+ *
18
+ * `title` is what this chrome used to use, and it was the wrong tool three
19
+ * ways: it can be neither styled nor positioned, it waits about a second and
20
+ * then disappears on a timer of its own, and on a control that already carries
21
+ * an `aria-label` some screen readers announce both strings. This reaches no UI
22
+ * toolkit, so the package's no-Material-UI guarantee holds.
23
+ *
24
+ * Takes a single element child and clones it rather than wrapping it: the
25
+ * controls that want a tooltip are absolutely positioned inside a legend or sit
26
+ * in a flex row, where an extra `<span>` moves them. The child keeps its own
27
+ * handlers — these compose on top of them. This is the same trigger pattern
28
+ * MUI's own `Tooltip` uses, which is what the rest of JBrowse's chrome is built
29
+ * on; the toolkit is what differs here, not the shape.
30
+ *
31
+ * ```tsx
32
+ * <Tooltip title="Hide legend">
33
+ * <button type="button" aria-label="Hide legend" onClick={onDismiss}>
34
+ * ×
35
+ * </button>
36
+ * </Tooltip>
37
+ * ```
38
+ *
39
+ * **The child still needs its own accessible name**, because this sets
40
+ * `aria-describedby` and never `aria-label` — see {@link useTooltip}, which is
41
+ * this without the cloning, for a host writing its own markup.
42
+ */
43
+ export default function Tooltip({ title, placement, children, }) {
44
+ const { triggerProps, tooltip } = useTooltip(title, { placement });
45
+ const props = children.props;
46
+ // eslint-disable-next-line @eslint-react/no-clone-element -- wrapping instead would move the absolutely-positioned and flex-row controls this exists for
47
+ const trigger = cloneElement(children, {
48
+ ...triggerProps,
49
+ ref: (node) => {
50
+ ;
51
+ triggerProps.ref(node);
52
+ const theirs = props.ref;
53
+ if (typeof theirs === 'function') {
54
+ theirs(node);
55
+ }
56
+ else if (theirs) {
57
+ theirs.current = node;
58
+ }
59
+ },
60
+ onPointerEnter: compose(props.onPointerEnter, triggerProps.onPointerEnter),
61
+ onPointerLeave: compose(props.onPointerLeave, triggerProps.onPointerLeave),
62
+ onPointerDown: compose(props.onPointerDown, triggerProps.onPointerDown),
63
+ onFocus: compose(props.onFocus, triggerProps.onFocus),
64
+ onBlur: compose(props.onBlur, triggerProps.onBlur),
65
+ });
66
+ return (_jsxs(_Fragment, { children: [trigger, tooltip] }));
67
+ }
@@ -0,0 +1,50 @@
1
+ import type { TooltipPlacement } from '@jbrowse/core/ui/BaseTooltip';
2
+ import type { ReactNode, Ref } from 'react';
3
+ export interface TooltipTrigger {
4
+ /** Whether the bubble is up, for a caller that wants to style the trigger. */
5
+ open: boolean;
6
+ /**
7
+ * Spread onto the control the tooltip describes. `aria-describedby`, not
8
+ * `aria-label`: the tooltip is a visual affordance, and a name pointing at a
9
+ * node that exists only during a hover leaves the control unnamed the rest of
10
+ * the time. **The control still needs its own accessible name.**
11
+ */
12
+ triggerProps: {
13
+ ref: Ref<HTMLElement>;
14
+ 'aria-describedby': string | undefined;
15
+ onPointerEnter: (event: React.PointerEvent<HTMLElement>) => void;
16
+ onPointerLeave: () => void;
17
+ onPointerDown: () => void;
18
+ onFocus: () => void;
19
+ onBlur: () => void;
20
+ };
21
+ /** Render this. It is `null` until the bubble is up, and portals itself. */
22
+ tooltip: ReactNode;
23
+ }
24
+ /**
25
+ * #api
26
+ * A hover/focus label for one control, as props to spread — the headless half
27
+ * of {@link Tooltip}, for a host writing its own chrome rather than restyling
28
+ * ours. Same relationship `useTrackControlMenu` has to `plainTrackControl`.
29
+ *
30
+ * ```tsx
31
+ * const { triggerProps, tooltip } = useTooltip('Hide legend')
32
+ * return (
33
+ * <>
34
+ * <button {...triggerProps} aria-label="Hide legend" onClick={onDismiss}>
35
+ * ×
36
+ * </button>
37
+ * {tooltip}
38
+ * </>
39
+ * )
40
+ * ```
41
+ *
42
+ * `triggerProps` carries no `onClick`, so a control's own handler does not
43
+ * collide with it. Any other handler on this list has to compose rather than
44
+ * replace — spread first, then call `triggerProps.onFocus` from yours.
45
+ */
46
+ export declare function useTooltip(
47
+ /** What the bubble says. It stays down while this is empty. */
48
+ title: ReactNode, { placement }?: {
49
+ placement?: TooltipPlacement;
50
+ }): TooltipTrigger;