loom-browser 0.0.4 → 0.0.5

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.
@@ -1,4 +1,4 @@
1
- import { jsxs, jsx } from 'react/jsx-runtime';
1
+ import { jsx, jsxs } from 'react/jsx-runtime';
2
2
  import { createContext, forwardRef, useRef, useState, useEffect, useImperativeHandle, useMemo, useContext, useCallback } from 'react';
3
3
  import { BigWig } from '@gmod/bbi';
4
4
  import { RemoteFile } from 'generic-filehandle2';
@@ -1337,6 +1337,35 @@ function parseLocus(str, cumOffsets) {
1337
1337
  return null;
1338
1338
  return { chr, start, end };
1339
1339
  }
1340
+ /**
1341
+ * Format a Locus object as a human-readable string with thousands separators.
1342
+ * e.g., { chr: 'chr17', start: 7668000, end: 7688000 } → "chr17:7,668,000-7,688,000"
1343
+ */
1344
+ function formatLocus(locus) {
1345
+ if (isWholeGenomeView(locus))
1346
+ return 'All Chromosomes';
1347
+ return `${locus.chr}:${Math.round(locus.start).toLocaleString()}-${Math.round(locus.end).toLocaleString()}`;
1348
+ }
1349
+ /**
1350
+ * Format a base pair length as a human-readable string.
1351
+ * e.g., 20000 → "20 kb", 2500000 → "2.5 Mb", 500 → "500 bp"
1352
+ */
1353
+ function formatBpLength(bp) {
1354
+ bp = Math.abs(bp);
1355
+ if (bp >= 1e9) {
1356
+ const val = bp / 1e9;
1357
+ return `${val % 1 === 0 ? val.toFixed(0) : val.toFixed(1)} Gb`;
1358
+ }
1359
+ if (bp >= 1e6) {
1360
+ const val = bp / 1e6;
1361
+ return `${val % 1 === 0 ? val.toFixed(0) : val.toFixed(1)} Mb`;
1362
+ }
1363
+ if (bp >= 1e3) {
1364
+ const val = bp / 1e3;
1365
+ return `${val % 1 === 0 ? val.toFixed(0) : val.toFixed(1)} kb`;
1366
+ }
1367
+ return `${Math.round(bp)} bp`;
1368
+ }
1340
1369
  /**
1341
1370
  * Clamp a locus to valid chromosome bounds.
1342
1371
  *
@@ -11210,6 +11239,13 @@ class GenomeBrowser extends HeadlessGenomeBrowser {
11210
11239
  this.events.on(BrowserEvent.DataLoaded, ({ track }) => {
11211
11240
  this.updateAxisContent(track);
11212
11241
  });
11242
+ // Repaint axis canvases when theme changes (track canvases update via
11243
+ // track.setTheme(), but axis sidebar is managed here in GenomeBrowser).
11244
+ this.events.on(BrowserEvent.ThemeChanged, () => {
11245
+ for (const mt of this.managedTracks) {
11246
+ this.updateAxisContent(mt.track);
11247
+ }
11248
+ });
11213
11249
  // Re-render ROI overlays when ROIs change or viewport moves
11214
11250
  this.events.on(BrowserEvent.ROIAdded, () => this.renderROIOverlays());
11215
11251
  this.events.on(BrowserEvent.ROIRemoved, () => this.renderROIOverlays());
@@ -11412,9 +11448,10 @@ class GenomeBrowser extends HeadlessGenomeBrowser {
11412
11448
  const { axisDiv } = entry;
11413
11449
  const rawInfo = (_a = track.getAxisInfo) === null || _a === void 0 ? void 0 : _a.call(track);
11414
11450
  if (!rawInfo) {
11415
- // No axis info — clear content and hide border (e.g., ruler tracks)
11451
+ // No axis info — keep the column for alignment but clear content
11416
11452
  axisDiv.innerHTML = "";
11417
11453
  axisDiv.style.borderRight = "none";
11454
+ axisDiv.style.backgroundColor = this.theme.palette.background;
11418
11455
  entry.axisCanvas = null;
11419
11456
  return;
11420
11457
  }
@@ -12578,7 +12615,7 @@ const LoomBrowser = forwardRef(function LoomBrowser(props, ref) {
12578
12615
  detachRemote() { var _a; (_a = browserRef.current) === null || _a === void 0 ? void 0 : _a.detachRemote(); },
12579
12616
  }), [browser]);
12580
12617
  const ctxValue = useMemo(() => ({ browser }), [browser]);
12581
- return (jsxs(GenomeBrowserContext.Provider, { value: ctxValue, children: [jsx("div", { ref: containerRef, className: className, style: style }), browser && children] }));
12618
+ return (jsx(GenomeBrowserContext.Provider, { value: ctxValue, children: jsxs("div", { className: className, style: style, children: [browser && children, jsx("div", { ref: containerRef })] }) }));
12582
12619
  });
12583
12620
 
12584
12621
  /**
@@ -12722,6 +12759,646 @@ function GtxTrack({ url, experimentId, config, height, background, windowFunctio
12722
12759
  return null;
12723
12760
  }
12724
12761
 
12762
+ /**
12763
+ * <loom-chromosome-select> — Chromosome dropdown for quick navigation.
12764
+ *
12765
+ * Lists main chromosomes (chr1-22, X, Y, M) plus "All" for whole genome view.
12766
+ * Updates automatically when the browser locus changes.
12767
+ */
12768
+ const template$5 = document.createElement('template');
12769
+ template$5.innerHTML = /* html */ `
12770
+ <style>
12771
+ :host {
12772
+ display: inline-flex;
12773
+ align-items: center;
12774
+ }
12775
+ select {
12776
+ font: var(--loom-font, 12px Arial, sans-serif);
12777
+ color: var(--loom-text-color, #333);
12778
+ background: var(--loom-input-bg, white);
12779
+ border: var(--loom-input-border, 1px solid #b0b0b0);
12780
+ border-radius: var(--loom-border-radius, 4px);
12781
+ height: var(--loom-input-height, 22px);
12782
+ padding: 0 4px;
12783
+ box-sizing: border-box;
12784
+ outline: none;
12785
+ cursor: pointer;
12786
+ transition: border-color 0.15s;
12787
+ }
12788
+ select:focus {
12789
+ border: var(--loom-input-focus-border, 1px solid #4A90D9);
12790
+ }
12791
+ </style>
12792
+ <select></select>
12793
+ `;
12794
+ class LoomChromosomeSelect extends HTMLElement {
12795
+ constructor() {
12796
+ super();
12797
+ this.unsubscribe = null;
12798
+ this._browser = null;
12799
+ this.attachShadow({ mode: 'open' });
12800
+ this.shadowRoot.appendChild(template$5.content.cloneNode(true));
12801
+ this.select = this.shadowRoot.querySelector('select');
12802
+ this.select.addEventListener('change', () => {
12803
+ if (!this._browser)
12804
+ return;
12805
+ const value = this.select.value;
12806
+ if (value === 'all') {
12807
+ this._browser.search('all');
12808
+ }
12809
+ else {
12810
+ const chromSizes = this._browser.chromSizes;
12811
+ if (chromSizes && chromSizes[value] != null) {
12812
+ this._browser.setLocus({ chr: value, start: 0, end: chromSizes[value] });
12813
+ }
12814
+ }
12815
+ });
12816
+ }
12817
+ set browser(b) {
12818
+ if (this.unsubscribe) {
12819
+ this.unsubscribe();
12820
+ this.unsubscribe = null;
12821
+ }
12822
+ this._browser = b;
12823
+ if (b) {
12824
+ this.populate(b);
12825
+ this.updateSelection(b.locus.chr);
12826
+ this.unsubscribe = b.on(BrowserEvent.LocusChange, ({ locus }) => {
12827
+ this.updateSelection(locus.chr);
12828
+ });
12829
+ }
12830
+ }
12831
+ get browser() {
12832
+ return this._browser;
12833
+ }
12834
+ populate(browser) {
12835
+ this.select.innerHTML = '';
12836
+ const chromSizes = browser.chromSizes;
12837
+ if (!chromSizes)
12838
+ return;
12839
+ // "All" option for whole genome view
12840
+ if (browser.cumulativeOffsets) {
12841
+ const opt = document.createElement('option');
12842
+ opt.value = 'all';
12843
+ opt.textContent = 'All';
12844
+ this.select.appendChild(opt);
12845
+ }
12846
+ // Main chromosomes in order
12847
+ const names = mainChromosomeNames(chromSizes);
12848
+ for (const name of names) {
12849
+ const opt = document.createElement('option');
12850
+ opt.value = name;
12851
+ opt.textContent = name;
12852
+ this.select.appendChild(opt);
12853
+ }
12854
+ }
12855
+ updateSelection(chr) {
12856
+ if (isWholeGenomeView({ chr, start: 0, end: 0 })) {
12857
+ this.select.value = 'all';
12858
+ }
12859
+ else {
12860
+ this.select.value = chr;
12861
+ }
12862
+ }
12863
+ disconnectedCallback() {
12864
+ if (this.unsubscribe) {
12865
+ this.unsubscribe();
12866
+ this.unsubscribe = null;
12867
+ }
12868
+ }
12869
+ }
12870
+
12871
+ /**
12872
+ * <loom-locus-input> — Locus search/display input.
12873
+ *
12874
+ * Shows the current locus as a formatted string. User can type a new locus
12875
+ * and press Enter to navigate. Updates automatically on locuschange events.
12876
+ */
12877
+ const template$4 = document.createElement('template');
12878
+ template$4.innerHTML = /* html */ `
12879
+ <style>
12880
+ :host {
12881
+ display: inline-flex;
12882
+ align-items: center;
12883
+ }
12884
+ .container {
12885
+ display: flex;
12886
+ align-items: center;
12887
+ gap: 4px;
12888
+ }
12889
+ input {
12890
+ font: var(--loom-font, 12px Arial, sans-serif);
12891
+ color: var(--loom-text-color, #333);
12892
+ background: var(--loom-input-bg, white);
12893
+ border: var(--loom-input-border, 1px solid #b0b0b0);
12894
+ border-radius: var(--loom-border-radius, 4px);
12895
+ width: var(--loom-input-width, 220px);
12896
+ height: var(--loom-input-height, 22px);
12897
+ padding: 0 8px;
12898
+ box-sizing: border-box;
12899
+ outline: none;
12900
+ transition: border-color 0.15s;
12901
+ }
12902
+ input:focus {
12903
+ border: var(--loom-input-focus-border, 1px solid #4A90D9);
12904
+ }
12905
+ input::placeholder {
12906
+ color: var(--loom-text-muted, #737373);
12907
+ }
12908
+ .search-btn {
12909
+ display: flex;
12910
+ align-items: center;
12911
+ justify-content: center;
12912
+ width: var(--loom-button-size, 24px);
12913
+ height: var(--loom-button-size, 24px);
12914
+ background: var(--loom-button-bg, white);
12915
+ border: var(--loom-button-border, 1px solid #b0b0b0);
12916
+ border-radius: var(--loom-border-radius, 4px);
12917
+ cursor: pointer;
12918
+ color: var(--loom-icon-color, #555);
12919
+ transition: background 0.15s;
12920
+ padding: 0;
12921
+ line-height: 1;
12922
+ }
12923
+ .search-btn:hover {
12924
+ background: var(--loom-button-hover, #e8e8e8);
12925
+ }
12926
+ .search-btn svg {
12927
+ width: var(--loom-icon-size, 14px);
12928
+ height: var(--loom-icon-size, 14px);
12929
+ fill: none;
12930
+ stroke: currentColor;
12931
+ stroke-width: 2;
12932
+ stroke-linecap: round;
12933
+ stroke-linejoin: round;
12934
+ }
12935
+ </style>
12936
+ <div class="container">
12937
+ <input type="text" placeholder="Locus Search" spellcheck="false" />
12938
+ <button class="search-btn" title="Go to locus">
12939
+ <svg viewBox="0 0 24 24"><circle cx="11" cy="11" r="7"/><line x1="16.5" y1="16.5" x2="21" y2="21"/></svg>
12940
+ </button>
12941
+ </div>
12942
+ `;
12943
+ class LoomLocusInput extends HTMLElement {
12944
+ constructor() {
12945
+ super();
12946
+ this.unsubscribe = null;
12947
+ this._browser = null;
12948
+ this.attachShadow({ mode: 'open' });
12949
+ this.shadowRoot.appendChild(template$4.content.cloneNode(true));
12950
+ this.input = this.shadowRoot.querySelector('input');
12951
+ this.input.addEventListener('keydown', (e) => {
12952
+ if (e.key === 'Enter') {
12953
+ this.doSearch();
12954
+ }
12955
+ });
12956
+ this.shadowRoot.querySelector('.search-btn').addEventListener('click', () => {
12957
+ this.doSearch();
12958
+ });
12959
+ }
12960
+ set browser(b) {
12961
+ if (this.unsubscribe) {
12962
+ this.unsubscribe();
12963
+ this.unsubscribe = null;
12964
+ }
12965
+ this._browser = b;
12966
+ if (b) {
12967
+ this.updateDisplay(b.locus);
12968
+ this.unsubscribe = b.on(BrowserEvent.LocusChange, ({ locus }) => {
12969
+ this.updateDisplay(locus);
12970
+ });
12971
+ }
12972
+ }
12973
+ get browser() {
12974
+ return this._browser;
12975
+ }
12976
+ updateDisplay(locus) {
12977
+ if (document.activeElement !== this.input && this.shadowRoot.activeElement !== this.input) {
12978
+ this.input.value = formatLocus(locus);
12979
+ }
12980
+ }
12981
+ doSearch() {
12982
+ if (!this._browser)
12983
+ return;
12984
+ const value = this.input.value.trim();
12985
+ if (value) {
12986
+ this._browser.search(value);
12987
+ }
12988
+ this.input.blur();
12989
+ }
12990
+ disconnectedCallback() {
12991
+ if (this.unsubscribe) {
12992
+ this.unsubscribe();
12993
+ this.unsubscribe = null;
12994
+ }
12995
+ }
12996
+ }
12997
+
12998
+ /**
12999
+ * <loom-zoom-controls> — Zoom in/out buttons.
13000
+ */
13001
+ const template$3 = document.createElement('template');
13002
+ template$3.innerHTML = /* html */ `
13003
+ <style>
13004
+ :host {
13005
+ display: inline-flex;
13006
+ align-items: center;
13007
+ }
13008
+ .container {
13009
+ display: flex;
13010
+ align-items: center;
13011
+ gap: 2px;
13012
+ }
13013
+ button {
13014
+ display: flex;
13015
+ align-items: center;
13016
+ justify-content: center;
13017
+ width: var(--loom-button-size, 24px);
13018
+ height: var(--loom-button-size, 24px);
13019
+ background: var(--loom-button-bg, white);
13020
+ border: var(--loom-button-border, 1px solid #b0b0b0);
13021
+ border-radius: var(--loom-border-radius, 4px);
13022
+ cursor: pointer;
13023
+ color: var(--loom-icon-color, #555);
13024
+ transition: background 0.15s;
13025
+ padding: 0;
13026
+ line-height: 1;
13027
+ }
13028
+ button:hover {
13029
+ background: var(--loom-button-hover, #e8e8e8);
13030
+ }
13031
+ button:active {
13032
+ transform: scale(0.95);
13033
+ }
13034
+ button svg {
13035
+ width: var(--loom-icon-size, 14px);
13036
+ height: var(--loom-icon-size, 14px);
13037
+ fill: none;
13038
+ stroke: currentColor;
13039
+ stroke-width: 2;
13040
+ stroke-linecap: round;
13041
+ }
13042
+ </style>
13043
+ <div class="container">
13044
+ <button class="zoom-out" title="Zoom out">
13045
+ <svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><line x1="8" y1="12" x2="16" y2="12"/></svg>
13046
+ </button>
13047
+ <button class="zoom-in" title="Zoom in">
13048
+ <svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><line x1="12" y1="8" x2="12" y2="16"/><line x1="8" y1="12" x2="16" y2="12"/></svg>
13049
+ </button>
13050
+ </div>
13051
+ `;
13052
+ class LoomZoomControls extends HTMLElement {
13053
+ constructor() {
13054
+ super();
13055
+ this._browser = null;
13056
+ this.attachShadow({ mode: 'open' });
13057
+ this.shadowRoot.appendChild(template$3.content.cloneNode(true));
13058
+ this.shadowRoot.querySelector('.zoom-out').addEventListener('click', () => {
13059
+ var _a;
13060
+ (_a = this._browser) === null || _a === void 0 ? void 0 : _a.zoomOut();
13061
+ });
13062
+ this.shadowRoot.querySelector('.zoom-in').addEventListener('click', () => {
13063
+ var _a;
13064
+ (_a = this._browser) === null || _a === void 0 ? void 0 : _a.zoomIn();
13065
+ });
13066
+ }
13067
+ set browser(b) {
13068
+ this._browser = b;
13069
+ }
13070
+ get browser() {
13071
+ return this._browser;
13072
+ }
13073
+ }
13074
+
13075
+ /**
13076
+ * <loom-window-size> — Read-only display of current viewport bp range.
13077
+ */
13078
+ const template$2 = document.createElement('template');
13079
+ template$2.innerHTML = /* html */ `
13080
+ <style>
13081
+ :host {
13082
+ display: inline-flex;
13083
+ align-items: center;
13084
+ }
13085
+ .label {
13086
+ font: var(--loom-font-small, 11px Arial, sans-serif);
13087
+ color: var(--loom-text-muted, #737373);
13088
+ white-space: nowrap;
13089
+ user-select: none;
13090
+ }
13091
+ </style>
13092
+ <span class="label"></span>
13093
+ `;
13094
+ class LoomWindowSize extends HTMLElement {
13095
+ constructor() {
13096
+ super();
13097
+ this.unsubscribe = null;
13098
+ this._browser = null;
13099
+ this.attachShadow({ mode: 'open' });
13100
+ this.shadowRoot.appendChild(template$2.content.cloneNode(true));
13101
+ this.label = this.shadowRoot.querySelector('.label');
13102
+ }
13103
+ set browser(b) {
13104
+ if (this.unsubscribe) {
13105
+ this.unsubscribe();
13106
+ this.unsubscribe = null;
13107
+ }
13108
+ this._browser = b;
13109
+ if (b) {
13110
+ this.update(b.locus);
13111
+ this.unsubscribe = b.on(BrowserEvent.LocusChange, ({ locus }) => {
13112
+ this.update(locus);
13113
+ });
13114
+ }
13115
+ }
13116
+ get browser() {
13117
+ return this._browser;
13118
+ }
13119
+ update(locus) {
13120
+ this.label.textContent = formatBpLength(locus.end - locus.start);
13121
+ }
13122
+ disconnectedCallback() {
13123
+ if (this.unsubscribe) {
13124
+ this.unsubscribe();
13125
+ this.unsubscribe = null;
13126
+ }
13127
+ }
13128
+ }
13129
+
13130
+ /**
13131
+ * <loom-export-controls> — SVG/PNG export buttons.
13132
+ *
13133
+ * Adds "Save SVG" and "Save PNG" buttons to the shell navbar.
13134
+ * Calls GenomeBrowser.saveSVGtoFile() and savePNGtoFile() respectively.
13135
+ */
13136
+ const template$1 = document.createElement('template');
13137
+ template$1.innerHTML = /* html */ `
13138
+ <style>
13139
+ :host {
13140
+ display: inline-flex;
13141
+ align-items: center;
13142
+ }
13143
+ .container {
13144
+ display: flex;
13145
+ align-items: center;
13146
+ gap: 2px;
13147
+ }
13148
+ button {
13149
+ display: flex;
13150
+ align-items: center;
13151
+ justify-content: center;
13152
+ width: var(--loom-button-size, 24px);
13153
+ height: var(--loom-button-size, 24px);
13154
+ background: var(--loom-button-bg, white);
13155
+ border: var(--loom-button-border, 1px solid #b0b0b0);
13156
+ border-radius: var(--loom-border-radius, 4px);
13157
+ cursor: pointer;
13158
+ color: var(--loom-icon-color, #555);
13159
+ transition: background 0.15s;
13160
+ padding: 0;
13161
+ line-height: 1;
13162
+ }
13163
+ button:hover {
13164
+ background: var(--loom-button-hover, #e8e8e8);
13165
+ }
13166
+ button:active {
13167
+ transform: scale(0.95);
13168
+ }
13169
+ button svg {
13170
+ width: var(--loom-icon-size, 14px);
13171
+ height: var(--loom-icon-size, 14px);
13172
+ fill: none;
13173
+ stroke: currentColor;
13174
+ stroke-width: 2;
13175
+ stroke-linecap: round;
13176
+ stroke-linejoin: round;
13177
+ }
13178
+ </style>
13179
+ <div class="container">
13180
+ <button class="save-svg" title="Save as SVG">
13181
+ <svg viewBox="0 0 24 24">
13182
+ <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
13183
+ <polyline points="7 10 12 15 17 10"/>
13184
+ <line x1="12" y1="15" x2="12" y2="3"/>
13185
+ </svg>
13186
+ </button>
13187
+ <button class="save-png" title="Save as PNG">
13188
+ <svg viewBox="0 0 24 24">
13189
+ <rect x="3" y="3" width="18" height="18" rx="2" ry="2"/>
13190
+ <circle cx="8.5" cy="8.5" r="1.5"/>
13191
+ <polyline points="21 15 16 10 5 21"/>
13192
+ </svg>
13193
+ </button>
13194
+ </div>
13195
+ `;
13196
+ class LoomExportControls extends HTMLElement {
13197
+ constructor() {
13198
+ super();
13199
+ this._browser = null;
13200
+ this.attachShadow({ mode: 'open' });
13201
+ this.shadowRoot.appendChild(template$1.content.cloneNode(true));
13202
+ this.shadowRoot.querySelector('.save-svg').addEventListener('click', () => {
13203
+ var _a;
13204
+ (_a = this._browser) === null || _a === void 0 ? void 0 : _a.saveSVGtoFile();
13205
+ });
13206
+ this.shadowRoot.querySelector('.save-png').addEventListener('click', () => {
13207
+ var _a;
13208
+ void ((_a = this._browser) === null || _a === void 0 ? void 0 : _a.savePNGtoFile());
13209
+ });
13210
+ }
13211
+ set browser(b) {
13212
+ this._browser = b;
13213
+ }
13214
+ get browser() {
13215
+ return this._browser;
13216
+ }
13217
+ }
13218
+
13219
+ /**
13220
+ * <loom-navbar> — Horizontal navigation bar composing locus input, window size, and zoom controls.
13221
+ */
13222
+ // Ensure child custom elements are registered
13223
+ if (!customElements.get('loom-chromosome-select')) {
13224
+ customElements.define('loom-chromosome-select', LoomChromosomeSelect);
13225
+ }
13226
+ if (!customElements.get('loom-locus-input')) {
13227
+ customElements.define('loom-locus-input', LoomLocusInput);
13228
+ }
13229
+ if (!customElements.get('loom-zoom-controls')) {
13230
+ customElements.define('loom-zoom-controls', LoomZoomControls);
13231
+ }
13232
+ if (!customElements.get('loom-window-size')) {
13233
+ customElements.define('loom-window-size', LoomWindowSize);
13234
+ }
13235
+ if (!customElements.get('loom-export-controls')) {
13236
+ customElements.define('loom-export-controls', LoomExportControls);
13237
+ }
13238
+ const template = document.createElement('template');
13239
+ template.innerHTML = /* html */ `
13240
+ <style>
13241
+ :host {
13242
+ display: block;
13243
+ }
13244
+ .navbar {
13245
+ display: flex;
13246
+ align-items: center;
13247
+ justify-content: space-between;
13248
+ height: var(--loom-navbar-height, 32px);
13249
+ padding: var(--loom-navbar-padding, 0 8px);
13250
+ background: var(--loom-navbar-bg, #f3f3f3);
13251
+ border-bottom: var(--loom-border, 1px solid #ccc);
13252
+ box-sizing: border-box;
13253
+ gap: var(--loom-gap, 8px);
13254
+ }
13255
+ .left {
13256
+ display: flex;
13257
+ align-items: center;
13258
+ gap: var(--loom-gap, 8px);
13259
+ flex: 1;
13260
+ min-width: 0;
13261
+ }
13262
+ .right {
13263
+ display: flex;
13264
+ align-items: center;
13265
+ gap: var(--loom-gap, 8px);
13266
+ flex-shrink: 0;
13267
+ }
13268
+ </style>
13269
+ <div class="navbar">
13270
+ <div class="left">
13271
+ <loom-chromosome-select></loom-chromosome-select>
13272
+ <loom-locus-input></loom-locus-input>
13273
+ <loom-window-size></loom-window-size>
13274
+ </div>
13275
+ <div class="right">
13276
+ <loom-export-controls></loom-export-controls>
13277
+ <loom-zoom-controls></loom-zoom-controls>
13278
+ </div>
13279
+ </div>
13280
+ `;
13281
+ class LoomNavbar extends HTMLElement {
13282
+ constructor() {
13283
+ super();
13284
+ this._browser = null;
13285
+ this.attachShadow({ mode: 'open' });
13286
+ this.shadowRoot.appendChild(template.content.cloneNode(true));
13287
+ this.chromSelect = this.shadowRoot.querySelector('loom-chromosome-select');
13288
+ this.locusInput = this.shadowRoot.querySelector('loom-locus-input');
13289
+ this.zoomControls = this.shadowRoot.querySelector('loom-zoom-controls');
13290
+ this.windowSize = this.shadowRoot.querySelector('loom-window-size');
13291
+ this.exportControls = this.shadowRoot.querySelector('loom-export-controls');
13292
+ }
13293
+ set browser(b) {
13294
+ this._browser = b;
13295
+ this.chromSelect.browser = b;
13296
+ this.locusInput.browser = b;
13297
+ this.zoomControls.browser = b;
13298
+ this.windowSize.browser = b;
13299
+ this.exportControls.browser = b;
13300
+ }
13301
+ get browser() {
13302
+ return this._browser;
13303
+ }
13304
+ }
13305
+
13306
+ /**
13307
+ * Ensures all Loom UI web components are registered as custom elements.
13308
+ * Safe to call multiple times — each element is only registered once.
13309
+ */
13310
+ let registered = false;
13311
+ function ensureRegistered() {
13312
+ if (registered || typeof customElements === 'undefined')
13313
+ return;
13314
+ registered = true;
13315
+ const elements = [
13316
+ ['loom-chromosome-select', LoomChromosomeSelect],
13317
+ ['loom-locus-input', LoomLocusInput],
13318
+ ['loom-zoom-controls', LoomZoomControls],
13319
+ ['loom-window-size', LoomWindowSize],
13320
+ ['loom-export-controls', LoomExportControls],
13321
+ ['loom-navbar', LoomNavbar],
13322
+ ];
13323
+ for (const [name, ctor] of elements) {
13324
+ if (!customElements.get(name)) {
13325
+ customElements.define(name, ctor);
13326
+ }
13327
+ }
13328
+ }
13329
+
13330
+ function ChromosomeSelect({ browser: browserProp }) {
13331
+ const contextBrowser = useGenomeBrowser();
13332
+ const browser = browserProp !== null && browserProp !== void 0 ? browserProp : contextBrowser;
13333
+ const ref = useRef(null);
13334
+ ensureRegistered();
13335
+ useEffect(() => {
13336
+ if (ref.current)
13337
+ ref.current.browser = browser;
13338
+ }, [browser]);
13339
+ return jsx("loom-chromosome-select", { ref: ref });
13340
+ }
13341
+
13342
+ function LocusInput({ browser: browserProp }) {
13343
+ const contextBrowser = useGenomeBrowser();
13344
+ const browser = browserProp !== null && browserProp !== void 0 ? browserProp : contextBrowser;
13345
+ const ref = useRef(null);
13346
+ ensureRegistered();
13347
+ useEffect(() => {
13348
+ if (ref.current)
13349
+ ref.current.browser = browser;
13350
+ }, [browser]);
13351
+ return jsx("loom-locus-input", { ref: ref });
13352
+ }
13353
+
13354
+ function ZoomControls({ browser: browserProp }) {
13355
+ const contextBrowser = useGenomeBrowser();
13356
+ const browser = browserProp !== null && browserProp !== void 0 ? browserProp : contextBrowser;
13357
+ const ref = useRef(null);
13358
+ ensureRegistered();
13359
+ useEffect(() => {
13360
+ if (ref.current)
13361
+ ref.current.browser = browser;
13362
+ }, [browser]);
13363
+ return jsx("loom-zoom-controls", { ref: ref });
13364
+ }
13365
+
13366
+ function WindowSize({ browser: browserProp }) {
13367
+ const contextBrowser = useGenomeBrowser();
13368
+ const browser = browserProp !== null && browserProp !== void 0 ? browserProp : contextBrowser;
13369
+ const ref = useRef(null);
13370
+ ensureRegistered();
13371
+ useEffect(() => {
13372
+ if (ref.current)
13373
+ ref.current.browser = browser;
13374
+ }, [browser]);
13375
+ return jsx("loom-window-size", { ref: ref });
13376
+ }
13377
+
13378
+ function ExportControls({ browser: browserProp }) {
13379
+ const contextBrowser = useGenomeBrowser();
13380
+ const browser = browserProp !== null && browserProp !== void 0 ? browserProp : contextBrowser;
13381
+ const ref = useRef(null);
13382
+ ensureRegistered();
13383
+ useEffect(() => {
13384
+ if (ref.current)
13385
+ ref.current.browser = browser;
13386
+ }, [browser]);
13387
+ return jsx("loom-export-controls", { ref: ref });
13388
+ }
13389
+
13390
+ function Navbar({ browser: browserProp }) {
13391
+ const contextBrowser = useGenomeBrowser();
13392
+ const browser = browserProp !== null && browserProp !== void 0 ? browserProp : contextBrowser;
13393
+ const ref = useRef(null);
13394
+ ensureRegistered();
13395
+ useEffect(() => {
13396
+ if (ref.current)
13397
+ ref.current.browser = browser;
13398
+ }, [browser]);
13399
+ return jsx("loom-navbar", { ref: ref });
13400
+ }
13401
+
12725
13402
  /**
12726
13403
  * <loom-context-menu> — Right-click context menu Web Component.
12727
13404
  *
@@ -13287,4 +13964,4 @@ var LoomInputDialog$1 = /*#__PURE__*/Object.freeze({
13287
13964
  LoomInputDialog: LoomInputDialog
13288
13965
  });
13289
13966
 
13290
- export { BedTrack, GeneTrack, GenomeBrowserContext, GtxTrack, InteractionTrack, LoomBrowser, RulerTrack, SequenceTrack, WigTrack, useBrowserEvent, useGenomeBrowser, useLocus };
13967
+ export { BedTrack, ChromosomeSelect, ExportControls, GeneTrack, GenomeBrowserContext, GtxTrack, InteractionTrack, LocusInput, LoomBrowser, Navbar, RulerTrack, SequenceTrack, WigTrack, WindowSize, ZoomControls, useBrowserEvent, useGenomeBrowser, useLocus };