@jbrowse/core 5.0.0-beta.1 → 5.0.0-beta.3

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 (199) hide show
  1. package/README.md +112 -43
  2. package/esm/BaseFeatureWidget/BaseFeatureDetail/FeatureDetails.d.ts +2 -3
  3. package/esm/BaseFeatureWidget/BaseFeatureDetail/ParentFeatureLine.d.ts +4 -0
  4. package/esm/BaseFeatureWidget/BaseFeatureDetail/ParentFeatureLine.js +17 -0
  5. package/esm/BaseFeatureWidget/BaseFeatureDetail/index.js +4 -3
  6. package/esm/BaseFeatureWidget/SequenceFeatureDetails/SequenceFeaturePanel.d.ts +2 -2
  7. package/esm/BaseFeatureWidget/SequenceFeatureDetails/test_data/NCDN.d.ts +2 -2
  8. package/esm/BaseFeatureWidget/index.d.ts +1 -1
  9. package/esm/BaseFeatureWidget/stateModelFactory.d.ts +8 -1
  10. package/esm/BaseFeatureWidget/stateModelFactory.js +6 -0
  11. package/esm/BaseFeatureWidget/types.d.ts +10 -2
  12. package/esm/PluginLoader.d.ts +1 -0
  13. package/esm/PluginLoader.js +52 -8
  14. package/esm/PluginManager.d.ts +99 -17
  15. package/esm/PluginManager.js +194 -26
  16. package/esm/ReExports/BaseFeatureDetails.js +1 -1
  17. package/esm/ReExports/MuiDataGridReExports.d.ts +1 -0
  18. package/esm/ReExports/MuiDataGridReExports.js +2 -2
  19. package/esm/ReExports/barrelOnlyNames.d.ts +5 -0
  20. package/esm/ReExports/barrelOnlyNames.js +63 -0
  21. package/esm/ReExports/modules.js +31 -109
  22. package/esm/ReExports/publicTracks.d.ts +1 -0
  23. package/esm/ReExports/publicTracks.js +14 -0
  24. package/esm/ReExports/publicUi.d.ts +4 -0
  25. package/esm/ReExports/publicUi.js +33 -0
  26. package/esm/ReExports/publicUtil.d.ts +1 -1
  27. package/esm/ReExports/publicUtil.js +8 -4
  28. package/esm/ReExports/sharedModules.d.ts +96 -0
  29. package/esm/ReExports/sharedModules.js +66 -0
  30. package/esm/ReExports/uiStub.d.ts +2 -0
  31. package/esm/ReExports/uiStub.js +11 -0
  32. package/esm/ReExports/workerModules.d.ts +2 -0
  33. package/esm/ReExports/workerModules.js +15 -0
  34. package/esm/ReExports/workerNamespaceNames.d.ts +11 -0
  35. package/esm/ReExports/workerNamespaceNames.js +281 -0
  36. package/esm/configuration/configurationSchema.d.ts +5 -38
  37. package/esm/configuration/configurationSchema.js +64 -40
  38. package/esm/configuration/index.d.ts +2 -2
  39. package/esm/configuration/index.js +1 -1
  40. package/esm/configuration/promotableDefaults.d.ts +54 -19
  41. package/esm/configuration/promotableDefaults.js +117 -48
  42. package/esm/configuration/promotablePin.d.ts +50 -40
  43. package/esm/configuration/promotableResolve.d.ts +1 -1
  44. package/esm/configuration/promotableResolve.js +1 -1
  45. package/esm/configuration/tabixIndexFields.d.ts +2 -2
  46. package/esm/configuration/tabixIndexFields.js +2 -2
  47. package/esm/data_adapters/BaseAdapter/BaseFeatureDataAdapter.d.ts +12 -0
  48. package/esm/data_adapters/BaseAdapter/BaseFeatureDataAdapter.js +35 -4
  49. package/esm/data_adapters/BaseAdapter/featureDensity.d.ts +41 -0
  50. package/esm/data_adapters/BaseAdapter/featureDensity.js +32 -0
  51. package/esm/data_adapters/BaseAdapter/index.d.ts +2 -1
  52. package/esm/data_adapters/BaseAdapter/index.js +1 -0
  53. package/esm/data_adapters/BaseAdapter/stats.d.ts +33 -1
  54. package/esm/data_adapters/BaseAdapter/stats.js +52 -13
  55. package/esm/data_adapters/BaseAdapter/types.d.ts +3 -8
  56. package/esm/pluggableElementTypes/AdapterType.d.ts +2 -1
  57. package/esm/pluggableElementTypes/AdapterType.js +1 -1
  58. package/esm/pluggableElementTypes/DisplayType.d.ts +4 -12
  59. package/esm/pluggableElementTypes/DisplayType.js +3 -10
  60. package/esm/pluggableElementTypes/LazyStateModelElement.d.ts +27 -0
  61. package/esm/pluggableElementTypes/LazyStateModelElement.js +71 -0
  62. package/esm/pluggableElementTypes/ViewType.d.ts +20 -4
  63. package/esm/pluggableElementTypes/ViewType.js +32 -4
  64. package/esm/pluggableElementTypes/extendElementType.d.ts +1 -2
  65. package/esm/pluggableElementTypes/extendElementType.js +9 -5
  66. package/esm/pluggableElementTypes/models/BaseDisplayModel.d.ts +44 -3
  67. package/esm/pluggableElementTypes/models/BaseDisplayModel.js +75 -2
  68. package/esm/pluggableElementTypes/models/BaseTrackModel.d.ts +21 -0
  69. package/esm/pluggableElementTypes/models/BaseTrackModel.js +34 -2
  70. package/esm/pluggableElementTypes/models/BaseViewModel.d.ts +49 -0
  71. package/esm/pluggableElementTypes/models/BaseViewModel.js +28 -0
  72. package/esm/pluggableElementTypes/models/assertDisplayContract.d.ts +14 -10
  73. package/esm/pluggableElementTypes/models/assertDisplayContract.js +25 -32
  74. package/esm/pluggableElementTypes/models/baseTrackConfig.js +7 -1
  75. package/esm/rpc/RpcRegistry.d.ts +10 -0
  76. package/esm/rpc/byteBudget.d.ts +9 -0
  77. package/esm/rpc/coreRpcMethods.d.ts +1 -0
  78. package/esm/rpc/coreRpcMethods.js +1 -0
  79. package/esm/rpc/methods/CoreGetFeatureDensity.d.ts +11 -0
  80. package/esm/rpc/methods/CoreGetFeatureDensity.js +22 -0
  81. package/esm/svg/svgReady.d.ts +1 -0
  82. package/esm/ui/BaseExportSvgDialog.d.ts +1 -1
  83. package/esm/ui/CascadingMenu.js +30 -8
  84. package/esm/ui/ErrorMessageStackTraceDialog.js +5 -6
  85. package/esm/ui/ExportSvgDialog.d.ts +1 -1
  86. package/esm/ui/LoadingOverlay.d.ts +14 -0
  87. package/esm/ui/LoadingOverlay.js +1 -1
  88. package/esm/ui/MenuTypes.d.ts +14 -5
  89. package/esm/ui/MenuTypes.js +3 -0
  90. package/esm/ui/PaletteContext.d.ts +11 -4
  91. package/esm/ui/PaletteContext.js +34 -5
  92. package/esm/ui/PinAdornment.js +31 -43
  93. package/esm/ui/ScrollChrome.d.ts +16 -0
  94. package/esm/ui/ScrollChrome.js +12 -0
  95. package/esm/ui/ScrollZoomToggle.d.ts +4 -3
  96. package/esm/ui/ScrollZoomToggle.js +4 -3
  97. package/esm/ui/SizeSliderRow.js +1 -1
  98. package/esm/ui/index.d.ts +3 -3
  99. package/esm/ui/index.js +4 -4
  100. package/esm/ui/launchViewMenu.js +7 -1
  101. package/esm/ui/legendMenuItem.d.ts +10 -16
  102. package/esm/ui/legendMenuItem.js +9 -20
  103. package/esm/ui/makeSizeMenu.js +1 -1
  104. package/esm/ui/menuItemAdornment.d.ts +2 -3
  105. package/esm/ui/menuItemAdornment.js +2 -3
  106. package/esm/ui/menuItems.d.ts +2 -3
  107. package/esm/ui/menuItems.js +1 -2
  108. package/esm/ui/palette.d.ts +11 -11
  109. package/esm/ui/palette.js +11 -11
  110. package/esm/ui/promotablePinCoverage.d.ts +16 -2
  111. package/esm/ui/promotablePinCoverage.js +40 -3
  112. package/esm/ui/theme.js +5 -0
  113. package/esm/ui/toggleMenuItems.d.ts +14 -9
  114. package/esm/ui/toggleMenuItems.js +32 -15
  115. package/esm/util/Base1DUtils.d.ts +4 -2
  116. package/esm/util/Base1DUtils.js +12 -4
  117. package/esm/util/SvgCanvas.js +4 -3
  118. package/esm/util/adapterConfigKey.d.ts +12 -0
  119. package/esm/util/adapterConfigKey.js +14 -0
  120. package/esm/util/adapterMetadata.js +2 -1
  121. package/esm/util/addAndShowTrack.d.ts +1 -1
  122. package/esm/util/addAndShowTrack.js +5 -1
  123. package/esm/util/addTrackFromWidget.js +2 -2
  124. package/esm/util/analytics.js +4 -5
  125. package/esm/util/cacheBudgets.d.ts +22 -19
  126. package/esm/util/cacheBudgets.js +22 -19
  127. package/esm/util/clampToContig.d.ts +17 -0
  128. package/esm/util/clampToContig.js +19 -0
  129. package/esm/util/colorBits.d.ts +2 -1
  130. package/esm/util/colorBits.js +2 -5
  131. package/esm/util/createStopTokenRotation.d.ts +9 -7
  132. package/esm/util/createStopTokenRotation.js +7 -27
  133. package/esm/util/featureTypes.d.ts +24 -0
  134. package/esm/util/featureTypes.js +35 -0
  135. package/esm/util/fetchContext.d.ts +2 -2
  136. package/esm/util/fetchContext.js +2 -2
  137. package/esm/util/fetchPhases.d.ts +10 -10
  138. package/esm/util/hooks.d.ts +8 -0
  139. package/esm/util/hooks.js +27 -0
  140. package/esm/util/index.d.ts +6 -5
  141. package/esm/util/index.js +5 -4
  142. package/esm/util/installFetch.d.ts +71 -34
  143. package/esm/util/installFetch.js +57 -29
  144. package/esm/util/installInitAutorun.d.ts +14 -13
  145. package/esm/util/installInitAutorun.js +17 -17
  146. package/esm/util/installPrerequisiteFetch.d.ts +86 -0
  147. package/esm/util/installPrerequisiteFetch.js +73 -0
  148. package/esm/util/lazyAfterAttach.d.ts +13 -0
  149. package/esm/util/lazyAfterAttach.js +31 -0
  150. package/esm/util/legendCandidates.d.ts +50 -0
  151. package/esm/util/legendCandidates.js +87 -0
  152. package/esm/util/mstUtils.d.ts +12 -1
  153. package/esm/util/mstUtils.js +20 -2
  154. package/esm/util/openFeatureWidget.d.ts +5 -0
  155. package/esm/util/openFeatureWidget.js +1 -0
  156. package/esm/util/openViews.d.ts +16 -0
  157. package/esm/util/openViews.js +22 -0
  158. package/esm/util/parentWalk.d.ts +1 -1
  159. package/esm/util/parentWalk.js +1 -1
  160. package/esm/util/pluginStore.d.ts +1 -0
  161. package/esm/util/pluginStore.js +8 -1
  162. package/esm/util/range.d.ts +13 -9
  163. package/esm/util/range.js +17 -8
  164. package/esm/util/renderToStaticMarkup.js +10 -0
  165. package/esm/util/rxjs.d.ts +16 -0
  166. package/esm/util/rxjs.js +49 -0
  167. package/esm/util/scheduleDetachedDestroy.d.ts +18 -0
  168. package/esm/util/scheduleDetachedDestroy.js +24 -0
  169. package/esm/util/scrollLatch.d.ts +1 -0
  170. package/esm/util/scrollLatch.js +8 -0
  171. package/esm/util/stopToken.d.ts +6 -5
  172. package/esm/util/tabix.d.ts +10 -4
  173. package/esm/util/tabix.js +11 -5
  174. package/esm/util/trackConfigDelta.d.ts +6 -6
  175. package/esm/util/trackConfigDelta.js +3 -3
  176. package/esm/util/tracks.d.ts +48 -0
  177. package/esm/util/tracks.js +192 -66
  178. package/esm/util/types/index.d.ts +58 -10
  179. package/esm/util/types/index.js +3 -0
  180. package/esm/util/unknownSnapshotKeys.d.ts +36 -0
  181. package/esm/util/unknownSnapshotKeys.js +69 -0
  182. package/esm/util/usePanelVirtualScroll.d.ts +6 -2
  183. package/esm/util/usePanelVirtualScroll.js +6 -2
  184. package/esm/util/useRowVirtualScroll.d.ts +1 -1
  185. package/esm/util/useRowVirtualScroll.js +1 -1
  186. package/esm/util/useVirtualScrollWheel.js +11 -1
  187. package/esm/util/viewStatus.d.ts +23 -0
  188. package/esm/util/viewStatus.js +17 -0
  189. package/esm/util/wheelZoom.d.ts +3 -1
  190. package/esm/util/wheelZoom.js +39 -14
  191. package/esm/util/whenViewSettled.d.ts +17 -4
  192. package/esm/util/whenViewSettled.js +19 -6
  193. package/esm/util/withLaunchInput.d.ts +168 -0
  194. package/esm/util/withLaunchInput.js +273 -0
  195. package/package.json +19 -11
  196. package/esm/ui/promotableMenuItems.d.ts +0 -19
  197. package/esm/ui/promotableMenuItems.js +0 -89
  198. package/esm/util/applyInitSettings.d.ts +0 -79
  199. package/esm/util/applyInitSettings.js +0 -91
