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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/esm/HistoryManagement/index.d.ts +3 -0
  2. package/esm/JBrowseConfig/RootConfiguration.d.ts +1 -8
  3. package/esm/JBrowseConfig/RootConfiguration.js +2 -6
  4. package/esm/JBrowseConfig/index.d.ts +1 -8
  5. package/esm/JBrowseModel/index.d.ts +1 -8
  6. package/esm/JbApi/jbApi.d.ts +2 -2
  7. package/esm/JbApi/jbApi.js +12 -10
  8. package/esm/SessionSpec/loadSessionSpec.js +2 -12
  9. package/esm/WorkspaceLayout/LayoutRenderer.d.ts +0 -10
  10. package/esm/WorkspaceLayout/LayoutRenderer.js +34 -101
  11. package/esm/WorkspaceLayout/PanelView.js +19 -45
  12. package/esm/WorkspaceLayout/TabStrip.d.ts +2 -2
  13. package/esm/WorkspaceLayout/TabStrip.js +36 -104
  14. package/esm/WorkspaceLayout/WorkspaceContainer.d.ts +0 -9
  15. package/esm/WorkspaceLayout/WorkspaceContainer.js +9 -34
  16. package/esm/WorkspaceLayout/WorkspacePanelActions.d.ts +1 -16
  17. package/esm/WorkspaceLayout/WorkspacePanelActions.js +45 -50
  18. package/esm/WorkspaceLayout/WorkspaceTab.d.ts +1 -16
  19. package/esm/WorkspaceLayout/WorkspaceTab.js +11 -47
  20. package/esm/WorkspaceLayout/dropZone.d.ts +7 -37
  21. package/esm/WorkspaceLayout/dropZone.js +7 -44
  22. package/esm/WorkspaceLayout/model.d.ts +42 -112
  23. package/esm/WorkspaceLayout/model.js +139 -268
  24. package/esm/WorkspaceLayout/panelChrome.d.ts +4 -7
  25. package/esm/WorkspaceLayout/panelChrome.js +0 -2
  26. package/esm/WorkspaceLayout/spec.d.ts +10 -51
  27. package/esm/WorkspaceLayout/spec.js +12 -80
  28. package/esm/WorkspaceLayout/splitter.d.ts +3 -19
  29. package/esm/WorkspaceLayout/splitter.js +5 -21
  30. package/esm/WorkspaceLayout/tabName.d.ts +8 -0
  31. package/esm/WorkspaceLayout/tabName.js +14 -0
  32. package/esm/WorkspaceLayout/tree.d.ts +19 -76
  33. package/esm/WorkspaceLayout/tree.js +29 -122
  34. package/esm/WorkspaceLayout/useLayoutDrag.d.ts +4 -22
  35. package/esm/WorkspaceLayout/useLayoutDrag.js +46 -174
  36. package/esm/WorkspaceLayout/usePointerGesture.d.ts +24 -0
  37. package/esm/WorkspaceLayout/usePointerGesture.js +83 -0
  38. package/esm/WorkspaceLayout/workspaceTheme.d.ts +27 -0
  39. package/esm/WorkspaceLayout/workspaceTheme.js +37 -0
  40. package/esm/ui/App/App.js +3 -0
  41. package/esm/ui/App/JBrowseTabMenu.js +1 -1
  42. package/esm/ui/App/ThemeModeButton.js +5 -4
  43. package/esm/ui/App/ViewMenu.js +47 -29
  44. package/esm/ui/App/ViewsContainer.js +3 -4
  45. package/esm/ui/App/lazyParts.d.ts +0 -5
  46. package/esm/ui/App/lazyParts.js +0 -1
  47. package/esm/ui/App/preloadAppShell.js +3 -6
  48. package/esm/ui/App/types.d.ts +3 -2
  49. package/esm/ui/App/useViewVisibility.d.ts +1 -1
  50. package/esm/ui/App/useViewVisibility.js +1 -1
  51. package/package.json +5 -5
  52. package/esm/WorkspaceLayout/dockviewTheme.d.ts +0 -69
  53. package/esm/WorkspaceLayout/dockviewTheme.js +0 -82
  54. package/esm/ui/App/ClassicViewsContainer.d.ts +0 -6
  55. package/esm/ui/App/ClassicViewsContainer.js +0 -22
