@jbrowse/app-core 4.3.0 → 5.0.0-beta.10

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 (165) hide show
  1. package/esm/AppSession/AppRootModel.d.ts +30 -0
  2. package/esm/AppSession/AppRootModel.js +1 -0
  3. package/esm/AppSession/AppSessionMixin.d.ts +1324 -0
  4. package/esm/AppSession/AppSessionMixin.js +62 -0
  5. package/esm/AppSession/index.d.ts +2 -0
  6. package/esm/AppSession/index.js +1 -0
  7. package/esm/Assemblies/AssembliesMixin.d.ts +294 -0
  8. package/esm/Assemblies/AssembliesMixin.js +154 -0
  9. package/esm/Assemblies/index.d.ts +1 -2
  10. package/esm/Assemblies/index.js +1 -2
  11. package/esm/HistoryManagement/index.d.ts +11 -0
  12. package/esm/HistoryManagement/index.js +43 -10
  13. package/esm/JBrowseConfig/RootConfiguration.d.ts +123 -78
  14. package/esm/JBrowseConfig/RootConfiguration.js +79 -7
  15. package/esm/JBrowseConfig/defaultShareUrl.d.ts +1 -0
  16. package/esm/JBrowseConfig/defaultShareUrl.js +5 -0
  17. package/esm/JBrowseConfig/index.d.ts +175 -84
  18. package/esm/JBrowseConfig/index.js +64 -2
  19. package/esm/JBrowseModel/index.d.ts +175 -97
  20. package/esm/JBrowseModel/index.js +132 -31
  21. package/esm/JbApi/index.d.ts +2 -0
  22. package/esm/JbApi/index.js +1 -0
  23. package/esm/JbApi/jbApi.d.ts +2767 -0
  24. package/esm/JbApi/jbApi.js +1383 -0
  25. package/esm/RootMenu/index.d.ts +69 -3
  26. package/esm/RootMenu/index.js +96 -61
  27. package/esm/SessionSpec/index.d.ts +6 -0
  28. package/esm/SessionSpec/index.js +3 -0
  29. package/esm/SessionSpec/lgvUrlInit.d.ts +79 -0
  30. package/esm/SessionSpec/lgvUrlInit.js +161 -0
  31. package/esm/SessionSpec/loadSessionSpec.d.ts +75 -0
  32. package/esm/SessionSpec/loadSessionSpec.js +473 -0
  33. package/esm/SessionSpec/parseSessionSpecUrl.d.ts +7 -0
  34. package/esm/SessionSpec/parseSessionSpecUrl.js +181 -0
  35. package/esm/SessionSpec/types.d.ts +37 -0
  36. package/esm/SessionSpec/types.js +1 -0
  37. package/esm/WorkspaceLayout/LayoutRenderer.d.ts +24 -0
  38. package/esm/WorkspaceLayout/LayoutRenderer.js +185 -0
  39. package/esm/WorkspaceLayout/PanelView.d.ts +13 -0
  40. package/esm/WorkspaceLayout/PanelView.js +99 -0
  41. package/esm/WorkspaceLayout/TabStrip.d.ts +14 -0
  42. package/esm/WorkspaceLayout/TabStrip.js +226 -0
  43. package/esm/WorkspaceLayout/WorkspaceContainer.d.ts +16 -0
  44. package/esm/WorkspaceLayout/WorkspaceContainer.js +107 -0
  45. package/esm/WorkspaceLayout/WorkspacePanelActions.d.ts +24 -0
  46. package/esm/WorkspaceLayout/WorkspacePanelActions.js +120 -0
  47. package/esm/WorkspaceLayout/WorkspaceTab.d.ts +27 -0
  48. package/esm/WorkspaceLayout/WorkspaceTab.js +105 -0
  49. package/esm/WorkspaceLayout/dockviewTheme.d.ts +69 -0
  50. package/esm/WorkspaceLayout/dockviewTheme.js +82 -0
  51. package/esm/WorkspaceLayout/dropZone.d.ts +105 -0
  52. package/esm/WorkspaceLayout/dropZone.js +113 -0
  53. package/esm/WorkspaceLayout/index.d.ts +8 -0
  54. package/esm/WorkspaceLayout/index.js +4 -0
  55. package/esm/WorkspaceLayout/model.d.ts +260 -0
  56. package/esm/WorkspaceLayout/model.js +586 -0
  57. package/esm/WorkspaceLayout/panelChrome.d.ts +19 -0
  58. package/esm/WorkspaceLayout/panelChrome.js +4 -0
  59. package/esm/WorkspaceLayout/spec.d.ts +88 -0
  60. package/esm/WorkspaceLayout/spec.js +226 -0
  61. package/esm/WorkspaceLayout/splitter.d.ts +25 -0
  62. package/esm/WorkspaceLayout/splitter.js +37 -0
  63. package/esm/WorkspaceLayout/tree.d.ts +158 -0
  64. package/esm/WorkspaceLayout/tree.js +379 -0
  65. package/esm/WorkspaceLayout/useLayoutDrag.d.ts +34 -0
  66. package/esm/WorkspaceLayout/useLayoutDrag.js +232 -0
  67. package/esm/index.d.ts +18 -9
  68. package/esm/index.js +16 -9
  69. package/esm/menus.d.ts +55 -68
  70. package/esm/menus.js +117 -86
  71. package/esm/queryParams.d.ts +9 -0
  72. package/esm/queryParams.js +93 -0
  73. package/esm/sessionModelFactory.d.ts +18 -0
  74. package/esm/sessionModelFactory.js +1 -0
  75. package/esm/ui/App/App.d.ts +3 -14
  76. package/esm/ui/App/App.js +39 -19
  77. package/esm/ui/App/AppFab.d.ts +12 -3
  78. package/esm/ui/App/AppFab.js +15 -8
  79. package/esm/ui/App/AppToolbar.d.ts +2 -14
  80. package/esm/ui/App/AppToolbar.js +45 -31
  81. package/esm/ui/App/ClassicViewsContainer.d.ts +4 -10
  82. package/esm/ui/App/ClassicViewsContainer.js +9 -8
  83. package/esm/ui/App/DialogQueue.d.ts +17 -1
  84. package/esm/ui/App/DialogQueue.js +30 -1
  85. package/esm/ui/App/Drawer.d.ts +1 -1
  86. package/esm/ui/App/Drawer.js +10 -39
  87. package/esm/ui/App/DrawerHeader.d.ts +2 -3
  88. package/esm/ui/App/DrawerHeader.js +24 -11
  89. package/esm/ui/App/DrawerHeaderHelpButton.d.ts +1 -1
  90. package/esm/ui/App/DrawerHeaderHelpButton.js +3 -3
  91. package/esm/ui/App/DrawerHeaderHelpDialog.d.ts +2 -2
  92. package/esm/ui/App/DrawerHeaderHelpDialog.js +3 -4
  93. package/esm/ui/App/DrawerWidget.d.ts +1 -1
  94. package/esm/ui/App/DrawerWidget.js +9 -21
  95. package/esm/ui/App/JBrowseTabMenu.d.ts +2 -3
  96. package/esm/ui/App/JBrowseTabMenu.js +24 -5
  97. package/esm/ui/App/ThemeModeButton.d.ts +12 -0
  98. package/esm/ui/App/ThemeModeButton.js +34 -0
  99. package/esm/ui/App/ViewButtons.d.ts +2 -4
  100. package/esm/ui/App/ViewButtons.js +15 -3
  101. package/esm/ui/App/ViewContainer.d.ts +3 -2
  102. package/esm/ui/App/ViewContainer.js +128 -24
  103. package/esm/ui/App/ViewContainerTitle.d.ts +1 -1
  104. package/esm/ui/App/ViewContainerTitle.js +21 -11
  105. package/esm/ui/App/ViewErrorFallback.d.ts +18 -0
  106. package/esm/ui/App/ViewErrorFallback.js +42 -0
  107. package/esm/ui/App/ViewHeader.d.ts +3 -4
  108. package/esm/ui/App/ViewHeader.js +28 -17
  109. package/esm/ui/App/ViewLauncher.d.ts +2 -8
  110. package/esm/ui/App/ViewLauncher.js +7 -5
  111. package/esm/ui/App/ViewMenu.d.ts +3 -5
  112. package/esm/ui/App/ViewMenu.js +87 -92
  113. package/esm/ui/App/ViewStack.d.ts +7 -0
  114. package/esm/ui/App/ViewStack.js +48 -0
  115. package/esm/ui/App/ViewWrapper.d.ts +15 -1
  116. package/esm/ui/App/ViewWrapper.js +26 -10
  117. package/esm/ui/App/ViewsContainer.d.ts +4 -11
  118. package/esm/ui/App/ViewsContainer.js +6 -9
  119. package/esm/ui/App/index.d.ts +2 -1
  120. package/esm/ui/App/index.js +3 -2
  121. package/esm/ui/App/lazyChunk.d.ts +13 -0
  122. package/esm/ui/App/lazyChunk.js +91 -0
  123. package/esm/ui/App/lazyParts.d.ts +32 -0
  124. package/esm/ui/App/lazyParts.js +8 -0
  125. package/esm/ui/App/preloadAppShell.d.ts +14 -0
  126. package/esm/ui/App/preloadAppShell.js +41 -0
  127. package/esm/ui/App/types.d.ts +17 -3
  128. package/esm/ui/App/types.js +1 -1
  129. package/esm/ui/App/useViewVisibility.d.ts +35 -0
  130. package/esm/ui/App/useViewVisibility.js +88 -0
  131. package/esm/ui/App/viewTitle.d.ts +12 -0
  132. package/esm/ui/App/viewTitle.js +17 -0
  133. package/esm/ui/index.d.ts +1 -1
  134. package/esm/ui/index.js +1 -1
  135. package/package.json +12 -17
  136. package/esm/AppFocus/index.d.ts +0 -5
  137. package/esm/AppFocus/index.js +0 -12
  138. package/esm/Assemblies/SessionAssembliesMixin.d.ts +0 -92
  139. package/esm/Assemblies/SessionAssembliesMixin.js +0 -53
  140. package/esm/Assemblies/TemporaryAssembliesMixin.d.ts +0 -10
  141. package/esm/Assemblies/TemporaryAssembliesMixin.js +0 -37
  142. package/esm/DockviewLayout/index.d.ts +0 -35
  143. package/esm/DockviewLayout/index.js +0 -111
  144. package/esm/ui/App/DockviewContext.d.ts +0 -22
  145. package/esm/ui/App/DockviewContext.js +0 -29
  146. package/esm/ui/App/DockviewLeftHeaderActions.d.ts +0 -2
  147. package/esm/ui/App/DockviewLeftHeaderActions.js +0 -58
  148. package/esm/ui/App/DockviewRightHeaderActions.d.ts +0 -2
  149. package/esm/ui/App/DockviewRightHeaderActions.js +0 -100
  150. package/esm/ui/App/DrawerControls.d.ts +0 -5
  151. package/esm/ui/App/DrawerControls.js +0 -25
  152. package/esm/ui/App/DrawerWidgetSelector.d.ts +0 -5
  153. package/esm/ui/App/DrawerWidgetSelector.js +0 -47
  154. package/esm/ui/App/JBrowseViewPanel.d.ts +0 -8
  155. package/esm/ui/App/JBrowseViewPanel.js +0 -40
  156. package/esm/ui/App/JBrowseViewTab.d.ts +0 -8
  157. package/esm/ui/App/JBrowseViewTab.js +0 -97
  158. package/esm/ui/App/ModalWidget.d.ts +0 -6
  159. package/esm/ui/App/ModalWidget.js +0 -48
  160. package/esm/ui/App/TiledViewsContainer.d.ts +0 -6
  161. package/esm/ui/App/TiledViewsContainer.js +0 -366
  162. package/esm/ui/App/copyView.d.ts +0 -1
  163. package/esm/ui/App/copyView.js +0 -29
  164. package/esm/ui/App/dockviewUtils.d.ts +0 -46
  165. package/esm/ui/App/dockviewUtils.js +0 -55
