@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,70 @@
1
+ import type { DisplayChromeOverlays } from './chromeOverlays.ts';
2
+ import type { TrackControlComponent } from './trackControl/types.ts';
3
+ import type { ReactNode } from 'react';
4
+ export declare function resolveOverlays(overlays?: Partial<DisplayChromeOverlays>): DisplayChromeOverlays;
5
+ /**
6
+ * #api
7
+ * Both bring-your-own seams at once: what a display draws that is not data.
8
+ *
9
+ * There are two contexts underneath because two different things render them —
10
+ * the chrome around a display draws the status states, the display itself draws
11
+ * its bottom-right controls — and that split is real at the implementation
12
+ * level, since `DisplayChromeBase` takes its overlay set as a *prop* and never
13
+ * renders a track control. It is not real for an embedder: nobody wants stock
14
+ * Material loading scrims with plain corner controls, or the reverse. Every
15
+ * consumer in this repo mounts the two together.
16
+ *
17
+ * ```tsx
18
+ * <DisplayUIProvider>{tracks}</DisplayUIProvider>
19
+ * ```
20
+ *
21
+ * Both props default to the plain, toolkit-free sets, so the common case — "I
22
+ * do not want Material UI in my app" — needs no arguments and no second import.
23
+ *
24
+ * **`overlays` is a partial set**, merged over the plain one, so replacing a
25
+ * single state is one entry rather than five:
26
+ *
27
+ * ```tsx
28
+ * <DisplayUIProvider overlays={{ ErrorBar: MyErrorBar }}>
29
+ * ```
30
+ *
31
+ * Partial rather than whole for two reasons. A host writing four of the five
32
+ * states by hand only ever wanted one of them, and every example of this had
33
+ * spread `plainChromeOverlays` in to say so. And a *sixth* state is a thing
34
+ * JBrowse can add: with a whole set the host's object goes stale on upgrade —
35
+ * a compile error if they typecheck, a missing component if they ship JS — while
36
+ * a partial one keeps working and picks up the new plain default.
37
+ *
38
+ * Declare that object at module scope if you can. The merge is stable per
39
+ * object (`resolveOverlays`), so a constant costs one merge for the life of the
40
+ * app; a literal written inline in JSX is a new object every render, and this
41
+ * value goes into a context, so every display beneath re-renders with it.
42
+ *
43
+ * **The contexts themselves still default to `undefined`, and that stays true.**
44
+ * A display rendering outside any provider — a unit test, the SVG export,
45
+ * breakpoint-split-view's `overlayUtils` — keeps JBrowse's own Material look,
46
+ * because a plain ambient default would degrade those invisibly. Defaulting
47
+ * *this component's props* is a different thing: mounting it is a deliberate
48
+ * act, and the act means "not the Material default". Nothing gets a plain set
49
+ * without someone having asked.
50
+ *
51
+ * Colors are not a seam and are not here. A display reads `usePalette()` for its
52
+ * own content colors, which is a palette of strings rather than a UI toolkit, so
53
+ * it arrives through `SessionPaletteProvider`
54
+ * (`@jbrowse/core/ui/PaletteContext`) whatever these are set to. A feature track
55
+ * needs it even with plain chrome.
56
+ *
57
+ * This module reaches no `@mui/*` module, and `muiFree.test.ts` keeps it
58
+ * that way — asking for less Material UI must not download more of it. What it
59
+ * cannot do is unship the Material components a *stock display* imports:
60
+ * `DisplayChrome` and `TrackControl` are in that display's chunk either way,
61
+ * and merely stop rendering. Keeping them out of the graph means writing your
62
+ * own display component over `DisplayChromeBase`, which takes `overlays` as a
63
+ * prop and imports no toolkit. See
64
+ * [DISPLAYCHROME.md](https://github.com/GMOD/jbrowse-components/blob/main/agent-docs/reference/DISPLAYCHROME.md).
65
+ */
66
+ export default function DisplayUIProvider({ overlays, trackControl, children, }: {
67
+ overlays?: Partial<DisplayChromeOverlays>;
68
+ trackControl?: TrackControlComponent;
69
+ children: ReactNode;
70
+ }): import("react").JSX.Element;
@@ -0,0 +1,96 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { DisplayChromeOverlayProvider } from './chromeOverlayContext.js';
3
+ import plainChromeOverlays from './plainChromeOverlays.js';
4
+ import plainTrackControl from './trackControl/plainTrackControl.js';
5
+ import { TrackControlProvider } from './trackControl/trackControlContext.js';
6
+ // One merged set per caller's object, for the life of that object.
7
+ //
8
+ // The value goes into a context, so a fresh identity on each render re-renders
9
+ // every display beneath it — and `observer()` wraps `React.memo`, so a new
10
+ // `overlays` prop defeats the memo on each display's whole chrome. Stability is
11
+ // the point; the allocation is nothing.
12
+ //
13
+ // A `useMemo` would do it and is worse in three small ways: it is per component
14
+ // *instance*, so two providers handed the same set produce two identities and
15
+ // two re-render groups; React may discard the cache; and it makes a plain
16
+ // merge into a hook, unusable from a test or a caller building the value by
17
+ // hand. Keyed weakly, so an inline literal — which misses either way — is
18
+ // collected rather than accumulated.
19
+ const merged = new WeakMap();
20
+ export function resolveOverlays(overlays) {
21
+ // the common case allocates nothing: no argument means the plain set itself,
22
+ // which is already a module constant
23
+ if (!overlays) {
24
+ return plainChromeOverlays;
25
+ }
26
+ let full = merged.get(overlays);
27
+ if (!full) {
28
+ full = { ...plainChromeOverlays, ...overlays };
29
+ merged.set(overlays, full);
30
+ }
31
+ return full;
32
+ }
33
+ /**
34
+ * #api
35
+ * Both bring-your-own seams at once: what a display draws that is not data.
36
+ *
37
+ * There are two contexts underneath because two different things render them —
38
+ * the chrome around a display draws the status states, the display itself draws
39
+ * its bottom-right controls — and that split is real at the implementation
40
+ * level, since `DisplayChromeBase` takes its overlay set as a *prop* and never
41
+ * renders a track control. It is not real for an embedder: nobody wants stock
42
+ * Material loading scrims with plain corner controls, or the reverse. Every
43
+ * consumer in this repo mounts the two together.
44
+ *
45
+ * ```tsx
46
+ * <DisplayUIProvider>{tracks}</DisplayUIProvider>
47
+ * ```
48
+ *
49
+ * Both props default to the plain, toolkit-free sets, so the common case — "I
50
+ * do not want Material UI in my app" — needs no arguments and no second import.
51
+ *
52
+ * **`overlays` is a partial set**, merged over the plain one, so replacing a
53
+ * single state is one entry rather than five:
54
+ *
55
+ * ```tsx
56
+ * <DisplayUIProvider overlays={{ ErrorBar: MyErrorBar }}>
57
+ * ```
58
+ *
59
+ * Partial rather than whole for two reasons. A host writing four of the five
60
+ * states by hand only ever wanted one of them, and every example of this had
61
+ * spread `plainChromeOverlays` in to say so. And a *sixth* state is a thing
62
+ * JBrowse can add: with a whole set the host's object goes stale on upgrade —
63
+ * a compile error if they typecheck, a missing component if they ship JS — while
64
+ * a partial one keeps working and picks up the new plain default.
65
+ *
66
+ * Declare that object at module scope if you can. The merge is stable per
67
+ * object (`resolveOverlays`), so a constant costs one merge for the life of the
68
+ * app; a literal written inline in JSX is a new object every render, and this
69
+ * value goes into a context, so every display beneath re-renders with it.
70
+ *
71
+ * **The contexts themselves still default to `undefined`, and that stays true.**
72
+ * A display rendering outside any provider — a unit test, the SVG export,
73
+ * breakpoint-split-view's `overlayUtils` — keeps JBrowse's own Material look,
74
+ * because a plain ambient default would degrade those invisibly. Defaulting
75
+ * *this component's props* is a different thing: mounting it is a deliberate
76
+ * act, and the act means "not the Material default". Nothing gets a plain set
77
+ * without someone having asked.
78
+ *
79
+ * Colors are not a seam and are not here. A display reads `usePalette()` for its
80
+ * own content colors, which is a palette of strings rather than a UI toolkit, so
81
+ * it arrives through `SessionPaletteProvider`
82
+ * (`@jbrowse/core/ui/PaletteContext`) whatever these are set to. A feature track
83
+ * needs it even with plain chrome.
84
+ *
85
+ * This module reaches no `@mui/*` module, and `muiFree.test.ts` keeps it
86
+ * that way — asking for less Material UI must not download more of it. What it
87
+ * cannot do is unship the Material components a *stock display* imports:
88
+ * `DisplayChrome` and `TrackControl` are in that display's chunk either way,
89
+ * and merely stop rendering. Keeping them out of the graph means writing your
90
+ * own display component over `DisplayChromeBase`, which takes `overlays` as a
91
+ * prop and imports no toolkit. See
92
+ * [DISPLAYCHROME.md](https://github.com/GMOD/jbrowse-components/blob/main/agent-docs/reference/DISPLAYCHROME.md).
93
+ */
94
+ export default function DisplayUIProvider({ overlays, trackControl = plainTrackControl, children, }) {
95
+ return (_jsx(DisplayChromeOverlayProvider, { value: resolveOverlays(overlays), children: _jsx(TrackControlProvider, { value: trackControl, children: children }) }));
96
+ }
@@ -0,0 +1,16 @@
1
+ import type { LegendItem, LegendSection } from '@jbrowse/core/ui/legendSpec';
2
+ import type { ReactNode } from 'react';
3
+ export type { LegendItem, LegendMark, LegendSection, LegendSpec, LegendSwatch, } from '@jbrowse/core/ui/legendSpec';
4
+ declare const FloatingLegend: ({ items, sections, title, onDismiss, onDismissSection, onItemClick, maxItems, maxWidth, top, children, }: {
5
+ items?: LegendItem[];
6
+ sections?: LegendSection[];
7
+ title?: string;
8
+ children?: ReactNode;
9
+ onDismiss?: () => void;
10
+ onDismissSection?: (id: string) => void;
11
+ onItemClick?: (item: LegendItem, section: LegendSection) => void;
12
+ maxItems?: number;
13
+ maxWidth?: number;
14
+ top?: number;
15
+ }) => import("react").JSX.Element | null;
16
+ export default FloatingLegend;
@@ -0,0 +1,213 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState } from 'react';
3
+ // deep subpaths, never the `@jbrowse/core/ui` barrel: a display renders this
4
+ // legend directly, so it sits behind neither bring-your-own seam, and one named
5
+ // import of that barrel lands FileSelector, FatalErrorDialog and the
6
+ // cascading-menu stack in the chunk. `muiFree.test.ts` fails on it.
7
+ import { LegendSwatchGlyph } from '@jbrowse/core/ui/LegendSwatchGlyph';
8
+ import { legendSwatches, nonEmptyLegendSections, } from '@jbrowse/core/ui/legendSpec';
9
+ import { cx, makeStyles } from '@jbrowse/core/util/tss-react';
10
+ import { observer } from 'mobx-react';
11
+ import Tooltip from './tooltip/Tooltip.js';
12
+ import { TrackOverlayPortal } from './trackOverlay/TrackOverlayPortal.js';
13
+ const useStyles = makeStyles()(theme => ({
14
+ legend: {
15
+ position: 'absolute',
16
+ right: 10,
17
+ background: theme.palette.background.paper,
18
+ // A hairline edge, because `background.paper` on a track whose canvas is
19
+ // also paper-white is no edge at all: the key read as loose text floating
20
+ // over the features rather than as a panel, and the swatches were the only
21
+ // thing announcing it (reviewer, on cookbook_color_by_type: "legend needs a
22
+ // light border ... very subtle. just enough to notice because it is hard to
23
+ // see at a glance"). `divider` is the theme's own hairline, so this stays
24
+ // right in both themes and against a dark track.
25
+ border: `1px solid ${theme.palette.divider}`,
26
+ borderRadius: 3,
27
+ padding: 3,
28
+ fontSize: 10,
29
+ zIndex: 100,
30
+ // the default; `maxWidth` overrides it per display
31
+ // portaled into the TrackContainer's above-the-masks overlay node, which is
32
+ // pointer-events:none so it doesn't eat canvas events; re-enable events on
33
+ // the legend itself so its close buttons/links stay clickable (harmless when
34
+ // rendered inline, where events already pass through)
35
+ pointerEvents: 'auto',
36
+ },
37
+ // A plain <button>, not MUI's IconButton, and the glyph is `×` rather than
38
+ // the Material Close icon.
39
+ //
40
+ // This component is rendered by canvas, alignments, variants and multi-wiggle
41
+ // displays *directly*, behind neither bring-your-own seam, so a Material
42
+ // widget here reaches an embedder who has opted out of both — and the
43
+ // build-your-own site's zero-MUI census, which no page currently trips only
44
+ // because no page turns on a colorBy that raises a legend. Same call
45
+ // `BaseTooltip` made in 2026-08: what it needed was colors, and colors have a
46
+ // toolkit-free home already. See DISPLAYCHROME.md, "a third seam was
47
+ // considered for the tooltip and rejected".
48
+ //
49
+ // `×` is what `SvgColorLegend` already draws for this control, so the
50
+ // exported legend and the on-screen one now agree.
51
+ closeButton: {
52
+ position: 'absolute',
53
+ top: 0,
54
+ right: 0,
55
+ font: 'inherit',
56
+ lineHeight: 1,
57
+ padding: '1px 3px',
58
+ border: 0,
59
+ background: 'none',
60
+ color: theme.palette.text.secondary,
61
+ cursor: 'pointer',
62
+ '&:hover': {
63
+ color: theme.palette.text.primary,
64
+ },
65
+ },
66
+ withClose: {
67
+ paddingRight: 20,
68
+ },
69
+ topTitle: {
70
+ fontWeight: 'bold',
71
+ whiteSpace: 'nowrap',
72
+ marginBottom: 2,
73
+ },
74
+ section: {
75
+ '&:not(:last-child)': {
76
+ marginBottom: 4,
77
+ },
78
+ },
79
+ sectionHeader: {
80
+ display: 'flex',
81
+ alignItems: 'center',
82
+ marginBottom: 2,
83
+ },
84
+ sectionTitle: {
85
+ fontWeight: 'bold',
86
+ whiteSpace: 'nowrap',
87
+ overflow: 'hidden',
88
+ textOverflow: 'ellipsis',
89
+ },
90
+ sectionClose: {
91
+ font: 'inherit',
92
+ lineHeight: 1,
93
+ padding: 0,
94
+ marginLeft: 2,
95
+ border: 0,
96
+ background: 'none',
97
+ color: theme.palette.text.secondary,
98
+ cursor: 'pointer',
99
+ '&:hover': {
100
+ color: theme.palette.text.primary,
101
+ },
102
+ },
103
+ item: {
104
+ display: 'flex',
105
+ alignItems: 'center',
106
+ marginBottom: 1,
107
+ '&:last-child': {
108
+ marginBottom: 0,
109
+ },
110
+ },
111
+ // A row that acts: the same layout, as a plain button (see `closeButton`
112
+ // for why not Material's), so a key naming groups of rows can focus one.
113
+ itemButton: {
114
+ font: 'inherit',
115
+ padding: 0,
116
+ border: 0,
117
+ background: 'none',
118
+ width: '100%',
119
+ textAlign: 'left',
120
+ color: 'inherit',
121
+ cursor: 'pointer',
122
+ '&:hover': {
123
+ background: theme.palette.action.hover,
124
+ },
125
+ },
126
+ swatches: {
127
+ display: 'flex',
128
+ gap: 2,
129
+ marginRight: 6,
130
+ flexShrink: 0,
131
+ },
132
+ label: {
133
+ whiteSpace: 'nowrap',
134
+ overflow: 'hidden',
135
+ textOverflow: 'ellipsis',
136
+ },
137
+ // was `<Link component="button" underline="hover">`, which is the same
138
+ // element with Material's typography on it — see `closeButton` above for why
139
+ // that mattered
140
+ toggle: {
141
+ font: 'inherit',
142
+ fontSize: 10,
143
+ display: 'block',
144
+ marginTop: 2,
145
+ padding: 0,
146
+ border: 0,
147
+ background: 'none',
148
+ textAlign: 'left',
149
+ color: theme.palette.primary.main,
150
+ cursor: 'pointer',
151
+ '&:hover': {
152
+ textDecoration: 'underline',
153
+ },
154
+ },
155
+ }));
156
+ const DEFAULT_MAX_ITEMS = 12;
157
+ const DEFAULT_MAX_WIDTH = 200;
158
+ // Side of one mark. The old fixed color box's size, kept so a legend of plain
159
+ // fills is pixel-identical to what shipped before marks existed.
160
+ const SWATCH = 12;
161
+ // One list of swatches with its own independent collapse state, so each section
162
+ // in a multi-section legend expands/collapses on its own.
163
+ const LegendItemList = observer(function LegendItemList({ items, maxItems, onItemClick, }) {
164
+ const { classes } = useStyles();
165
+ const [expanded, setExpanded] = useState(false);
166
+ const collapsible = items.length > maxItems;
167
+ const shown = collapsible && !expanded ? items.slice(0, maxItems) : items;
168
+ const hiddenCount = items.length - maxItems;
169
+ // Every row reserves the widest row's swatch column, so labels stay in one
170
+ // vertical line whether a row draws one mark or two — and so a color-less
171
+ // heading row indents like the swatches it heads, as the fixed-size box it
172
+ // replaces used to.
173
+ const columns = Math.max(1, ...shown.map(i => legendSwatches(i).length));
174
+ return (_jsxs(_Fragment, { children: [shown.map((item, i) => {
175
+ const row = (_jsxs(_Fragment, { children: [_jsx("div", { className: classes.swatches, style: { minWidth: columns * SWATCH + (columns - 1) * 2 }, children: legendSwatches(item).map(swatch => (_jsx("svg", { width: SWATCH, height: SWATCH, children: _jsx(LegendSwatchGlyph, { swatch: swatch, size: SWATCH }) }, `${swatch.color}-${swatch.mark ?? 'fill'}`))) }), _jsx("span", { className: classes.label, children: item.label })] }));
176
+ return onItemClick ? (_jsx("button", { type: "button", className: cx(classes.item, classes.itemButton), title: "Show only these rows", onClick: () => {
177
+ onItemClick(item);
178
+ }, children: row }, `${item.label}-${i}`)) : (_jsx("div", { className: classes.item, children: row }, `${item.label}-${i}`));
179
+ }), collapsible ? (_jsx("button", { type: "button", className: classes.toggle, onClick: () => {
180
+ setExpanded(!expanded);
181
+ }, children: expanded ? 'Show less' : `Show ${hiddenCount} more…` })) : null] }));
182
+ });
183
+ // Floating color-key overlay. Pass a flat `items` list for a single scheme, or
184
+ // `sections` to show several titled, individually-closable panels in one box
185
+ // (e.g. genotype colors vs. sample-grouping colors on the multi-sample variant
186
+ // display). Section titles + per-section close buttons only appear when there
187
+ // is more than one section, so a single-scheme legend looks unchanged. `title`
188
+ // is a heading for the whole box, shown regardless of section count.
189
+ //
190
+ // `onItemClick` makes the rows act — for a key whose entries name groups of
191
+ // rows, clicking one focuses those rows. It is handed the section too, so a
192
+ // display with several vocabularies can act on the one that names rows and
193
+ // leave the others inert.
194
+ const FloatingLegend = observer(function FloatingLegend({ items, sections, title, onDismiss, onDismissSection, onItemClick, maxItems = DEFAULT_MAX_ITEMS, maxWidth = DEFAULT_MAX_WIDTH, top = 10, children, }) {
195
+ const { classes } = useStyles();
196
+ const nonEmpty = nonEmptyLegendSections({ items, sections });
197
+ if (nonEmpty.length === 0 && !children) {
198
+ return null;
199
+ }
200
+ const multiSection = nonEmpty.length > 1;
201
+ // portal above the inter-region padding masks so the key isn't buried at
202
+ // whole-genome / multi-region scale (see TrackOverlayPortal)
203
+ return (_jsx(TrackOverlayPortal, { children: _jsxs("div", { className: cx(classes.legend, onDismiss && classes.withClose), style: { top, maxWidth }, "data-testid": "floating-legend", "data-gesture-owner": "true", children: [onDismiss ? (_jsx(Tooltip, { title: "Hide legend", children: _jsx("button", { type: "button", className: classes.closeButton, "aria-label": "Hide legend", onClick: () => {
204
+ onDismiss();
205
+ }, children: "\u00D7" }) })) : null, title ? _jsx("div", { className: classes.topTitle, children: title }) : null, children, nonEmpty.map(section => (_jsxs("div", { className: classes.section, children: [multiSection && section.title ? (_jsxs("div", { className: classes.sectionHeader, children: [_jsx("span", { className: classes.sectionTitle, children: section.title }), onDismissSection ? (_jsx(Tooltip, { title: `Hide ${section.title}`, children: _jsx("button", { type: "button", className: classes.sectionClose, "aria-label": `Hide ${section.title}`, onClick: () => {
206
+ onDismissSection(section.id);
207
+ }, children: "\u00D7" }) })) : null] })) : null, _jsx(LegendItemList, { items: section.items, maxItems: maxItems, onItemClick: onItemClick
208
+ ? item => {
209
+ onItemClick(item, section);
210
+ }
211
+ : undefined })] }, section.id)))] }) }));
212
+ });
213
+ export default FloatingLegend;
@@ -0,0 +1,26 @@
1
+ import type { MouseState, MouseTracker } from '@jbrowse/core/ui/useMouseTracking';
2
+ import type { ReactNode } from 'react';
3
+ /**
4
+ * The part of a display that follows the pointer — guides, a tooltip, a
5
+ * highlighted column — rendered off the chrome's `mouseTracker` in a leaf of
6
+ * its own, so a mousemove re-renders this and nothing else. Read in the
7
+ * component that mounts `DisplayChrome` instead, the position would re-render
8
+ * the chrome, its status container and every overlay under it on each move
9
+ * (see `useMouseTracking`).
10
+ *
11
+ * `rowsTopOffset` answers whether the pointer is over the rows rather than a
12
+ * band stacked above them, for a display that gates its crosshair to the rows:
13
+ * a guide drawn while the pointer is over a variant lane or a connector zone
14
+ * names a row the pointer is not on. The tooltip is usually not gated with it,
15
+ * which is why the child decides. `children` is also handed an absent state, so
16
+ * a layer that has to render while nothing is hovered — the matrix display's
17
+ * connector lines, with no column highlighted — can.
18
+ *
19
+ * Four displays used to write this component out, with the same paragraph over
20
+ * each.
21
+ */
22
+ export declare function PointerLayer({ mouseTracker, rowsTopOffset, children, }: {
23
+ mouseTracker: MouseTracker;
24
+ rowsTopOffset?: number;
25
+ children: (state: MouseState | undefined, inRows: boolean) => ReactNode;
26
+ }): import("react").JSX.Element;
@@ -0,0 +1,27 @@
1
+ import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
2
+ // deep subpath, never the `@jbrowse/core/ui` barrel: this package reaches no
3
+ // UI toolkit, and the barrel pulls MUI in
4
+ import { useMouseState } from '@jbrowse/core/ui/useMouseTracking';
5
+ /**
6
+ * The part of a display that follows the pointer — guides, a tooltip, a
7
+ * highlighted column — rendered off the chrome's `mouseTracker` in a leaf of
8
+ * its own, so a mousemove re-renders this and nothing else. Read in the
9
+ * component that mounts `DisplayChrome` instead, the position would re-render
10
+ * the chrome, its status container and every overlay under it on each move
11
+ * (see `useMouseTracking`).
12
+ *
13
+ * `rowsTopOffset` answers whether the pointer is over the rows rather than a
14
+ * band stacked above them, for a display that gates its crosshair to the rows:
15
+ * a guide drawn while the pointer is over a variant lane or a connector zone
16
+ * names a row the pointer is not on. The tooltip is usually not gated with it,
17
+ * which is why the child decides. `children` is also handed an absent state, so
18
+ * a layer that has to render while nothing is hovered — the matrix display's
19
+ * connector lines, with no column highlighted — can.
20
+ *
21
+ * Four displays used to write this component out, with the same paragraph over
22
+ * each.
23
+ */
24
+ export function PointerLayer({ mouseTracker, rowsTopOffset = 0, children, }) {
25
+ const state = useMouseState(mouseTracker);
26
+ return _jsx(_Fragment, { children: children(state, !!state && state.y > rowsTopOffset) });
27
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * #api
3
+ * The one box per display that owns the bottom-right corner.
4
+ *
5
+ * It lives beside the overlay contract rather than with the chrome that mounts
6
+ * it, because it is the half of that contract the types cannot carry:
7
+ * `DisplayChromeOverlays.BackgroundProgress` is the one state told to render an
8
+ * in-flow chip with no `position` and no corner offsets, and this is the box it
9
+ * is laid out in. A host writing their own display over `DisplayChromeBase`
10
+ * needs to be able to build that corner; while this sat one layer up in the LGV
11
+ * plugin, the rule was a paragraph they could only obey by hand.
12
+ *
13
+ * Two independent things want that corner and neither could see the other:
14
+ * the display's own control row (`BottomRightIndicators` — track sizing, the
15
+ * isoform notice, the solo chip) and the chrome's background-progress chip
16
+ * (`DisplayChromeOverlays.BackgroundProgress`, the status channel for work with
17
+ * no fetch behind it). Both portal into the *same* per-track overlay node, and
18
+ * both used to claim `bottom: 2; right: 2` there with their own
19
+ * `position: absolute` box — so they simply drew on top of each other, the
20
+ * controls winning on z-index and the status text disappearing under them.
21
+ *
22
+ * It has never been reachable: the two displays that render the control row
23
+ * (alignments, canvas) are not among the four that report a `ready`-phase
24
+ * status (clustering, on multi-wiggle / multi-row features / maf / the
25
+ * multi-sample variant pair). That is the *reason* to make it structural rather
26
+ * than to leave it — nothing on either side is aware of the constraint, so the
27
+ * first display to want both would find the bug, and it presents as a status
28
+ * message that silently never appears.
29
+ *
30
+ * `BottomRightIndicators` already described itself as "the single anchor point
31
+ * for every bottom-right overlay ... so they lay out as one row instead of each
32
+ * picking their own position and colliding". This makes that true of the chip
33
+ * too: the chrome renders the anchor, puts its chip in it, and publishes the
34
+ * node here so the display's row — rendered several components away, inside the
35
+ * chrome's body — lands in the same flex box instead of over it.
36
+ *
37
+ * `null` outside a chrome (a display an embedder mounts standalone, a unit
38
+ * test, the SVG export), where `BottomRightIndicators` keeps its own anchored
39
+ * box. That fallback is why this can be added without every consumer changing.
40
+ */
41
+ export declare const BottomRightCornerContext: import("react").Context<HTMLElement | null>;
42
+ /**
43
+ * Members are stacked, not laid side by side, and ordered explicitly.
44
+ *
45
+ * Stacked because the status chip is much wider than a control (it carries a
46
+ * label and a 120px bar): in one horizontal row it would shove the controls
47
+ * leftwards whenever a background job started and let them snap back when it
48
+ * finished.
49
+ *
50
+ * Ordered by `order` rather than by DOM position because one member arrives as
51
+ * a portal and the other as an ordinary child, and the relative order of those
52
+ * two inside the container is not something React documents. Only the control
53
+ * row needs a value: the status chip takes CSS's default 0, so anything the
54
+ * chrome adds to the corner later sorts above the controls unless it says
55
+ * otherwise — which is the right default, the controls being the thing whose
56
+ * position in the corner users learn.
57
+ */
58
+ export declare const BOTTOM_RIGHT_CONTROLS_ORDER = 1;
@@ -0,0 +1,59 @@
1
+ import { createContext } from 'react';
2
+ /**
3
+ * #api
4
+ * The one box per display that owns the bottom-right corner.
5
+ *
6
+ * It lives beside the overlay contract rather than with the chrome that mounts
7
+ * it, because it is the half of that contract the types cannot carry:
8
+ * `DisplayChromeOverlays.BackgroundProgress` is the one state told to render an
9
+ * in-flow chip with no `position` and no corner offsets, and this is the box it
10
+ * is laid out in. A host writing their own display over `DisplayChromeBase`
11
+ * needs to be able to build that corner; while this sat one layer up in the LGV
12
+ * plugin, the rule was a paragraph they could only obey by hand.
13
+ *
14
+ * Two independent things want that corner and neither could see the other:
15
+ * the display's own control row (`BottomRightIndicators` — track sizing, the
16
+ * isoform notice, the solo chip) and the chrome's background-progress chip
17
+ * (`DisplayChromeOverlays.BackgroundProgress`, the status channel for work with
18
+ * no fetch behind it). Both portal into the *same* per-track overlay node, and
19
+ * both used to claim `bottom: 2; right: 2` there with their own
20
+ * `position: absolute` box — so they simply drew on top of each other, the
21
+ * controls winning on z-index and the status text disappearing under them.
22
+ *
23
+ * It has never been reachable: the two displays that render the control row
24
+ * (alignments, canvas) are not among the four that report a `ready`-phase
25
+ * status (clustering, on multi-wiggle / multi-row features / maf / the
26
+ * multi-sample variant pair). That is the *reason* to make it structural rather
27
+ * than to leave it — nothing on either side is aware of the constraint, so the
28
+ * first display to want both would find the bug, and it presents as a status
29
+ * message that silently never appears.
30
+ *
31
+ * `BottomRightIndicators` already described itself as "the single anchor point
32
+ * for every bottom-right overlay ... so they lay out as one row instead of each
33
+ * picking their own position and colliding". This makes that true of the chip
34
+ * too: the chrome renders the anchor, puts its chip in it, and publishes the
35
+ * node here so the display's row — rendered several components away, inside the
36
+ * chrome's body — lands in the same flex box instead of over it.
37
+ *
38
+ * `null` outside a chrome (a display an embedder mounts standalone, a unit
39
+ * test, the SVG export), where `BottomRightIndicators` keeps its own anchored
40
+ * box. That fallback is why this can be added without every consumer changing.
41
+ */
42
+ export const BottomRightCornerContext = createContext(null);
43
+ /**
44
+ * Members are stacked, not laid side by side, and ordered explicitly.
45
+ *
46
+ * Stacked because the status chip is much wider than a control (it carries a
47
+ * label and a 120px bar): in one horizontal row it would shove the controls
48
+ * leftwards whenever a background job started and let them snap back when it
49
+ * finished.
50
+ *
51
+ * Ordered by `order` rather than by DOM position because one member arrives as
52
+ * a portal and the other as an ordinary child, and the relative order of those
53
+ * two inside the container is not something React documents. Only the control
54
+ * row needs a value: the status chip takes CSS's default 0, so anything the
55
+ * chrome adds to the corner later sorts above the controls unless it says
56
+ * otherwise — which is the right default, the controls being the thing whose
57
+ * position in the corner users learn.
58
+ */
59
+ export const BOTTOM_RIGHT_CONTROLS_ORDER = 1;
@@ -0,0 +1,7 @@
1
+ import type { DisplayChromeOverlays } from './chromeOverlays.ts';
2
+ export declare const DisplayChromeOverlayProvider: import("react").Provider<DisplayChromeOverlays | undefined>;
3
+ /**
4
+ * The set a provider installed, or `undefined` for "nobody asked". The caller
5
+ * supplies the default, which is what keeps this module free of one.
6
+ */
7
+ export declare function useChromeOverlayOverride(): DisplayChromeOverlays | undefined;
@@ -0,0 +1,44 @@
1
+ import { createContext, use } from 'react';
2
+ // The context alone, in a module of its own, so that consuming the seam does not
3
+ // import the thing the seam exists to replace.
4
+ //
5
+ // `DisplayChrome.tsx` is where the Material set is bound, and it used to hold
6
+ // this `createContext` too. That put 45 `@mui/*`-importing modules one hop from
7
+ // `DisplayUIProvider` — the component whose whole purpose is "I do not want
8
+ // Material UI in my app" — and the build agreed: the build-your-own site's
9
+ // "Removing Material UI" page carried 34 first-party eager modules importing
10
+ // `@mui/material` against 16 on the page that deliberately leaves the Material
11
+ // chrome on screen, and 53 KB gzip more. Nothing rendered them. A DOM census
12
+ // cannot see a module graph, so nothing said so.
13
+ //
14
+ // The rule that falls out, and it applies to any seam: **the override channel
15
+ // must not live in the module that binds the default.** Everything toolkit-free
16
+ // (`plainChromeOverlays`, `plainTrackControl`, `DisplayChromeBase`,
17
+ // `DisplayUIProvider`) reaches zero `@mui/*` modules, and
18
+ // `muiFreeSeam.test.ts` fails naming the import trail if that stops being true.
19
+ /**
20
+ * Runtime override for the overlay set. **Defaults to undefined, not to a
21
+ * component set** — that is deliberate. A plain default would silently degrade
22
+ * every display that renders outside a provider (unit tests, SVG export,
23
+ * breakpoint-split-view's overlayUtils), and that degradation is invisible: the
24
+ * display still works, it just stops looking like JBrowse. Undefined means
25
+ * "nobody asked", so `DisplayChrome` keeps its MUI set and nothing changes.
26
+ *
27
+ * This is the escape hatch for embedders who want JBrowse's *own* displays
28
+ * (wiggle, alignments, variants — all of which import `DisplayChrome` directly
29
+ * and so can't be redirected at the import level) to draw with their overlays:
30
+ * no MUI rendered, no ThemeProvider needed, no emotion in their page.
31
+ *
32
+ * Prefer `DisplayUIProvider`, which mounts this and the track-control context
33
+ * together and accepts a partial set. Reach for this one only to override
34
+ * exactly one of the two seams.
35
+ */
36
+ const DisplayChromeOverlayContext = createContext(undefined);
37
+ export const DisplayChromeOverlayProvider = DisplayChromeOverlayContext.Provider;
38
+ /**
39
+ * The set a provider installed, or `undefined` for "nobody asked". The caller
40
+ * supplies the default, which is what keeps this module free of one.
41
+ */
42
+ export function useChromeOverlayOverride() {
43
+ return use(DisplayChromeOverlayContext);
44
+ }