@@ -8,9 +8,9 @@ export { getFileName } from './getFileName.js';
8
8
  // it. `fetchContext.ts` takes it from mstUtils instead, which is what keeps the
9
9
  // configuration schemas this file pulls in out of the fetch harness's graph.
10
10
  export { getRpcSessionId } from './parentWalk.js';
11
- import { getParent, getSnapshot, isStateTreeNode, } from '@jbrowse/mobx-state-tree';
11
+ import { getParent, getSnapshot, hasParent, isAlive, isStateTreeNode, } from '@jbrowse/mobx-state-tree';
12
12
  import { observable, runInAction, untracked } from 'mobx';
13
- import { isConfigurationSlot, preProcessSlotValues, readConfObject, } from '../configuration/index.js';
13
+ import { readConfObject } from '../configuration/index.js';
14
14
  import { getFileHandle, storeFileHandle, verifyPermission, } from './fileHandleStore.js';
15
15
  import { getFileName } from './getFileName.js';
16
16
  import { getContainingView, getEnv, getSession, objectHash, } from './mstUtils.js';
@@ -76,12 +76,22 @@ export function isSameAssemblyName(a, b, assemblyManager) {
76
76
  * `ReferenceSequenceTrack` does not — its schema omits the slot deliberately
77
77
  * (see `createReferenceSeqTrackConfig`), because the assembly config holding it
78
78
  * IS its assembly, so the name is read off the parent instead.
79
+ *
80
+ * That makes a sequence track's assembly **positional**, so a copy of one that
81
+ * is not inside its assembly cannot answer at all — and a detached config is
82
+ * what every hydration path produces (`hydrateTrackConfig`, and the working
83
+ * copy a non-admin's edits would go to, ADR-032). `hasParent` is the guard:
84
+ * without it the walk raises MST's "Failed to find the parent" from a helper
85
+ * whose other two failure modes are a plain `undefined`.
79
86
  */
80
87
  function confAssemblyNames(conf) {
81
88
  const trackAssemblyNames = readConfObject(conf, 'assemblyNames');
82
89
  if (trackAssemblyNames) {
83
90
  return trackAssemblyNames;
84
91
  }
92
+ if (!hasParent(conf)) {
93
+ return undefined;
94
+ }
85
95
  const parent = getParent(conf);
86
96
  return 'sequence' in parent
87
97
  ? [readConfObject(parent, 'name')]
@@ -368,8 +378,12 @@ export function guessAdapter(file, index, adapterHint, model) {
368
378
  }
369
379
  export function guessTrackType(adapterType, model, file) {
370
380
  if (model) {
371
- const session = getSession(model);
372
- const trackTypeGuesser = getEnv(session).pluginManager.evaluateExtensionPoint(
381
+ // getEnv(model), not getEnv(getSession(model)): the env is tree-wide, so
382
+ // the session hop only narrowed what may be passed here — a session itself
383
+ // has no session ancestor, so guessAdapter above accepted one and this
384
+ // threw on it
385
+ const { pluginManager } = getEnv(model);
386
+ const trackTypeGuesser = pluginManager.evaluateExtensionPoint(
373
387
  /** #extensionPoint Core-guessTrackTypeForLocation | sync | Guess a track type from a file location */
374
388
  'Core-guessTrackTypeForLocation', () => undefined);
375
389
  const trackType = trackTypeGuesser(adapterType, file);
@@ -392,24 +406,35 @@ export function guessTrackType(adapterType, model, file) {
392
406
  */
393
407
  export function guessTrackConf(input, pluginManager, assemblyName) {
394
408
  const { uri, index, ...extra } = typeof input === 'string' ? { uri: input } : input;
395
- const file = { uri, locationType: 'UriLocation' };
396
- const indexLocation = index
397
- ? { uri: index, locationType: 'UriLocation' }
398
- : undefined;
409
+ return {
410
+ ...guessTrackConfForLocation({ uri, locationType: 'UriLocation' }, index ? { uri: index, locationType: 'UriLocation' } : undefined, pluginManager, assemblyName),
411
+ ...extra,
412
+ };
413
+ }
414
+ /**
415
+ * {@link guessTrackConf} over an already-built {@link FileLocation}, for a
416
+ * caller holding a location the loose `{ uri }` vocabulary cannot spell — a
417
+ * desktop local path. Kept separate so `LooseTrackInput` stays URI-only: it
418
+ * sits on the track-config union and the root config's `tracks`, where a
419
+ * local path would be a config a web deployment could not open.
420
+ *
421
+ * The `trackId` is a content hash of the adapter, not a timestamp, so adding
422
+ * the same file twice is idempotent — `addToSession` dedupes on it.
423
+ */
424
+ export function guessTrackConfForLocation(file, index, pluginManager, assemblyName) {
399
425
  const adapterGuesser = pluginManager.evaluateExtensionPoint('Core-guessAdapterForLocation', () => undefined);
400
- const adapter = adapterGuesser(file, indexLocation, undefined);
426
+ const adapter = adapterGuesser(file, index, undefined);
427
+ const name = getFileName(file);
401
428
  if (!adapter || adapter.type === UNKNOWN) {
402
- throw new Error(`could not infer a track type from "${uri}"; pass a full track config instead`);
429
+ throw new Error(`could not infer a track type from "${name}"; pass a full track config instead`);
403
430
  }
404
431
  const trackTypeGuesser = pluginManager.evaluateExtensionPoint('Core-guessTrackTypeForLocation', () => undefined);
405
- const name = getFileName(file);
406
432
  return {
407
433
  trackId: `${name}-${objectHash(adapter).slice(0, 8)}`,
408
434
  type: trackTypeGuesser(adapter.type, file) ?? 'FeatureTrack',
409
435
  name,
410
436
  assemblyNames: assemblyName ? [assemblyName] : [],
411
437
  adapter,
412
- ...extra,
413
438
  };
414
439
  }
415
440
  /**
@@ -572,6 +597,57 @@ export function normalizeTrackInit(t) {
572
597
  };
573
598
  }
574
599
  }
600
+ function resolveTrackDisplayChoice(self, trackId, displayInitialSnapshot, inlineConf) {
601
+ const { pluginManager } = getEnv(self);
602
+ const session = getSession(self);
603
+ // `trackId` seeds the expansion because `guessTrackConf` otherwise
604
+ // synthesizes one from the filename, and the dedupe in the callers and
605
+ // `hideTrackGeneric` both look the track up by the id the caller passed.
606
+ // Only for the loose form: spreading a live MST node (SvInspector's
607
+ // circular track) would flatten it out of the `isStateTreeNode` branch.
608
+ const inlineRawConf = isLooseTrackConfig(inlineConf)
609
+ ? expandLooseTrackConfig({ trackId, ...inlineConf }, pluginManager)
610
+ : inlineConf;
611
+ const rawConf = inlineRawConf ?? session.getTrackById(trackId);
612
+ if (!rawConf) {
613
+ throw new Error(`Could not resolve identifier "${trackId}"`);
614
+ }
615
+ const confSnapshot = structuredClone(isStateTreeNode(rawConf) ? getSnapshot(rawConf) : rawConf);
616
+ const conf = pluginManager.evaluateExtensionPoint('Core-preProcessTrackConfig', confSnapshot);
617
+ const trackType = pluginManager.getTrackType(conf.type);
618
+ // Validate only a conf that is not already in the tree. `configSchema.create`
619
+ // here builds an entire config node and throws it away purely to surface a
620
+ // nice error; its preProcessSnapshot also re-runs Core-preProcessTrackConfig
621
+ // on a snapshot this function just preprocessed. A conf that is a state tree
622
+ // node was validated when MST created it, so all of that is pure waste on
623
+ // the common showTrack path.
624
+ if (!isStateTreeNode(rawConf)) {
625
+ try {
626
+ trackType.configSchema.create(conf, getEnv(self));
627
+ }
628
+ catch (e) {
629
+ throw new Error(`Track "${trackId}" has an invalid configuration: ${e}`, {
630
+ cause: e,
631
+ });
632
+ }
633
+ }
634
+ // A track container that isn't itself a view — a synteny level, which owns
635
+ // a track list but has no width and no registered view type — takes the
636
+ // display choice from the view it sits in, which is the same node
637
+ // getContainingView already resolves to for everything else beneath it.
638
+ const view = isViewModel(self) ? self : getContainingView(self);
639
+ const viewType = pluginManager.getViewType(view.type);
640
+ const picked = pickDisplayForView({
641
+ declaredDisplays: conf.displays ?? [],
642
+ requestedType: displayInitialSnapshot.type,
643
+ trackDisplayTypes: trackType.displayTypes.map(d => d.name),
644
+ viewDisplayTypes: viewType.displayTypes.map(d => d.name),
645
+ });
646
+ if (!picked) {
647
+ throw new Error(`Could not find a compatible display for view type ${view.type}`);
648
+ }
649
+ return { rawConf, conf, trackType, picked };
650
+ }
575
651
  export function showTrackGeneric(self, trackId,
576
652
  // `object`, annotated, rather than inferred from the default. A bare `{}`
577
653
  // accepts every non-nullish value, numbers included, which is how a synteny
@@ -597,49 +673,28 @@ inlineConf) {
597
673
  // snackbars. Config is validated before the push so the open set never holds
598
674
  // a broken track.
599
675
  try {
600
- const rawConf = (inlineConf && expandLooseTrackConfig(inlineConf, pluginManager)) ??
601
- session.getTrackById(trackId);
602
- if (!rawConf) {
603
- throw new Error(`Could not resolve identifier "${trackId}"`);
604
- }
605
- const confSnapshot = structuredClone(isStateTreeNode(rawConf) ? getSnapshot(rawConf) : rawConf);
606
- const conf = pluginManager.evaluateExtensionPoint('Core-preProcessTrackConfig', confSnapshot);
607
- const trackType = pluginManager.getTrackType(conf.type);
608
- // Validate only a conf that is not already in the tree. `configSchema.create`
609
- // here builds an entire config node and throws it away purely to surface a
610
- // nice error; its preProcessSnapshot also re-runs Core-preProcessTrackConfig
611
- // on a snapshot this function just preprocessed. A conf that is a state tree
612
- // node was validated when MST created it, so all of that is pure waste on
613
- // the common showTrack path.
614
- if (!isStateTreeNode(rawConf)) {
615
- try {
616
- trackType.configSchema.create(conf, getEnv(self));
617
- }
618
- catch (e) {
619
- throw new Error(`Track "${trackId}" has an invalid configuration: ${e}`, { cause: e });
620
- }
621
- }
622
- // A track container that isn't itself a view — a synteny level, which owns
623
- // a track list but has no width and no registered view type — takes the
624
- // display choice from the view it sits in, which is the same node
625
- // getContainingView already resolves to for everything else beneath it.
626
- const view = isViewModel(self) ? self : getContainingView(self);
627
- const viewType = pluginManager.getViewType(view.type);
628
- const picked = pickDisplayForView({
629
- declaredDisplays: conf.displays ?? [],
630
- requestedType: displayInitialSnapshot.type,
631
- trackDisplayTypes: trackType.displayTypes.map(d => d.name),
632
- viewDisplayTypes: viewType.displayTypes.map(d => d.name),
633
- });
634
- if (!picked) {
635
- throw new Error(`Could not find a compatible display for view type ${view.type}`);
636
- }
676
+ const { rawConf, conf, trackType, picked } = resolveTrackDisplayChoice(self, trackId, displayInitialSnapshot, inlineConf);
637
677
  const { type: displayType, conf: displayConf } = picked;
638
678
  const displayId = displayConf?.displayId ?? `${trackId}-${displayType}`;
679
+ // a registered display whose model is not loaded yet: the load re-enters
680
+ // through the view's own showTrack action, so a synchronous caller gets the
681
+ // track a tick late; a caller that needs it back awaits launchTrack
682
+ const displayRecord = pluginManager.resolveDisplayTypeRecord(displayType);
683
+ if (displayRecord && !displayRecord.isStateModelLoaded) {
684
+ void launchTrackGeneric(self, trackId, initialSnapshot, displayInitialSnapshot, inlineConf);
685
+ return undefined;
686
+ }
639
687
  const track = trackType.stateModel.create({
640
688
  ...initialSnapshot,
641
689
  type: conf.type,
642
- configuration: inlineConf ?? trackId,
690
+ // `rawConf`, not the caller's `inlineConf`: the loose `{ trackId, uri }`
691
+ // form has no `type` for the config schema to dispatch on, and the
692
+ // concrete schema this lands on carries no loose preprocessor — that one
693
+ // is on `pluggableConfigSchemaType('track')`, which a track model's
694
+ // `ConfigurationReference` is not. Not `conf` either: that has already
695
+ // run Core-preProcessTrackConfig, which the schema's own
696
+ // preProcessSnapshot runs again.
697
+ configuration: inlineConf ? rawConf : trackId,
643
698
  displays: [
644
699
  {
645
700
  ...displayConf,
@@ -652,23 +707,30 @@ inlineConf) {
652
707
  self.tracks.push(track);
653
708
  // Display settings (height, color, …) are config slots now, not display
654
709
  // instance props — passed in the display snapshot they'd be dropped as
655
- // unknown MST keys. Route the ones that are real slots onto the persistent
656
- // display config so they take effect and survive hide/retick (#5591). Runs
657
- // after the push so the display's config reference can resolve.
710
+ // unknown MST keys. applyDisplaySettings routes the ones that are real
711
+ // slots onto the persistent display config so they take effect and
712
+ // survive hide/retick (#5591), running the schema's preProcessSnapshot
713
+ // first so shorthand expansions and legacy-key migrations reach this path
714
+ // too — a session spec, share link, or embed writes slots here rather
715
+ // than creating a config from a snapshot, and those surfaces are meant to
716
+ // speak the same vocabulary. Runs after the push so the display's config
717
+ // reference can resolve. Slots only, deliberately: this is the
718
+ // declarative surface, and `allowSetters` stays off so a spec key cannot
719
+ // reach internal display actions.
658
720
  //
659
- // preProcessSlotValues first, so a display schema's shorthand expansions
660
- // and legacy-key migrations reach this path too: a session spec, share
661
- // link, or embed writes slots here rather than creating a config from a
662
- // snapshot, and those two surfaces are meant to speak the same vocabulary.
663
- const display = track.displays[0];
664
- const displaySlots = preProcessSlotValues(display.configuration, displayInitialSnapshot);
665
- for (const [key, value] of Object.entries(displaySlots)) {
666
- if (key !== 'type' && isConfigurationSlot(display.configuration, key)) {
667
- // the key comes from a snapshot at runtime, and setConf's slot name is
668
- // a compile-time type
669
- // eslint-disable-next-line no-restricted-syntax
670
- display.configuration.setSlot(key, value);
671
- }
721
+ // `type` is dropped first because this function already consumed it
722
+ // (pickDisplayForView above), and only `failed` — a key whose write threw —
723
+ // is notified: `unapplied` here also holds the MST display props the
724
+ // snapshot spread above already applied, so notifying on it would report a
725
+ // correct call as broken. A throwing slot value used to reach the outer
726
+ // catch and raise a snackbar; per-key catching kept the track intact but
727
+ // took the message away with it.
728
+ const displaySettings = Object.fromEntries(Object.entries(displayInitialSnapshot).filter(([key]) => key !== 'type'));
729
+ const report = track.applyDisplaySettings(displaySettings);
730
+ if (report.failed.length) {
731
+ session.notifyError(`Track "${trackId}" rejected ${report.failed
732
+ .map(f => `${f.key}: ${f.error}`)
733
+ .join('; ')}`);
672
734
  }
673
735
  // if this track came from a connection, persist its config so it survives
674
736
  // reload without re-establishing the connection (no-op otherwise)
@@ -708,6 +770,32 @@ export function toggleTrackGeneric(self, trackId) {
708
770
  ? false
709
771
  : !!showTrackGeneric(self, trackId);
710
772
  }
773
+ export async function launchTrackGeneric(self, trackId, initialSnapshot = {}, displayInitialSnapshot = {}, inlineConf) {
774
+ const { pluginManager } = getEnv(self);
775
+ const session = getSession(self);
776
+ const found = self.tracks.find(t => t.configuration.trackId === trackId);
777
+ if (found) {
778
+ return found;
779
+ }
780
+ try {
781
+ const { picked } = resolveTrackDisplayChoice(self, trackId, displayInitialSnapshot, inlineConf);
782
+ await pluginManager.resolveDisplayTypeRecord(picked.type)?.loadStateModel();
783
+ }
784
+ catch (e) {
785
+ session.notifyError(`${e}`, e);
786
+ return undefined;
787
+ }
788
+ if (!isAlive(self)) {
789
+ return undefined;
790
+ }
791
+ return self.showTrack?.(trackId, initialSnapshot, displayInitialSnapshot, inlineConf);
792
+ }
793
+ // the hide runs in the synchronous prologue, so it keeps action context
794
+ export async function launchToggleTrackGeneric(self, trackId) {
795
+ return hideTrackGeneric(self, trackId)
796
+ ? false
797
+ : !!(await launchTrackGeneric(self, trackId));
798
+ }
711
799
  /**
712
800
  * Every track config the session can show, connection-supplied ones included.
713
801
  *
@@ -726,3 +814,41 @@ export function allSessionTracks(session) {
726
814
  ? [...session.tracks, ...connectionTracks]
727
815
  : session.tracks;
728
816
  }
817
+ /**
818
+ * The session's annotation for `assemblyName`, as trackIds: what a view opened
819
+ * on another genome starts with. Only FeatureTracks, not everything configured
820
+ * for the assembly — an alignments track turned on behind the user's back
821
+ * fetches data nobody asked for, and a FeatureTrack over a few kb is one tabix
822
+ * range query.
823
+ */
824
+ export function annotationTrackIds(session, assemblyName) {
825
+ return allSessionTracks(session).flatMap(track => readConfObject(track, 'type') === 'FeatureTrack' &&
826
+ readConfObject(track, 'assemblyNames').some(name => isSameAssemblyName(name, assemblyName, session.assemblyManager))
827
+ ? [readConfObject(track, 'trackId')]
828
+ : []);
829
+ }
830
+ function isNavigableView(view) {
831
+ return 'navToLocString' in view;
832
+ }
833
+ /**
834
+ * One assembly on its own in a linear genome view, at `loc`. Keyed on `id`:
835
+ * following several alignments to one genome re-navigates the view rather than
836
+ * stacking a new one each time.
837
+ */
838
+ export async function openAssemblyInLinearView({ session, id, assemblyName, loc, tracks, }) {
839
+ const view = session.views.find(v => v.id === id);
840
+ if (view && isNavigableView(view)) {
841
+ await view.navToLocString(loc, assemblyName);
842
+ }
843
+ else {
844
+ session.addView('LinearGenomeView', {
845
+ id,
846
+ assembly: assemblyName,
847
+ loc,
848
+ tracks,
849
+ });
850
+ }
851
+ }
852
+ export function openMateLabel(assemblyName) {
853
+ return `Open ${assemblyName} at the matching region`;
854
+ }
@@ -1,3 +1,4 @@
1
+ import type { ViewSnapshotInput } from '../../PluginManager.ts';
1
2
  import type TextSearchManager from '../../TextSearch/TextSearchManager.ts';
2
3
  import type { AnyConfigurationModel, ResolvableDisplay } from '../../configuration/index.ts';
3
4
  import type { BaseInternetAccountModel } from '../../pluggableElementTypes/models/index.ts';
@@ -22,7 +23,8 @@ export type { AugmentedRegion, BasePlugin, BlobLocation, FileHandleLocation, Fil
22
23
  export interface AbstractViewContainer extends IStateTreeNode {
23
24
  views: AbstractViewModel[];
24
25
  removeView(view: AbstractViewModel): void;
25
- addView(typeName: string, initialState?: Record<string, unknown>): AbstractViewModel;
26
+ addView<N extends string>(typeName: N, initialState?: NoInfer<ViewSnapshotInput<N>>): AbstractViewModel;
27
+ launchView(typeName: string, initialState?: Record<string, unknown>): Promise<AbstractViewModel>;
26
28
  }
27
29
  export declare function isViewContainer(thing: unknown): thing is AbstractViewContainer;
28
30
  /**
@@ -32,6 +34,7 @@ export declare function isViewContainer(thing: unknown): thing is AbstractViewCo
32
34
  */
33
35
  export interface TrackActionView {
34
36
  showTrack: (id: string) => void;
37
+ launchTrack: (id: string) => Promise<unknown>;
35
38
  getActiveDisplayId?: (trackId: string) => string | undefined;
36
39
  }
37
40
  /**
@@ -153,6 +156,21 @@ export interface SessionWithConfigEditing extends AbstractSessionModel {
153
156
  }): void;
154
157
  }
155
158
  export declare function isSessionModelWithConfigEditing(t: unknown): t is SessionWithConfigEditing;
159
+ /**
160
+ * abstract interface for a session that keeps a list of views, so that `addView`
161
+ * appends and the view a launch came out of survives it.
162
+ *
163
+ * `AbstractViewContainer` alone does not say this. The single-view embedded
164
+ * products satisfy it with a `views` getter that wraps their one `view` in a
165
+ * fresh plain array and an `addView` that overwrites that view, so "Open in new
166
+ * view" there would destroy the view it was offered from. A view list that is
167
+ * itself part of the state tree is what tells the two apart: only a session
168
+ * that stores its views can hold one more.
169
+ */
170
+ export interface SessionWithMultipleViews extends AbstractSessionModel {
171
+ views: AbstractViewModel[] & IStateTreeNode;
172
+ }
173
+ export declare function isSessionWithMultipleViews(t: unknown): t is SessionWithMultipleViews;
156
174
  /**
157
175
  * abstract interface for a session that can swap one of its views for a new one
158
176
  * of another type, in the slot the old view occupied.
@@ -164,7 +182,7 @@ export declare function isSessionModelWithConfigEditing(t: unknown): t is Sessio
164
182
  * and the guard is what a launcher asks before offering it.
165
183
  */
166
184
  export interface SessionWithViewReplacement extends AbstractSessionModel {
167
- replaceView(view: AbstractViewModel, typeName: string, initialState?: Record<string, unknown>): AbstractViewModel;
185
+ replaceView<N extends string>(view: AbstractViewModel, typeName: N, initialState?: NoInfer<ViewSnapshotInput<N>>): AbstractViewModel;
168
186
  }
169
187
  export declare function isSessionWithViewReplacement(t: unknown): t is SessionWithViewReplacement;
170
188
  /**
@@ -188,10 +206,10 @@ export declare function canReplaceView(session: AbstractViewContainer, view: Abs
188
206
  * that can't replace a view falls back to appending, so a caller passes the
189
207
  * source view unconditionally and never has to ask twice.
190
208
  */
191
- export declare function addOrReplaceView({ session, typeName, initialState, replacing, }: {
209
+ export declare function addOrReplaceView<N extends string>({ session, typeName, initialState, replacing, }: {
192
210
  session: AbstractViewContainer;
193
- typeName: string;
194
- initialState?: Record<string, unknown>;
211
+ typeName: N;
212
+ initialState?: NoInfer<ViewSnapshotInput<N>>;
195
213
  replacing?: AbstractViewModel;
196
214
  }): AbstractViewModel;
197
215
  /**
@@ -358,21 +376,30 @@ export interface SessionWithSessionPlugins extends AbstractSessionModel {
358
376
  removeSessionPlugin: (plugin: PluginDefinition) => void;
359
377
  }
360
378
  export declare function isSessionWithSessionPlugins(thing: unknown): thing is SessionWithSessionPlugins;
379
+ export interface PermanentPluginsSafeMode {
380
+ reason: 'requested' | 'previousLaunchFailed';
381
+ /** what was loading when the launch that failed did not finish */
382
+ suspects: string[];
383
+ }
361
384
  /**
362
385
  * A session whose product keeps a plugin list of its own, outside both the
363
386
  * config and the session — jbrowse-web's permanent plugins, stored in the
364
387
  * browser against the config being viewed, so an install survives a new session
365
388
  * and the next visit.
366
389
  *
367
- * The list is not MST state, so `permanentPlugins` re-reads storage on each
368
- * call and nothing observes it. That is enough for its two readers, both of
369
- * which run in response to a click: adding or removing one asks for the whole-
370
- * app reload every plugin change needs anyway.
390
+ * The plugin store is the whole surface: the pin beside an installed plugin
391
+ * writes the list, and an entry the load skipped is a row there with a switch
392
+ * on it, which is what makes the safe mode below something a user can act on.
371
393
  */
372
394
  export interface SessionWithPermanentPlugins extends AbstractSessionModel {
373
- permanentPlugins: PluginDefinition[];
395
+ permanentPlugins: (PluginDefinition & {
396
+ disabled?: boolean;
397
+ })[];
398
+ permanentPluginsSafeMode: PermanentPluginsSafeMode | undefined;
374
399
  addPermanentPlugin: (plugin: PluginDefinition) => void;
375
400
  removePermanentPlugin: (plugin: PluginDefinition) => void;
401
+ setPermanentPluginDisabled: (plugin: PluginDefinition, disabled: boolean) => void;
402
+ reloadWithPermanentPlugins: () => void;
376
403
  }
377
404
  export declare function isSessionWithPermanentPlugins(thing: unknown): thing is SessionWithPermanentPlugins;
378
405
  /** abstract interface for a session that manages a global selection */
@@ -410,6 +437,13 @@ export interface TrackContainer {
410
437
  * silence.
411
438
  */
412
439
  showTrack: (trackId: string, initialSnapshot?: object, displayInitialSnapshot?: Record<string, unknown>, inlineConf?: Record<string, unknown>) => unknown;
440
+ /**
441
+ * The sync pair still works on an unloaded display — it loads and shows a
442
+ * tick later, returning undefined — so these are for a caller that needs
443
+ * the track back.
444
+ */
445
+ launchTrack: (trackId: string, initialSnapshot?: object, displayInitialSnapshot?: Record<string, unknown>, inlineConf?: Record<string, unknown>) => Promise<unknown>;
446
+ launchToggleTrack: (trackId: string) => Promise<unknown>;
413
447
  hideTrack: (trackId: string) => unknown;
414
448
  toggleTrack: (trackId: string) => unknown;
415
449
  }
@@ -453,6 +487,20 @@ export interface AbstractViewModel {
453
487
  * `trackContainerFor` cannot answer that — a walker has no id to ask with.
454
488
  */
455
489
  trackContainers?: TrackContainer[];
490
+ /**
491
+ * what this view contributes to the census of what is open: the tracks it
492
+ * holds itself, and the views nested directly inside it. `BaseViewModel`
493
+ * answers both with nothing, so a view declares what it owns instead of the
494
+ * base reading properties whose meaning belongs to the view — see
495
+ * `util/openViews.ts`, where the recursion over them lives.
496
+ *
497
+ * Required, like `bodyMounted`, so a stand-in that means to be enumerated
498
+ * says so; the empty default means forgetting under-reports rather than
499
+ * putting something that is not a track in front of the readiness marker.
500
+ */
501
+ ownTracks: AbstractTrackModel[];
502
+ /** the views nested directly inside this one — see `ownTracks` */
503
+ ownViews: AbstractViewModel[];
456
504
  }
457
505
  export declare function isViewModel(thing: unknown): thing is AbstractViewModel;
458
506
  type Display = {
@@ -18,6 +18,9 @@ export function isSessionModel(thing) {
18
18
  export function isSessionModelWithConfigEditing(t) {
19
19
  return isSessionModel(t) && 'editConfiguration' in t;
20
20
  }
21
+ export function isSessionWithMultipleViews(t) {
22
+ return isSessionModel(t) && isStateTreeNode(t.views);
23
+ }
21
24
  export function isSessionWithViewReplacement(t) {
22
25
  return isSessionModel(t) && 'replaceView' in t;
23
26
  }
@@ -0,0 +1,36 @@
1
+ import type { IStateTreeNode } from '@jbrowse/mobx-state-tree';
2
+ /** what a diagnostic calls the view: its snapshot `type`, or the model name */
3
+ export declare function viewLabel(self: IStateTreeNode): string;
4
+ /**
5
+ * What every surface calls this mistake. A session spec launches a view without
6
+ * ever building a snapshot, so it classifies its own keys and reports them
7
+ * through its own session rather than through the sink below — one wording, so
8
+ * a typo reads the same whether it was written in a config or in a URL.
9
+ */
10
+ export declare function unknownKeysMessage(label: string, keys: string[]): string;
11
+ /**
12
+ * Name the keys a view was handed and could not place, from the partition
13
+ * `withLaunchInput` runs on the snapshot.
14
+ */
15
+ export declare function reportUnknownKeys(self: IStateTreeNode, keys: string[]): void;
16
+ /**
17
+ * Name the row lists a view refused whole. `withLaunchInput` splits an authored
18
+ * array into the recipes a launcher resolves and the built snapshots MST
19
+ * restores, per entry — but a row list is indexed against the view's `levels`
20
+ * and per-level tracks, so a list holding both kinds has no correct split and
21
+ * is refused rather than renumbered.
22
+ */
23
+ export declare function reportMalformedRows(self: IStateTreeNode, keys: string[]): void;
24
+ /**
25
+ * What every surface calls v4's nested `init`. v5 takes every setting directly
26
+ * on the view object and unwraps the nesting on the way in, so the settings
27
+ * still arrive — this names the spelling to fix while they do.
28
+ */
29
+ export declare function legacyInitMessage(label: string): string;
30
+ /**
31
+ * Name a snapshot that still writes v4's nested `init`. The console alone,
32
+ * unlike the two reporters above: the settings were applied, so there is
33
+ * nothing for a visitor to act on and a warning toast over a working view is
34
+ * noise. `jbrowse validate` is where the author of the config hears about it.
35
+ */
36
+ export declare function reportLegacyInit(self: IStateTreeNode): void;
@@ -0,0 +1,69 @@
1
+ import { getType } from '@jbrowse/mobx-state-tree';
2
+ import { getNotificationSink } from './sessionServices.js';
3
+ /** what a diagnostic calls the view: its snapshot `type`, or the model name */
4
+ export function viewLabel(self) {
5
+ return self.type ?? getType(self).name;
6
+ }
7
+ /**
8
+ * What every surface calls this mistake. A session spec launches a view without
9
+ * ever building a snapshot, so it classifies its own keys and reports them
10
+ * through its own session rather than through the sink below — one wording, so
11
+ * a typo reads the same whether it was written in a config or in a URL.
12
+ */
13
+ export function unknownKeysMessage(label, keys) {
14
+ return `${label} ignored unknown key(s): ${keys.join(', ')}`;
15
+ }
16
+ /**
17
+ * Name the keys a view was handed and could not place, from the partition
18
+ * `withLaunchInput` runs on the snapshot.
19
+ */
20
+ export function reportUnknownKeys(self, keys) {
21
+ if (!keys.length) {
22
+ return;
23
+ }
24
+ const message = unknownKeysMessage(viewLabel(self), keys);
25
+ console.warn(message);
26
+ try {
27
+ getNotificationSink(self).notify(message, 'warning');
28
+ }
29
+ catch {
30
+ // a view built outside a session has nowhere to put it
31
+ }
32
+ }
33
+ /**
34
+ * Name the row lists a view refused whole. `withLaunchInput` splits an authored
35
+ * array into the recipes a launcher resolves and the built snapshots MST
36
+ * restores, per entry — but a row list is indexed against the view's `levels`
37
+ * and per-level tracks, so a list holding both kinds has no correct split and
38
+ * is refused rather than renumbered.
39
+ */
40
+ export function reportMalformedRows(self, keys) {
41
+ if (!keys.length) {
42
+ return;
43
+ }
44
+ const message = `${viewLabel(self)} refused ${keys.join(', ')}: the list mixes built view snapshots with recipes to open one, and the rows index against the levels between them. Write all of them one way.`;
45
+ console.warn(message);
46
+ try {
47
+ getNotificationSink(self).notify(message, 'warning');
48
+ }
49
+ catch {
50
+ // a view built outside a session has nowhere to put it
51
+ }
52
+ }
53
+ /**
54
+ * What every surface calls v4's nested `init`. v5 takes every setting directly
55
+ * on the view object and unwraps the nesting on the way in, so the settings
56
+ * still arrive — this names the spelling to fix while they do.
57
+ */
58
+ export function legacyInitMessage(label) {
59
+ return `${label} nests its settings under "init", which is deprecated: write every setting directly on the view object.`;
60
+ }
61
+ /**
62
+ * Name a snapshot that still writes v4's nested `init`. The console alone,
63
+ * unlike the two reporters above: the settings were applied, so there is
64
+ * nothing for a visitor to act on and a warning toast over a working view is
65
+ * noise. `jbrowse validate` is where the author of the config hears about it.
66
+ */
67
+ export function reportLegacyInit(self) {
68
+ console.warn(legacyInitMessage(viewLabel(self)));
69
+ }
@@ -8,11 +8,15 @@ interface PanelScrollTarget {
8
8
  * the canvas basic display: a plain wheel scrolls it, `shift` keeps scrolling it
9
9
  * once `scrollZoom` has claimed the plain wheel for the view, and ctrl/meta is
10
10
  * left to the browser and the view's pinch-zoom. Nothing here resizes anything.
11
+ * A sideways-dominant wheel is the view's pan and is declined
12
+ * (`useVirtualScrollWheel`, which says what latching does with that).
11
13
  *
12
14
  * These panels paint a fixed-size canvas at `-scrollTop` rather than living in a
13
15
  * DOM overflow container, so nothing scrolls them without this. The latch (inside
14
- * `applyScroll`) owns `preventDefault` but never `stopPropagation`, so a diagonal
15
- * wheel still bubbles its horizontal component to the LGV for panning.
16
+ * `applyScroll`) owns `preventDefault` but never `stopPropagation`: the event
17
+ * still reaches the LGV, which reads that flag and declines a gesture this panel
18
+ * has claimed, so a diagonal wheel scrolls the reads without also panning the
19
+ * genome sideways.
16
20
  *
17
21
  * ADR-027 keeps wheel-*intent* dispatch per handler, because the panels do not
18
22
  * agree on what a gesture means. This is the second of the two rules that
@@ -4,11 +4,15 @@ import { useVirtualScrollWheel } from './useVirtualScrollWheel.js';
4
4
  * the canvas basic display: a plain wheel scrolls it, `shift` keeps scrolling it
5
5
  * once `scrollZoom` has claimed the plain wheel for the view, and ctrl/meta is
6
6
  * left to the browser and the view's pinch-zoom. Nothing here resizes anything.
7
+ * A sideways-dominant wheel is the view's pan and is declined
8
+ * (`useVirtualScrollWheel`, which says what latching does with that).
7
9
  *
8
10
  * These panels paint a fixed-size canvas at `-scrollTop` rather than living in a
9
11
  * DOM overflow container, so nothing scrolls them without this. The latch (inside
10
- * `applyScroll`) owns `preventDefault` but never `stopPropagation`, so a diagonal
11
- * wheel still bubbles its horizontal component to the LGV for panning.
12
+ * `applyScroll`) owns `preventDefault` but never `stopPropagation`: the event
13
+ * still reaches the LGV, which reads that flag and declines a gesture this panel
14
+ * has claimed, so a diagonal wheel scrolls the reads without also panning the
15
+ * genome sideways.
12
16
  *
13
17
  * ADR-027 keeps wheel-*intent* dispatch per handler, because the panels do not
14
18
  * agree on what a gesture means. This is the second of the two rules that
@@ -10,7 +10,7 @@ interface RowScrollTarget {
10
10
  * Wheel gestures over a **row-stack** panel — the multi-sample variant displays
11
11
  * and MAF: `shift`+wheel resizes the rows (a vertical-zoom that keeps the row
12
12
  * under the cursor put), a plain wheel scrolls them, and anything the panel
13
- * can't consume is left to bubble to the view.
13
+ * can't consume — a sideways swipe included — is left to bubble to the view.
14
14
  *
15
15
  * These panels paint a fixed-size canvas at `-scrollTop` rather than living in a
16
16
  * DOM overflow container, so nothing scrolls them without this.
@@ -4,7 +4,7 @@ import { useVirtualScrollWheel } from './useVirtualScrollWheel.js';
4
4
  * Wheel gestures over a **row-stack** panel — the multi-sample variant displays
5
5
  * and MAF: `shift`+wheel resizes the rows (a vertical-zoom that keeps the row
6
6
  * under the cursor put), a plain wheel scrolls them, and anything the panel
7
- * can't consume is left to bubble to the view.
7
+ * can't consume — a sideways swipe included — is left to bubble to the view.
8
8
  *
9
9
  * These panels paint a fixed-size canvas at `-scrollTop` rather than living in a
10
10
  * DOM overflow container, so nothing scrolls them without this.