@@ -0,0 +1,37 @@
1
+ import type { LayoutSpecNode } from '../WorkspaceLayout/spec.ts';
2
+ import type { TrackInit } from '@jbrowse/core/util/tracks';
3
+ export interface ViewSpec {
4
+ type: string;
5
+ id?: string;
6
+ displayName?: string;
7
+ tracks?: TrackInit[];
8
+ assembly?: string;
9
+ /**
10
+ * The live view's spelling of the key above, accepted because that is where
11
+ * a spec gets written from: one name is the `assembly`, and several are
12
+ * refused naming it.
13
+ */
14
+ assemblyNames?: string[];
15
+ loc?: string;
16
+ /**
17
+ * @deprecated v4's nesting. Write every setting directly on the view object;
18
+ * this is unwrapped on the way in and warns.
19
+ */
20
+ init?: Record<string, unknown>;
21
+ }
22
+ /**
23
+ * A spec's `layout`: the one layout shape, with a leaf's `views` counting into
24
+ * the spec's own `views` array (an index names every view that entry created)
25
+ * or naming a view id the spec pinned with `id`.
26
+ *
27
+ * ```json
28
+ * {
29
+ * "direction": "horizontal",
30
+ * "children": [
31
+ * { "views": [0, 1], "size": 70 },
32
+ * { "views": [2], "size": 30 }
33
+ * ]
34
+ * }
35
+ * ```
36
+ */
37
+ export type LayoutNode = LayoutSpecNode;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,24 @@
1
+ import type { WorkspaceLayout } from './model.ts';
2
+ import type { PanelChrome } from './panelChrome.ts';
3
+ import type { LayoutTree } from './tree.ts';
4
+ import type { DragState } from './useLayoutDrag.ts';
5
+ /**
6
+ * The layout, rendered. There is no imperative api and no event to listen to:
7
+ * the tree is state, this is a function of it, and a gesture is an action.
8
+ *
9
+ * Sizes are `flex-grow`, which is why there is no resize handling anywhere in
10
+ * here. A branch's children divide its space in proportion to their `size`
11
+ * whatever that space becomes, so a window resize is the browser's problem —
12
+ * this is the layout maths dockview does in pixels, and it is the part of a
13
+ * grid engine most likely to be got subtly wrong.
14
+ */
15
+ interface Props {
16
+ node: LayoutTree;
17
+ layout: WorkspaceLayout;
18
+ /** the app's half of a panel, forwarded unchanged all the way down */
19
+ chrome: PanelChrome;
20
+ /** the in-flight drag, so the cell under the pointer can show where it lands */
21
+ drag?: DragState;
22
+ }
23
+ export declare const LayoutRenderer: (props: Props) => import("react").JSX.Element;
24
+ export {};
@@ -0,0 +1,185 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Fragment, useCallback, useRef } from 'react';
3
+ import { makeStyles } from '@jbrowse/core/util/tss-react';
4
+ import { observer } from 'mobx-react';
5
+ import { PanelView } from './PanelView.js';
6
+ import { dv } from './dockviewTheme.js';
7
+ import { pairSpan, withBoundaryAt } from './splitter.js';
8
+ import { isBranch } from './tree.js';
9
+ /**
10
+ * A node's share of its parent's space — the whole grid engine. The `min*: 0`
11
+ * are what stop a flex item refusing to shrink below its content; without them
12
+ * a wide view pushes its own cell past its share.
13
+ */
14
+ function paneStyle(size) {
15
+ return {
16
+ display: 'flex',
17
+ flexGrow: size,
18
+ flexBasis: 0,
19
+ minWidth: 0,
20
+ minHeight: 0,
21
+ };
22
+ }
23
+ export const LayoutRenderer = observer(function LayoutRenderer(props) {
24
+ const { node, layout, drag } = props;
25
+ if (!isBranch(node)) {
26
+ return (_jsx("div", { style: paneStyle(node.size), children: _jsx(PanelView, { panel: node, layout: layout, chrome: props.chrome, drop: drag?.panelId === node.id ? drag : undefined }) }));
27
+ }
28
+ return (_jsx("div", { style: { ...paneStyle(node.size), flexDirection: node.direction }, children: node.children.map((child, i) => (_jsxs(Fragment, { children: [i > 0 && _jsx(Splitter, { branch: node, index: i, layout: layout }), _jsx(LayoutRenderer, { ...props, node: child })] }, child.id))) }));
29
+ });
30
+ /**
31
+ * The pixels the two panes either side of a handle occupy. Read from the DOM
32
+ * because sizes are shares of a container whose width only the browser knows.
33
+ * The panes are the handle's siblings minus the other handles, which is what
34
+ * `data-splitter` marks.
35
+ */
36
+ function measurePairPx(handle, index, horizontal) {
37
+ const panes = [...(handle.parentElement?.children ?? [])].filter(el => !Object.hasOwn(el.dataset, 'splitter'));
38
+ const before = panes[index - 1]?.getBoundingClientRect();
39
+ const after = panes[index]?.getBoundingClientRect();
40
+ if (!before || !after) {
41
+ return 0;
42
+ }
43
+ return horizontal ? before.width + after.width : before.height + after.height;
44
+ }
45
+ // dockview's sash is a transparent grab strip with a 1px separator line drawn
46
+ // down the middle of it — the line is what you see, the 4px is what you can
47
+ // hit. Its dark theme deliberately gives the sash no hover colour at all.
48
+ const useSplitterStyles = makeStyles()({
49
+ splitter: {
50
+ flex: `0 0 ${dv.sashSize}px`,
51
+ position: 'relative',
52
+ background: 'transparent',
53
+ touchAction: 'none',
54
+ // it is focusable, so it has to show focus — dockview's sash deliberately
55
+ // has no HOVER colour, which is a different thing and still holds
56
+ '&:focus-visible': {
57
+ outline: `2px solid ${dv.edgeDockIndicatorColor}`,
58
+ outlineOffset: -1,
59
+ },
60
+ '&::before': {
61
+ content: '""',
62
+ position: 'absolute',
63
+ background: dv.separatorBorder,
64
+ },
65
+ },
66
+ horizontal: {
67
+ '&::before': { top: 0, bottom: 0, left: '50%', width: 1 },
68
+ },
69
+ vertical: {
70
+ '&::before': { left: 0, right: 0, top: '50%', height: 1 },
71
+ },
72
+ });
73
+ /**
74
+ * Drags the boundary between children `index - 1` and `index`.
75
+ *
76
+ * The DOM half only: it measures, and hands position and pair span to
77
+ * `splitter.ts`, which decides where the boundary is allowed to land. Same
78
+ * split as the drag — geometry pure, wiring dumb.
79
+ */
80
+ const Splitter = observer(function Splitter({ branch, index, layout, }) {
81
+ const { classes, cx } = useSplitterStyles();
82
+ const dragRef = useRef(undefined);
83
+ const horizontal = branch.direction === 'row';
84
+ /**
85
+ * The same three rules the tab drag owns (see `useLayoutDrag`), on the handle
86
+ * that was written before they were written down.
87
+ *
88
+ * A resize starts on the PRIMARY button of the primary pointer. A right-press
89
+ * armed the drag and then lost its `pointerup` to the native context menu,
90
+ * and capture routes every later move back here — so a button-less pointer
91
+ * went on resizing from the start position the right-press recorded.
92
+ */
93
+ const onPointerDown = useCallback((event) => {
94
+ if (event.button !== 0 || !event.isPrimary) {
95
+ return;
96
+ }
97
+ const pairPx = measurePairPx(event.currentTarget, index, horizontal);
98
+ if (pairPx <= 0) {
99
+ return;
100
+ }
101
+ dragRef.current = {
102
+ pointerId: event.pointerId,
103
+ axis: horizontal ? 'clientX' : 'clientY',
104
+ start: horizontal ? event.clientX : event.clientY,
105
+ pairPx,
106
+ startSizes: branch.children.map(c => c.size),
107
+ };
108
+ event.currentTarget.setPointerCapture(event.pointerId);
109
+ }, [branch, index, horizontal]);
110
+ // One `pointerId` per gesture: a second finger landing on the handle steered
111
+ // the first one's resize from the first one's start position, and its release
112
+ // ended the gesture the first one was still holding.
113
+ const onPointerMove = useCallback((event) => {
114
+ const drag = dragRef.current;
115
+ if (drag?.pointerId !== event.pointerId) {
116
+ return;
117
+ }
118
+ // the pointer's travel as a fraction of the pair's pixels, applied to the
119
+ // pair's share — so the handle tracks the pointer whatever the units are
120
+ const delta = (event[drag.axis] - drag.start) / drag.pairPx;
121
+ const sizes = drag.startSizes;
122
+ layout.setSizes(branch.id, withBoundaryAt(sizes, index, sizes[index - 1] + delta * pairSpan(sizes, index), drag.pairPx));
123
+ }, [branch.id, index, layout]);
124
+ const endDrag = useCallback(() => {
125
+ dragRef.current = undefined;
126
+ }, []);
127
+ const onPointerUp = useCallback((event) => {
128
+ if (dragRef.current && dragRef.current.pointerId !== event.pointerId) {
129
+ return;
130
+ }
131
+ endDrag();
132
+ event.currentTarget.releasePointerCapture(event.pointerId);
133
+ }, [endDrag]);
134
+ // `pointercancel` ends the gesture — a touch long-press opens the platform's
135
+ // context menu and cancels the pointer with no `pointerup` behind it. A
136
+ // browser also fires `lostpointercapture` for that, so this is the rule
137
+ // stated where it belongs rather than inferred from a second event.
138
+ const onPointerCancel = useCallback((event) => {
139
+ if (dragRef.current?.pointerId === event.pointerId) {
140
+ endDrag();
141
+ }
142
+ }, [endDrag]);
143
+ // A `separator` that can be moved is focusable and takes the arrow keys; one
144
+ // that cannot is just decoration and should not claim the role. This one
145
+ // claimed it while being neither focusable nor operable, which announces an
146
+ // affordance to a screen reader that its user then cannot find.
147
+ const onKeyDown = useCallback((event) => {
148
+ const sizes = branch.children.map(c => c.size);
149
+ const pair = pairSpan(sizes, index);
150
+ const decrease = horizontal ? 'ArrowLeft' : 'ArrowUp';
151
+ const increase = horizontal ? 'ArrowRight' : 'ArrowDown';
152
+ // 2% of the PAIR per press, matching the drag: the boundary moves within
153
+ // the two panes either side of it and every other pane holds still
154
+ const step = pair * 0.02;
155
+ let moved;
156
+ if (event.key === decrease) {
157
+ moved = sizes[index - 1] - step;
158
+ }
159
+ else if (event.key === increase) {
160
+ moved = sizes[index - 1] + step;
161
+ }
162
+ else if (event.key === 'Home') {
163
+ moved = 0;
164
+ }
165
+ else if (event.key === 'End') {
166
+ moved = pair;
167
+ }
168
+ if (moved === undefined) {
169
+ return;
170
+ }
171
+ event.preventDefault();
172
+ // Home/End mean "as far as this goes", which is the minimum rather than
173
+ // zero — the same stop the drag hits
174
+ layout.setSizes(branch.id, withBoundaryAt(sizes, index, moved, measurePairPx(event.currentTarget, index, horizontal)));
175
+ }, [branch, index, horizontal, layout]);
176
+ // the pane BEFORE the handle, as a percentage of the pair it divides — which
177
+ // is what the handle actually moves
178
+ const sizes = branch.children.map(c => c.size);
179
+ const valueNow = Math.round((sizes[index - 1] / pairSpan(sizes, index)) * 100);
180
+ return (_jsx("div", { "data-splitter": true, role: "separator", tabIndex: 0, "aria-orientation": horizontal ? 'vertical' : 'horizontal', "aria-label": horizontal ? 'Resize panels' : 'Resize rows', "aria-valuenow": valueNow, "aria-valuemin": 0, "aria-valuemax": 100, onKeyDown: onKeyDown, onPointerDown: onPointerDown, onPointerMove: onPointerMove, onPointerUp: onPointerUp, onPointerCancel: onPointerCancel,
181
+ // capture can end without a pointerup — the browser drops it if the
182
+ // element is removed, and then the next pointer that merely passes over
183
+ // the handle would go on resizing from the stale start sizes
184
+ onLostPointerCapture: endDrag, className: cx(classes.splitter, horizontal ? classes.horizontal : classes.vertical), style: { cursor: horizontal ? 'ew-resize' : 'ns-resize' } }));
185
+ });
@@ -0,0 +1,13 @@
1
+ import type { DropTarget } from './dropZone.ts';
2
+ import type { WorkspaceLayout } from './model.ts';
3
+ import type { PanelChrome } from './panelChrome.ts';
4
+ import type { PanelNode } from './tree.ts';
5
+ interface PanelViewProps {
6
+ panel: PanelNode;
7
+ layout: WorkspaceLayout;
8
+ chrome: PanelChrome;
9
+ /** where an in-flight drag would land in THIS cell, if it is over this one */
10
+ drop?: DropTarget;
11
+ }
12
+ export declare const PanelView: ({ panel, layout, chrome, drop, }: PanelViewProps) => import("react").JSX.Element;
13
+ export {};
@@ -0,0 +1,99 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { persistentScrollbarStyle } from '@jbrowse/core/ui/persistentScrollbarStyle';
3
+ import { useScrollPortHeightVar } from '@jbrowse/core/util/hooks';
4
+ import { makeStyles } from '@jbrowse/core/util/tss-react';
5
+ import { observer } from 'mobx-react';
6
+ import { TabStrip } from './TabStrip.js';
7
+ import { dv } from './dockviewTheme.js';
8
+ import { indicatorRect } from './dropZone.js';
9
+ import { tabDomId, tabPanelDomId } from './panelChrome.js';
10
+ import { activeTabIn } from './tree.js';
11
+ /**
12
+ * One cell of the grid: a tab strip, and the content of whichever tab is
13
+ * showing.
14
+ *
15
+ * The strip and its state are `TabStrip`; this is the frame around them, and
16
+ * has no state of its own — it is a function of its node, which is the whole
17
+ * point of the layout being one MST tree.
18
+ *
19
+ * The line between chrome and content is the strip's lower edge: everything
20
+ * above it is dockview's dark theme in either JBrowse theme (see
21
+ * `dockviewTheme.ts`), everything below it follows the app's.
22
+ */
23
+ const useStyles = makeStyles()(theme => ({
24
+ // `flex: 1` and `minWidth: 0` are load-bearing, not tidiness. This is a child
25
+ // of a `display: flex` row, so without them its width is its CONTENT's width
26
+ // — and a view measures its container to decide how wide to draw
27
+ // (useWidthSetter), so the two settle at the view's intrinsic width and the
28
+ // panel renders at half the window with dead space beside it.
29
+ panel: {
30
+ position: 'relative',
31
+ display: 'flex',
32
+ flexDirection: 'column',
33
+ flex: 1,
34
+ minWidth: 0,
35
+ minHeight: 0,
36
+ // the surface the views sit on, so it is the app's background and not
37
+ // dockview's — see `dockviewTheme.ts`. A cell is taller than its views
38
+ // whenever they don't fill it, so this is most of what a panel shows.
39
+ background: theme.palette.background.default,
40
+ },
41
+ content: {
42
+ flex: 1,
43
+ minWidth: 0,
44
+ minHeight: 0,
45
+ overflow: 'auto',
46
+ ...persistentScrollbarStyle(theme),
47
+ },
48
+ indicator: {
49
+ position: 'absolute',
50
+ pointerEvents: 'none',
51
+ // above this panel's own content and nothing else — the panel is
52
+ // position:relative, so an app-wide z-index would let it cover menus
53
+ zIndex: 1,
54
+ background: dv.dragOverBackground,
55
+ outline: `1px solid ${dv.edgeDockIndicatorColor}`,
56
+ },
57
+ // where a tab dragged onto the strip would land
58
+ caret: {
59
+ position: 'absolute',
60
+ pointerEvents: 'none',
61
+ zIndex: 1,
62
+ top: 0,
63
+ width: 2,
64
+ height: dv.tabsHeight,
65
+ background: dv.edgeDockIndicatorColor,
66
+ },
67
+ }));
68
+ export const PanelView = observer(function PanelView({ panel, layout, chrome, drop, }) {
69
+ const { classes } = useStyles();
70
+ const active = activeTabIn(panel);
71
+ const contentRef = useScrollPortHeightVar();
72
+ return (_jsxs("div", { "data-panel-id": panel.id, className: classes.panel, onPointerDownCapture: () => {
73
+ // clicking anywhere in a cell makes it the one a new view lands in.
74
+ // Capture, so it still registers when the click is consumed by a
75
+ // control inside the view.
76
+ if (layout.activePanelId !== panel.id) {
77
+ layout.setActivePanelId(panel.id);
78
+ }
79
+ }, children: [_jsx(TabStrip, { panel: panel, layout: layout, chrome: chrome, active: active, groupActive: layout.activePanelId === panel.id }), _jsx("div", { role: "tabpanel", id: tabPanelDomId(panel.id), "aria-labelledby": active ? tabDomId(active.id) : undefined, className: classes.content, ref: contentRef, children: active ? chrome.renderTabContent(active) : null }), _jsx(DropIndicator, { drop: drop, classes: classes })] }));
80
+ });
81
+ /**
82
+ * Where a drop would land, drawn over the cell.
83
+ *
84
+ * A strip drop gets a caret at the gap rather than a wash over half the cell:
85
+ * it is a position in the tab order, and shading half the panel would say
86
+ * "split this cell", which is the wrong thing.
87
+ */
88
+ const DropIndicator = observer(function DropIndicator({ drop, classes, }) {
89
+ if (drop?.strip) {
90
+ return (_jsx("div", { "data-drop-caret": drop.strip.index, className: classes.caret,
91
+ // A scrolled strip's left-most gap has a negative panel-relative x, and
92
+ // the panel has no `overflow: hidden` — the caret drew into the
93
+ // neighbour. Clamped here because `stripDropAt` is deliberately
94
+ // ignorant of which coordinate space it was handed, so it cannot know
95
+ // where zero is.
96
+ style: { left: Math.max(0, drop.strip.left) } }));
97
+ }
98
+ return drop ? (_jsx("div", { "data-drop-indicator": drop.zone, className: classes.indicator, style: indicatorRect(drop.zone) })) : null;
99
+ });
@@ -0,0 +1,14 @@
1
+ import type { WorkspaceLayout } from './model.ts';
2
+ import type { PanelChrome } from './panelChrome.ts';
3
+ import type { PanelNode, TabNode } from './tree.ts';
4
+ interface TabStripProps {
5
+ panel: PanelNode;
6
+ layout: WorkspaceLayout;
7
+ chrome: PanelChrome;
8
+ /** the tab this panel is showing — the strip does not decide it */
9
+ active: TabNode | undefined;
10
+ /** whether this is the cell a newly launched view lands in */
11
+ groupActive: boolean;
12
+ }
13
+ export declare const TabStrip: ({ panel, layout, chrome, active, groupActive, }: TabStripProps) => import("react").JSX.Element;
14
+ export {};
@@ -0,0 +1,226 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useRef, useState } from 'react';
3
+ import { makeStyles } from '@jbrowse/core/util/tss-react';
4
+ import { observer } from 'mobx-react';
5
+ import { dv, tabColors } from './dockviewTheme.js';
6
+ import { tabDomId, tabPanelDomId } from './panelChrome.js';
7
+ /**
8
+ * A panel's tab strip: the tabs, and the panel's own buttons beside them.
9
+ *
10
+ * Everything with state in a cell lives here — the roving tabindex, the wheel
11
+ * translation, the scroll-into-view — which leaves `PanelView` a pure function
12
+ * of its node. The strip is dockview's dark theme, transcribed in
13
+ * `dockviewTheme.ts`; see there for why it is fixed rather than derived from
14
+ * the MUI theme.
15
+ */
16
+ const useStyles = makeStyles()({
17
+ strip: {
18
+ display: 'flex',
19
+ flexShrink: 0,
20
+ boxSizing: 'border-box',
21
+ height: dv.tabsHeight,
22
+ fontSize: dv.tabsFontSize,
23
+ background: dv.tabsBackground,
24
+ },
25
+ tabs: {
26
+ display: 'flex',
27
+ overflowX: 'auto',
28
+ // shrink but never GROW: the panel actions are the next child of the strip,
29
+ // so a tab list that takes the leftover space pushes the `+` to the far
30
+ // right edge, arbitrarily far from the tab it acts on. `0 1 auto` sizes the
31
+ // list to its tabs and leaves the `+` beside the last one, and the shrink
32
+ // half is what makes it scroll here rather than push the `+` off the strip.
33
+ flex: '0 1 auto',
34
+ minWidth: 0,
35
+ // the strip is chrome, so a scrollbar across it would be noise
36
+ scrollbarWidth: 'none',
37
+ '&::-webkit-scrollbar': { display: 'none' },
38
+ },
39
+ tab: {
40
+ display: 'flex',
41
+ alignItems: 'center',
42
+ flexShrink: 0,
43
+ boxSizing: 'border-box',
44
+ maxWidth: 240,
45
+ padding: '0.25rem 0.5rem',
46
+ cursor: 'pointer',
47
+ userSelect: 'none',
48
+ touchAction: 'none',
49
+ borderRight: `1px solid ${dv.tabDividerColor}`,
50
+ '&:hover .jbrowse-tab-menu': { visibility: 'visible' },
51
+ // a keyboard user needs the ⋮ too, and hover is not a thing they can do
52
+ '&:focus-within .jbrowse-tab-menu': { visibility: 'visible' },
53
+ // the strip is dark in either theme, so the focus ring is the drop
54
+ // indicator's blue rather than the UA default black-on-dark
55
+ '&:focus-visible': {
56
+ outline: `2px solid ${dv.edgeDockIndicatorColor}`,
57
+ outlineOffset: -2,
58
+ },
59
+ },
60
+ });
61
+ /**
62
+ * A wheel event's delta in PIXELS.
63
+ *
64
+ * `deltaY` is only pixels when `deltaMode` says so. Firefox reports whole lines
65
+ * for a mouse wheel — `deltaMode: 1`, `deltaY: ±3` — where Chrome reports
66
+ * `deltaMode: 0`, `deltaY: ±100`. Using the raw number moves the strip three
67
+ * pixels per notch there: scrolling, technically, and unusable in practice.
68
+ *
69
+ * The line height is nominal rather than measured. `getComputedStyle` per wheel
70
+ * event to resolve a `line-height: normal` that is itself font-dependent buys
71
+ * accuracy nobody can perceive in a scroll gesture, and the strip is a fixed
72
+ * 35px of chrome in one size.
73
+ */
74
+ const WHEEL_LINE_PX = 16;
75
+ function wheelDeltaPixels(event, pageSize) {
76
+ switch (event.deltaMode) {
77
+ case 1:
78
+ return event.deltaY * WHEEL_LINE_PX;
79
+ case 2:
80
+ return event.deltaY * pageSize;
81
+ default:
82
+ return event.deltaY;
83
+ }
84
+ }
85
+ export const TabStrip = observer(function TabStrip({ panel, layout, chrome, active, groupActive, }) {
86
+ const { classes } = useStyles();
87
+ const { renderPanelActions } = chrome;
88
+ // Roving tabindex: the strip is ONE tab stop, and the arrow keys move within
89
+ // it. Focus is tracked separately from selection because activation here is
90
+ // manual (see `onKeyDown`), so the two genuinely differ while arrowing.
91
+ const stripRef = useRef(null);
92
+ const [focusedTabId, setFocusedTabId] = useState(undefined);
93
+ const roving = panel.tabs.find(t => t.id === focusedTabId)?.id ?? active?.id;
94
+ // by dataset rather than an interpolated attribute selector: a tab id is
95
+ // nanoid output and would need CSS.escape, which jsdom does not have
96
+ function tabElement(tabId) {
97
+ return [...(stripRef.current?.children ?? [])].find(child => child.dataset.tabId === tabId);
98
+ }
99
+ function focusTab(tabId) {
100
+ setFocusedTabId(tabId);
101
+ // focusing scrolls it into view on its own, which is the whole reason the
102
+ // keyboard could always reach an overflowing strip when the mouse could not
103
+ tabElement(tabId)?.focus();
104
+ }
105
+ /**
106
+ * Keep the shown tab in view.
107
+ *
108
+ * The strip scrolls, so a tab can become current while sitting outside it —
109
+ * `+` on a full strip appends a tab, makes it active, and leaves the user
110
+ * looking at the tabs it scrolled past. Clicking never needs this (you
111
+ * clicked something visible) and arrowing gets it from `focus()`, so this is
112
+ * for the tab that becomes current without being touched.
113
+ *
114
+ * `block: 'nearest'` so it cannot scroll an ancestor vertically. jsdom has no
115
+ * scrollIntoView at all, but `config/jest/scrollIntoView.js` already no-ops it
116
+ * for the whole suite — don't add an optional call for that, it reads as a
117
+ * browser that might not have the method.
118
+ */
119
+ useEffect(() => {
120
+ tabElement(active?.id)?.scrollIntoView({
121
+ block: 'nearest',
122
+ inline: 'nearest',
123
+ });
124
+ }, [active?.id]);
125
+ /**
126
+ * MANUAL activation: arrows move focus, Enter/Space selects.
127
+ *
128
+ * The automatic form (arrowing selects as it goes) is the more common reading
129
+ * of the tabs pattern, and is wrong here for the reason WAI-ARIA names as its
130
+ * exception — showing a tab is expensive. Only the selected tab's views are
131
+ * mounted, and a JBrowse view costs a WebGL2 context per display against a
132
+ * ceiling of 16, so arrowing across five tabs would build and tear down five
133
+ * sets of them to pass through.
134
+ */
135
+ function onKeyDown(event, tabId) {
136
+ const ids = panel.tabs.map(t => t.id);
137
+ const i = ids.indexOf(tabId);
138
+ const step = event.key === 'ArrowRight' ? 1 : event.key === 'ArrowLeft' ? -1 : 0;
139
+ let next;
140
+ if (step !== 0) {
141
+ next = ids[(i + step + ids.length) % ids.length];
142
+ }
143
+ else if (event.key === 'Home') {
144
+ next = ids[0];
145
+ }
146
+ else if (event.key === 'End') {
147
+ next = ids.at(-1);
148
+ }
149
+ else if (event.key === 'Enter' || event.key === ' ') {
150
+ event.preventDefault();
151
+ layout.setActiveTab(panel.id, tabId);
152
+ return;
153
+ }
154
+ if (next !== undefined) {
155
+ event.preventDefault();
156
+ focusTab(next);
157
+ }
158
+ }
159
+ return (_jsxs("div", { "data-tab-strip": true, className: classes.strip,
160
+ // Double-clicking the strip's empty space maximizes the cell and
161
+ // restores it — the IDE convention, and free here because the strip's
162
+ // only other `onDoubleClick` is the one on a tab's own label, for rename.
163
+ //
164
+ // `target === currentTarget` is what keeps those two apart, and it is the
165
+ // test rather than a `stopPropagation` in `WorkspaceTab` because it holds
166
+ // for anything ever put on the strip: a rename double-click bubbles out
167
+ // of the label to here, and so would a double-click on the `+`.
168
+ onDoubleClick: event => {
169
+ if (event.target === event.currentTarget) {
170
+ layout.toggleMaximizedPanel(panel.id);
171
+ }
172
+ }, children: [_jsx("div", { role: "tablist", ref: stripRef, className: classes.tabs,
173
+ // A mouse wheel only has a vertical axis, and this scrolls
174
+ // horizontally — so without translating it, a strip with more tabs
175
+ // than fit is reachable by trackpad swipe and by keyboard and NOT AT
176
+ // ALL by mouse. The scrollbar is hidden on purpose (the strip is
177
+ // chrome) which removes the other way of noticing there is more.
178
+ //
179
+ // `deltaX` is a trackpad's own horizontal axis and the browser has
180
+ // already applied it; taking the larger axis leaves that gesture alone
181
+ // rather than doubling it.
182
+ onWheel: event => {
183
+ const el = stripRef.current;
184
+ if (!el || Math.abs(event.deltaX) > Math.abs(event.deltaY)) {
185
+ return;
186
+ }
187
+ el.scrollLeft += wheelDeltaPixels(event, el.clientWidth);
188
+ }, children: panel.tabs.map(tab => (_jsx(Tab, { tab: tab, panel: panel, layout: layout, chrome: chrome, className: classes.tab, selected: tab.id === active?.id, groupActive: groupActive, tabIndex: tab.id === roving ? 0 : -1, onKeyDown: onKeyDown, onFocus: setFocusedTabId }, tab.id))) }), renderPanelActions?.(panel)] }));
189
+ });
190
+ const Tab = observer(function Tab({ tab, panel, layout, chrome, className, selected, groupActive, tabIndex, onKeyDown, onFocus, }) {
191
+ const { renderTabLabel, dragHandlers, onTabClose } = chrome;
192
+ return (_jsx("div", { role: "tab", id: tabDomId(tab.id), "data-tab-id": tab.id, "aria-selected": selected, "aria-controls": selected ? tabPanelDomId(panel.id) : undefined, tabIndex: tabIndex, onKeyDown: event => {
193
+ onKeyDown(event, tab.id);
194
+ }, onFocus: () => {
195
+ onFocus(tab.id);
196
+ }, onPointerDown: event => {
197
+ // The middle button closes rather than drags, and its default action is
198
+ // the browser's autoscroll — which would otherwise start the moment a
199
+ // tab is middle-pressed.
200
+ //
201
+ // Cancelling pointerdown suppresses the compatibility mouse events
202
+ // (which is what stops the autoscroll) and NOT `click` / `auxclick`:
203
+ // Pointer Events L3 dispatches those two independently of that mapping.
204
+ // So the close below still fires, which is the thing this looks like it
205
+ // would break.
206
+ if (event.button === 1) {
207
+ event.preventDefault();
208
+ return;
209
+ }
210
+ // Showing a tab is expensive — it mounts a stack of views, each display
211
+ // costing a WebGL2 context — so it is the LEFT button that asks for it,
212
+ // the same gate dockview puts on `_activateOnPointerDown`. A right-press
213
+ // is on its way to a context menu and is not a request to look at
214
+ // anything.
215
+ if (event.button !== 0) {
216
+ return;
217
+ }
218
+ layout.setActiveTab(panel.id, tab.id);
219
+ dragHandlers.onTabPointerDown(tab.id, event);
220
+ }, onAuxClick: event => {
221
+ if (event.button === 1) {
222
+ event.preventDefault();
223
+ onTabClose?.(tab.id);
224
+ }
225
+ }, onPointerMove: dragHandlers.onTabPointerMove, onPointerUp: dragHandlers.onTabPointerUp, onPointerCancel: dragHandlers.onTabPointerCancel, className: className, style: tabColors(groupActive, selected), children: renderTabLabel(tab) }));
226
+ });
@@ -0,0 +1,16 @@
1
+ import type { WorkspaceSessionType } from '../ui/App/types.ts';
2
+ import type { WorkspaceLayout } from './model.ts';
3
+ type WorkspaceSession = WorkspaceSessionType & WorkspaceLayout;
4
+ /**
5
+ * The workspace. Compare `TiledViewsContainer` + `useDockviewController`, which
6
+ * this replaces: there is no api to hold, no `onReady`, no event to subscribe
7
+ * to, and no reconciliation — the layout is session state and this renders it.
8
+ *
9
+ * The one reaction left is homing: `session.views` is owned by the session, so
10
+ * a view launched from a menu arrives belonging to no tab and has to land
11
+ * somewhere. That is one-directional and idempotent, and nothing reads back.
12
+ */
13
+ export declare const WorkspaceContainer: ({ session, }: {
14
+ session: WorkspaceSession;
15
+ }) => import("react").JSX.Element;
16
+ export {};