@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
package/esm/ui/palette.js CHANGED
@@ -231,16 +231,16 @@ const darkNeutrals = {
231
231
  export const colorFwdStrand = '#EC8B8B';
232
232
  /** #color alignments-strand | Reverse strand | Read maps to the reverse strand */
233
233
  export const colorRevStrand = '#8F8FD8';
234
- /** #color alignments-pair-orientation | LR (→ ←, normal proper pair) | Concordant */
234
+ /** #color alignments-pair-orientation | LR (→ ←) | Normal proper pair */
235
235
  export const colorPairLR = '#d3d3d3';
236
236
  // Dimmer grey for dark mode: the light #d3d3d3 reads as near-white glaring
237
237
  // blocks against a dark track background. Wired into the dark alignmentFill.
238
238
  export const colorPairLRDark = '#8a8a8a';
239
- /** #color alignments-pair-orientation | RL (← →, mates point away from each other) | Abnormal orientation */
239
+ /** #color alignments-pair-orientation | RL (← →) | Mates point away from each other */
240
240
  export const colorPairRL = '#0099bb';
241
- /** #color alignments-pair-orientation | LL (→ →, both mates forward strand) | Abnormal orientation */
241
+ /** #color alignments-pair-orientation | LL (→ →) | Both mates on the forward strand */
242
242
  export const colorPairLL = '#4d9a4d';
243
- /** #color alignments-pair-orientation | RR (← ←, both mates reverse strand) | Abnormal orientation */
243
+ /** #color alignments-pair-orientation | RR (← ←) | Both mates on the reverse strand */
244
244
  export const colorPairRR = '#5555bb';
245
245
  // Was `colorNostrand`, which named the one thing it essentially never paints. A
246
246
  // SAM record's strand is DEFINED from the reverse flag inside the adapter's
@@ -266,8 +266,8 @@ export const colorPairRR = '#5555bb';
266
266
  // settle it, and the dark-mode half is `agent-docs/TODO.md`.
267
267
  export const colorNeutralRead = '#c8c8c8';
268
268
  /**
269
- * #color alignments-pair-orientation | Inter-chromosomal | Mate maps to a different chromosome; colored distinctly rather than by orientation
270
- * #color alignments-insert-size | Mate on a different chromosome | Suggests an inter-chromosomal event
269
+ * #color alignments-pair-orientation | Inter-chromosomal | Mate on another chromosome
270
+ * #color alignments-insert-size | Inter-chromosomal | Mate on another chromosome
271
271
  */
272
272
  // THIS SLOT AND `colorUnmappedMate` WERE CHOSEN TOGETHER, and reading either one
273
273
  // alone is how the previous pair got stuck. Both are warm, both belong to all
@@ -299,9 +299,9 @@ export const colorNeutralRead = '#c8c8c8';
299
299
  // neighbour that must not be confusable: red means "insert too long", and a
300
300
  // second red would read as a second statement about insert size.
301
301
  export const colorInterchrom = '#af4d19';
302
- /** #color alignments-insert-size | Insert larger than expected | Suggests a deletion spanning the pair */
302
+ /** #color alignments-insert-size | Insert larger than expected | Deletion between the mates */
303
303
  export const colorLongInsert = '#ff0000';
304
- /** #color alignments-insert-size | Insert smaller than expected | Suggests an insertion between the pair */
304
+ /** #color alignments-insert-size | Insert smaller than expected | Insertion between the mates */
305
305
  // This slot has TWO neighbours it must stay clear of, and every past value has
306
306
  // solved one by walking into the other. They sit at opposite ends of the
307
307
  // lightness axis, which is what makes it a balance rather than a direction:
@@ -349,8 +349,8 @@ export const colorLongInsert = '#ff0000';
349
349
  // them.
350
350
  export const colorShortInsert = '#f582c0';
351
351
  /**
352
- * #color alignments-pair-orientation | Mate unmapped | The other end of the pair aligned nowhere, so orientation and insert size say nothing
353
- * #color alignments-insert-size | Mate unmapped | The other end of the pair aligned nowhere, so insert size says nothing
352
+ * #color alignments-pair-orientation | Mate unmapped | Mate aligned nowhere
353
+ * #color alignments-insert-size | Mate unmapped | Mate aligned nowhere
354
354
  */
355
355
  // A NON-COLOUR, at the dark end of the neutral scale on BOTH themes. It takes
356
356
  // the first half of the `readOverlap` pattern and not the second, and that split
@@ -421,7 +421,7 @@ export const colorUnmappedMate = '#000000';
421
421
  // the least that still reads as a read rather than as a hole.
422
422
  export const colorUnmappedMateDark = '#4a4a4a';
423
423
  export const colorLongreadInv = '#7755bb';
424
- /** #color alignments-pair-orientation | Split paired-end read (inverted) | A paired read's supplementary segment maps opposite-strand to its primary, so the junction is inverted — an inversion or an inverted duplication */
424
+ /** #color alignments-pair-orientation | Split read (inverted) | Supplementary segment on the opposite strand to its primary */
425
425
  export const colorSplitReadInversion = '#9b30b0';
426
426
  export const colorSupplementary = '#f0b878';
427
427
  // Qualitative palette for coloring reads by an arbitrary tag value (e.g. the HP
@@ -26,10 +26,24 @@ export declare function displayTypesWithPromotableSlots(displayTypes: readonly {
26
26
  * Reads `pin.control.slot` and nothing else. A raw `endAdornment` is
27
27
  * deliberately not counted: it is an arbitrary element (synteny's colour
28
28
  * swatch), so it cannot say which slot it promotes, and a pin built by hand
29
- * rather than through `promotableToggleItem`/`promotableRadioItem` is a thing to
30
- * find, not to accept.
29
+ * rather than through `makePin`/`makeTogglePin` is a thing to find, not to
30
+ * accept.
31
31
  */
32
32
  export declare function pinnedSlots(items: MenuItem[]): Set<string>;
33
+ /**
34
+ * Rows whose pin is the wrong kind for the row, submenus included: a checkbox
35
+ * row carrying a value pin, or a radio row carrying a toggle pin. Empty is the
36
+ * healthy answer.
37
+ *
38
+ * The row builders type the option so this cannot happen through them; this
39
+ * is for a `pin` written by hand as a literal, which the type system lets
40
+ * through as the union. A checkbox row's pin has to be the toggle kind because
41
+ * that is what a click beside a checkbox is read as — "turn this on for all" —
42
+ * and a value pin there applies the row's *current* state instead, which
43
+ * beside an unchecked box is a visible no-op. That shipped once (2026-09-07)
44
+ * and was caught in a live session, not by a test; this is the test.
45
+ */
46
+ export declare function misKindedPins(items: MenuItem[]): string[];
33
47
  /**
34
48
  * A display's promotable slots that its own track menu offers no pin for,
35
49
  * sorted. Empty is the healthy answer.
@@ -23,6 +23,7 @@
23
23
  * result against a baseline.
24
24
  */
25
25
  import { promotableSlotNames } from '../configuration/promotableSlots.js';
26
+ import { resolveSubMenu } from './MenuTypes.js';
26
27
  /**
27
28
  * The registered display types whose config schema declares a promotable slot,
28
29
  * sorted — the set a pin-coverage check has to reach, answered off the
@@ -50,8 +51,8 @@ export function displayTypesWithPromotableSlots(displayTypes) {
50
51
  * Reads `pin.control.slot` and nothing else. A raw `endAdornment` is
51
52
  * deliberately not counted: it is an arbitrary element (synteny's colour
52
53
  * swatch), so it cannot say which slot it promotes, and a pin built by hand
53
- * rather than through `promotableToggleItem`/`promotableRadioItem` is a thing to
54
- * find, not to accept.
54
+ * rather than through `makePin`/`makeTogglePin` is a thing to find, not to
55
+ * accept.
55
56
  */
56
57
  export function pinnedSlots(items) {
57
58
  const found = new Set();
@@ -61,7 +62,43 @@ export function pinnedSlots(items) {
61
62
  found.add(item.pin.control.slot);
62
63
  }
63
64
  if ('subMenu' in item) {
64
- walk(item.subMenu);
65
+ walk(resolveSubMenu(item));
66
+ }
67
+ }
68
+ };
69
+ walk(items);
70
+ return found;
71
+ }
72
+ /**
73
+ * Rows whose pin is the wrong kind for the row, submenus included: a checkbox
74
+ * row carrying a value pin, or a radio row carrying a toggle pin. Empty is the
75
+ * healthy answer.
76
+ *
77
+ * The row builders type the option so this cannot happen through them; this
78
+ * is for a `pin` written by hand as a literal, which the type system lets
79
+ * through as the union. A checkbox row's pin has to be the toggle kind because
80
+ * that is what a click beside a checkbox is read as — "turn this on for all" —
81
+ * and a value pin there applies the row's *current* state instead, which
82
+ * beside an unchecked box is a visible no-op. That shipped once (2026-09-07)
83
+ * and was caught in a live session, not by a test; this is the test.
84
+ */
85
+ export function misKindedPins(items) {
86
+ const found = [];
87
+ const walk = (list) => {
88
+ for (const item of list) {
89
+ if ('pin' in item && item.pin && 'type' in item) {
90
+ const { kind } = item.pin.control;
91
+ const wanted = item.type === 'checkbox'
92
+ ? 'toggle'
93
+ : item.type === 'radio'
94
+ ? 'value'
95
+ : undefined;
96
+ if (wanted && kind !== wanted) {
97
+ found.push(`${item.pin.label}: ${kind} pin on a ${item.type} row`);
98
+ }
99
+ }
100
+ if ('subMenu' in item) {
101
+ walk(resolveSubMenu(item));
65
102
  }
66
103
  }
67
104
  };
package/esm/ui/theme.js CHANGED
@@ -209,6 +209,11 @@ const baseThemeOptions = {
209
209
  MuiFormLabel: {
210
210
  styleOverrides: darkModeContrastOverride(['&.Mui-focused']),
211
211
  },
212
+ // the rail, track and thumb are all `currentColor` off the root, so a
213
+ // midnight root paints the whole control dark-on-dark
214
+ MuiSlider: {
215
+ styleOverrides: darkModeContrastOverride(),
216
+ },
212
217
  MuiAccordionSummary: {
213
218
  styleOverrides: {
214
219
  root: ({ theme: t }) => ({
@@ -1,3 +1,4 @@
1
+ import type { TogglePin, ValuePin } from '../configuration/promotablePin.ts';
1
2
  import type { CheckboxMenuItem, MenuItem, RadioMenuItem } from './MenuTypes.ts';
2
3
  export interface SettingRowOptions {
3
4
  helpText?: string;
@@ -5,8 +6,14 @@ export interface SettingRowOptions {
5
6
  disabledHelpText?: string;
6
7
  keepMenuOpen?: boolean;
7
8
  }
8
- /** #menuBuilder checkboxItem | one checkbox setting row */
9
- export declare function checkboxItem(label: string, checked: boolean, onToggle: () => void, opts?: SettingRowOptions): CheckboxMenuItem;
9
+ export interface CheckboxRowOptions extends SettingRowOptions {
10
+ pin?: TogglePin;
11
+ }
12
+ export interface RadioRowOptions extends SettingRowOptions {
13
+ pin?: ValuePin;
14
+ }
15
+ /** #menuBuilder checkboxItem | one checkbox setting row, with a toggle pin when the setting is promotable */
16
+ export declare function checkboxItem(label: string, checked: boolean, onToggle: () => void, opts?: CheckboxRowOptions): CheckboxMenuItem;
10
17
  /**
11
18
  * #menuBuilder toggleItem | a checkbox row whose setter takes the new value
12
19
  *
@@ -23,17 +30,15 @@ export declare function checkboxItem(label: string, checked: boolean, onToggle:
23
30
  * `SettingRowOptions` to a hand-narrowed one-field bag — the exact drift the
24
31
  * comment on that interface warns about.
25
32
  */
26
- export declare function toggleItem(label: string, value: boolean, setValue: (value: boolean) => void, opts?: SettingRowOptions): CheckboxMenuItem;
33
+ export declare function toggleItem(label: string, value: boolean, setValue: (value: boolean) => void, opts?: CheckboxRowOptions): CheckboxMenuItem;
27
34
  /** #menuBuilder radioItem | one radio setting row; the singular of `radioItems` */
28
- export declare function radioItem(label: string, checked: boolean, onClick: () => void, opts?: SettingRowOptions): RadioMenuItem;
29
- export interface RadioOption<T extends string> {
35
+ export declare function radioItem(label: string, checked: boolean, onClick: () => void, opts?: RadioRowOptions): RadioMenuItem;
36
+ export interface RadioOption<T extends string> extends SettingRowOptions {
30
37
  value: T;
31
38
  label: string;
32
- helpText?: string;
33
- keepMenuOpen?: boolean;
34
39
  }
35
- /** #menuBuilder radioItems | a radio group, one row per option */
36
- export declare function radioItems<T extends string>(options: readonly RadioOption<T>[], current: T | undefined, setMode: (m: T) => void): RadioMenuItem[];
40
+ /** #menuBuilder radioItems | a radio group, one row per option, with a value pin per option when the setting is promotable */
41
+ export declare function radioItems<T extends string>(options: readonly RadioOption<T>[], current: T | undefined, setMode: (m: T) => void, pin?: (value: T) => ValuePin): RadioMenuItem[];
37
42
  /**
38
43
  * #menuBuilder withSubHeader | a section heading, present only if the section is
39
44
  *
@@ -1,12 +1,16 @@
1
- /** #menuBuilder checkboxItem | one checkbox setting row */
2
- export function checkboxItem(label, checked, onToggle, opts) {
3
- return {
1
+ function withPin(row, pin) {
2
+ return pin ? { ...row, pin: { control: pin, label: row.label } } : row;
3
+ }
4
+ /** #menuBuilder checkboxItem | one checkbox setting row, with a toggle pin when the setting is promotable */
5
+ export function checkboxItem(label, checked, onToggle, opts = {}) {
6
+ const { pin, ...rest } = opts;
7
+ return withPin({
4
8
  label,
5
9
  type: 'checkbox',
6
10
  checked,
7
11
  onClick: onToggle,
8
- ...opts,
9
- };
12
+ ...rest,
13
+ }, pin);
10
14
  }
11
15
  /**
12
16
  * #menuBuilder toggleItem | a checkbox row whose setter takes the new value
@@ -29,24 +33,37 @@ export function toggleItem(label, value, setValue, opts) {
29
33
  setValue(!value);
30
34
  }, opts);
31
35
  }
32
- // One radio row. The singular of `radioItems`, and what
33
- // `promotableRadioItem` builds through, so a lone radio row and a member of a
34
- // group are the same object plus or minus its pin.
36
+ // One radio row. The singular of `radioItems`, for a group the plural form
37
+ // can't express: a row with no single value to promote yet (the colorBy
38
+ // "Tag..." row before a tag is picked), a display whose slot isn't promotable
39
+ // at all (the shared colorBy menu on gwas/variants), or a group gated row by
40
+ // row and mixing in a non-promotable peer (the alignments size presets and
41
+ // their "Custom..." row). **Reach for `radioItems` first**: every option in a
42
+ // group has to get a pin, and hand-naming the rows is what leaves one without.
35
43
  /** #menuBuilder radioItem | one radio setting row; the singular of `radioItems` */
36
- export function radioItem(label, checked, onClick, opts) {
37
- return {
44
+ export function radioItem(label, checked, onClick, opts = {}) {
45
+ const { pin, ...rest } = opts;
46
+ return withPin({
38
47
  label,
39
48
  type: 'radio',
40
49
  checked,
41
50
  onClick,
42
- ...opts,
43
- };
51
+ ...rest,
52
+ }, pin);
44
53
  }
45
- /** #menuBuilder radioItems | a radio group, one row per option */
46
- export function radioItems(options, current, setMode) {
54
+ // `pin` is a factory rather than a per-option field so a promotable group can't
55
+ // be a row short. **Every option in a group gets a pin, the `promotedBase`
56
+ // value included**: once a non-base value is promoted, taking the base row's
57
+ // offer is the per-value way to undo it, and a radio group with one row silently
58
+ // missing its trailing control reads as a bug. `sashimiArcsMode`'s base looked
59
+ // unpinnable precisely because each row had been named by hand. Pass
60
+ // `value => makePin(self, slot, value)`, or a model member of that shape where
61
+ // the menu module is handed a duck-typed model (alignments).
62
+ /** #menuBuilder radioItems | a radio group, one row per option, with a value pin per option when the setting is promotable */
63
+ export function radioItems(options, current, setMode, pin) {
47
64
  return options.map(({ value, label, ...opts }) => radioItem(label, current === value, () => {
48
65
  setMode(value);
49
- }, opts));
66
+ }, { ...opts, pin: pin?.(value) }));
50
67
  }
51
68
  /**
52
69
  * #menuBuilder withSubHeader | a section heading, present only if the section is
@@ -121,7 +121,7 @@ export declare function bpToPx({ refName, coord, displayedRegionIndex, self, }:
121
121
  * Undefined when no displayed region holds the coord, same as `bpToPx`; a
122
122
  * caller that means "as far as the region goes" clamps before asking.
123
123
  */
124
- export declare function bpToOffset({ refName, coord, displayedRegions, }: {
124
+ export declare function bpToOffset({ refName, coord, displayedRegions, displayedRegionIndex, }: {
125
125
  refName: string;
126
126
  coord: number;
127
127
  displayedRegions: {
@@ -130,6 +130,7 @@ export declare function bpToOffset({ refName, coord, displayedRegions, }: {
130
130
  end: number;
131
131
  reversed?: boolean;
132
132
  }[];
133
+ displayedRegionIndex?: number;
133
134
  }): BpOffset | undefined;
134
135
  /**
135
136
  * Where a 0-based genomic coord sits in the LINEARIZED bp space — the
@@ -145,7 +146,7 @@ export declare function bpToOffset({ refName, coord, displayedRegions, }: {
145
146
  * `undefined` when no displayed region holds the coord, exactly as `bpToPx`
146
147
  * answers: a refName the view is not showing has no place on its screen.
147
148
  */
148
- export declare function bpToLinearBp({ refName, coord, displayedRegions, }: {
149
+ export declare function bpToLinearBp({ refName, coord, displayedRegions, displayedRegionIndex, }: {
149
150
  refName: string;
150
151
  coord: number;
151
152
  displayedRegions: {
@@ -154,6 +155,7 @@ export declare function bpToLinearBp({ refName, coord, displayedRegions, }: {
154
155
  end: number;
155
156
  reversed?: boolean;
156
157
  }[];
158
+ displayedRegionIndex?: number;
157
159
  }): number | undefined;
158
160
  /**
159
161
  * Screen order of two {@link BpOffset}s: negative when `a` is to the left of
@@ -187,9 +187,12 @@ export function bpToPx({ refName, coord, displayedRegionIndex, self, }) {
187
187
  * Undefined when no displayed region holds the coord, same as `bpToPx`; a
188
188
  * caller that means "as far as the region goes" clamps before asking.
189
189
  */
190
- export function bpToOffset({ refName, coord, displayedRegions, }) {
190
+ export function bpToOffset({ refName, coord, displayedRegions, displayedRegionIndex, }) {
191
191
  for (const [index, r] of displayedRegions.entries()) {
192
- if (refName === r.refName && coord >= r.start && coord <= r.end) {
192
+ if (refName === r.refName &&
193
+ coord >= r.start &&
194
+ coord <= r.end &&
195
+ (displayedRegionIndex === undefined || displayedRegionIndex === index)) {
193
196
  return { refName, index, offset: bpOffsetInRegion(r, coord) };
194
197
  }
195
198
  }
@@ -209,8 +212,13 @@ export function bpToOffset({ refName, coord, displayedRegions, }) {
209
212
  * `undefined` when no displayed region holds the coord, exactly as `bpToPx`
210
213
  * answers: a refName the view is not showing has no place on its screen.
211
214
  */
212
- export function bpToLinearBp({ refName, coord, displayedRegions, }) {
213
- const at = bpToOffset({ refName, coord, displayedRegions });
215
+ export function bpToLinearBp({ refName, coord, displayedRegions, displayedRegionIndex, }) {
216
+ const at = bpToOffset({
217
+ refName,
218
+ coord,
219
+ displayedRegions,
220
+ displayedRegionIndex,
221
+ });
214
222
  return at ? cumulativeBp(displayedRegions, at.index, at.offset) : undefined;
215
223
  }
216
224
  /**
@@ -479,9 +479,10 @@ export class SvgCanvas {
479
479
  // The family goes through, not just the size: `measureText` switches to the
480
480
  // fixed monospace advance on seeing one, so dropping it measured a monospace
481
481
  // string against the proportional table — the same string, laid out one way on
482
- // a real canvas and another in the export, with nothing to show which. No draw
483
- // function calls ctx.measureText today; this is here so the first one to do it
484
- // doesn't inherit a silent discrepancy.
482
+ // a real canvas and another in the export, with nothing to show which. An
483
+ // estimate all the same: a draw whose layout turns on a width (synteny's
484
+ // off-screen mate labels) measures through a real canvas and passes the
485
+ // result in, so the figure names what the screen named.
485
486
  measureText(text) {
486
487
  const { size, family } = parseFont(this.font);
487
488
  return { width: measureTextWidth(text, Number(size), family) };
@@ -0,0 +1,12 @@
1
+ /**
2
+ * The adapter-config axis of a fetch key: the config a fetch was issued
3
+ * against, as one string. Every fetch that refetches on an adapter edit folds
4
+ * this in — the comparative displays, the prerequisite reads, the density
5
+ * tier, the global family's signature — so the axis is spelled once and cannot
6
+ * drift between them.
7
+ *
8
+ * Not `adapterConfigCacheKey`: that one short-circuits on `adapterId`, which is
9
+ * right for sharing one adapter instance and wrong for freshness, where an
10
+ * edit to a config carrying an id still has to read as a different fetch.
11
+ */
12
+ export declare function adapterConfigKey(adapterConfig: Record<string, unknown>): string;
@@ -0,0 +1,14 @@
1
+ /**
2
+ * The adapter-config axis of a fetch key: the config a fetch was issued
3
+ * against, as one string. Every fetch that refetches on an adapter edit folds
4
+ * this in — the comparative displays, the prerequisite reads, the density
5
+ * tier, the global family's signature — so the axis is spelled once and cannot
6
+ * drift between them.
7
+ *
8
+ * Not `adapterConfigCacheKey`: that one short-circuits on `adapterId`, which is
9
+ * right for sharing one adapter instance and wrong for freshness, where an
10
+ * edit to a config carrying an id still has to read as a different fetch.
11
+ */
12
+ export function adapterConfigKey(adapterConfig) {
13
+ return JSON.stringify(adapterConfig);
14
+ }
@@ -1,3 +1,4 @@
1
+ import { adapterConfigKey } from './adapterConfigKey.js';
1
2
  import { getRpcSessionId } from './parentWalk.js';
2
3
  import { getRpcHost } from './sessionServices.js';
3
4
  /**
@@ -19,7 +20,7 @@ import { getRpcHost } from './sessionServices.js';
19
20
  export function createAdapterMetadataFetch(self) {
20
21
  let cached;
21
22
  return () => {
22
- const key = JSON.stringify(self.adapterConfig);
23
+ const key = adapterConfigKey(self.adapterConfig);
23
24
  if (cached?.key !== key) {
24
25
  const promise = getRpcHost(self)
25
26
  // eslint-disable-next-line no-restricted-syntax -- nothing to narrate, nothing a cancel could save
@@ -15,7 +15,7 @@ import type { SessionWithAddSessionTrack } from './types/index.ts';
15
15
  export declare function addAndShowTrack(session: SessionWithAddSessionTrack, conf: Parameters<SessionWithAddSessionTrack['addSessionTrackConf']>[0] & {
16
16
  trackId: string;
17
17
  }, view?: {
18
- showTrack: (trackId: string) => void;
18
+ launchTrack: (trackId: string) => Promise<unknown>;
19
19
  }): (import("@jbrowse/mobx-state-tree").ModelInstanceTypeProps<Record<string, any>> & {
20
20
  setSubschema(slotName: string, data: Record<string, unknown>): any;
21
21
  setSlot(slotName: string, value: unknown): void;
@@ -16,8 +16,12 @@ export function addAndShowTrack(session, conf, view) {
16
16
  // addSessionTrackConf already notified on an invalid conf; showing a trackId
17
17
  // that was never added would only add a second "Could not resolve identifier"
18
18
  // snackbar
19
+ //
20
+ // `launchTrack`, voided: the display's state model may still be a dynamic
21
+ // import away, and every caller here is a menu item or a dialog button whose
22
+ // return value is the added config rather than the opened track
19
23
  if (added) {
20
- view?.showTrack(conf.trackId);
24
+ void view?.launchTrack(conf.trackId);
21
25
  }
22
26
  return added;
23
27
  }
@@ -68,7 +68,7 @@ export function addTrackFromWidget({ model, session, conf, }) {
68
68
  session.notify(`Could not add "${conf.name ?? conf.trackId}": the view it was being added to has closed, and it uses an assembly that only that view had.`, 'warning');
69
69
  return undefined;
70
70
  }
71
- trackContainer.showTrack(conf.trackId, {}, {}, conf);
71
+ void trackContainer.launchTrack(conf.trackId, {}, {}, conf);
72
72
  finishAddTrack(model, session);
73
73
  return conf;
74
74
  }
@@ -77,7 +77,7 @@ export function addTrackFromWidget({ model, session, conf, }) {
77
77
  return undefined;
78
78
  }
79
79
  if (containerDisplaysAssembly(trackContainer, conf.assemblyNames, session.assemblyManager)) {
80
- trackContainer?.showTrack(conf.trackId);
80
+ void trackContainer?.launchTrack(conf.trackId);
81
81
  }
82
82
  else {
83
83
  const assemblies = conf.assemblyNames?.filter(a => !!a) ?? [];
@@ -9,11 +9,10 @@ export async function writeAWSAnalytics(rootModel, initialTimestamp, sessionQuer
9
9
  try {
10
10
  const url = 'https://analytics.jbrowse.org/api/v1';
11
11
  const multiAssemblyTracks = rootModel.jbrowse.tracks.filter(track => readConfObject(track, 'assemblyNames').length > 1).length;
12
- // which rendering backend is actually drawing (WebGPU/WebGL2/Canvas2D) —
13
- // capabilities *and* any page-wide pin, so a session that fell back to
14
- // Canvas2D is counted as Canvas2D rather than as what the hardware could
15
- // have done. Reporting the capability answer here is what made "how many
16
- // users end up on Canvas2D" unanswerable.
12
+ // The rung createGpuHal built where a display has built one by idle time,
13
+ // else the ladder's prediction with any page-wide pin applied — so a
14
+ // session that fell back to Canvas2D is counted as Canvas2D rather than as
15
+ // what the hardware could have done.
17
16
  const capabilities = await getGraphicsCapabilities();
18
17
  const renderer = effectiveRenderer(capabilities);
19
18
  // The coarse bit only. `glRenderer` names the exact driver and stays local
@@ -1,10 +1,10 @@
1
1
  import { SharedBudget } from '@gmod/shared-read-cache';
2
2
  /**
3
- * Ceilings on what the indexed-format libraries retain, shared by every adapter
4
- * in this JS context — so one per RPC worker, plus one on the main thread,
5
- * which is the scope that actually runs out of memory.
3
+ * The ceiling on what the indexed-format libraries retain, shared by every
4
+ * adapter in this JS context — so one per RPC worker, plus one on the main
5
+ * thread, which is the scope that actually runs out of memory.
6
6
  *
7
- * ## Why these are not per file
7
+ * ## Why this is not per file
8
8
  *
9
9
  * `@gmod/bam`, `@gmod/tabix` and `@gmod/cram` each take a per-file budget,
10
10
  * sized so that a single track panning never falls off the cliff where a cache
@@ -29,24 +29,27 @@ import { SharedBudget } from '@gmod/shared-read-cache';
29
29
  * at hand their space to the one being panned, so the active track keeps a
30
30
  * whole working set however many are open.
31
31
  *
32
- * ## The numbers are not new
32
+ * ## The number is not new
33
33
  *
34
- * Both are the libraries' own per-file defaults, moved from per-file to
35
- * per-context. That makes this a strict tightening — retention can only fall —
36
- * and avoids inventing a number that would need its own justification. The
37
- * budget still has to clear one query's working set or the cliff returns, and
38
- * on that workload 512 MB shared across three tracks already cost 43 refills
39
- * where 1 GB cost 4.
34
+ * 1 GB is the libraries' own per-file default (`DEFAULT_MAX_CACHE_BYTES` in
35
+ * both `@gmod/bam` and `@gmod/cram`), moved from per-file to per-context. That
36
+ * makes this a strict tightening — retention can only fall — and avoids
37
+ * inventing a number that would need its own justification. With one file open
38
+ * the two ceilings coincide; from the second file on, this one is the one that
39
+ * binds. The budget still has to clear one query's working set or the cliff
40
+ * returns, and on that workload 512 MB shared across three tracks already cost
41
+ * 43 refills where 1 GB cost 4.
40
42
  *
41
- * ## Two budgets, because units cannot mix
43
+ * ## One budget, because every member weighs bytes
42
44
  *
43
45
  * `SharedBudget.total` is a sum over its members, so they must all weigh in the
44
- * same unit. `@gmod/bam` and `@gmod/tabix` weigh decompressed bytes;
45
- * `@gmod/cram` weighs decoded records, because a decoded record has no cheap
46
- * size. A single budget spanning them would add records to bytes and bound
47
- * neither. `sizeOf` is opaque, so nothing can catch that at runtime — hence two
48
- * budgets named for their units.
46
+ * same unit, and `sizeOf` is opaque, so nothing could catch a mismatch at
47
+ * runtime. `@gmod/bam` and `@gmod/tabix` weigh decompressed bytes, and since 14
48
+ * `@gmod/cram` weighs a decoded slice by `DecodedSlice.byteLength` — its typed
49
+ * columns exactly and its strings by estimate, within 1–8% of the measured heap
50
+ * (its ADR 0013). Before that a CRAM slice weighed its record count, which is
51
+ * why this used to be two budgets named for their units: a million records
52
+ * could not be added to bytes, and never bound long-read data either, where a
53
+ * 6 MB slice counted as 37.
49
54
  */
50
55
  export declare const decompressedBytesBudget: SharedBudget;
51
- /** See {@link decompressedBytesBudget}. Records, not bytes — CRAM only. */
52
- export declare const decodedRecordsBudget: SharedBudget;
@@ -1,10 +1,10 @@
1
1
  import { SharedBudget } from '@gmod/shared-read-cache';
2
2
  /**
3
- * Ceilings on what the indexed-format libraries retain, shared by every adapter
4
- * in this JS context — so one per RPC worker, plus one on the main thread,
5
- * which is the scope that actually runs out of memory.
3
+ * The ceiling on what the indexed-format libraries retain, shared by every
4
+ * adapter in this JS context — so one per RPC worker, plus one on the main
5
+ * thread, which is the scope that actually runs out of memory.
6
6
  *
7
- * ## Why these are not per file
7
+ * ## Why this is not per file
8
8
  *
9
9
  * `@gmod/bam`, `@gmod/tabix` and `@gmod/cram` each take a per-file budget,
10
10
  * sized so that a single track panning never falls off the cliff where a cache
@@ -29,24 +29,27 @@ import { SharedBudget } from '@gmod/shared-read-cache';
29
29
  * at hand their space to the one being panned, so the active track keeps a
30
30
  * whole working set however many are open.
31
31
  *
32
- * ## The numbers are not new
32
+ * ## The number is not new
33
33
  *
34
- * Both are the libraries' own per-file defaults, moved from per-file to
35
- * per-context. That makes this a strict tightening — retention can only fall —
36
- * and avoids inventing a number that would need its own justification. The
37
- * budget still has to clear one query's working set or the cliff returns, and
38
- * on that workload 512 MB shared across three tracks already cost 43 refills
39
- * where 1 GB cost 4.
34
+ * 1 GB is the libraries' own per-file default (`DEFAULT_MAX_CACHE_BYTES` in
35
+ * both `@gmod/bam` and `@gmod/cram`), moved from per-file to per-context. That
36
+ * makes this a strict tightening — retention can only fall — and avoids
37
+ * inventing a number that would need its own justification. With one file open
38
+ * the two ceilings coincide; from the second file on, this one is the one that
39
+ * binds. The budget still has to clear one query's working set or the cliff
40
+ * returns, and on that workload 512 MB shared across three tracks already cost
41
+ * 43 refills where 1 GB cost 4.
40
42
  *
41
- * ## Two budgets, because units cannot mix
43
+ * ## One budget, because every member weighs bytes
42
44
  *
43
45
  * `SharedBudget.total` is a sum over its members, so they must all weigh in the
44
- * same unit. `@gmod/bam` and `@gmod/tabix` weigh decompressed bytes;
45
- * `@gmod/cram` weighs decoded records, because a decoded record has no cheap
46
- * size. A single budget spanning them would add records to bytes and bound
47
- * neither. `sizeOf` is opaque, so nothing can catch that at runtime — hence two
48
- * budgets named for their units.
46
+ * same unit, and `sizeOf` is opaque, so nothing could catch a mismatch at
47
+ * runtime. `@gmod/bam` and `@gmod/tabix` weigh decompressed bytes, and since 14
48
+ * `@gmod/cram` weighs a decoded slice by `DecodedSlice.byteLength` — its typed
49
+ * columns exactly and its strings by estimate, within 1–8% of the measured heap
50
+ * (its ADR 0013). Before that a CRAM slice weighed its record count, which is
51
+ * why this used to be two budgets named for their units: a million records
52
+ * could not be added to bytes, and never bound long-read data either, where a
53
+ * 6 MB slice counted as 37.
49
54
  */
50
55
  export const decompressedBytesBudget = new SharedBudget(1024 * 2 ** 20);
51
- /** See {@link decompressedBytesBudget}. Records, not bytes — CRAM only. */
52
- export const decodedRecordsBudget = new SharedBudget(1_000_000);
@@ -24,3 +24,20 @@ export declare function clampToContig(assembly: Assembly, region: {
24
24
  start: number;
25
25
  end: number;
26
26
  }): Region | undefined;
27
+ /**
28
+ * `clampToContig` for a refName that may not be on the assembly at all, which
29
+ * is the shape a location nobody requested arrives in — a BAM's `nextRef`, an
30
+ * SA record's contig (REFNAME_NAMESPACES.md). `clampToContig` alone passes an
31
+ * unknown name through with nothing to clamp against, so it cannot tell "past
32
+ * the end of a contig we have" from "a contig we do not have", and both callers
33
+ * need that apart to say which happened.
34
+ */
35
+ export declare function clampToListedContig(assembly: Assembly, region: {
36
+ refName: string;
37
+ start: number;
38
+ end: number;
39
+ }): {
40
+ refName: string;
41
+ onAssembly: boolean;
42
+ region: Region | undefined;
43
+ };