@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.
- package/LICENSE +201 -0
- package/README.md +433 -0
- package/esm/DisplayUIProvider.d.ts +70 -0
- package/esm/DisplayUIProvider.js +96 -0
- package/esm/FloatingLegend.d.ts +16 -0
- package/esm/FloatingLegend.js +213 -0
- package/esm/PointerLayer.d.ts +26 -0
- package/esm/PointerLayer.js +27 -0
- package/esm/bottomRightCorner.d.ts +58 -0
- package/esm/bottomRightCorner.js +59 -0
- package/esm/chromeOverlayContext.d.ts +7 -0
- package/esm/chromeOverlayContext.js +44 -0
- package/esm/chromeOverlays.d.ts +88 -0
- package/esm/chromeOverlays.js +1 -0
- package/esm/index.d.ts +22 -0
- package/esm/index.js +51 -0
- package/esm/isLiveModel.d.ts +21 -0
- package/esm/isLiveModel.js +24 -0
- package/esm/plainChromeOverlays.d.ts +18 -0
- package/esm/plainChromeOverlays.js +189 -0
- package/esm/tooLargeBannerText.d.ts +18 -0
- package/esm/tooLargeBannerText.js +34 -0
- package/esm/tooltip/Tooltip.d.ts +52 -0
- package/esm/tooltip/Tooltip.js +67 -0
- package/esm/tooltip/useTooltip.d.ts +50 -0
- package/esm/tooltip/useTooltip.js +116 -0
- package/esm/trackControl/plainTrackControl.d.ts +17 -0
- package/esm/trackControl/plainTrackControl.js +133 -0
- package/esm/trackControl/trackControlContext.d.ts +7 -0
- package/esm/trackControl/trackControlContext.js +30 -0
- package/esm/trackControl/types.d.ts +58 -0
- package/esm/trackControl/types.js +1 -0
- package/esm/trackControl/useTrackControlMenu.d.ts +51 -0
- package/esm/trackControl/useTrackControlMenu.js +163 -0
- package/esm/trackOverlay/TrackOverlayContext.d.ts +1 -0
- package/esm/trackOverlay/TrackOverlayContext.js +18 -0
- package/esm/trackOverlay/TrackOverlayPortal.d.ts +18 -0
- package/esm/trackOverlay/TrackOverlayPortal.js +66 -0
- package/esm/trackOverlay/TrackOverlaySlot.d.ts +54 -0
- package/esm/trackOverlay/TrackOverlaySlot.js +60 -0
- 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
|
+
}
|