loom-browser 0.0.5 → 0.0.6

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.
@@ -8231,6 +8231,72 @@ class SequenceDataSource {
8231
8231
  }
8232
8232
  }
8233
8233
 
8234
+ /**
8235
+ * DataSource backed by an in-memory feature array.
8236
+ *
8237
+ * Wraps a pre-loaded feature array in a FeatureCache for spatial queries,
8238
+ * enabling inline features to flow through the same data lifecycle as
8239
+ * URL-based data sources.
8240
+ *
8241
+ * Layer 1 (Data + Layout): no DOM, no canvas.
8242
+ */
8243
+ /**
8244
+ * DataSource that serves features from an in-memory array.
8245
+ *
8246
+ * Features are indexed in a FeatureCache on construction for efficient
8247
+ * spatial queries. Calling `setFeatures()` replaces the cache entirely.
8248
+ */
8249
+ class MemoryDataSource {
8250
+ constructor(features) {
8251
+ this.cache = new FeatureCache(features);
8252
+ }
8253
+ /** Replace the in-memory features and rebuild the spatial index. */
8254
+ setFeatures(features) {
8255
+ this.cache = new FeatureCache(features);
8256
+ }
8257
+ /** Set a chromosome name resolver for alias resolution. */
8258
+ setChromNameResolver(resolver) {
8259
+ this._resolveChromName = resolver;
8260
+ }
8261
+ /** Set cumulative offsets for whole genome view coordinate transformation. */
8262
+ setCumulativeOffsets(offsets) {
8263
+ this._cumulativeOffsets = offsets;
8264
+ }
8265
+ async fetch(locus, _bpPerPixel, _signal) {
8266
+ if (isWholeGenomeView(locus) && this._cumulativeOffsets) {
8267
+ return this.fetchWG();
8268
+ }
8269
+ const chr = this._resolveChromName
8270
+ ? this._resolveChromName(locus.chr)
8271
+ : locus.chr;
8272
+ return this.cache.queryFeatures(chr, locus.start, locus.end);
8273
+ }
8274
+ fetchWG() {
8275
+ const offsets = this._cumulativeOffsets;
8276
+ const chrNames = mainChromosomeNames(Object.fromEntries(offsets.chromosomeNames.map(name => { var _a; return [name, (_a = offsets.offsets[name]) !== null && _a !== void 0 ? _a : 0]; })));
8277
+ const wgFeatures = [];
8278
+ const allByChrom = this.cache.getAllFeatures();
8279
+ for (const chr of chrNames) {
8280
+ const features = allByChrom[chr];
8281
+ if (!features)
8282
+ continue;
8283
+ const offset = offsets.offsets[chr];
8284
+ if (offset === undefined)
8285
+ continue;
8286
+ for (const f of features) {
8287
+ wgFeatures.push({
8288
+ ...f,
8289
+ chr: 'all',
8290
+ start: offset + f.start,
8291
+ end: offset + f.end,
8292
+ });
8293
+ }
8294
+ }
8295
+ wgFeatures.sort((a, b) => a.start - b.start);
8296
+ return wgFeatures;
8297
+ }
8298
+ }
8299
+
8234
8300
  /**
8235
8301
  * Stateless renderer for interaction (arc/BEDPE) tracks.
8236
8302
  *
@@ -8801,6 +8867,11 @@ function createDataSource(config, workerProvider) {
8801
8867
  indexed: config.indexed,
8802
8868
  workerProvider,
8803
8869
  });
8870
+ case 'memory':
8871
+ // Memory data sources are created directly with features by the caller.
8872
+ // This path is only hit during session restore, where in-memory features
8873
+ // are not available — return an empty MemoryDataSource as a placeholder.
8874
+ return new MemoryDataSource([]);
8804
8875
  }
8805
8876
  }
8806
8877
  // ─── Built-in track creators ─────────────────────────────────────────────────
@@ -8979,6 +9050,9 @@ function dataSourceCacheKey(config) {
8979
9050
  return `ucsc:${(_b = config.genome) !== null && _b !== void 0 ? _b : ''}:${(_c = config.track) !== null && _c !== void 0 ? _c : ''}`;
8980
9051
  case 'text':
8981
9052
  return `text:${config.url}:${(_d = config.format) !== null && _d !== void 0 ? _d : ''}:${(_e = config.indexURL) !== null && _e !== void 0 ? _e : ''}`;
9053
+ case 'memory':
9054
+ // Each memory data source is unique — no deduplication.
9055
+ return `memory:${Math.random()}`;
8982
9056
  }
8983
9057
  }
8984
9058
 
@@ -10089,6 +10163,60 @@ class HeadlessGenomeBrowser {
10089
10163
  this.managedTracks[this.managedTracks.length - 1].metadata = options.metadata;
10090
10164
  return track;
10091
10165
  }
10166
+ /** Add a BigWig-style signal track backed by in-memory features (no URL required). */
10167
+ addWigTrackWithFeatures(features, options) {
10168
+ const { canvas } = this.canvasProvider.createCanvas(0, 0);
10169
+ const track = new WigTrackCanvas(canvas, {
10170
+ locus: this._locus,
10171
+ features: [],
10172
+ config: options === null || options === void 0 ? void 0 : options.config,
10173
+ height: options === null || options === void 0 ? void 0 : options.height,
10174
+ background: options === null || options === void 0 ? void 0 : options.background,
10175
+ theme: this.theme,
10176
+ canvasProvider: this.canvasProvider,
10177
+ name: options === null || options === void 0 ? void 0 : options.name,
10178
+ sequenceProvider: this.sequenceProvider,
10179
+ });
10180
+ const dataSource = new MemoryDataSource(features);
10181
+ if (this.cumulativeOffsets) {
10182
+ dataSource.setCumulativeOffsets(this.cumulativeOffsets);
10183
+ }
10184
+ if (this.genome) {
10185
+ dataSource.setChromNameResolver(alias => this.genome.getChromosomeName(alias));
10186
+ }
10187
+ const dataSourceConfig = { type: 'memory' };
10188
+ this.addTrack(track, dataSource, dataSourceConfig, options === null || options === void 0 ? void 0 : options.maxTrackHeight);
10189
+ if (options === null || options === void 0 ? void 0 : options.metadata)
10190
+ this.managedTracks[this.managedTracks.length - 1].metadata = options.metadata;
10191
+ return track;
10192
+ }
10193
+ /** Add a BED/annotation track backed by in-memory features (no URL required). Features must include `chr`. */
10194
+ addBedTrackWithFeatures(features, options) {
10195
+ const { canvas } = this.canvasProvider.createCanvas(0, 0);
10196
+ const track = new AnnotationTrackCanvas(canvas, {
10197
+ locus: this._locus,
10198
+ features: [],
10199
+ config: options === null || options === void 0 ? void 0 : options.config,
10200
+ height: options === null || options === void 0 ? void 0 : options.height,
10201
+ background: options === null || options === void 0 ? void 0 : options.background,
10202
+ theme: this.theme,
10203
+ canvasProvider: this.canvasProvider,
10204
+ workerProvider: this.workerProvider,
10205
+ name: options === null || options === void 0 ? void 0 : options.name,
10206
+ });
10207
+ const dataSource = new MemoryDataSource(features);
10208
+ if (this.cumulativeOffsets) {
10209
+ dataSource.setCumulativeOffsets(this.cumulativeOffsets);
10210
+ }
10211
+ if (this.genome) {
10212
+ dataSource.setChromNameResolver(alias => this.genome.getChromosomeName(alias));
10213
+ }
10214
+ const dataSourceConfig = { type: 'memory' };
10215
+ this.addTrack(track, dataSource, dataSourceConfig, options === null || options === void 0 ? void 0 : options.maxTrackHeight);
10216
+ if (options === null || options === void 0 ? void 0 : options.metadata)
10217
+ this.managedTracks[this.managedTracks.length - 1].metadata = options.metadata;
10218
+ return track;
10219
+ }
10092
10220
  /** Add a DNA/RNA sequence track. Data fetching is handled automatically via the genome's sequence provider. */