@@ -15,10 +15,13 @@ export declare function HistoryManagementMixin(): import("@jbrowse/mobx-state-tr
15
15
  }, {
16
16
  history: unknown[];
17
17
  notTrackingUndo: boolean;
18
+ hasPendingRecord: boolean;
18
19
  } & {
19
20
  readonly canUndo: boolean;
20
21
  readonly canRedo: boolean;
21
22
  } & {
23
+ setHasPendingRecord(value: boolean): void;
24
+ flushPendingRecord(): void;
22
25
  stopTrackingUndo(): void;
23
26
  resumeTrackingUndo(): void;
24
27
  addUndoState(snapshot: unknown): void;
@@ -123,11 +123,6 @@ export default function RootConfiguration({ pluginManager, extraConfigSlots, }:
123
123
  } & {
124
124
  defaultValue: boolean;
125
125
  };
126
- readonly useWorkspaces: {
127
- type: "boolean";
128
- } & {
129
- defaultValue: boolean;
130
- };
131
126
  readonly developerMode: {
132
127
  type: "boolean";
133
128
  } & {
@@ -145,8 +140,6 @@ export default function RootConfiguration({ pluginManager, extraConfigSlots, }:
145
140
  defaultValue: unknown;
146
141
  };
147
142
  readonly logoPath: {
148
- type: "fileLocation";
149
- } & {
150
- defaultValue: unknown;
143
+ type: "maybeFileLocation";
151
144
  };
152
145
  }, import("@jbrowse/core/configuration/configurationSchema").ConfigurationSchemaOptions<undefined, undefined, undefined, import("@jbrowse/core/configuration").ConfigurationSchemaRequirement<Partial<Record<string, string[]>>, string>, any>>;
@@ -90,12 +90,8 @@ export default function RootConfiguration({ pluginManager, extraConfigSlots = {}
90
90
  * #slot configuration.logoPath
91
91
  */
92
92
  logoPath: {
93
- type: 'fileLocation',
94
- description: 'path to a custom logo image displayed in the app header',
95
- defaultValue: {
96
- uri: '',
97
- locationType: 'UriLocation',
98
- },
93
+ type: 'maybeFileLocation',
94
+ description: 'a custom logo image displayed in the app header; unset, the JBrowse logo',
99
95
  advanced: true,
100
96
  },
101
97
  /**
@@ -126,11 +126,6 @@ export declare function JBrowseConfigF({ pluginManager, assemblyConfigSchema, ex
126
126
  } & {
127
127
  defaultValue: boolean;
128
128
  };
129
- readonly useWorkspaces: {
130
- type: "boolean";
131
- } & {
132
- defaultValue: boolean;
133
- };
134
129
  readonly developerMode: {
135
130
  type: "boolean";
136
131
  } & {
@@ -148,9 +143,7 @@ export declare function JBrowseConfigF({ pluginManager, assemblyConfigSchema, ex
148
143
  defaultValue: unknown;
149
144
  };
150
145
  readonly logoPath: {
151
- type: "fileLocation";
152
- } & {
153
- defaultValue: unknown;
146
+ type: "maybeFileLocation";
154
147
  };
155
148
  }, import("@jbrowse/core/configuration/configurationSchema").ConfigurationSchemaOptions<undefined, undefined, undefined, import("@jbrowse/core/configuration").ConfigurationSchemaRequirement<Partial<Record<string, string[]>>, string>, any>>;
156
149
  /**
@@ -115,11 +115,6 @@ export declare function JBrowseModelF({ pluginManager, assemblyConfigSchema, ext
115
115
  } & {
116
116
  defaultValue: boolean;
117
117
  };
118
- readonly useWorkspaces: {
119
- type: "boolean";
120
- } & {
121
- defaultValue: boolean;
122
- };
123
118
  readonly developerMode: {
124
119
  type: "boolean";
125
120
  } & {
@@ -137,9 +132,7 @@ export declare function JBrowseModelF({ pluginManager, assemblyConfigSchema, ext
137
132
  defaultValue: unknown;
138
133
  };
139
134
  readonly logoPath: {
140
- type: "fileLocation";
141
- } & {
142
- defaultValue: unknown;
135
+ type: "maybeFileLocation";
143
136
  };
144
137
  }, import("@jbrowse/core/configuration/configurationSchema").ConfigurationSchemaOptions<undefined, undefined, undefined, import("@jbrowse/core/configuration").ConfigurationSchemaRequirement<Partial<Record<string, string[]>>, string>, any>>;
145
138
  plugins: import("@jbrowse/mobx-state-tree").IArrayType<import("@jbrowse/mobx-state-tree").IType<PluginDefinition, PluginDefinition, PluginDefinition>>;
@@ -1018,9 +1018,9 @@ export declare function createJbApi(pluginManager: PluginManager, options?: JbAp
1018
1018
  total: number;
1019
1019
  note?: string | undefined;
1020
1020
  tracks: {
1021
- trackId: string;
1021
+ trackId: any;
1022
1022
  name: string;
1023
- type: string;
1023
+ type: any;
1024
1024
  assemblyNames: string[];
1025
1025
  }[];
1026
1026
  };
@@ -1,5 +1,5 @@
1
1
  import { getReExportRegistry, setReExportRegistry, } from '@jbrowse/core/ReExports/registry';
2
- import { getConf, getConfigurationSchemaDefinition, getConfigurationSchemaMetadata, isSlotDefinitionEntry, readConfObject, shorthandTargets, } from '@jbrowse/core/configuration';
2
+ import { getConf, getConfigurationSchemaDefinition, getConfigurationSchemaMetadata, hydrateTrackConfig, isSlotDefinitionEntry, readConfObject, shorthandTargets, } from '@jbrowse/core/configuration';
3
3
  import { releaseAdapterSession, retainAdapterSession, } from '@jbrowse/core/data_adapters/adapterSessionRefcount';
4
4
  import { adapterByteLimit } from '@jbrowse/core/rpc/byteBudget';
5
5
  import { isElectron, isSessionWithAddSessionTrack, objectHash, parseLocString, sum, } from '@jbrowse/core/util';
@@ -594,7 +594,7 @@ async function fitToWindow(session, settleMs, root = document) {
594
594
  function trackEntry(conf) {
595
595
  return {
596
596
  trackId: conf.trackId,
597
- name: readConfObject(conf, 'name'),
597
+ name: conf.name ?? '',
598
598
  type: conf.type,
599
599
  // getConfAssemblyNamesOrNone, not the slot: an assembly's sequence track
600
600
  // has no assemblyNames slot and answers through its parent assembly
@@ -826,8 +826,13 @@ async function visibleRegionsOf(session, viewId, preferTrackId, trackAssemblies
826
826
  // file's own refNames during serialization (RpcMethodTypeWithRenameRegions),
827
827
  // and the features come back rebuilt as SimpleFeature. What crosses the worker
828
828
  // boundary is bounded by the byte gate that runs first.
829
- async function fetchFeatures(session, trackId, regions, requestedByteLimit) {
830
- const conf = session.getTrackById(trackId);
829
+ async function fetchFeatures(pluginManager, session, trackId, regions, requestedByteLimit) {
830
+ const entry = session.getTrackById(trackId);
831
+ // the node a shown track reads: its adapter is in the hydrated form, which is
832
+ // what the shown track's session id hashes
833
+ const conf = isStateTreeNode(entry)
834
+ ? entry
835
+ : entry && hydrateTrackConfig(pluginManager, entry);
831
836
  if (!conf) {
832
837
  throw new Error(`No track with trackId "${trackId}" — jb.listTracks() shows what is available`);
833
838
  }
@@ -1019,7 +1024,7 @@ const JB_HELP = `jb drives this JBrowse app programmatically (window.jb in a bro
1019
1024
 
1020
1025
  Orient first: jb.sessionSummary(). Introspect, never guess: jb.listTracks(search?, limit?) answers { total, tracks } with the trackIds; jb.describeSlots(jb.trackModel('someTrackId').activeDisplay.configuration) for the settings keys a display accepts — an unknown settings key is not an error, it lands in applyDisplaySettings' "unapplied" list as { key, reason }, so read the report; jb.inspect(node) — jb.view(), jb.trackModel('someTrackId'), session.views[0] — for its getters, actions and modelType.
1021
1026
 
1022
- The model is mobx-state-tree: mutate only through actions (raw assignment throws), and write display settings with track.applyDisplaySettings(settings). A feature or variant track groups and colors through two settings, track.applyDisplaySettings({ facet: 'strand' or { field, domain? }, color: a CSS color or { field: 'type', domain?, range? } }), where null resets any setting; it filters with activeDisplay.setFilter(['jexl:feature.score > 10']). An alignments track takes the same facet, its field a read dimension ('pairOrientation', 'mapq', ...) or a tag ('tags.HP'). LinearMarkDisplay plots a track's features as ggplot2 does, on a feature, alignments, variant or quantitative track, in five settings: marks, a list drawn in order of { mark: 'bar' | 'point' | 'rule' | 'line' | 'span' | 'text' | 'link', encoding: { y: 'score', x?, x2?, row?, color?, shape?, text?, size? }, each a feature field or a jexl: expression, a constant written { value: 'red' }, transform?: steps such as { type: 'coverage' } or a bin then an aggregate, which fill the y they write, minBpPerPx?, maxBpPerPx? }, beside transform, facet, rows and scales ({ y: { type, domainMin, domainMax, autoscaleGroup, title } }). await view.launchTrack(trackId, {}, { type: 'LinearMarkDisplay', marks: [...] }) draws a track with it, shown already or not (on a GWASTrack, type 'LinearManhattanDisplay'). Its markPlot is those five as JSON: edit structuredClone(activeDisplay.markPlot), check it with activeDisplay.plotProblems(plot), and apply it with applyDisplaySettings(plot), on this track or another track's mark display; activeDisplay.notices names a mark the config cannot draw, or one whose field no loaded feature holds. Build views declaratively with jb.loadSessionSpec({ views: [{ type: 'LinearGenomeView', assembly, loc, tracks: [...] }] }), which replaces the session; jb.addView(oneViewSpec) opens one more view beside what is open; jb.setSession(document) rewrites the session as a document — what jb.mst.getSnapshot(jb.session) answers, edited: a view keeps its id and is patched in place, loc on it navigates, a { trackId } entry in its tracks opens that track. Act on a view with await view.navToLocString('BRCA1' or 'chr1:1-1000'), which also SHOWS the track whose search index answered a gene name unless a 4th arg { showHitTrack: false } says not to; await view.launchTrack(trackId, {}, settings) shows a track with settings, view.hideTrack(trackId) hides it. Arrange open views into panels with session.layoutViews({ direction: 'horizontal', children: [{ views: [viewId] }, ...] }), a leaf naming view ids or session.views indexes; await jb.fitToWindow() shrinks what is open until the session fits the window; add data with jb.addTrack({ location }) (an absolute path or a URL), or show a track the catalog already holds with jb.addTrack({ trackId, settings? }); read data with await jb.getFeatures({ trackId, loc?, assembly?, viewId?, regions?, byteLimit? }), which renames refNames ("chr1" vs "1") so the file answers and reads on the worker the track's display uses. Anything lower level is jb.require('@jbrowse/core/util') and friends, the module registry plugins link against. After changing anything, await jb.waitReady(ms) and read its notifications and notReady lists before trusting the screen.
1027
+ The model is mobx-state-tree: mutate only through actions (raw assignment throws), and write display settings with track.applyDisplaySettings(settings). A display's grammar settings are its plot, activeDisplay.plot, by slot name: facet ('strand' or { field, domain? }), color (a CSS color or { field: 'type', domain?, range? }), rows, unit (what a row stands for: 'read' | 'chain' on alignments, 'sample' | 'haplotype' on multi-sample variants), scales, filter (['jexl:feature.score > 10']), and on a mark display marks and transform. Edit structuredClone(activeDisplay.plot), check it with activeDisplay.plotProblems(draft), which throws what a config refuses, and write it with activeDisplay.applyPlot(draft): each setting replaced whole, null resetting one. An alignments track takes the same facet, its field a read dimension ('pairOrientation', 'mapq', ...) or a tag ('tags.HP'). LinearMarkDisplay plots a track's features as ggplot2 does, on a feature, alignments, variant or quantitative track, in seven settings: marks, a list drawn in order of { mark: 'bar' | 'point' | 'rule' | 'line' | 'span' | 'text' | 'link', encoding: { y: 'score', x?, x2?, row?, color?, shape?, text?, size? }, each a feature field or a jexl: expression, a constant written { value: 'red' }, transform?: steps such as { type: 'coverage' } or a bin then an aggregate, which fill the y they write, minBpPerPx?, maxBpPerPx? }, beside transform, facet, rows, rowColor, scales ({ y: { type, domainMin, domainMax, autoscaleGroup, title } }) and filter. await view.launchTrack(trackId, {}, { type: 'LinearMarkDisplay', marks: [...] }) draws a track with it, shown already or not (on a GWASTrack, type 'LinearManhattanDisplay'). activeDisplay.notices names a mark the config cannot draw, or one whose field no loaded feature holds. Build views declaratively with jb.loadSessionSpec({ views: [{ type: 'LinearGenomeView', assembly, loc, tracks: [...] }] }), which replaces the session; jb.addView(oneViewSpec) opens one more view beside what is open; jb.setSession(document) rewrites the session as a document — what jb.mst.getSnapshot(jb.session) answers, edited: a view keeps its id and is patched in place, loc on it navigates, a { trackId } entry in its tracks opens that track. Act on a view with await view.navToLocString('BRCA1' or 'chr1:1-1000'), which also SHOWS the track whose search index answered a gene name unless a 4th arg { showHitTrack: false } says not to; await view.launchTrack(trackId, {}, settings) shows a track with settings, view.hideTrack(trackId) hides it. Arrange open views into panels with session.layoutViews({ direction: 'horizontal', children: [{ views: [viewId] }, ...] }), a leaf naming view ids or session.views indexes; await jb.fitToWindow() shrinks what is open until the session fits the window; add data with jb.addTrack({ location }) (an absolute path or a URL), or show a track the catalog already holds with jb.addTrack({ trackId, settings? }); read data with await jb.getFeatures({ trackId, loc?, assembly?, viewId?, regions?, byteLimit? }), which renames refNames ("chr1" vs "1") so the file answers and reads on the worker the track's display uses. Anything lower level is jb.require('@jbrowse/core/util') and friends, the module registry plugins link against. After changing anything, await jb.waitReady(ms) and read its notifications and notReady lists before trusting the screen.
1023
1028
 
1024
1029
  Views nest and several can be open. jb.view(viewId?) is the open view, and jb.view(), jb.trackModel(trackId), jb.visibleRegions(), jb.addTrack and jb.getFeatures over a visible region throw naming the candidates rather than picking one when more than one view could answer — pass viewId (from jb.sessionSummary()) to say which. A synteny or breakpoint view's viewId also covers its rows: the named view answers when it can, and otherwise its rows do.
1025
1030
 
@@ -1107,7 +1112,7 @@ export function createJbApi(pluginManager, options = {}) {
1107
1112
  await locToRegion(session, conf, fetchArgs.loc, fetchArgs.assembly),
1108
1113
  ]
1109
1114
  : await visibleRegionsOf(session, fetchArgs.viewId, fetchArgs.trackId, getConfAssemblyNamesOrNone(conf));
1110
- return fetchFeatures(session, fetchArgs.trackId, regions, fetchArgs.byteLimit);
1115
+ return fetchFeatures(pluginManager, session, fetchArgs.trackId, regions, fetchArgs.byteLimit);
1111
1116
  },
1112
1117
  };
1113
1118
  return jb;
@@ -1192,9 +1197,6 @@ async function addTrack(pluginManager, session, args) {
1192
1197
  }
1193
1198
  // A track already in the catalog, shown: the same view choice and the same
1194
1199
  // settle the file route ends in.
1195
- // `conf` is whatever `session.getTrackById` answered, which is a track config
1196
- // but not necessarily a `BaseTrackConfig` — an assembly's ReferenceSequenceTrack
1197
- // declares a subset of those slots. The four reads below are all ones it has.
1198
1200
  async function showCatalogTrack(pluginManager, session, conf, args) {
1199
1201
  // index, assembly and name describe the file being added, and the catalog's
1200
1202
  // copy already has all three — taking them here would drop them in silence
@@ -1207,7 +1209,7 @@ async function showCatalogTrack(pluginManager, session, conf, args) {
1207
1209
  trackType: conf.type,
1208
1210
  assembly: getConfAssemblyNamesOrNone(conf)[0],
1209
1211
  };
1210
- const adapterType = readConfObject(conf, ['adapter', 'type']);
1212
+ const adapterType = conf.adapter?.type;
1211
1213
  const summary = { ...track, ...(adapterType ? { adapterType } : {}) };
1212
1214
  // show:false over a catalog trackId asks for nothing — there is no file to
1213
1215
  // add — and showing it anyway would contradict the flag
@@ -132,18 +132,8 @@ function unresolvedLayoutRefs(node, viewCount, knownIds) {
132
132
  : []),
133
133
  ];
134
134
  }
135
- // A `tabs` node shares one cell between its children rather than dividing the
136
- // space, so a `size` on one of them describes nothing. This is the ONLY case
137
- // left where a stated size is dropped.
138
- //
139
- // It used to be far wider — dockview forces its branches to alternate
140
- // orientation by depth, so a nested container had no branch to size against and
141
- // the whole sizing pass bailed, top-level numbers included. That is gone
142
- // (ADR-068): the spec's nesting is the tree's nesting, `size` applies wherever
143
- // it is written, and a bare sibling takes an equal share of the remainder. The
144
- // wider check outlived the limitation it reported and told an author their
145
- // nested or partial sizes had been ignored while the layout honoured them —
146
- // which is also what website/docs/urlparams.md promises.
135
+ // A `tabs` node shares one cell between its children, so a `size` on one of
136
+ // them describes nothing; it is the only stated size the layout drops.
147
137
  function unsizeableLayoutNodes(layout) {
148
138
  const children = Array.isArray(layout.children) ? layout.children : [];
149
139
  const sizedTabsChild = layout.direction === 'tabs' && children.some(c => c.size !== undefined);
@@ -2,16 +2,6 @@ import type { WorkspaceLayout } from './model.ts';
2
2
  import type { PanelChrome } from './panelChrome.ts';
3
3
  import type { LayoutTree } from './tree.ts';
4
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
5
  interface Props {
16
6
  node: LayoutTree;
17
7
  layout: WorkspaceLayout;
@@ -1,15 +1,15 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Fragment, useCallback, useRef } from 'react';
2
+ import { Fragment } from 'react';
3
3
  import { makeStyles } from '@jbrowse/core/util/tss-react';
4
4
  import { observer } from 'mobx-react';
5
5
  import { PanelView } from './PanelView.js';
6
- import { dv } from './dockviewTheme.js';
7
6
  import { pairSpan, withBoundaryAt } from './splitter.js';
8
7
  import { isBranch } from './tree.js';
8
+ import { usePointerGesture } from './usePointerGesture.js';
9
+ import { workspaceTheme } from './workspaceTheme.js';
9
10
  /**
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.
11
+ * A node's share of its parent's space, as `flex-grow`, so window resizes need
12
+ * no code. `min*: 0` stops a wide view pushing its cell past its share.
13
13
  */
14
14
  function paneStyle(size) {
15
15
  return {
@@ -27,12 +27,7 @@ export const LayoutRenderer = observer(function LayoutRenderer(props) {
27
27
  }
28
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
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
- */
30
+ /** The pixels the two panes either side of a handle occupy. */
36
31
  function measurePairPx(handle, index, horizontal) {
37
32
  const panes = [...(handle.parentElement?.children ?? [])].filter(el => !Object.hasOwn(el.dataset, 'splitter'));
38
33
  const before = panes[index - 1]?.getBoundingClientRect();
@@ -42,25 +37,21 @@ function measurePairPx(handle, index, horizontal) {
42
37
  }
43
38
  return horizontal ? before.width + after.width : before.height + after.height;
44
39
  }
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.
40
+ // a transparent grab strip with a 1px line drawn down its middle
48
41
  const useSplitterStyles = makeStyles()({
49
42
  splitter: {
50
- flex: `0 0 ${dv.sashSize}px`,
43
+ flex: `0 0 ${workspaceTheme.splitterSize}px`,
51
44
  position: 'relative',
52
45
  background: 'transparent',
53
46
  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
47
  '&:focus-visible': {
57
- outline: `2px solid ${dv.edgeDockIndicatorColor}`,
48
+ outline: `2px solid ${workspaceTheme.accent}`,
58
49
  outlineOffset: -1,
59
50
  },
60
51
  '&::before': {
61
52
  content: '""',
62
53
  position: 'absolute',
63
- background: dv.separatorBorder,
54
+ background: workspaceTheme.splitterLine,
64
55
  },
65
56
  },
66
57
  horizontal: {
@@ -71,86 +62,35 @@ const useSplitterStyles = makeStyles()({
71
62
  },
72
63
  });
73
64
  /**
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.
65
+ * Drags the boundary between children `index - 1` and `index`; `splitter.ts`
66
+ * decides where it may land.
79
67
  */
80
68
  const Splitter = observer(function Splitter({ branch, index, layout, }) {
81
69
  const { classes, cx } = useSplitterStyles();
82
- const dragRef = useRef(undefined);
83
70
  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) => {
71
+ const gesture = usePointerGesture({
72
+ start(event) {
73
+ const pairPx = measurePairPx(event.currentTarget, index, horizontal);
74
+ return pairPx > 0
75
+ ? {
76
+ axis: horizontal ? 'clientX' : 'clientY',
77
+ start: horizontal ? event.clientX : event.clientY,
78
+ pairPx,
79
+ startSizes: branch.children.map(c => c.size),
80
+ }
81
+ : undefined;
82
+ },
83
+ move(drag, event) {
84
+ const delta = (event[drag.axis] - drag.start) / drag.pairPx;
85
+ const sizes = drag.startSizes;
86
+ layout.setSizes(branch.id, withBoundaryAt(sizes, index, sizes[index - 1] + delta * pairSpan(sizes, index), drag.pairPx));
87
+ },
88
+ });
89
+ const onKeyDown = (event) => {
148
90
  const sizes = branch.children.map(c => c.size);
149
91
  const pair = pairSpan(sizes, index);
150
92
  const decrease = horizontal ? 'ArrowLeft' : 'ArrowUp';
151
93
  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
94
  const step = pair * 0.02;
155
95
  let moved;
156
96
  if (event.key === decrease) {
@@ -169,17 +109,10 @@ const Splitter = observer(function Splitter({ branch, index, layout, }) {
169
109
  return;
170
110
  }
171
111
  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
112
  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
113
+ };
114
+ // the pane before the handle, as a percentage of the pair it divides
178
115
  const sizes = branch.children.map(c => c.size);
179
116
  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' } }));
117
+ 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: gesture.onPointerDown, onPointerMove: gesture.onPointerMove, onPointerUp: gesture.onPointerUp, onPointerCancel: gesture.onPointerCancel, onLostPointerCapture: gesture.onLostPointerCapture, className: cx(classes.splitter, horizontal ? classes.horizontal : classes.vertical), style: { cursor: horizontal ? 'ew-resize' : 'ns-resize' } }));
185
118
  });
@@ -4,28 +4,13 @@ import { useScrollPortHeightVar } from '@jbrowse/core/util/hooks';
4
4
  import { makeStyles } from '@jbrowse/core/util/tss-react';
5
5
  import { observer } from 'mobx-react';
6
6
  import { TabStrip } from './TabStrip.js';
7
- import { dv } from './dockviewTheme.js';
8
7
  import { indicatorRect } from './dropZone.js';
9
8
  import { tabDomId, tabPanelDomId } from './panelChrome.js';
10
9
  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
- */
10
+ import { workspaceTheme } from './workspaceTheme.js';
23
11
  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.
12
+ // without `flex: 1` and `minWidth: 0` the cell takes its content's width, and
13
+ // a view sizes itself to its container, so both settle too narrow
29
14
  panel: {
30
15
  position: 'relative',
31
16
  display: 'flex',
@@ -33,9 +18,7 @@ const useStyles = makeStyles()(theme => ({
33
18
  flex: 1,
34
19
  minWidth: 0,
35
20
  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.
21
+ // below the strip the cell follows the app theme, not `workspaceTheme`
39
22
  background: theme.palette.background.default,
40
23
  },
41
24
  content: {
@@ -48,51 +31,42 @@ const useStyles = makeStyles()(theme => ({
48
31
  indicator: {
49
32
  position: 'absolute',
50
33
  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
34
+ // local to the position:relative panel, so it cannot cover menus
53
35
  zIndex: 1,
54
- background: dv.dragOverBackground,
55
- outline: `1px solid ${dv.edgeDockIndicatorColor}`,
36
+ background: workspaceTheme.dropWash,
37
+ outline: `1px solid ${workspaceTheme.accent}`,
56
38
  },
57
- // where a tab dragged onto the strip would land
58
39
  caret: {
59
40
  position: 'absolute',
60
41
  pointerEvents: 'none',
61
42
  zIndex: 1,
62
43
  top: 0,
63
44
  width: 2,
64
- height: dv.tabsHeight,
65
- background: dv.edgeDockIndicatorColor,
45
+ height: workspaceTheme.stripHeight,
46
+ background: workspaceTheme.accent,
66
47
  },
67
48
  }));
68
49
  export const PanelView = observer(function PanelView({ panel, layout, chrome, drop, }) {
69
50
  const { classes } = useStyles();
70
51
  const active = activeTabIn(panel);
71
- const contentRef = useScrollPortHeightVar();
72
52
  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.
53
+ // capture phase, so a control inside the view that consumes the click
54
+ // still activates the cell
76
55
  if (layout.activePanelId !== panel.id) {
77
56
  layout.setActivePanelId(panel.id);
78
57
  }
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 })] }));
58
+ }, children: [layout.panels.length > 1 || panel.tabs.length > 1 ? (_jsx(TabStrip, { panel: panel, layout: layout, chrome: chrome, active: active, cellActive: layout.activePanelId === panel.id })) : null, active ? (_jsx(TabPanel, { panelId: panel.id, tab: active, chrome: chrome, className: classes.content }, active.id)) : (_jsx("div", { role: "tabpanel", className: classes.content })), _jsx(DropIndicator, { drop: drop, classes: classes })] }));
80
59
  });
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
- */
60
+ // keyed by tab, so a switch gets a fresh ViewStack and scroll port
61
+ const TabPanel = observer(function TabPanel({ panelId, tab, chrome, className, }) {
62
+ const ref = useScrollPortHeightVar();
63
+ return (_jsx("div", { role: "tabpanel", id: tabPanelDomId(panelId), "aria-labelledby": tabDomId(tab.id), className: className, ref: ref, children: chrome.renderTabContent(tab) }));
64
+ });
65
+ /** Where a drop would land: a caret at a strip gap, or a wash over a zone. */
88
66
  const DropIndicator = observer(function DropIndicator({ drop, classes, }) {
89
67
  if (drop?.strip) {
90
68
  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.
69
+ // a scrolled strip's first gap is left of the panel, which does not clip
96
70
  style: { left: Math.max(0, drop.strip.left) } }));
97
71
  }
98
72
  return drop ? (_jsx("div", { "data-drop-indicator": drop.zone, className: classes.indicator, style: indicatorRect(drop.zone) })) : null;
@@ -8,7 +8,7 @@ interface TabStripProps {
8
8
  /** the tab this panel is showing — the strip does not decide it */
9
9
  active: TabNode | undefined;
10
10
  /** whether this is the cell a newly launched view lands in */
11
- groupActive: boolean;
11
+ cellActive: boolean;
12
12
  }
13
- export declare const TabStrip: ({ panel, layout, chrome, active, groupActive, }: TabStripProps) => import("react").JSX.Element;
13
+ export declare const TabStrip: ({ panel, layout, chrome, active, cellActive, }: TabStripProps) => import("react").JSX.Element;
14
14
  export {};