10093
10221
  addSequenceTrack(options) {
10094
10222
  const trackConfig = { type: 'sequence', config: options === null || options === void 0 ? void 0 : options.config };
@@ -12472,6 +12600,23 @@ class GenomeBrowser extends HeadlessGenomeBrowser {
12472
12600
 
12473
12601
  const GenomeBrowserContext = createContext(null);
12474
12602
 
12603
+ /** Infer the shell theme from a RenderTheme's palette. */
12604
+ function inferShellTheme(theme) {
12605
+ var _a;
12606
+ const bg = (_a = theme === null || theme === void 0 ? void 0 : theme.palette) === null || _a === void 0 ? void 0 : _a.background;
12607
+ if (!bg)
12608
+ return 'modern';
12609
+ // Simple luminance check: dark backgrounds → dark shell theme
12610
+ const el = document.createElement('canvas');
12611
+ const ctx = el.getContext('2d');
12612
+ ctx.fillStyle = bg;
12613
+ const hex = ctx.fillStyle; // normalized to #rrggbb
12614
+ const r = parseInt(hex.slice(1, 3), 16);
12615
+ const g = parseInt(hex.slice(3, 5), 16);
12616
+ const b = parseInt(hex.slice(5, 7), 16);
12617
+ const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
12618
+ return luminance < 0.5 ? 'dark' : 'modern';
12619
+ }
12475
12620
  const LoomBrowser = forwardRef(function LoomBrowser(props, ref) {
12476
12621
  const { defaultLocus, locus, onLocusChange, theme, genome, interactive, wheelZoom, workerProvider, popupProvider, contextMenuProvider, ruler, genes, sequence, session, tracks: trackConfigs, remoteSocket, className, style, children, } = props;
12477
12622
  const containerRef = useRef(null);
@@ -12614,7 +12759,8 @@ const LoomBrowser = forwardRef(function LoomBrowser(props, ref) {
12614
12759
  attachRemote(socket) { var _a; (_a = browserRef.current) === null || _a === void 0 ? void 0 : _a.attachRemote(socket); },
12615
12760
  detachRemote() { var _a; (_a = browserRef.current) === null || _a === void 0 ? void 0 : _a.detachRemote(); },
12616
12761
  }), [browser]);
12617
- const ctxValue = useMemo(() => ({ browser }), [browser]);
12762
+ const shellTheme = useMemo(() => inferShellTheme(theme), [theme]);
12763
+ const ctxValue = useMemo(() => ({ browser, shellTheme }), [browser, shellTheme]);
12618
12764
  return (jsx(GenomeBrowserContext.Provider, { value: ctxValue, children: jsxs("div", { className: className, style: style, children: [browser && children, jsx("div", { ref: containerRef })] }) }));
12619
12765
  });
12620
12766
 
@@ -12723,8 +12869,15 @@ function RulerTrack({ config, maxTrackHeight }) {
12723
12869
  return null;
12724
12870
  }
12725
12871
 
12726
- function WigTrack({ url, config, height, background, windowFunction, maxTrackHeight, name, metadata }) {
12727
- useTrackManager((browser) => browser.addWigTrack(url, { config, height, background, windowFunction, maxTrackHeight, name, metadata }), [url, windowFunction], (track) => { if (config)
12872
+ function WigTrack({ url, features, config, height, background, windowFunction, maxTrackHeight, name, metadata }) {
12873
+ useTrackManager((browser) => {
12874
+ if (features) {
12875
+ return browser.addWigTrackWithFeatures(features, { config, height, background, maxTrackHeight, name, metadata });
12876
+ }
12877
+ if (!url)
12878
+ throw new Error('WigTrack requires either a `url` or `features` prop');
12879
+ return browser.addWigTrack(url, { config, height, background, windowFunction, maxTrackHeight, name, metadata });
12880
+ }, [url, features, windowFunction], (track) => { if (config)
12728
12881
  track.setConfig(config); }, [config, height, background, name]);
12729
12882
  return null;
12730
12883
  }
@@ -12735,8 +12888,15 @@ function GeneTrack({ config, height, background, genome, track, maxTrackHeight,
12735
12888
  return null;
12736
12889
  }
12737
12890
 
12738
- function BedTrack({ url, config, height, background, format, indexURL, indexed, maxTrackHeight, name, metadata }) {
12739
- useTrackManager((browser) => browser.addBedTrack(url, { config, height, background, format, indexURL, indexed, maxTrackHeight, name, metadata }), [url, format, indexURL, indexed], (track) => { if (config)
12891
+ function BedTrack({ url, features, config, height, background, format, indexURL, indexed, maxTrackHeight, name, metadata }) {
12892
+ useTrackManager((browser) => {
12893
+ if (features) {
12894
+ return browser.addBedTrackWithFeatures(features, { config, height, background, maxTrackHeight, name, metadata });
12895
+ }
12896
+ if (!url)
12897
+ throw new Error('BedTrack requires either a `url` or `features` prop');
12898
+ return browser.addBedTrack(url, { config, height, background, format, indexURL, indexed, maxTrackHeight, name, metadata });
12899
+ }, [url, features, format, indexURL, indexed], (track) => { if (config)
12740
12900
  track.setConfig(config); }, [config, height, background, name]);
12741
12901
  return null;
12742
12902
  }
@@ -13387,15 +13547,126 @@ function ExportControls({ browser: browserProp }) {
13387
13547
  return jsx("loom-export-controls", { ref: ref });
13388
13548
  }
13389
13549
 
13390
- function Navbar({ browser: browserProp }) {
13550
+ /**
13551
+ * CSS custom property themes for UI shell web components.
13552
+ *
13553
+ * Themes are injected as <style> blocks into Shadow DOM roots.
13554
+ * Components reference CSS custom properties (--loom-*) for all visual styling.
13555
+ */
13556
+ /** Classic theme — matches igv.js navbar look. */
13557
+ const classicThemeCSS = /* css */ `
13558
+ :host {
13559
+ --loom-navbar-bg: #f3f3f3;
13560
+ --loom-navbar-height: 32px;
13561
+ --loom-navbar-padding: 0 8px;
13562
+ --loom-font: 12px Arial, sans-serif;
13563
+ --loom-font-small: 11px Arial, sans-serif;
13564
+ --loom-text-color: #333;
13565
+ --loom-text-muted: #737373;
13566
+ --loom-border: 1px solid #ccc;
13567
+ --loom-border-radius: 4px;
13568
+ --loom-button-bg: white;
13569
+ --loom-button-hover: #e8e8e8;
13570
+ --loom-button-border: 1px solid #b0b0b0;
13571
+ --loom-button-size: 24px;
13572
+ --loom-input-bg: white;
13573
+ --loom-input-border: 1px solid #b0b0b0;
13574
+ --loom-input-focus-border: 1px solid #4A90D9;
13575
+ --loom-input-width: 220px;
13576
+ --loom-input-height: 22px;
13577
+ --loom-accent: #4A90D9;
13578
+ --loom-icon-color: #555;
13579
+ --loom-icon-size: 14px;
13580
+ --loom-gap: 8px;
13581
+ }
13582
+ `;
13583
+ /** Modern theme — softer colors, rounded corners, taller navbar. */
13584
+ const modernThemeCSS = /* css */ `
13585
+ :host {
13586
+ --loom-navbar-bg: #fafbfc;
13587
+ --loom-navbar-height: 40px;
13588
+ --loom-navbar-padding: 0 12px;
13589
+ --loom-font: 13px Inter, system-ui, -apple-system, sans-serif;
13590
+ --loom-font-small: 11px Inter, system-ui, -apple-system, sans-serif;
13591
+ --loom-text-color: #1a1a1a;
13592
+ --loom-text-muted: #6b7280;
13593
+ --loom-border: 1px solid rgba(0, 0, 0, 0.08);
13594
+ --loom-border-radius: 8px;
13595
+ --loom-button-bg: white;
13596
+ --loom-button-hover: #f0f4ff;
13597
+ --loom-button-border: 1px solid rgba(0, 0, 0, 0.1);
13598
+ --loom-button-size: 28px;
13599
+ --loom-input-bg: white;
13600
+ --loom-input-border: 1px solid rgba(0, 0, 0, 0.12);
13601
+ --loom-input-focus-border: 1px solid #4A90D9;
13602
+ --loom-input-width: 260px;
13603
+ --loom-input-height: 28px;
13604
+ --loom-accent: #4A90D9;
13605
+ --loom-icon-color: #6b7280;
13606
+ --loom-icon-size: 16px;
13607
+ --loom-gap: 10px;
13608
+ }
13609
+ `;
13610
+ /** Dark theme — dark backgrounds, light text, high contrast. */
13611
+ const darkThemeCSS = /* css */ `
13612
+ :host {
13613
+ --loom-shell-bg: #16162a;
13614
+ --loom-navbar-bg: #1a1a2e;
13615
+ --loom-navbar-height: 36px;
13616
+ --loom-navbar-padding: 0 10px;
13617
+ --loom-font: 12px Arial, sans-serif;
13618
+ --loom-font-small: 11px Arial, sans-serif;
13619
+ --loom-text-color: #e0e0e0;
13620
+ --loom-text-muted: #888;
13621
+ --loom-border: 1px solid #333;
13622
+ --loom-border-radius: 4px;
13623
+ --loom-button-bg: #2a2a3e;
13624
+ --loom-button-hover: #3a3a50;
13625
+ --loom-button-border: 1px solid #444;
13626
+ --loom-button-size: 24px;
13627
+ --loom-input-bg: #2a2a3e;
13628
+ --loom-input-border: 1px solid #444;
13629
+ --loom-input-focus-border: 1px solid #6a9fd9;
13630
+ --loom-input-width: 220px;
13631
+ --loom-input-height: 22px;
13632
+ --loom-accent: #6a9fd9;
13633
+ --loom-icon-color: #b0b0b0;
13634
+ --loom-icon-size: 14px;
13635
+ --loom-gap: 8px;
13636
+ }
13637
+ `;
13638
+ function getThemeCSS(theme) {
13639
+ if (theme === 'modern')
13640
+ return modernThemeCSS;
13641
+ if (theme === 'dark')
13642
+ return darkThemeCSS;
13643
+ return classicThemeCSS;
13644
+ }
13645
+
13646
+ function Navbar({ browser: browserProp, theme: themeProp }) {
13647
+ var _a;
13648
+ const ctx = useContext(GenomeBrowserContext);
13391
13649
  const contextBrowser = useGenomeBrowser();
13392
13650
  const browser = browserProp !== null && browserProp !== void 0 ? browserProp : contextBrowser;
13651
+ const theme = (_a = themeProp !== null && themeProp !== void 0 ? themeProp : ctx === null || ctx === void 0 ? void 0 : ctx.shellTheme) !== null && _a !== void 0 ? _a : 'modern';
13393
13652
  const ref = useRef(null);
13653
+ const styleRef = useRef(null);
13394
13654
  ensureRegistered();
13395
13655
  useEffect(() => {
13396
13656
  if (ref.current)
13397
13657
  ref.current.browser = browser;
13398
13658
  }, [browser]);
13659
+ // Inject shell theme CSS into the navbar's shadow root
13660
+ useEffect(() => {
13661
+ const el = ref.current;
13662
+ if (!(el === null || el === void 0 ? void 0 : el.shadowRoot))
13663
+ return;
13664
+ if (!styleRef.current) {
13665
+ styleRef.current = document.createElement('style');
13666
+ el.shadowRoot.prepend(styleRef.current);
13667
+ }
13668
+ styleRef.current.textContent = getThemeCSS(theme);
13669
+ }, [theme]);
13399
13670
  return jsx("loom-navbar", { ref: ref });
13400
13671
  }
13401
13672