gramene-search 2.1.11 → 2.2.0

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 (35) hide show
  1. package/.claude/launch.json +11 -0
  2. package/.claude/settings.local.json +6 -1
  3. package/.env.example +6 -0
  4. package/.parcel-cache/13f2d5707e7af45c-RequestGraph +0 -0
  5. package/.parcel-cache/5ae0570a78c0dba3-AssetGraph +0 -0
  6. package/.parcel-cache/9ac092379278e465-BundleGraph +0 -0
  7. package/.parcel-cache/data.mdb +0 -0
  8. package/.parcel-cache/lock.mdb +0 -0
  9. package/.parcel-cache/snapshot-13f2d5707e7af45c.txt +2 -2
  10. package/dist/index.js +2065 -154
  11. package/dist/index.js.map +1 -1
  12. package/package.json +5 -2
  13. package/src/bundles/api.js +10 -4
  14. package/src/bundles/exprViz.js +97 -1
  15. package/src/bundles/index.js +4 -1
  16. package/src/bundles/ontologyEnrichment.js +14 -1
  17. package/src/bundles/savedViews.js +335 -0
  18. package/src/bundles/uiViewState.js +174 -0
  19. package/src/bundles/viewSnapshot.js +313 -0
  20. package/src/bundles/views.js +22 -0
  21. package/src/components/Auth.js +23 -3
  22. package/src/components/SaveView.js +157 -0
  23. package/src/components/exprViz/ExprVizView.js +16 -11
  24. package/src/components/exprViz/ParallelCoordsPlot.js +15 -0
  25. package/src/components/results/GeneList.js +45 -15
  26. package/src/components/results/OntologyEnrichment.js +13 -6
  27. package/src/components/results/details/BAR.js +148 -0
  28. package/src/components/results/details/Expression.js +43 -11
  29. package/src/components/results/details/Homology.js +170 -32
  30. package/src/components/results/details/Pathways.js +4 -2
  31. package/src/components/results/details/Sequences.js +24 -8
  32. package/src/demo.js +22 -17
  33. package/src/index.js +2 -1
  34. package/src/suppressDevWarnings.js +13 -0
  35. package/src/utils/bootView.js +38 -0
package/dist/index.js CHANGED
@@ -7,8 +7,8 @@ require("ag-grid-community/styles/ag-grid.css");
7
7
  require("ag-grid-community/styles/ag-theme-quartz.css");
8
8
  var $gXNCa$reduxbundler = require("redux-bundler");
9
9
  var $gXNCa$lodash = require("lodash");
10
- var $gXNCa$gramenebinsclient = require("gramene-bins-client");
11
- var $gXNCa$gramenetreesclient = require("gramene-trees-client");
10
+ var $gXNCa$gramenebinsclientsrcbins = require("gramene-bins-client/src/bins");
11
+ var $gXNCa$gramenetreesclientsrctaxonomy = require("gramene-trees-client/src/taxonomy");
12
12
  var $gXNCa$gramenetaxonomywithgenomes = require("gramene-taxonomy-with-genomes");
13
13
  var $gXNCa$reactga4 = require("react-ga4");
14
14
  var $gXNCa$moneyclip = require("money-clip");
@@ -17,8 +17,8 @@ var $gXNCa$reactswitch = require("react-switch");
17
17
  var $gXNCa$reacticonsio5 = require("react-icons/io5");
18
18
  var $gXNCa$reacticonsbs = require("react-icons/bs");
19
19
  var $gXNCa$reacticonsgr = require("react-icons/gr");
20
- var $gXNCa$grameneefpbrowser = require("gramene-efp-browser");
21
20
  var $gXNCa$gramenegenetreevis = require("gramene-genetree-vis");
21
+ var $gXNCa$gramenetreesclientsrcgenetree = require("gramene-trees-client/src/genetree");
22
22
  var $gXNCa$tbrowse = require("tbrowse");
23
23
  var $gXNCa$reactdom = require("react-dom");
24
24
  var $gXNCa$lodashkeyBy = require("lodash/keyBy");
@@ -222,6 +222,7 @@ $parcel$export(module.exports, "Filters", () => $693dd8c7a5607c3a$export$92a576f
222
222
  $parcel$export(module.exports, "Results", () => $693dd8c7a5607c3a$export$4a34de49b46a2bfa);
223
223
  $parcel$export(module.exports, "Views", () => $693dd8c7a5607c3a$export$5cb791131c501f6a);
224
224
  $parcel$export(module.exports, "Auth", () => $c5d403787de8b05f$export$2e2bcd8739ae039);
225
+ $parcel$export(module.exports, "bootViewFromUrl", () => $c69c61828e36a328$export$2e2bcd8739ae039);
225
226
 
226
227
 
227
228
 
@@ -638,8 +639,8 @@ const $9d9aeaf9299e61a1$var$grameneTaxDist = {
638
639
  grameneTaxonomy[tid].name = map.display_name;
639
640
  });
640
641
  const binnedResults = $9d9aeaf9299e61a1$var$formatFacetCountsForViz(grameneSearch.facet_counts.facet_fields.fixed_1000__bin);
641
- let speciesTree = (0, ($parcel$interopDefault($gXNCa$gramenetreesclient))).taxonomy.tree(Object.values(grameneTaxonomy));
642
- let binMapper = (0, ($parcel$interopDefault($gXNCa$gramenebinsclient))).bins(grameneMaps);
642
+ let speciesTree = (0, ($parcel$interopDefault($gXNCa$gramenetreesclientsrctaxonomy))).tree(Object.values(grameneTaxonomy));
643
+ let binMapper = (0, ($parcel$interopDefault($gXNCa$gramenebinsclientsrcbins)))(grameneMaps);
643
644
  let taxDist = (0, $gXNCa$gramenetaxonomywithgenomes.build)(binMapper, speciesTree);
644
645
  taxDist.setBinType('fixed', 1000);
645
646
  taxDist.setResults(binnedResults);
@@ -1549,6 +1550,26 @@ const $24971af0a229e0e3$var$grameneViews = {
1549
1550
  newState = Object.assign({}, state);
1550
1551
  newState.options.forEach((v)=>v.shouldScroll = false);
1551
1552
  return newState;
1553
+ case 'GRAMENE_VIEWS_REPLACED':
1554
+ {
1555
+ // Snapshot restore. payload: { on: Set|Array<id>, touched: {id: true} }
1556
+ // Sets options[].show directly to 'on' or 'off' based on membership in
1557
+ // `on`. Leaves 'disabled' entries untouched (site config wins).
1558
+ const onSet = payload.on instanceof Set ? payload.on : new Set(payload.on || []);
1559
+ newState = Object.assign({}, state);
1560
+ newState.touched = {
1561
+ ...payload.touched || {}
1562
+ };
1563
+ newState.options = state.options.map((v)=>{
1564
+ if (v.show === 'disabled') return v;
1565
+ return {
1566
+ ...v,
1567
+ show: onSet.has(v.id) ? 'on' : 'off',
1568
+ shouldScroll: false
1569
+ };
1570
+ });
1571
+ return newState;
1572
+ }
1552
1573
  default:
1553
1574
  return state;
1554
1575
  }
@@ -1572,6 +1593,19 @@ const $24971af0a229e0e3$var$grameneViews = {
1572
1593
  payload: null
1573
1594
  });
1574
1595
  },
1596
+ // Snapshot restore. `on` is an array of view ids to switch on; everything
1597
+ // else (except disabled views) goes off. `touched` is the same map the
1598
+ // user-toggled view bundle maintains, so the auto-default logic in
1599
+ // selectGrameneViews respects what the user has explicitly set.
1600
+ doReplaceGrameneViews: ({ on: on, touched: touched })=>({ dispatch: dispatch })=>{
1601
+ dispatch({
1602
+ type: 'GRAMENE_VIEWS_REPLACED',
1603
+ payload: {
1604
+ on: on,
1605
+ touched: touched
1606
+ }
1607
+ });
1608
+ },
1575
1609
  selectRawGrameneViews: (state)=>state.grameneViews,
1576
1610
  selectGrameneViews: (0, $gXNCa$reduxbundler.createSelector)('selectRawGrameneViews', 'selectConfiguration', 'selectGrameneSearch', 'selectGrameneFilters', (raw, config, search, filters)=>{
1577
1611
  const overrides = config && config.views || null;
@@ -3913,8 +3947,12 @@ var $c921a0d2b34aadb6$export$2e2bcd8739ae039 = $c921a0d2b34aadb6$var$ontologies;
3913
3947
  // activeTaxon: <taxon_id|null>,
3914
3948
  // byTaxon: {
3915
3949
  // [taxon_id]: {
3916
- // selectedFields: [<solr field name>...],
3950
+ // selectedFields: [<solr field name>...], // order = column/axis order
3917
3951
  // fieldsModalOpen: <bool>,
3952
+ // vizMode: 'heatmap'|'parallel', // persisted view config
3953
+ // scale: 'linear'|'log', // persisted view config
3954
+ // brushes: { [field]: [lo, hi] }, // persisted parallel-coords brushes
3955
+ // pendingLoad: <bool>, // transient: a restored view wants a re-fetch
3918
3956
  // fetch: { status, offset, total, signature, requestId, error },
3919
3957
  // rows: [<doc>...]
3920
3958
  // }
@@ -3964,6 +4002,10 @@ const $4f15cd8a7d970b18$var$exprViz = {
3964
4002
  return {
3965
4003
  selectedFields: [],
3966
4004
  fieldsModalOpen: false,
4005
+ vizMode: 'heatmap',
4006
+ scale: 'linear',
4007
+ brushes: {},
4008
+ pendingLoad: false,
3967
4009
  fetch: {
3968
4010
  status: 'idle',
3969
4011
  offset: 0,
@@ -4046,6 +4088,9 @@ const $4f15cd8a7d970b18$var$exprViz = {
4046
4088
  [payload.taxon]: {
4047
4089
  ...t,
4048
4090
  selectedFields: payload.fields,
4091
+ // A new field selection invalidates any prior brushes (they
4092
+ // reference axes that may no longer exist) and the loaded rows.
4093
+ brushes: {},
4049
4094
  rows: [],
4050
4095
  fetch: {
4051
4096
  status: 'idle',
@@ -4059,6 +4104,77 @@ const $4f15cd8a7d970b18$var$exprViz = {
4059
4104
  }
4060
4105
  };
4061
4106
  }
4107
+ case 'EXPRVIZ_VIZMODE_SET':
4108
+ {
4109
+ const t = ensureTaxon(state, payload.taxon);
4110
+ return {
4111
+ ...state,
4112
+ byTaxon: {
4113
+ ...state.byTaxon,
4114
+ [payload.taxon]: {
4115
+ ...t,
4116
+ vizMode: payload.vizMode
4117
+ }
4118
+ }
4119
+ };
4120
+ }
4121
+ case 'EXPRVIZ_SCALE_SET':
4122
+ {
4123
+ const t = ensureTaxon(state, payload.taxon);
4124
+ return {
4125
+ ...state,
4126
+ byTaxon: {
4127
+ ...state.byTaxon,
4128
+ [payload.taxon]: {
4129
+ ...t,
4130
+ scale: payload.scale
4131
+ }
4132
+ }
4133
+ };
4134
+ }
4135
+ case 'EXPRVIZ_BRUSHES_SET':
4136
+ {
4137
+ const t = ensureTaxon(state, payload.taxon);
4138
+ return {
4139
+ ...state,
4140
+ byTaxon: {
4141
+ ...state.byTaxon,
4142
+ [payload.taxon]: {
4143
+ ...t,
4144
+ brushes: payload.brushes || {}
4145
+ }
4146
+ }
4147
+ };
4148
+ }
4149
+ case 'EXPRVIZ_RESTORED':
4150
+ {
4151
+ // Re-apply persisted view config from a saved-view snapshot. Async
4152
+ // data (rows) is NOT in the snapshot — instead each taxon with a
4153
+ // field selection is flagged pendingLoad so reactExprVizRestoreLoad
4154
+ // re-fetches it once the search context is ready.
4155
+ const byTaxon = {
4156
+ ...state.byTaxon
4157
+ };
4158
+ const cfg = payload.byTaxon || {};
4159
+ for (const tid of Object.keys(cfg)){
4160
+ const base = ensureTaxon(state, tid);
4161
+ const c = cfg[tid] || {};
4162
+ const selectedFields = Array.isArray(c.selectedFields) ? c.selectedFields : [];
4163
+ byTaxon[tid] = {
4164
+ ...base,
4165
+ selectedFields: selectedFields,
4166
+ vizMode: c.vizMode || 'heatmap',
4167
+ scale: c.scale || 'linear',
4168
+ brushes: c.brushes || {},
4169
+ pendingLoad: selectedFields.length > 0
4170
+ };
4171
+ }
4172
+ return {
4173
+ ...state,
4174
+ activeTaxon: payload.activeTaxon || state.activeTaxon,
4175
+ byTaxon: byTaxon
4176
+ };
4177
+ }
4062
4178
  case 'EXPRVIZ_FIELDS_REORDERED':
4063
4179
  {
4064
4180
  const t = state.byTaxon[payload.taxon];
@@ -4089,6 +4205,7 @@ const $4f15cd8a7d970b18$var$exprViz = {
4089
4205
  ...state.byTaxon,
4090
4206
  [payload.taxon]: {
4091
4207
  ...t,
4208
+ pendingLoad: false,
4092
4209
  fetch: {
4093
4210
  ...t.fetch,
4094
4211
  status: 'loading',
@@ -4346,6 +4463,41 @@ const $4f15cd8a7d970b18$var$exprViz = {
4346
4463
  fields: fields
4347
4464
  }
4348
4465
  }),
4466
+ doSetExprVizVizMode: (taxon, vizMode)=>({ dispatch: dispatch })=>dispatch({
4467
+ type: 'EXPRVIZ_VIZMODE_SET',
4468
+ payload: {
4469
+ taxon: taxon,
4470
+ vizMode: vizMode
4471
+ }
4472
+ }),
4473
+ doSetExprVizScale: (taxon, scale)=>({ dispatch: dispatch })=>dispatch({
4474
+ type: 'EXPRVIZ_SCALE_SET',
4475
+ payload: {
4476
+ taxon: taxon,
4477
+ scale: scale
4478
+ }
4479
+ }),
4480
+ doSetExprVizBrushes: (taxon, brushes)=>({ dispatch: dispatch })=>dispatch({
4481
+ type: 'EXPRVIZ_BRUSHES_SET',
4482
+ payload: {
4483
+ taxon: taxon,
4484
+ brushes: brushes
4485
+ }
4486
+ }),
4487
+ // Re-apply persisted exprViz view config from a saved-view snapshot. Called
4488
+ // by viewSnapshot's doApplyViewSnapshot after filters/views have been
4489
+ // restored, so the re-fetch (driven by reactExprVizRestoreLoad) sees the
4490
+ // restored query context.
4491
+ doApplyExprVizSnapshot: (snap)=>({ dispatch: dispatch })=>{
4492
+ if (!snap || typeof snap !== 'object') return;
4493
+ dispatch({
4494
+ type: 'EXPRVIZ_RESTORED',
4495
+ payload: {
4496
+ activeTaxon: snap.activeTaxon || null,
4497
+ byTaxon: snap.byTaxon || {}
4498
+ }
4499
+ });
4500
+ },
4349
4501
  doFetchExprVizData: (taxon)=>({ dispatch: dispatch, store: store })=>{
4350
4502
  const ev = store.selectExprViz();
4351
4503
  const t = ev.byTaxon[taxon];
@@ -4466,6 +4618,25 @@ const $4f15cd8a7d970b18$var$exprViz = {
4466
4618
  actionCreator: 'doFetchExprVizPivot'
4467
4619
  };
4468
4620
  }),
4621
+ // After a saved view is applied, EXPRVIZ_RESTORED flags each taxon that had
4622
+ // a field selection with pendingLoad. Once the search context is live (not
4623
+ // 'init') and the view is on, re-fetch that taxon's rows. Fires one taxon at
4624
+ // a time; EXPRVIZ_FETCH_STARTED clears the flag so this won't loop. Robust to
4625
+ // a GRAMENE_SEARCH_CLEARED that wipes rows mid-restore — the flag persists.
4626
+ reactExprVizRestoreLoad: (0, $gXNCa$reduxbundler.createSelector)('selectExprViz', 'selectGrameneFiltersStatus', 'selectGrameneViews', (ev, filtersStatus, views)=>{
4627
+ if (!ev || filtersStatus === 'init') return;
4628
+ const onView = views && views.options && views.options.find((v)=>v.id === 'exprViz');
4629
+ if (!onView || onView.show !== 'on') return;
4630
+ for (const tid of Object.keys(ev.byTaxon || {})){
4631
+ const t = ev.byTaxon[tid];
4632
+ if (t && t.pendingLoad && t.selectedFields.length > 0 && t.rows.length === 0 && t.fetch.status !== 'loading') return {
4633
+ actionCreator: 'doFetchExprVizData',
4634
+ args: [
4635
+ tid
4636
+ ]
4637
+ };
4638
+ }
4639
+ }),
4469
4640
  selectExprViz: (state)=>state.exprViz,
4470
4641
  selectExprVizPivot: (state)=>state.exprViz.pivot,
4471
4642
  selectExprVizActiveTaxon: (state)=>state.exprViz.activeTaxon,
@@ -4569,7 +4740,10 @@ const $d365d8c287ab0c94$var$ontologyEnrichment = {
4569
4740
  maxGSSize: 500,
4570
4741
  mostSpecific: false,
4571
4742
  ontology: 'all',
4572
- search: ''
4743
+ search: '',
4744
+ // Per-section table sort, keyed by ontology section id (e.g.
4745
+ // 'GO:biological_process'): { [sectionKey]: { key, dir } }.
4746
+ sort: {}
4573
4747
  }
4574
4748
  };
4575
4749
  function ensureTaxon(state, tid) {
@@ -4773,6 +4947,21 @@ const $d365d8c287ab0c94$var$ontologyEnrichment = {
4773
4947
  type: 'ONTOLOGY_ENRICHMENT_UI_SET',
4774
4948
  payload: patch
4775
4949
  }),
4950
+ // Re-apply persisted view config from a saved-view snapshot. Setting the
4951
+ // active taxon (last) lets reactOntologyEnrichmentFetch re-fetch the
4952
+ // foreground/background facets for the restored filters — no bulk data is
4953
+ // carried in the snapshot.
4954
+ doApplyOntologyEnrichmentSnapshot: (snap)=>({ dispatch: dispatch })=>{
4955
+ if (!snap || typeof snap !== 'object') return;
4956
+ if (snap.ui) dispatch({
4957
+ type: 'ONTOLOGY_ENRICHMENT_UI_SET',
4958
+ payload: snap.ui
4959
+ });
4960
+ if (snap.activeTaxon) dispatch({
4961
+ type: 'ONTOLOGY_ENRICHMENT_ACTIVE_TAXON_SET',
4962
+ payload: snap.activeTaxon
4963
+ });
4964
+ },
4776
4965
  doFetchOntologyEnrichmentForeground: (taxon)=>({ dispatch: dispatch, store: store })=>{
4777
4966
  const q = store.selectGrameneFiltersQueryString();
4778
4967
  const signature = $d365d8c287ab0c94$var$fgSig(q, taxon);
@@ -5130,6 +5319,1009 @@ function $d365d8c287ab0c94$var$collapseToMostSpecific(ontKey, rows, recs) {
5130
5319
  var $d365d8c287ab0c94$export$2e2bcd8739ae039 = $d365d8c287ab0c94$var$ontologyEnrichment;
5131
5320
 
5132
5321
 
5322
+ // Per-gene UI state lifted out of <Gene> and <Homology> class components.
5323
+ // Keyed by geneId so it survives unmount/remount (e.g. scrolling a row out
5324
+ // of view and back), and so a snapshot serializer can later round-trip it
5325
+ // for the shareable-views feature.
5326
+ //
5327
+ // What lives here:
5328
+ // - expandedDetail: which detail tab is open in the gene-list card
5329
+ // - fullscreen: whether the expanded detail is full-screened
5330
+ // - homology.viewer: 'treevis' | 'tbrowse'
5331
+ // - homology.height: drag-resize height of the homology detail pane
5332
+ // - homology.tbrowse: the tbrowse ViewState (collapsedNodeIds, prunedNodeIds,
5333
+ // swappedNodeIds, compressedNodeIds, nodeOfInterestId, zones, zoneStates,
5334
+ // search, selectedNodeId). Tbrowse is driven in controlled mode from this.
5335
+ // - sequences: the Sequences detail's internal state — tab ('dna'|'rna'|'pep'),
5336
+ // tid (selected transcript id), upstream/downstream (flanking bp). Driven in
5337
+ // controlled mode from this so a saved view restores the chosen sub-tab and
5338
+ // isoform.
5339
+ // - expression: the Expression detail's internal state — activeTab
5340
+ // ('gene'|'paralogs'|'eFP'), atlasExperiment (selected GXA experiment id),
5341
+ // barStudy (selected eFP/BAR study). Driven in controlled mode from this so a
5342
+ // saved view restores the chosen sub-tab and study.
5343
+ //
5344
+ // What does NOT live here:
5345
+ // - derived/computed state like `details` (per-render config from
5346
+ // props.config.details + capabilities)
5347
+ // - async-fetched data caches in <Homology> (neighborhood, geneStructures)
5348
+ const $7f865ea0feda21af$var$initialState = {
5349
+ byGene: {}
5350
+ };
5351
+ const $7f865ea0feda21af$var$ensureGene = (state, geneId)=>{
5352
+ if (state.byGene[geneId]) return state;
5353
+ return {
5354
+ ...state,
5355
+ byGene: {
5356
+ ...state.byGene,
5357
+ [geneId]: {
5358
+ homology: {}
5359
+ }
5360
+ }
5361
+ };
5362
+ };
5363
+ const $7f865ea0feda21af$var$setGene = (state, geneId, patch)=>{
5364
+ const next = $7f865ea0feda21af$var$ensureGene(state, geneId);
5365
+ return {
5366
+ ...next,
5367
+ byGene: {
5368
+ ...next.byGene,
5369
+ [geneId]: {
5370
+ ...next.byGene[geneId],
5371
+ ...patch
5372
+ }
5373
+ }
5374
+ };
5375
+ };
5376
+ const $7f865ea0feda21af$var$setHomology = (state, geneId, patch)=>{
5377
+ const next = $7f865ea0feda21af$var$ensureGene(state, geneId);
5378
+ const prev = next.byGene[geneId];
5379
+ return {
5380
+ ...next,
5381
+ byGene: {
5382
+ ...next.byGene,
5383
+ [geneId]: {
5384
+ ...prev,
5385
+ homology: {
5386
+ ...prev.homology || {},
5387
+ ...patch
5388
+ }
5389
+ }
5390
+ }
5391
+ };
5392
+ };
5393
+ const $7f865ea0feda21af$var$setSequences = (state, geneId, patch)=>{
5394
+ const next = $7f865ea0feda21af$var$ensureGene(state, geneId);
5395
+ const prev = next.byGene[geneId];
5396
+ return {
5397
+ ...next,
5398
+ byGene: {
5399
+ ...next.byGene,
5400
+ [geneId]: {
5401
+ ...prev,
5402
+ sequences: {
5403
+ ...prev.sequences || {},
5404
+ ...patch
5405
+ }
5406
+ }
5407
+ }
5408
+ };
5409
+ };
5410
+ const $7f865ea0feda21af$var$setExpression = (state, geneId, patch)=>{
5411
+ const next = $7f865ea0feda21af$var$ensureGene(state, geneId);
5412
+ const prev = next.byGene[geneId];
5413
+ return {
5414
+ ...next,
5415
+ byGene: {
5416
+ ...next.byGene,
5417
+ [geneId]: {
5418
+ ...prev,
5419
+ expression: {
5420
+ ...prev.expression || {},
5421
+ ...patch
5422
+ }
5423
+ }
5424
+ }
5425
+ };
5426
+ };
5427
+ const $7f865ea0feda21af$var$uiViewState = {
5428
+ name: 'uiViewState',
5429
+ getReducer: ()=>(state = $7f865ea0feda21af$var$initialState, { type: type, payload: payload })=>{
5430
+ switch(type){
5431
+ case 'UI_GENE_DETAIL_EXPANDED':
5432
+ // payload: { geneId, detail } (detail = null to collapse)
5433
+ return $7f865ea0feda21af$var$setGene(state, payload.geneId, {
5434
+ expandedDetail: payload.detail,
5435
+ // collapsing also exits fullscreen
5436
+ fullscreen: payload.detail === null ? false : state.byGene[payload.geneId]?.fullscreen || false
5437
+ });
5438
+ case 'UI_GENE_FULLSCREEN_SET':
5439
+ // payload: { geneId, fullscreen }
5440
+ return $7f865ea0feda21af$var$setGene(state, payload.geneId, {
5441
+ fullscreen: !!payload.fullscreen
5442
+ });
5443
+ case 'UI_HOMOLOGY_VIEWER_SET':
5444
+ // payload: { geneId, viewer }
5445
+ return $7f865ea0feda21af$var$setHomology(state, payload.geneId, {
5446
+ viewer: payload.viewer
5447
+ });
5448
+ case 'UI_HOMOLOGY_HEIGHT_SET':
5449
+ // payload: { geneId, height }
5450
+ return $7f865ea0feda21af$var$setHomology(state, payload.geneId, {
5451
+ height: payload.height
5452
+ });
5453
+ case 'UI_HOMOLOGY_TBROWSE_SET':
5454
+ // payload: { geneId, tbrowse } -- a full ViewState object
5455
+ return $7f865ea0feda21af$var$setHomology(state, payload.geneId, {
5456
+ tbrowse: payload.tbrowse
5457
+ });
5458
+ case 'UI_SEQUENCES_SET':
5459
+ // payload: { geneId, patch } -- merges into the sequences slice
5460
+ // (e.g. { tab }, { tid }, { upstream }, { downstream })
5461
+ return $7f865ea0feda21af$var$setSequences(state, payload.geneId, payload.patch);
5462
+ case 'UI_EXPRESSION_SET':
5463
+ // payload: { geneId, patch } -- merges into the expression slice
5464
+ // (e.g. { activeTab }, { atlasExperiment }, { barStudy })
5465
+ return $7f865ea0feda21af$var$setExpression(state, payload.geneId, payload.patch);
5466
+ case 'UI_VIEW_STATE_REPLACED':
5467
+ // payload: { byGene } -- used by the snapshot loader in a later phase
5468
+ return {
5469
+ byGene: payload.byGene || {}
5470
+ };
5471
+ default:
5472
+ return state;
5473
+ }
5474
+ },
5475
+ selectUiViewState: (state)=>state.uiViewState,
5476
+ doExpandGeneDetail: ({ geneId: geneId, detail: detail })=>({ dispatch: dispatch })=>{
5477
+ dispatch({
5478
+ type: 'UI_GENE_DETAIL_EXPANDED',
5479
+ payload: {
5480
+ geneId: geneId,
5481
+ detail: detail
5482
+ }
5483
+ });
5484
+ },
5485
+ doSetGeneFullscreen: ({ geneId: geneId, fullscreen: fullscreen })=>({ dispatch: dispatch })=>{
5486
+ dispatch({
5487
+ type: 'UI_GENE_FULLSCREEN_SET',
5488
+ payload: {
5489
+ geneId: geneId,
5490
+ fullscreen: fullscreen
5491
+ }
5492
+ });
5493
+ },
5494
+ doSetHomologyViewer: ({ geneId: geneId, viewer: viewer })=>({ dispatch: dispatch })=>{
5495
+ dispatch({
5496
+ type: 'UI_HOMOLOGY_VIEWER_SET',
5497
+ payload: {
5498
+ geneId: geneId,
5499
+ viewer: viewer
5500
+ }
5501
+ });
5502
+ },
5503
+ doSetHomologyHeight: ({ geneId: geneId, height: height })=>({ dispatch: dispatch })=>{
5504
+ dispatch({
5505
+ type: 'UI_HOMOLOGY_HEIGHT_SET',
5506
+ payload: {
5507
+ geneId: geneId,
5508
+ height: height
5509
+ }
5510
+ });
5511
+ },
5512
+ doSetHomologyTbrowseViewState: ({ geneId: geneId, tbrowse: tbrowse })=>({ dispatch: dispatch })=>{
5513
+ dispatch({
5514
+ type: 'UI_HOMOLOGY_TBROWSE_SET',
5515
+ payload: {
5516
+ geneId: geneId,
5517
+ tbrowse: tbrowse
5518
+ }
5519
+ });
5520
+ },
5521
+ doSetSequencesState: ({ geneId: geneId, patch: patch })=>({ dispatch: dispatch })=>{
5522
+ dispatch({
5523
+ type: 'UI_SEQUENCES_SET',
5524
+ payload: {
5525
+ geneId: geneId,
5526
+ patch: patch
5527
+ }
5528
+ });
5529
+ },
5530
+ doSetExpressionState: ({ geneId: geneId, patch: patch })=>({ dispatch: dispatch })=>{
5531
+ dispatch({
5532
+ type: 'UI_EXPRESSION_SET',
5533
+ payload: {
5534
+ geneId: geneId,
5535
+ patch: patch
5536
+ }
5537
+ });
5538
+ },
5539
+ doReplaceUiViewState: (byGene)=>({ dispatch: dispatch })=>{
5540
+ dispatch({
5541
+ type: 'UI_VIEW_STATE_REPLACED',
5542
+ payload: {
5543
+ byGene: byGene
5544
+ }
5545
+ });
5546
+ }
5547
+ };
5548
+ var $7f865ea0feda21af$export$2e2bcd8739ae039 = $7f865ea0feda21af$var$uiViewState;
5549
+
5550
+
5551
+ // Serializer/deserializer for shareable gene-search views.
5552
+ //
5553
+ // `selectViewSnapshot` produces a versioned JSON-safe blob from the current
5554
+ // store state. `doApplyViewSnapshot` validates a blob and restores it via
5555
+ // existing action creators. Persistence + UI live in later phases — this
5556
+ // bundle is the pure state-shape layer they build on.
5557
+ //
5558
+ // Snapshot schema, v1:
5559
+ // {
5560
+ // v: 1,
5561
+ // capturedAt: <ISO 8601 string>,
5562
+ // site: <subsite id>,
5563
+ // filters: <grameneFilters tree, stripped of ephemeral UI flags>,
5564
+ // views: { on: [id,...], touched: {id: true, ...} },
5565
+ // genomeSubset: { taxonId: true, ... } | null,
5566
+ // searchPage: { offset: number, rows: number } | null,
5567
+ // exprViz: { // Expression-visualization view
5568
+ // activeTaxon: <taxon_id|null>,
5569
+ // byTaxon: { [taxon_id]: { selectedFields: [field,...], vizMode, scale, brushes } }
5570
+ // } | undefined, // loaded rows re-fetch on apply
5571
+ // ontologyEnrichment: { // Ontology Enrichment view
5572
+ // activeTaxon: <taxon_id|null>,
5573
+ // ui: { pAdjCutoff, minGSSize, maxGSSize, mostSpecific, ontology, search, sort }
5574
+ // } | undefined, // facets re-fetch on apply
5575
+ // expandedDetails: [
5576
+ // { geneId,
5577
+ // expandedDetail: string|null,
5578
+ // fullscreen: boolean,
5579
+ // homology: { viewer, height, tbrowse: <ViewState> } | undefined,
5580
+ // sequences: { tab, tid, upstream, downstream } | undefined,
5581
+ // expression: { activeTab, atlasExperiment, barStudy } | undefined
5582
+ // }, ...
5583
+ // ]
5584
+ // }
5585
+ //
5586
+ // Anything not in this schema is intentionally NOT persisted — async fetch
5587
+ // caches, search-results, pagination cursors mid-flight, etc. all rehydrate
5588
+ // from the API once the snapshot is applied.
5589
+ const $472fe3745b238881$var$SCHEMA_VERSION = 1;
5590
+ // Keys on a filter node we want to keep. Everything else (showMenu, marked,
5591
+ // status flags from the root, etc.) is ephemeral UI and not meaningful in a
5592
+ // shared view.
5593
+ const $472fe3745b238881$var$FILTER_NODE_KEEP = [
5594
+ 'leftIdx',
5595
+ 'rightIdx',
5596
+ 'operation',
5597
+ 'negate',
5598
+ 'fq_field',
5599
+ 'fq_value',
5600
+ 'name',
5601
+ 'category',
5602
+ 'warning'
5603
+ ];
5604
+ const $472fe3745b238881$var$FILTER_ROOT_KEEP = [
5605
+ 'operation',
5606
+ 'negate',
5607
+ 'leftIdx',
5608
+ 'rightIdx',
5609
+ 'children',
5610
+ 'searchOffset',
5611
+ 'rows'
5612
+ ];
5613
+ const $472fe3745b238881$var$cleanFilterNode = (node)=>{
5614
+ const out = {};
5615
+ for (const k of $472fe3745b238881$var$FILTER_NODE_KEEP)if (node[k] !== undefined) out[k] = node[k];
5616
+ if (Array.isArray(node.children)) out.children = node.children.map($472fe3745b238881$var$cleanFilterNode);
5617
+ return out;
5618
+ };
5619
+ const $472fe3745b238881$var$cleanFilters = (filters)=>{
5620
+ const out = {};
5621
+ for (const k of $472fe3745b238881$var$FILTER_ROOT_KEEP)if (filters[k] !== undefined) out[k] = filters[k];
5622
+ if (Array.isArray(filters.children)) out.children = filters.children.map($472fe3745b238881$var$cleanFilterNode);
5623
+ return out;
5624
+ };
5625
+ // Drop bundle entries that hold no meaningful state (e.g. a row the user
5626
+ // scrolled past but never opened). Otherwise snapshots bloat with empties.
5627
+ const $472fe3745b238881$var$isMeaningfulGene = (entry)=>{
5628
+ if (!entry) return false;
5629
+ if (entry.expandedDetail) return true;
5630
+ if (entry.fullscreen) return true;
5631
+ if (entry.homology && (entry.homology.viewer !== undefined || entry.homology.height !== undefined || entry.homology.tbrowse !== undefined)) return true;
5632
+ if (entry.sequences && Object.keys(entry.sequences).length) return true;
5633
+ if (entry.expression && Object.keys(entry.expression).length) return true;
5634
+ return false;
5635
+ };
5636
+ const $472fe3745b238881$var$viewSnapshot = {
5637
+ name: 'viewSnapshot',
5638
+ // No reducer — this bundle is pure derivation + dispatch orchestration.
5639
+ getReducer: ()=>(state = {})=>state,
5640
+ selectViewSnapshot: $472fe3745b238881$var$createSnapshotSelector(),
5641
+ // Apply a snapshot to the store. Returns an array of unresolved-id warnings
5642
+ // so the caller can surface a non-blocking notice. The action creators we
5643
+ // delegate to are existing ones; nothing here knows about API/UI/share-link
5644
+ // — that's later phases.
5645
+ doApplyViewSnapshot: (snapshot)=>({ dispatch: dispatch, store: store })=>{
5646
+ const warnings = $472fe3745b238881$var$validateSnapshot(snapshot);
5647
+ if (warnings.fatal) {
5648
+ console.warn('viewSnapshot: refusing to apply', warnings.fatal);
5649
+ return {
5650
+ applied: false,
5651
+ warnings: warnings
5652
+ };
5653
+ }
5654
+ // 1. Filters (also clears search; replays a fresh fetch).
5655
+ if (snapshot.filters) dispatch({
5656
+ type: 'BATCH_ACTIONS',
5657
+ actions: [
5658
+ {
5659
+ type: 'GRAMENE_SEARCH_CLEARED'
5660
+ },
5661
+ {
5662
+ type: 'GRAMENE_FILTERS_REPLACED',
5663
+ payload: $472fe3745b238881$var$cloneForDispatch(snapshot.filters)
5664
+ }
5665
+ ]
5666
+ });
5667
+ // 2. Views.
5668
+ if (snapshot.views) dispatch({
5669
+ type: 'GRAMENE_VIEWS_REPLACED',
5670
+ payload: {
5671
+ on: snapshot.views.on || [],
5672
+ touched: snapshot.views.touched || {}
5673
+ }
5674
+ });
5675
+ // 3. Genome subset, if present.
5676
+ if (snapshot.genomeSubset && Object.keys(snapshot.genomeSubset).length) dispatch({
5677
+ type: 'GRAMENE_GENOMES_UPDATED',
5678
+ payload: {
5679
+ ...snapshot.genomeSubset
5680
+ }
5681
+ });
5682
+ // 4. Per-gene UI state (expandedDetail, fullscreen, homology viewer/
5683
+ // height, tbrowse view state). Replace wholesale via the uiViewState
5684
+ // bundle's bulk action.
5685
+ const byGene = {};
5686
+ if (Array.isArray(snapshot.expandedDetails)) for (const e of snapshot.expandedDetails){
5687
+ if (!e || !e.geneId) continue;
5688
+ byGene[e.geneId] = {
5689
+ expandedDetail: e.expandedDetail || null,
5690
+ fullscreen: !!e.fullscreen,
5691
+ homology: e.homology ? {
5692
+ ...e.homology
5693
+ } : {},
5694
+ sequences: e.sequences ? {
5695
+ ...e.sequences
5696
+ } : {},
5697
+ expression: e.expression ? {
5698
+ ...e.expression
5699
+ } : {}
5700
+ };
5701
+ }
5702
+ dispatch({
5703
+ type: 'UI_VIEW_STATE_REPLACED',
5704
+ payload: {
5705
+ byGene: byGene
5706
+ }
5707
+ });
5708
+ // 5. Expression-visualization view config. Delegate to the exprViz bundle,
5709
+ // which flags each taxon for a re-fetch now that the restored filters/
5710
+ // genomes are in place. Done last so the re-fetch sees the live query.
5711
+ if (snapshot.exprViz && store.doApplyExprVizSnapshot) store.doApplyExprVizSnapshot(snapshot.exprViz);
5712
+ // 6. Ontology Enrichment view config. Setting the active taxon lets that
5713
+ // bundle's fetch reactor re-derive the enrichment for the restored
5714
+ // filters/genomes.
5715
+ if (snapshot.ontologyEnrichment && store.doApplyOntologyEnrichmentSnapshot) store.doApplyOntologyEnrichmentSnapshot(snapshot.ontologyEnrichment);
5716
+ return {
5717
+ applied: true,
5718
+ warnings: warnings
5719
+ };
5720
+ },
5721
+ // Helper exposed for the bootstrap path that wants to read warnings without
5722
+ // dispatching. Returns { fatal: <string|null>, unresolved: { ids/taxa/views/genes lists } }.
5723
+ doValidateViewSnapshot: (snapshot)=>()=>$472fe3745b238881$var$validateSnapshot(snapshot)
5724
+ };
5725
+ function $472fe3745b238881$var$createSnapshotSelector() {
5726
+ // We deliberately don't memoize with createSelector — snapshots are taken
5727
+ // on demand (Save button click, link generation) rather than on every
5728
+ // store tick, so a fresh build is fine and avoids stale-dep bugs.
5729
+ return (state)=>$472fe3745b238881$var$buildSnapshot(state);
5730
+ }
5731
+ function $472fe3745b238881$var$buildSnapshot(state) {
5732
+ const snap = {
5733
+ v: $472fe3745b238881$var$SCHEMA_VERSION,
5734
+ capturedAt: new Date().toISOString(),
5735
+ site: state.config && state.config.id || null
5736
+ };
5737
+ if (state.grameneFilters) {
5738
+ snap.filters = $472fe3745b238881$var$cleanFilters(state.grameneFilters);
5739
+ snap.searchPage = {
5740
+ offset: state.grameneFilters.searchOffset || 0,
5741
+ rows: state.grameneFilters.rows || 20
5742
+ };
5743
+ }
5744
+ if (state.grameneViews && Array.isArray(state.grameneViews.options)) snap.views = {
5745
+ on: state.grameneViews.options.filter((v)=>v && v.show === 'on').map((v)=>v.id),
5746
+ touched: {
5747
+ ...state.grameneViews.touched || {}
5748
+ }
5749
+ };
5750
+ if (state.grameneGenomes && state.grameneGenomes.active) {
5751
+ const keys = Object.keys(state.grameneGenomes.active);
5752
+ if (keys.length) {
5753
+ snap.genomeSubset = {};
5754
+ for (const k of keys)snap.genomeSubset[k] = true;
5755
+ } else snap.genomeSubset = null;
5756
+ }
5757
+ if (state.uiViewState && state.uiViewState.byGene) snap.expandedDetails = Object.entries(state.uiViewState.byGene).filter(([, e])=>$472fe3745b238881$var$isMeaningfulGene(e)).map(([geneId, e])=>({
5758
+ geneId: geneId,
5759
+ expandedDetail: e.expandedDetail || null,
5760
+ fullscreen: !!e.fullscreen,
5761
+ homology: e.homology && Object.keys(e.homology).length ? {
5762
+ ...e.homology
5763
+ } : undefined,
5764
+ sequences: e.sequences && Object.keys(e.sequences).length ? {
5765
+ ...e.sequences
5766
+ } : undefined,
5767
+ expression: e.expression && Object.keys(e.expression).length ? {
5768
+ ...e.expression
5769
+ } : undefined
5770
+ }));
5771
+ else snap.expandedDetails = [];
5772
+ // Expression-visualization view config. Capture only the selection/config
5773
+ // (active genome tab, per-taxon selected fields + column order, viz mode,
5774
+ // scale, brush ranges) — never the bulk fetched rows, which rehydrate via
5775
+ // the view's re-fetch on apply.
5776
+ if (state.exprViz) {
5777
+ const ev = state.exprViz;
5778
+ const byTaxon = {};
5779
+ for (const tid of Object.keys(ev.byTaxon || {})){
5780
+ const t = ev.byTaxon[tid];
5781
+ if (!t) continue;
5782
+ const selectedFields = Array.isArray(t.selectedFields) ? t.selectedFields : [];
5783
+ const brushes = t.brushes && Object.keys(t.brushes).length ? {
5784
+ ...t.brushes
5785
+ } : undefined;
5786
+ const nonDefaultMode = t.vizMode && t.vizMode !== 'heatmap';
5787
+ const nonDefaultScale = t.scale && t.scale !== 'linear';
5788
+ if (selectedFields.length || brushes || nonDefaultMode || nonDefaultScale) byTaxon[tid] = {
5789
+ selectedFields: selectedFields,
5790
+ vizMode: t.vizMode || 'heatmap',
5791
+ scale: t.scale || 'linear',
5792
+ ...brushes ? {
5793
+ brushes: brushes
5794
+ } : {}
5795
+ };
5796
+ }
5797
+ if (ev.activeTaxon || Object.keys(byTaxon).length) snap.exprViz = {
5798
+ activeTaxon: ev.activeTaxon || null,
5799
+ byTaxon: byTaxon
5800
+ };
5801
+ }
5802
+ // Ontology Enrichment view config: active genome + the analysis knobs
5803
+ // (significance cutoff, gene-set-size bounds, most-specific, ontology
5804
+ // section, term search, per-section sort). The enrichment facets re-fetch
5805
+ // on apply, so no bulk term data is carried.
5806
+ if (state.ontologyEnrichment) {
5807
+ const oe = state.ontologyEnrichment;
5808
+ const ui = oe.ui || {};
5809
+ const DEF = {
5810
+ pAdjCutoff: 0.05,
5811
+ minGSSize: 10,
5812
+ maxGSSize: 500,
5813
+ mostSpecific: false,
5814
+ ontology: 'all',
5815
+ search: ''
5816
+ };
5817
+ const hasSort = ui.sort && Object.keys(ui.sort).length;
5818
+ const uiChanged = Object.keys(DEF).some((k)=>ui[k] !== DEF[k]) || hasSort;
5819
+ if (oe.activeTaxon || uiChanged) snap.ontologyEnrichment = {
5820
+ activeTaxon: oe.activeTaxon || null,
5821
+ ui: {
5822
+ pAdjCutoff: ui.pAdjCutoff != null ? ui.pAdjCutoff : DEF.pAdjCutoff,
5823
+ minGSSize: ui.minGSSize != null ? ui.minGSSize : DEF.minGSSize,
5824
+ maxGSSize: ui.maxGSSize != null ? ui.maxGSSize : DEF.maxGSSize,
5825
+ mostSpecific: !!ui.mostSpecific,
5826
+ ontology: ui.ontology || DEF.ontology,
5827
+ search: ui.search || DEF.search,
5828
+ ...hasSort ? {
5829
+ sort: {
5830
+ ...ui.sort
5831
+ }
5832
+ } : {}
5833
+ }
5834
+ };
5835
+ }
5836
+ return snap;
5837
+ }
5838
+ function $472fe3745b238881$var$validateSnapshot(snapshot) {
5839
+ const warnings = {
5840
+ fatal: null,
5841
+ unresolved: {
5842
+ views: [],
5843
+ genes: [],
5844
+ taxa: []
5845
+ }
5846
+ };
5847
+ if (!snapshot || typeof snapshot !== 'object') {
5848
+ warnings.fatal = 'snapshot is not an object';
5849
+ return warnings;
5850
+ }
5851
+ if (snapshot.v !== $472fe3745b238881$var$SCHEMA_VERSION) {
5852
+ warnings.fatal = `unsupported snapshot version ${snapshot.v} (expected ${$472fe3745b238881$var$SCHEMA_VERSION})`;
5853
+ return warnings;
5854
+ }
5855
+ // Strict-id checks happen later — they need the store's grameneMaps,
5856
+ // grameneTaxonomy, etc. which are async-loaded. The boot path will call
5857
+ // doValidateViewSnapshot again after those land. For now we just
5858
+ // structurally validate.
5859
+ return warnings;
5860
+ }
5861
+ // JSON.parse(JSON.stringify(x)) — bundles see a wholly fresh object so
5862
+ // reducers can't accidentally mutate the snapshot we're holding for retry.
5863
+ function $472fe3745b238881$var$cloneForDispatch(x) {
5864
+ return JSON.parse(JSON.stringify(x));
5865
+ }
5866
+ var $472fe3745b238881$export$2e2bcd8739ae039 = $472fe3745b238881$var$viewSnapshot;
5867
+
5868
+
5869
+ // Boot-time hydration of a shared view link.
5870
+ //
5871
+ // Entry points call `bootViewFromUrl(store)` once the store is created.
5872
+ // Public views resolve anonymously. For private views, the initial call
5873
+ // will 401; the Auth panel then re-invokes with `{user}` once Firebase has
5874
+ // emitted its first signed-in state, and the fetch retries with a Bearer
5875
+ // token. On success we apply the snapshot and strip `?view=` from the URL
5876
+ // so subsequent user actions don't sit under a stale shared-state URL.
5877
+ //
5878
+ // All errors are non-fatal — they surface in state.savedViews.fetchError
5879
+ // for any UI that wants to show them.
5880
+ const $c69c61828e36a328$var$PARAM = 'view';
5881
+ function $c69c61828e36a328$export$2e2bcd8739ae039(store, opts = {}) {
5882
+ if (typeof window === 'undefined') return Promise.resolve(null);
5883
+ const url = new URL(window.location.href);
5884
+ const hash = url.searchParams.get($c69c61828e36a328$var$PARAM);
5885
+ if (!hash) return Promise.resolve(null);
5886
+ const { user: user = null } = opts;
5887
+ return store.doFetchView({
5888
+ hash: hash,
5889
+ user: user
5890
+ }).then(({ snapshot: snapshot })=>{
5891
+ store.doApplyViewSnapshot(snapshot);
5892
+ url.searchParams.delete($c69c61828e36a328$var$PARAM);
5893
+ window.history.replaceState({}, '', url.toString());
5894
+ return {
5895
+ hash: hash,
5896
+ applied: true
5897
+ };
5898
+ }).catch((err)=>{
5899
+ // 401 here on the anonymous pass is expected for private views — the
5900
+ // Auth panel will retry once it has a user. Other errors (404, 5xx,
5901
+ // network) are reported and leave the param so a manual refresh can
5902
+ // retry too.
5903
+ console.warn('bootViewFromUrl:', err.message || err);
5904
+ return {
5905
+ hash: hash,
5906
+ applied: false,
5907
+ error: err.message || String(err)
5908
+ };
5909
+ });
5910
+ }
5911
+
5912
+
5913
+ // Client for the saved-views API ({api}/saved_views — see gramene-swagger
5914
+ // phase 3b). Mirrors the gene_lists pattern: Firebase Bearer ID token,
5915
+ // hash-based addressing, public vs private scope.
5916
+ //
5917
+ // Action creators all return Promises so calling UI (modal, boot path,
5918
+ // list view) can await them and surface success/error inline.
5919
+ //
5920
+ // Dev-mode mock: set `window.__SAVED_VIEWS_MOCK__ = true` in the console
5921
+ // and saves/fetches go through localStorage instead of the network. Lets us
5922
+ // drive Phase 4 (UI) without waiting on Phase 3b (server). Production
5923
+ // builds never touch the mock unless the flag is set at runtime, so this
5924
+ // is safe to ship.
5925
+ const $85a9f6732ceb79db$var$STORAGE_PREFIX = 'gramene_saved_view_mock_v1::';
5926
+ const $85a9f6732ceb79db$var$initialState = {
5927
+ saving: false,
5928
+ saveError: null,
5929
+ lastSavedHash: null,
5930
+ fetching: false,
5931
+ fetchError: null,
5932
+ lastFetched: null,
5933
+ privateList: null,
5934
+ publicList: null,
5935
+ listError: null
5936
+ };
5937
+ const $85a9f6732ceb79db$var$savedViews = {
5938
+ name: 'savedViews',
5939
+ getReducer: ()=>(state = $85a9f6732ceb79db$var$initialState, { type: type, payload: payload })=>{
5940
+ switch(type){
5941
+ case 'SAVED_VIEW_SAVE_STARTED':
5942
+ return {
5943
+ ...state,
5944
+ saving: true,
5945
+ saveError: null
5946
+ };
5947
+ case 'SAVED_VIEW_SAVE_SUCCEEDED':
5948
+ return {
5949
+ ...state,
5950
+ saving: false,
5951
+ lastSavedHash: payload.hash
5952
+ };
5953
+ case 'SAVED_VIEW_SAVE_FAILED':
5954
+ return {
5955
+ ...state,
5956
+ saving: false,
5957
+ saveError: payload.error
5958
+ };
5959
+ case 'SAVED_VIEW_FETCH_STARTED':
5960
+ return {
5961
+ ...state,
5962
+ fetching: true,
5963
+ fetchError: null
5964
+ };
5965
+ case 'SAVED_VIEW_FETCH_SUCCEEDED':
5966
+ return {
5967
+ ...state,
5968
+ fetching: false,
5969
+ lastFetched: payload
5970
+ };
5971
+ case 'SAVED_VIEW_FETCH_FAILED':
5972
+ return {
5973
+ ...state,
5974
+ fetching: false,
5975
+ fetchError: payload.error
5976
+ };
5977
+ case 'SAVED_VIEW_LIST_RECEIVED':
5978
+ return {
5979
+ ...state,
5980
+ [payload.kind === 'public' ? 'publicList' : 'privateList']: payload.rows,
5981
+ listError: null
5982
+ };
5983
+ case 'SAVED_VIEW_LIST_FAILED':
5984
+ return {
5985
+ ...state,
5986
+ listError: payload.error
5987
+ };
5988
+ case 'SAVED_VIEW_RESET':
5989
+ return {
5990
+ ...$85a9f6732ceb79db$var$initialState
5991
+ };
5992
+ default:
5993
+ return state;
5994
+ }
5995
+ },
5996
+ selectSavedViews: (state)=>state.savedViews,
5997
+ // POST a snapshot. Returns Promise<{hash, shareUrl}> on success.
5998
+ // `user` is a Firebase user object (must respond to .getIdToken()).
5999
+ doSaveView: ({ user: user, label: label, description: description, isPublic: isPublic })=>async ({ dispatch: dispatch, store: store })=>{
6000
+ dispatch({
6001
+ type: 'SAVED_VIEW_SAVE_STARTED'
6002
+ });
6003
+ try {
6004
+ const snapshot = store.selectViewSnapshot();
6005
+ const hash = await $85a9f6732ceb79db$var$computeContentHash(snapshot);
6006
+ const site = store.selectConfiguration && store.selectConfiguration().id || '';
6007
+ const token = user && user.getIdToken ? await user.getIdToken() : null;
6008
+ if (!token) throw new Error('Not signed in');
6009
+ const meta = {
6010
+ hash: hash,
6011
+ label: label,
6012
+ site: site,
6013
+ isPublic: !!isPublic,
6014
+ description: description || ''
6015
+ };
6016
+ if ($85a9f6732ceb79db$var$useMock()) $85a9f6732ceb79db$var$mockSave({
6017
+ ...meta,
6018
+ state: snapshot,
6019
+ uid: user.uid || 'mock',
6020
+ createdAt: new Date().toISOString()
6021
+ });
6022
+ else {
6023
+ const api = store.selectGrameneAPI();
6024
+ const res = await fetch(`${api}/saved_views`, {
6025
+ method: 'POST',
6026
+ headers: {
6027
+ 'Content-Type': 'application/json',
6028
+ Authorization: `Bearer ${token}`
6029
+ },
6030
+ body: JSON.stringify({
6031
+ ...meta,
6032
+ state: snapshot
6033
+ })
6034
+ });
6035
+ if (!res.ok) throw new Error(`Save failed (${res.status})`);
6036
+ }
6037
+ const shareUrl = $85a9f6732ceb79db$var$buildShareUrl(hash);
6038
+ dispatch({
6039
+ type: 'SAVED_VIEW_SAVE_SUCCEEDED',
6040
+ payload: {
6041
+ hash: hash
6042
+ }
6043
+ });
6044
+ return {
6045
+ hash: hash,
6046
+ shareUrl: shareUrl
6047
+ };
6048
+ } catch (err) {
6049
+ dispatch({
6050
+ type: 'SAVED_VIEW_SAVE_FAILED',
6051
+ payload: {
6052
+ error: err.message || String(err)
6053
+ }
6054
+ });
6055
+ throw err;
6056
+ }
6057
+ },
6058
+ // GET a snapshot by share hash. Anonymous-OK for public views; token is
6059
+ // optional and only sent if the caller provides a user.
6060
+ // Returns Promise<{snapshot, meta}>.
6061
+ doFetchView: ({ hash: hash, user: user })=>async ({ dispatch: dispatch, store: store })=>{
6062
+ dispatch({
6063
+ type: 'SAVED_VIEW_FETCH_STARTED'
6064
+ });
6065
+ try {
6066
+ let row;
6067
+ if ($85a9f6732ceb79db$var$useMock()) {
6068
+ row = $85a9f6732ceb79db$var$mockFetch(hash);
6069
+ if (!row) throw new Error(`No saved view with hash ${hash}`);
6070
+ } else {
6071
+ const api = store.selectGrameneAPI();
6072
+ const headers = {
6073
+ Accept: 'application/json'
6074
+ };
6075
+ if (user && user.getIdToken) try {
6076
+ headers.Authorization = `Bearer ${await user.getIdToken()}`;
6077
+ } catch (_) {}
6078
+ const res = await fetch(`${api}/saved_views?hash=${encodeURIComponent(hash)}`, {
6079
+ headers: headers
6080
+ });
6081
+ if (res.status === 401) throw new Error('Sign in to load this private view.');
6082
+ if (res.status === 404) throw new Error('Saved view not found (it may have been deleted).');
6083
+ if (!res.ok) throw new Error(`Fetch failed (${res.status})`);
6084
+ row = await res.json();
6085
+ }
6086
+ const out = {
6087
+ hash: hash,
6088
+ snapshot: row.state,
6089
+ meta: {
6090
+ label: row.label,
6091
+ description: row.description || '',
6092
+ site: row.site,
6093
+ isPublic: !!row.isPublic,
6094
+ owner: row.owner || null,
6095
+ createdAt: row.createdAt || null
6096
+ }
6097
+ };
6098
+ dispatch({
6099
+ type: 'SAVED_VIEW_FETCH_SUCCEEDED',
6100
+ payload: out
6101
+ });
6102
+ return out;
6103
+ } catch (err) {
6104
+ dispatch({
6105
+ type: 'SAVED_VIEW_FETCH_FAILED',
6106
+ payload: {
6107
+ error: err.message || String(err)
6108
+ }
6109
+ });
6110
+ throw err;
6111
+ }
6112
+ },
6113
+ // List views for the current site. `scope` is 'public' (anonymous-OK)
6114
+ // or 'private' (requires `user`).
6115
+ doListSavedViews: ({ scope: scope, user: user })=>async ({ dispatch: dispatch, store: store })=>{
6116
+ try {
6117
+ const site = store.selectConfiguration && store.selectConfiguration().id || '';
6118
+ let rows;
6119
+ if ($85a9f6732ceb79db$var$useMock()) rows = $85a9f6732ceb79db$var$mockList({
6120
+ site: site,
6121
+ scope: scope,
6122
+ uid: user && user.uid
6123
+ });
6124
+ else {
6125
+ const api = store.selectGrameneAPI();
6126
+ const headers = {
6127
+ Accept: 'application/json'
6128
+ };
6129
+ if (scope === 'private') {
6130
+ if (!user || !user.getIdToken) throw new Error('Not signed in');
6131
+ headers.Authorization = `Bearer ${await user.getIdToken()}`;
6132
+ }
6133
+ const url = `${api}/saved_views?site=${encodeURIComponent(site)}&isPublic=${scope === 'public'}`;
6134
+ const res = await fetch(url, {
6135
+ headers: headers
6136
+ });
6137
+ if (!res.ok) throw new Error(`List failed (${res.status})`);
6138
+ rows = await res.json();
6139
+ }
6140
+ dispatch({
6141
+ type: 'SAVED_VIEW_LIST_RECEIVED',
6142
+ payload: {
6143
+ kind: scope,
6144
+ rows: rows
6145
+ }
6146
+ });
6147
+ return rows;
6148
+ } catch (err) {
6149
+ dispatch({
6150
+ type: 'SAVED_VIEW_LIST_FAILED',
6151
+ payload: {
6152
+ error: err.message || String(err)
6153
+ }
6154
+ });
6155
+ throw err;
6156
+ }
6157
+ },
6158
+ // PATCH label / isPublic on a view I own. Mirrors updateList.
6159
+ doUpdateSavedView: ({ viewId: viewId, user: user, label: label, isPublic: isPublic })=>async ({ store: store })=>{
6160
+ const updates = {};
6161
+ if (typeof label === 'string') updates.label = label;
6162
+ if (typeof isPublic === 'boolean') updates.isPublic = isPublic;
6163
+ if (!Object.keys(updates).length) return;
6164
+ if ($85a9f6732ceb79db$var$useMock()) {
6165
+ $85a9f6732ceb79db$var$mockUpdate(viewId, updates);
6166
+ return {
6167
+ viewId: viewId,
6168
+ updates: updates
6169
+ };
6170
+ }
6171
+ const token = await user.getIdToken();
6172
+ const api = store.selectGrameneAPI();
6173
+ const res = await fetch(`${api}/saved_views?viewId=${encodeURIComponent(viewId)}`, {
6174
+ method: 'PATCH',
6175
+ headers: {
6176
+ 'Content-Type': 'application/json',
6177
+ Authorization: `Bearer ${token}`
6178
+ },
6179
+ body: JSON.stringify(updates)
6180
+ });
6181
+ if (!res.ok) throw new Error(`Update failed (${res.status})`);
6182
+ return res.json();
6183
+ },
6184
+ // DELETE one of my saved views. Mirrors deleteList.
6185
+ doDeleteSavedView: ({ viewId: viewId, user: user })=>async ({ store: store })=>{
6186
+ if ($85a9f6732ceb79db$var$useMock()) {
6187
+ $85a9f6732ceb79db$var$mockDelete(viewId);
6188
+ return {
6189
+ viewId: viewId
6190
+ };
6191
+ }
6192
+ const token = await user.getIdToken();
6193
+ const api = store.selectGrameneAPI();
6194
+ const res = await fetch(`${api}/saved_views?viewId=${encodeURIComponent(viewId)}`, {
6195
+ method: 'DELETE',
6196
+ headers: {
6197
+ Authorization: `Bearer ${token}`
6198
+ }
6199
+ });
6200
+ if (!res.ok) throw new Error(`Delete failed (${res.status})`);
6201
+ return res.json();
6202
+ },
6203
+ doResetSavedViewState: ()=>({ dispatch: dispatch })=>dispatch({
6204
+ type: 'SAVED_VIEW_RESET'
6205
+ }),
6206
+ // Connect-friendly wrapper around bootViewFromUrl. Auth.js calls this on
6207
+ // each auth-state emission, passing the current Firebase user (or null);
6208
+ // bootView.js no-ops when ?view= isn't in the URL, so this is cheap to
6209
+ // call repeatedly.
6210
+ doBootSharedView: ({ user: user } = {})=>({ store: store })=>{
6211
+ return (0, $c69c61828e36a328$export$2e2bcd8739ae039)(store, {
6212
+ user: user
6213
+ });
6214
+ }
6215
+ };
6216
+ // ── helpers ─────────────────────────────────────────────────────────────
6217
+ // Test/dev: either set the runtime-only flag (`window.__SAVED_VIEWS_MOCK__ = true`,
6218
+ // cleared on reload), or persist via localStorage (`localStorage.setItem(
6219
+ // '__SAVED_VIEWS_MOCK__', 'true')`, survives reloads — needed for the
6220
+ // share-link round-trip which navigates).
6221
+ function $85a9f6732ceb79db$var$useMock() {
6222
+ if (typeof window === 'undefined') return false;
6223
+ if (window.__SAVED_VIEWS_MOCK__) return true;
6224
+ try {
6225
+ return typeof localStorage !== 'undefined' && localStorage.getItem('__SAVED_VIEWS_MOCK__') === 'true';
6226
+ } catch (_) {
6227
+ return false;
6228
+ }
6229
+ }
6230
+ function $85a9f6732ceb79db$var$buildShareUrl(hash) {
6231
+ if (typeof window === 'undefined') return `?view=${hash}`;
6232
+ const u = new URL(window.location.href);
6233
+ u.search = '';
6234
+ u.searchParams.set('view', hash);
6235
+ return u.toString();
6236
+ }
6237
+ // Content-addressable, ~72-bit short hash. The snapshot's `capturedAt` is
6238
+ // excluded from the hash input so two saves of the same view by the same
6239
+ // user produce the same hash (and the server's $setOnInsert preserves the
6240
+ // original createdAt).
6241
+ async function $85a9f6732ceb79db$var$computeContentHash(snapshot) {
6242
+ const forHash = {
6243
+ ...snapshot,
6244
+ capturedAt: undefined
6245
+ };
6246
+ const text = $85a9f6732ceb79db$var$canonicalize(forHash);
6247
+ if (typeof crypto !== 'undefined' && crypto.subtle) {
6248
+ const buf = new TextEncoder().encode(text);
6249
+ const digest = await crypto.subtle.digest('SHA-256', buf);
6250
+ const bytes = new Uint8Array(digest);
6251
+ let b64 = '';
6252
+ for(let i = 0; i < bytes.length; i++)b64 += String.fromCharCode(bytes[i]);
6253
+ return btoa(b64).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '').slice(0, 12);
6254
+ }
6255
+ // Last-resort fallback for environments without WebCrypto.
6256
+ let h = 0;
6257
+ for(let i = 0; i < text.length; i++)h = (h << 5) - h + text.charCodeAt(i) | 0;
6258
+ return ('h' + (h >>> 0).toString(36)).slice(0, 12);
6259
+ }
6260
+ // Stable stringify: sort object keys recursively so semantically-equivalent
6261
+ // snapshots hash identically.
6262
+ function $85a9f6732ceb79db$var$canonicalize(v) {
6263
+ if (v === null || typeof v !== 'object') return JSON.stringify(v);
6264
+ if (Array.isArray(v)) return '[' + v.map($85a9f6732ceb79db$var$canonicalize).join(',') + ']';
6265
+ const keys = Object.keys(v).filter((k)=>v[k] !== undefined).sort();
6266
+ return '{' + keys.map((k)=>JSON.stringify(k) + ':' + $85a9f6732ceb79db$var$canonicalize(v[k])).join(',') + '}';
6267
+ }
6268
+ // ── mock backend (localStorage) ─────────────────────────────────────────
6269
+ function $85a9f6732ceb79db$var$mockSave(row) {
6270
+ if (typeof localStorage === 'undefined') return;
6271
+ localStorage.setItem($85a9f6732ceb79db$var$STORAGE_PREFIX + row.hash, JSON.stringify({
6272
+ ...row,
6273
+ _id: row.hash + ' ' + row.uid
6274
+ }));
6275
+ }
6276
+ function $85a9f6732ceb79db$var$mockFetch(hash) {
6277
+ if (typeof localStorage === 'undefined') return null;
6278
+ const raw = localStorage.getItem($85a9f6732ceb79db$var$STORAGE_PREFIX + hash);
6279
+ return raw ? JSON.parse(raw) : null;
6280
+ }
6281
+ function $85a9f6732ceb79db$var$mockList({ site: site, scope: scope, uid: uid }) {
6282
+ if (typeof localStorage === 'undefined') return [];
6283
+ const out = [];
6284
+ for(let i = 0; i < localStorage.length; i++){
6285
+ const k = localStorage.key(i);
6286
+ if (!k || !k.startsWith($85a9f6732ceb79db$var$STORAGE_PREFIX)) continue;
6287
+ const row = JSON.parse(localStorage.getItem(k));
6288
+ if (row.site !== site) continue;
6289
+ if (scope === 'public' && !row.isPublic) continue;
6290
+ if (scope === 'private' && (row.isPublic || row.uid !== uid)) continue;
6291
+ out.push(row);
6292
+ }
6293
+ return out.sort((a, b)=>(b.createdAt || '').localeCompare(a.createdAt || ''));
6294
+ }
6295
+ function $85a9f6732ceb79db$var$mockUpdate(viewId, updates) {
6296
+ if (typeof localStorage === 'undefined') return;
6297
+ for(let i = 0; i < localStorage.length; i++){
6298
+ const k = localStorage.key(i);
6299
+ if (!k || !k.startsWith($85a9f6732ceb79db$var$STORAGE_PREFIX)) continue;
6300
+ const row = JSON.parse(localStorage.getItem(k));
6301
+ if (row._id === viewId) {
6302
+ localStorage.setItem(k, JSON.stringify({
6303
+ ...row,
6304
+ ...updates
6305
+ }));
6306
+ return;
6307
+ }
6308
+ }
6309
+ }
6310
+ function $85a9f6732ceb79db$var$mockDelete(viewId) {
6311
+ if (typeof localStorage === 'undefined') return;
6312
+ for(let i = 0; i < localStorage.length; i++){
6313
+ const k = localStorage.key(i);
6314
+ if (!k || !k.startsWith($85a9f6732ceb79db$var$STORAGE_PREFIX)) continue;
6315
+ const row = JSON.parse(localStorage.getItem(k));
6316
+ if (row._id === viewId) {
6317
+ localStorage.removeItem(k);
6318
+ return;
6319
+ }
6320
+ }
6321
+ }
6322
+ var $85a9f6732ceb79db$export$2e2bcd8739ae039 = $85a9f6732ceb79db$var$savedViews;
6323
+
6324
+
5133
6325
  var $5df6c55c1bef3469$export$2e2bcd8739ae039 = [
5134
6326
  ...(0, $9d9aeaf9299e61a1$export$2e2bcd8739ae039),
5135
6327
  (0, $671312b287158a8a$export$2e2bcd8739ae039),
@@ -5140,7 +6332,10 @@ var $5df6c55c1bef3469$export$2e2bcd8739ae039 = [
5140
6332
  (0, $1508f5a42be6e7b5$export$2e2bcd8739ae039),
5141
6333
  (0, $c921a0d2b34aadb6$export$2e2bcd8739ae039),
5142
6334
  (0, $4f15cd8a7d970b18$export$2e2bcd8739ae039),
5143
- (0, $d365d8c287ab0c94$export$2e2bcd8739ae039)
6335
+ (0, $d365d8c287ab0c94$export$2e2bcd8739ae039),
6336
+ (0, $7f865ea0feda21af$export$2e2bcd8739ae039),
6337
+ (0, $472fe3745b238881$export$2e2bcd8739ae039),
6338
+ (0, $85a9f6732ceb79db$export$2e2bcd8739ae039)
5144
6339
  ];
5145
6340
 
5146
6341
 
@@ -5536,32 +6731,251 @@ const $541b8b0d8c5501d2$var$Suggestions = (props)=>{
5536
6731
  ]
5537
6732
  }, idx);
5538
6733
  }),
5539
- /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)($541b8b0d8c5501d2$var$TryAnotherSite, {
5540
- query: props.suggestionsQuery,
5541
- config: props.configuration
6734
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)($541b8b0d8c5501d2$var$TryAnotherSite, {
6735
+ query: props.suggestionsQuery,
6736
+ config: props.configuration
6737
+ })
6738
+ ]
6739
+ });
6740
+ } else return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {});
6741
+ };
6742
+ var $541b8b0d8c5501d2$export$2e2bcd8739ae039 = (0, $gXNCa$reduxbundlerreact.connect)('selectGrameneSuggestions', 'selectSuggestionsQuery', 'selectGrameneTaxonomy', 'selectConfiguration', 'doAcceptSuggestion', 'doAcceptGrameneSuggestion', $541b8b0d8c5501d2$var$Suggestions);
6743
+
6744
+
6745
+
6746
+
6747
+
6748
+
6749
+
6750
+
6751
+
6752
+
6753
+
6754
+
6755
+
6756
+
6757
+
6758
+
6759
+
6760
+
6761
+ // eFP Browser embed — was its own package (gramene-efp-browser@1.0.11),
6762
+ // absorbed here on 2026-05-29 to drop the unmaintained nwb/webpack-4 build
6763
+ // pipeline and remove a dependency that needed --openssl-legacy-provider.
6764
+ // All upstream URLs and species-specific id formatters preserved as-is.
6765
+
6766
+
6767
+ const $59a241f3912e631e$var$urls = {
6768
+ image: (genome, study, gene)=>`https://bar.utoronto.ca/api/efp_image/efp_${genome}/${study}/Absolute/${gene}`,
6769
+ app: (genome, study, gene)=>`https://bar.utoronto.ca/efp_${genome}/cgi-bin/efpWeb.cgi?dataSource=${study}&mode=Absolute&primaryGene=${gene}`,
6770
+ studies: (genome)=>`https://bar.utoronto.ca/api/efp_image/get_efp_data_source/${genome}`,
6771
+ logo: 'https://bar.utoronto.ca/bbc_logo_small.gif',
6772
+ spinner: 'https://www.sorghumbase.org/static/images/dna_spinner.svg'
6773
+ };
6774
+ const $59a241f3912e631e$var$zmv4_re = /Zm00001d/;
6775
+ const $59a241f3912e631e$var$browsers = {
6776
+ sorghum_bicolor: {
6777
+ formatGene: (gene)=>gene._id.replace('SORBI_3', 'Sobic.'),
6778
+ genome: 'sorghum'
6779
+ },
6780
+ vitis_vinifera: {
6781
+ formatGene: (gene)=>gene._id,
6782
+ genome: 'grape'
6783
+ },
6784
+ arabidopsis_thaliana: {
6785
+ formatGene: (gene)=>gene._id,
6786
+ fixStudies: (studies)=>{
6787
+ studies = studies.filter((s)=>s.value !== 'Klepikova_Atlas');
6788
+ studies.unshift({
6789
+ value: 'Klepikova_Atlas',
6790
+ label: 'Klepikova Atlas'
6791
+ });
6792
+ return studies;
6793
+ },
6794
+ genome: 'arabidopsis'
6795
+ },
6796
+ zea_mays: {
6797
+ formatGene: (gene)=>{
6798
+ let id = gene._id;
6799
+ if (gene.synonyms) gene.synonyms.forEach((syn)=>{
6800
+ if ($59a241f3912e631e$var$zmv4_re.test(syn)) id = syn;
6801
+ });
6802
+ return id;
6803
+ },
6804
+ fixStudies: (studies)=>{
6805
+ studies = studies.filter((s)=>s.value !== 'Hoopes_et_al_Atlas' && s.value !== 'Hoopes_et_al_Stress');
6806
+ studies.unshift({
6807
+ value: 'Hoopes_et_al_Stress',
6808
+ label: 'Hoopes et. al., Stress'
6809
+ });
6810
+ studies.unshift({
6811
+ value: 'Hoopes_et_al_Atlas',
6812
+ label: 'Hoopes et. al., Atlas'
6813
+ });
6814
+ return studies;
6815
+ },
6816
+ genome: 'maize'
6817
+ },
6818
+ glycine_max: {
6819
+ formatGene: (gene)=>gene._id.replace('GLYMA_', 'Glyma.'),
6820
+ fixStudies: (studies)=>studies.filter((s)=>s.value !== 'soybean_senescence'),
6821
+ genome: 'soybean'
6822
+ },
6823
+ oryza_sativa: {
6824
+ genome: 'rice',
6825
+ formatGene: (gene)=>gene.MSU_id,
6826
+ fixStudies: (studies)=>{
6827
+ studies = studies.filter((s)=>s.value !== 'rice_rma' && s.value !== 'rice_mas');
6828
+ studies.unshift({
6829
+ value: 'rice_rma',
6830
+ label: 'rice rma'
6831
+ });
6832
+ studies.unshift({
6833
+ value: 'rice_mas',
6834
+ label: 'rice mas'
6835
+ });
6836
+ return studies;
6837
+ }
6838
+ }
6839
+ };
6840
+ // Subsites that refer to maize b73 as `zea_maysb73`.
6841
+ $59a241f3912e631e$var$browsers.zea_maysb73 = $59a241f3912e631e$var$browsers.zea_mays;
6842
+ const $59a241f3912e631e$var$ImageLoader = ({ url: url })=>{
6843
+ const [loading, setLoading] = (0, $gXNCa$react.useState)(true);
6844
+ const [error, setError] = (0, $gXNCa$react.useState)(false);
6845
+ (0, $gXNCa$react.useEffect)(()=>{
6846
+ setLoading(true);
6847
+ setError(false);
6848
+ const image = new Image();
6849
+ image.src = url;
6850
+ image.onload = ()=>setLoading(false);
6851
+ image.onerror = ()=>{
6852
+ setLoading(false);
6853
+ setError(true);
6854
+ };
6855
+ }, [
6856
+ url
6857
+ ]);
6858
+ return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
6859
+ style: {
6860
+ padding: 20
6861
+ },
6862
+ children: [
6863
+ loading && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("img", {
6864
+ src: $59a241f3912e631e$var$urls.spinner,
6865
+ alt: "Loading\u2026"
6866
+ }),
6867
+ !loading && !error && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("img", {
6868
+ style: {
6869
+ maxWidth: '100%'
6870
+ },
6871
+ src: url,
6872
+ alt: "eFP browser output"
6873
+ }),
6874
+ error && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("p", {
6875
+ children: "Error: Failed to load image"
6876
+ })
6877
+ ]
6878
+ });
6879
+ };
6880
+ class $59a241f3912e631e$export$2e2bcd8739ae039 extends (0, $gXNCa$react.Component) {
6881
+ constructor(props){
6882
+ super(props);
6883
+ this.state = {
6884
+ currentStudy: null
6885
+ };
6886
+ }
6887
+ getStudies(browser) {
6888
+ fetch($59a241f3912e631e$var$urls.studies(browser.genome)).then((res)=>res.json()).then((res)=>{
6889
+ if (res.wasSuccessful) {
6890
+ let studies = res.data.sort().map((v)=>({
6891
+ value: v,
6892
+ label: v.replace(/_/g, ' ')
6893
+ }));
6894
+ if (browser.fixStudies) studies = browser.fixStudies(studies);
6895
+ this.setState({
6896
+ studies: studies
6897
+ });
6898
+ }
6899
+ }).catch(console.error);
6900
+ }
6901
+ render() {
6902
+ const gene = this.props.gene;
6903
+ const browser = $59a241f3912e631e$var$browsers[gene.system_name];
6904
+ if (!browser) return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
6905
+ children: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("h2", {
6906
+ children: [
6907
+ "Can't find eFP browser for ",
6908
+ gene.system_name
6909
+ ]
6910
+ })
6911
+ });
6912
+ if (!this.state.studies) {
6913
+ this.getStudies(browser);
6914
+ return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("img", {
6915
+ src: $59a241f3912e631e$var$urls.spinner,
6916
+ alt: "Loading\u2026"
6917
+ });
6918
+ }
6919
+ const efp_gene = browser.formatGene(gene);
6920
+ // `study`/`onStudyChange` let a host drive the selection (so a saved view
6921
+ // can restore it); fall back to local state when uncontrolled.
6922
+ const study = this.props.study || this.state.currentStudy || this.state.studies[0].value;
6923
+ const onSelect = (e)=>{
6924
+ if (this.props.onStudyChange) this.props.onStudyChange(e.target.value);
6925
+ else this.setState({
6926
+ currentStudy: e.target.value
6927
+ });
6928
+ };
6929
+ return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
6930
+ style: {
6931
+ paddingTop: 10
6932
+ },
6933
+ children: [
6934
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("label", {
6935
+ style: {
6936
+ paddingLeft: 20,
6937
+ paddingRight: 10
6938
+ },
6939
+ children: "Select a study:"
6940
+ }),
6941
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("select", {
6942
+ value: study,
6943
+ onChange: onSelect,
6944
+ children: this.state.studies.map((s, idx)=>/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("option", {
6945
+ value: s.value,
6946
+ children: s.label
6947
+ }, idx))
6948
+ }),
6949
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("br", {}),
6950
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)($59a241f3912e631e$var$ImageLoader, {
6951
+ url: $59a241f3912e631e$var$urls.image(browser.genome, study, efp_gene)
6952
+ }),
6953
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("a", {
6954
+ style: {
6955
+ paddingLeft: 100
6956
+ },
6957
+ href: $59a241f3912e631e$var$urls.app(browser.genome, study, efp_gene),
6958
+ target: "_blank",
6959
+ rel: "noreferrer",
6960
+ children: [
6961
+ "Powered by ",
6962
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("img", {
6963
+ src: $59a241f3912e631e$var$urls.logo,
6964
+ style: {
6965
+ maxWidth: 40
6966
+ },
6967
+ alt: "BBC"
6968
+ }),
6969
+ " Webservices"
6970
+ ]
5542
6971
  })
5543
6972
  ]
5544
6973
  });
5545
- } else return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {});
5546
- };
5547
- var $541b8b0d8c5501d2$export$2e2bcd8739ae039 = (0, $gXNCa$reduxbundlerreact.connect)('selectGrameneSuggestions', 'selectSuggestionsQuery', 'selectGrameneTaxonomy', 'selectConfiguration', 'doAcceptSuggestion', 'doAcceptGrameneSuggestion', $541b8b0d8c5501d2$var$Suggestions);
5548
-
5549
-
5550
-
5551
-
5552
-
5553
-
5554
-
5555
-
5556
-
5557
-
5558
-
5559
-
5560
-
5561
-
5562
-
5563
-
5564
-
6974
+ }
6975
+ }
6976
+ function $59a241f3912e631e$export$1bc0f3596ef2efe9(gene) {
6977
+ return $59a241f3912e631e$var$browsers.hasOwnProperty(gene.system_name);
6978
+ }
5565
6979
 
5566
6980
 
5567
6981
  function $9e29a4f60318db7a$var$DynamicIframe(props) {
@@ -5587,15 +7001,43 @@ function $9e29a4f60318db7a$var$DynamicIframe(props) {
5587
7001
  });
5588
7002
  }
5589
7003
  const $9e29a4f60318db7a$var$Detail = (props)=>{
5590
- const gene = props.geneDocs[props.searchResult.id];
5591
- const [atlasExperiment, setAtlasExperiment] = (0, $gXNCa$react.useState)(null);
7004
+ const geneId = props.searchResult.id;
7005
+ const gene = props.geneDocs[geneId];
7006
+ // User-selected view state (active sub-tab, chosen GXA experiment, chosen eFP
7007
+ // study) lives in the uiViewState bundle keyed by geneId, so the shareable-
7008
+ // views snapshot can round-trip it. Fetched data + the dev-only Local API
7009
+ // toggle stay in local state. Defaults match the old local-state initials.
7010
+ const expr = props.uiViewState && props.uiViewState.byGene[geneId] && props.uiViewState.byGene[geneId].expression || {};
7011
+ const activeTab = expr.activeTab || 'gene';
7012
+ const atlasExperiment = expr.atlasExperiment || null;
7013
+ const setActiveTab = (k)=>props.doSetExpressionState({
7014
+ geneId: geneId,
7015
+ patch: {
7016
+ activeTab: k
7017
+ }
7018
+ });
7019
+ const setAtlasExperiment = (v)=>props.doSetExpressionState({
7020
+ geneId: geneId,
7021
+ patch: {
7022
+ atlasExperiment: v
7023
+ }
7024
+ });
5592
7025
  const [atlasExperimentList, setAtlasExperimentList] = (0, $gXNCa$react.useState)([]);
5593
7026
  const [atlasFacets, setAtlasFacets] = (0, $gXNCa$react.useState)(null);
5594
7027
  const [isLocal, setIsLocal] = (0, $gXNCa$react.useState)(false);
5595
- const [activeTab, setActiveTab] = (0, $gXNCa$react.useState)('gene');
5596
7028
  const handleLocalAPIChange = (event)=>{
5597
7029
  setIsLocal(event.target.checked);
5598
7030
  };
7031
+ // The expressionStudies resource is otherwise fetched only when a top-level
7032
+ // expression view (exprViz/expression/export) is on — but this per-gene
7033
+ // Expression detail also needs it (the Paralogs sub-tab's experiment list and
7034
+ // the atlasExperiment selection both derive from it). Self-fetch on mount so
7035
+ // opening the detail populates the studies even when no such view is enabled.
7036
+ (0, $gXNCa$react.useEffect)(()=>{
7037
+ if (!props.expressionStudies && props.doFetchExpressionStudies) props.doFetchExpressionStudies();
7038
+ }, [
7039
+ props.expressionStudies
7040
+ ]);
5599
7041
  (0, $gXNCa$react.useEffect)(()=>{
5600
7042
  if (!props.expressionStudies) return;
5601
7043
  const tid = Math.floor(gene.taxon_id / 1000);
@@ -5618,10 +7060,15 @@ const $9e29a4f60318db7a$var$Detail = (props)=>{
5618
7060
  });
5619
7061
  setAtlasExperimentList(eList);
5620
7062
  setAtlasFacets(facets);
5621
- let refExp = eList.filter((e)=>e.isRef);
5622
- if (refExp.length === 1) setAtlasExperiment(refExp[0]._id);
5623
- else // no reference experiment - choose first
5624
- setAtlasExperiment(eList[0]._id);
7063
+ // Only pick a default experiment when the user (or a restored snapshot)
7064
+ // hasn't already chosen one — otherwise we'd clobber a saved selection
7065
+ // the moment the studies list loads.
7066
+ if (!atlasExperiment) {
7067
+ let refExp = eList.filter((e)=>e.isRef);
7068
+ if (refExp.length === 1) setAtlasExperiment(refExp[0]._id);
7069
+ else // no reference experiment - choose first
7070
+ setAtlasExperiment(eList[0]._id);
7071
+ }
5625
7072
  }
5626
7073
  }, [
5627
7074
  props.expressionStudies
@@ -5653,6 +7100,7 @@ const $9e29a4f60318db7a$var$Detail = (props)=>{
5653
7100
  /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Form).Select, {
5654
7101
  "aria-label": "experiment selector",
5655
7102
  placeholder: "Select experiment",
7103
+ value: atlasExperiment || '',
5656
7104
  onChange: (e)=>setAtlasExperiment(e.target.value),
5657
7105
  children: atlasExperimentList.map((e, idx)=>/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("option", {
5658
7106
  value: e._id,
@@ -5676,18 +7124,25 @@ const $9e29a4f60318db7a$var$Detail = (props)=>{
5676
7124
  url: gene_url
5677
7125
  })
5678
7126
  }, "gxa"),
5679
- (0, $gXNCa$grameneefpbrowser.haveBAR)(gene) && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Tab), {
7127
+ (0, $59a241f3912e631e$export$1bc0f3596ef2efe9)(gene) && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Tab), {
5680
7128
  tabClassName: "eFP",
5681
7129
  eventKey: "eFP",
5682
7130
  title: "eFP Browser",
5683
- children: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, ($parcel$interopDefault($gXNCa$grameneefpbrowser))), {
5684
- gene: gene
7131
+ children: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $59a241f3912e631e$export$2e2bcd8739ae039), {
7132
+ gene: gene,
7133
+ study: expr.barStudy,
7134
+ onStudyChange: (v)=>props.doSetExpressionState({
7135
+ geneId: geneId,
7136
+ patch: {
7137
+ barStudy: v
7138
+ }
7139
+ })
5685
7140
  })
5686
7141
  }, "bar")
5687
7142
  ]
5688
7143
  });
5689
7144
  };
5690
- var $9e29a4f60318db7a$export$2e2bcd8739ae039 = (0, $gXNCa$reduxbundlerreact.connect)('selectGrameneParalogs', 'selectExpressionStudies', 'doRequestParalogs', //'doRequestParalogExpression',
7145
+ var $9e29a4f60318db7a$export$2e2bcd8739ae039 = (0, $gXNCa$reduxbundlerreact.connect)('selectGrameneParalogs', 'selectExpressionStudies', 'selectUiViewState', 'doRequestParalogs', 'doFetchExpressionStudies', 'doSetExpressionState', //'doRequestParalogExpression',
5691
7146
  $9e29a4f60318db7a$var$Detail);
5692
7147
 
5693
7148
 
@@ -5706,6 +7161,7 @@ $9e29a4f60318db7a$var$Detail);
5706
7161
 
5707
7162
 
5708
7163
 
7164
+
5709
7165
  const $5c2c79352d3d7b81$export$ec6b54b688be1708 = ({ fullscreen: fullscreen, onExitFullscreen: onExitFullscreen, title: title, className: className, children: children })=>{
5710
7166
  const [stableNode] = (0, $gXNCa$react.useState)(()=>typeof document !== 'undefined' ? document.createElement('div') : null);
5711
7167
  const inlineRef = (0, $gXNCa$react.useRef)(null);
@@ -5923,20 +7379,140 @@ const $64fad37f770d2bfe$var$TBROWSE_ZONES = [
5923
7379
  class $64fad37f770d2bfe$var$Homology extends (0, ($parcel$interopDefault($gXNCa$react))).Component {
5924
7380
  constructor(props){
5925
7381
  super(props);
7382
+ // Async data caches stay in local state (per mount, per tree). The
7383
+ // user-controlled view state (viewer toggle, resize height, tbrowse
7384
+ // ViewState) is lifted into the uiViewState bundle, keyed by geneId.
7385
+ //
7386
+ // `*Status` fields track per-zone load lifecycle so the TBrowse
7387
+ // toolbar can render loading/error affordances via its zoneStatus
7388
+ // prop. They start `undefined` (no signal); flipped to 'loading'
7389
+ // when the fetch kicks off and 'ready' / 'error' on settle.
5926
7390
  this.state = {
5927
- viewer: 'treevis',
5928
7391
  neighborhood: null,
5929
7392
  neighborhoodTreeId: null,
7393
+ neighborhoodStatus: undefined,
5930
7394
  geneStructures: null,
5931
7395
  geneStructuresTreeId: null,
5932
- height: 600
7396
+ geneStructuresStatus: undefined
5933
7397
  };
5934
7398
  if (!props.geneDocs.hasOwnProperty(props.searchResult.id)) props.requestGene(props.searchResult.id);
5935
- this.taxonomy = (0, ($parcel$interopDefault($gXNCa$gramenetreesclient))).taxonomy.tree(Object.values(props.grameneTaxonomy));
7399
+ this.taxonomy = (0, ($parcel$interopDefault($gXNCa$gramenetreesclientsrctaxonomy))).tree(Object.values(props.grameneTaxonomy));
7400
+ }
7401
+ // ----- uiViewState accessors (with sensible defaults) -----
7402
+ getGeneId() {
7403
+ return this.props.searchResult.id;
7404
+ }
7405
+ getHomologySlice() {
7406
+ const slice = this.props.uiViewState && this.props.uiViewState.byGene[this.getGeneId()];
7407
+ return slice && slice.homology || {};
7408
+ }
7409
+ getViewer() {
7410
+ return this.getHomologySlice().viewer || 'treevis';
7411
+ }
7412
+ getHeight() {
7413
+ const h = this.getHomologySlice().height;
7414
+ return typeof h === 'number' ? h : 600;
7415
+ }
7416
+ setViewer(viewer) {
7417
+ this.props.doSetHomologyViewer({
7418
+ geneId: this.getGeneId(),
7419
+ viewer: viewer
7420
+ });
7421
+ }
7422
+ setHeight(height) {
7423
+ this.props.doSetHomologyHeight({
7424
+ geneId: this.getGeneId(),
7425
+ height: height
7426
+ });
7427
+ }
7428
+ setTbrowseViewState(viewState) {
7429
+ this.props.doSetHomologyTbrowseViewState({
7430
+ geneId: this.getGeneId(),
7431
+ tbrowse: viewState
7432
+ });
7433
+ }
7434
+ // Seed the bundle with a pivot-computed initial tbrowse view state once the
7435
+ // tree data is available and the user is actually looking at the tbrowse
7436
+ // viewer. Called from lifecycle (not render) to avoid dispatching mid-render.
7437
+ maybeSeedTbrowseViewState() {
7438
+ if (this.getViewer() !== 'tbrowse') return;
7439
+ if (this.getHomologySlice().tbrowse) return;
7440
+ const id = this.getGeneId();
7441
+ if (!this.props.geneDocs.hasOwnProperty(id)) return;
7442
+ const gene = this.props.geneDocs[id];
7443
+ if (!gene.homology) return;
7444
+ const treeId = gene.homology.gene_tree.id;
7445
+ const raw = this.props.grameneTrees[treeId];
7446
+ if (!raw || !raw.taxon_id) return;
7447
+ const adapted = (0, $gXNCa$tbrowse.fromGrameneGenetree)([
7448
+ raw
7449
+ ]);
7450
+ const pivot = (0, $gXNCa$tbrowse.computePivotState)(adapted.tree, gene._id);
7451
+ this.setTbrowseViewState({
7452
+ selectedNodeId: null,
7453
+ collapsedNodeIds: pivot ? pivot.collapsedNodeIds : [],
7454
+ prunedNodeIds: [],
7455
+ swappedNodeIds: pivot ? pivot.swappedNodeIds : [],
7456
+ compressedNodeIds: [],
7457
+ nodeOfInterestId: pivot ? pivot.targetId : null,
7458
+ // Use each zone's intended fr-share + initial visibility from its
7459
+ // definition (tree:30 / labels:20 / msa:50 / neighborhood:30 /
7460
+ // genome:32) instead of uniform 25 across the board. Gives the MSA
7461
+ // a wider initial pane and the tree a narrower one.
7462
+ //
7463
+ // `defaultVisible ?? true` matches tbrowse's own buildInitialViewState
7464
+ // logic — zones default to visible unless their definition opts out
7465
+ // (e.g. neighborhood and genome opt out so they only appear once
7466
+ // their async data lands; tbrowse's Layout auto-flips them on then).
7467
+ zones: $64fad37f770d2bfe$var$TBROWSE_ZONES.map((z)=>({
7468
+ id: z.id,
7469
+ width: z.defaultWidth,
7470
+ visible: z.defaultVisible ?? true
7471
+ })),
7472
+ zoneStates: {},
7473
+ search: null
7474
+ });
7475
+ }
7476
+ componentDidMount() {
7477
+ this.maybeSeedTbrowseViewState();
7478
+ this.maybeFetchTbrowseData();
7479
+ }
7480
+ componentDidUpdate() {
7481
+ this.maybeSeedTbrowseViewState();
7482
+ this.maybeFetchTbrowseData();
7483
+ }
7484
+ // Kick off the neighborhood + gene-structure fetches from lifecycle,
7485
+ // not render. Each fetch internally dedupes on `_*FetchedFor === treeId`
7486
+ // so calling on every commit is cheap. Gated on the user actually being
7487
+ // on the tbrowse viewer + the tree data being loaded — otherwise we'd
7488
+ // pay network cost for genes the user never looks at.
7489
+ maybeFetchTbrowseData() {
7490
+ if (this.getViewer() !== 'tbrowse') return;
7491
+ const id = this.getGeneId();
7492
+ if (!this.props.geneDocs.hasOwnProperty(id)) return;
7493
+ const gene = this.props.geneDocs[id];
7494
+ if (!gene.homology) return;
7495
+ const treeId = gene.homology.gene_tree.id;
7496
+ const raw = this.props.grameneTrees[treeId];
7497
+ if (!raw || !raw.taxon_id) return;
7498
+ // Lazily compute the adapted tbrowse data the same way renderTBrowse
7499
+ // does, so fetchGeneStructures has the leaf ids without rerunning
7500
+ // fromGrameneGenetree on every commit.
7501
+ if (this._tbrowseTreeId !== treeId) {
7502
+ this._tbrowseTreeId = treeId;
7503
+ this._tbrowseData = (0, $gXNCa$tbrowse.fromGrameneGenetree)([
7504
+ raw
7505
+ ]);
7506
+ }
7507
+ this.fetchNeighborhood(treeId);
7508
+ this.fetchGeneStructures(treeId, this._tbrowseData.tree);
5936
7509
  }
5937
7510
  fetchNeighborhood(treeId) {
5938
7511
  if (this._neighborhoodFetchedFor === treeId) return;
5939
7512
  this._neighborhoodFetchedFor = treeId;
7513
+ this.setState({
7514
+ neighborhoodStatus: 'loading'
7515
+ });
5940
7516
  const api = this.props.grameneAPI;
5941
7517
  const url = new URL(`${api}/search`);
5942
7518
  url.searchParams.set('fl', 'id,name,gene_tree,gene_idx,region,start,end,strand,biotype,system_name,description');
@@ -5954,10 +7530,16 @@ class $64fad37f770d2bfe$var$Homology extends (0, ($parcel$interopDefault($gXNCa$
5954
7530
  if (this._neighborhoodFetchedFor !== treeId) return;
5955
7531
  this.setState({
5956
7532
  neighborhood: (0, $gXNCa$tbrowse.fromGrameneNeighborhood)(json),
5957
- neighborhoodTreeId: treeId
7533
+ neighborhoodTreeId: treeId,
7534
+ neighborhoodStatus: 'ready'
5958
7535
  });
5959
7536
  }).catch((err)=>{
5960
7537
  console.warn('tbrowse neighborhood fetch failed:', err);
7538
+ // Surface the failure via the zoneStatus prop. Reset the
7539
+ // dedupe key so a re-mount or tree-change can retry.
7540
+ if (this._neighborhoodFetchedFor === treeId) this.setState({
7541
+ neighborhoodStatus: 'error'
7542
+ });
5961
7543
  this._neighborhoodFetchedFor = null;
5962
7544
  });
5963
7545
  }
@@ -5966,6 +7548,9 @@ class $64fad37f770d2bfe$var$Homology extends (0, ($parcel$interopDefault($gXNCa$
5966
7548
  this._geneStructuresFetchedFor = treeId;
5967
7549
  const ids = Object.values(tree.nodes).filter((n)=>n.isLeaf && n.geneId).map((n)=>n.geneId);
5968
7550
  if (ids.length === 0) return;
7551
+ this.setState({
7552
+ geneStructuresStatus: 'loading'
7553
+ });
5969
7554
  const api = this.props.grameneAPI;
5970
7555
  const BATCH_SIZE = 50;
5971
7556
  const batches = [];
@@ -5988,22 +7573,24 @@ class $64fad37f770d2bfe$var$Homology extends (0, ($parcel$interopDefault($gXNCa$
5988
7573
  const combined = [].concat(...results.map((r)=>Array.isArray(r) ? r : []));
5989
7574
  this.setState({
5990
7575
  geneStructures: (0, $gXNCa$tbrowse.fromGrameneGeneStructures)(combined),
5991
- geneStructuresTreeId: treeId
7576
+ geneStructuresTreeId: treeId,
7577
+ geneStructuresStatus: 'ready'
5992
7578
  });
5993
7579
  }).catch((err)=>{
5994
7580
  console.warn('tbrowse gene-structures fetch failed:', err);
7581
+ if (this._geneStructuresFetchedFor === treeId) this.setState({
7582
+ geneStructuresStatus: 'error'
7583
+ });
5995
7584
  this._geneStructuresFetchedFor = null;
5996
7585
  });
5997
7586
  }
5998
7587
  startResize(e) {
5999
7588
  e.preventDefault();
6000
7589
  const startY = e.clientY;
6001
- const startHeight = this.state.height;
7590
+ const startHeight = this.getHeight();
6002
7591
  const onMouseMove = (moveEvent)=>{
6003
7592
  const newHeight = Math.max(200, startHeight + (moveEvent.clientY - startY));
6004
- this.setState({
6005
- height: newHeight
6006
- });
7593
+ this.setHeight(newHeight);
6007
7594
  };
6008
7595
  const onMouseUp = ()=>{
6009
7596
  window.removeEventListener('mousemove', onMouseMove);
@@ -6026,7 +7613,7 @@ class $64fad37f770d2bfe$var$Homology extends (0, ($parcel$interopDefault($gXNCa$
6026
7613
  /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
6027
7614
  className: "gene-genetree",
6028
7615
  style: {
6029
- height: this.state.height,
7616
+ height: this.getHeight(),
6030
7617
  width: '100%'
6031
7618
  },
6032
7619
  children: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, ($parcel$interopDefault($gXNCa$gramenegenetreevis))), {
@@ -6048,41 +7635,36 @@ class $64fad37f770d2bfe$var$Homology extends (0, ($parcel$interopDefault($gXNCa$
6048
7635
  }
6049
7636
  renderTBrowse() {
6050
7637
  const treeId = this.gene.homology.gene_tree.id;
7638
+ // Adapted tbrowse data is computed lazily in maybeFetchTbrowseData
7639
+ // (called from lifecycle). If the user just flipped to tbrowse and
7640
+ // the lifecycle hasn't fired yet, compute it once here without
7641
+ // calling any fetches — those will fire from componentDidUpdate.
6051
7642
  if (this._tbrowseTreeId !== treeId) {
6052
- const raw = this.props.grameneTrees[treeId];
6053
- const adapted = (0, $gXNCa$tbrowse.fromGrameneGenetree)([
6054
- raw
6055
- ]);
6056
7643
  this._tbrowseTreeId = treeId;
6057
- this._tbrowseData = adapted;
6058
- const pivot = (0, $gXNCa$tbrowse.computePivotState)(adapted.tree, this.gene._id);
6059
- const zoneIds = $64fad37f770d2bfe$var$TBROWSE_ZONES.map((z)=>z.id);
6060
- this._tbrowseInitialViewState = {
6061
- selectedNodeId: null,
6062
- collapsedNodeIds: pivot ? pivot.collapsedNodeIds : [],
6063
- prunedNodeIds: [],
6064
- swappedNodeIds: pivot ? pivot.swappedNodeIds : [],
6065
- compressedNodeIds: [],
6066
- nodeOfInterestId: pivot ? pivot.targetId : null,
6067
- zones: zoneIds.map((id)=>({
6068
- id: id,
6069
- width: 25,
6070
- visible: true
6071
- })),
6072
- zoneStates: {},
6073
- search: null
6074
- };
7644
+ this._tbrowseData = (0, $gXNCa$tbrowse.fromGrameneGenetree)([
7645
+ this.props.grameneTrees[treeId]
7646
+ ]);
6075
7647
  }
6076
- this.fetchNeighborhood(treeId);
6077
- this.fetchGeneStructures(treeId, this._tbrowseData.tree);
6078
7648
  const neighborhood = this.state.neighborhoodTreeId === treeId ? this.state.neighborhood : undefined;
6079
7649
  const geneStructures = this.state.geneStructuresTreeId === treeId ? this.state.geneStructures : undefined;
7650
+ // Per-zone status for the TBrowse toolbar — pulses while a fetch
7651
+ // is in flight, turns red on failure. Tracked per-tree so a
7652
+ // tree-change resets any stale 'ready'/'error' from the prior gene.
7653
+ const zoneStatus = {
7654
+ neighborhood: this.state.neighborhoodTreeId === treeId ? this.state.neighborhoodStatus : this.state.neighborhoodStatus === 'loading' ? 'loading' : undefined,
7655
+ genome: this.state.geneStructuresTreeId === treeId ? this.state.geneStructuresStatus : this.state.geneStructuresStatus === 'loading' ? 'loading' : undefined
7656
+ };
7657
+ // Bundle-driven (controlled) view state. If we're rendering tbrowse before
7658
+ // componentDidMount/Update has seeded the bundle slice, skip this turn and
7659
+ // let the re-render with the seeded state do the work.
7660
+ const tbrowseVS = this.getHomologySlice().tbrowse;
7661
+ if (!tbrowseVS) return null;
6080
7662
  return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)((0, $gXNCa$reactjsxruntime.Fragment), {
6081
7663
  children: [
6082
7664
  /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
6083
7665
  className: "gene-genetree",
6084
7666
  style: {
6085
- height: this.state.height,
7667
+ height: this.getHeight(),
6086
7668
  width: '100%'
6087
7669
  },
6088
7670
  children: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$tbrowse.TBrowse), {
@@ -6096,7 +7678,13 @@ class $64fad37f770d2bfe$var$Homology extends (0, ($parcel$interopDefault($gXNCa$
6096
7678
  geneStructures: geneStructures,
6097
7679
  zones: $64fad37f770d2bfe$var$TBROWSE_ZONES,
6098
7680
  nodeOfInterest: this.gene._id,
6099
- initialViewState: this._tbrowseInitialViewState
7681
+ viewState: tbrowseVS,
7682
+ onViewStateChange: (next)=>this.setTbrowseViewState(next),
7683
+ defaultOpenSections: {
7684
+ zones: true,
7685
+ search: true
7686
+ },
7687
+ zoneStatus: zoneStatus
6100
7688
  })
6101
7689
  }),
6102
7690
  this.renderResizeHandle()
@@ -6104,12 +7692,10 @@ class $64fad37f770d2bfe$var$Homology extends (0, ($parcel$interopDefault($gXNCa$
6104
7692
  });
6105
7693
  }
6106
7694
  renderViewerToggle() {
6107
- const { viewer: viewer } = this.state;
7695
+ const viewer = this.getViewer();
6108
7696
  const btn = (id, label)=>/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("button", {
6109
7697
  type: "button",
6110
- onClick: ()=>this.setState({
6111
- viewer: id
6112
- }),
7698
+ onClick: ()=>this.setViewer(id),
6113
7699
  style: {
6114
7700
  padding: '4px 10px',
6115
7701
  marginRight: 4,
@@ -6238,7 +7824,7 @@ class $64fad37f770d2bfe$var$Homology extends (0, ($parcel$interopDefault($gXNCa$
6238
7824
  else {
6239
7825
  const tree = this.props.grameneTrees[treeId];
6240
7826
  if (tree.hasOwnProperty('taxon_id')) {
6241
- this.tree = (0, ($parcel$interopDefault($gXNCa$gramenetreesclient))).genetree.tree([
7827
+ this.tree = (0, ($parcel$interopDefault($gXNCa$gramenetreesclientsrcgenetree))).tree([
6242
7828
  this.props.grameneTrees[treeId]
6243
7829
  ]);
6244
7830
  this.orthologs = this.orthologList();
@@ -6253,7 +7839,7 @@ class $64fad37f770d2bfe$var$Homology extends (0, ($parcel$interopDefault($gXNCa$
6253
7839
  this.tree && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)((0, $5c2c79352d3d7b81$export$7c6e2c02157bb7d2), {
6254
7840
  children: [
6255
7841
  this.renderViewerToggle(),
6256
- this.state.viewer === 'tbrowse' ? this.renderTBrowse() : this.renderTreeVis()
7842
+ this.getViewer() === 'tbrowse' ? this.renderTBrowse() : this.renderTreeVis()
6257
7843
  ]
6258
7844
  }, "content"),
6259
7845
  this.tree && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $5c2c79352d3d7b81$export$900f1bfcd1cb6476), {
@@ -6266,7 +7852,7 @@ class $64fad37f770d2bfe$var$Homology extends (0, ($parcel$interopDefault($gXNCa$
6266
7852
  });
6267
7853
  }
6268
7854
  }
6269
- var $64fad37f770d2bfe$export$2e2bcd8739ae039 = (0, $gXNCa$reduxbundlerreact.connect)('selectGrameneTaxonomy', 'selectGrameneTrees', 'selectGrameneGenomes', 'selectGrameneAPI', 'selectConfiguration', 'selectCuration', 'doRequestGrameneTree', 'doAcceptGrameneSuggestion', 'doReplaceGrameneFilters', $64fad37f770d2bfe$var$Homology);
7855
+ var $64fad37f770d2bfe$export$2e2bcd8739ae039 = (0, $gXNCa$reduxbundlerreact.connect)('selectGrameneTaxonomy', 'selectGrameneTrees', 'selectGrameneGenomes', 'selectGrameneAPI', 'selectConfiguration', 'selectCuration', 'selectUiViewState', 'doRequestGrameneTree', 'doSetHomologyViewer', 'doSetHomologyHeight', 'doSetHomologyTbrowseViewState', 'doAcceptGrameneSuggestion', 'doReplaceGrameneFilters', $64fad37f770d2bfe$var$Homology);
6270
7856
 
6271
7857
 
6272
7858
 
@@ -6757,7 +8343,7 @@ function $54c74a4689d5a778$var$buildIframeSrcDoc() {
6757
8343
  class $54c74a4689d5a778$var$Pathways extends (0, ($parcel$interopDefault($gXNCa$react))).Component {
6758
8344
  constructor(props){
6759
8345
  super(props);
6760
- this.taxonomy = (0, ($parcel$interopDefault($gXNCa$gramenetreesclient))).taxonomy.tree(Object.values(props.grameneTaxonomy));
8346
+ this.taxonomy = (0, ($parcel$interopDefault($gXNCa$gramenetreesclientsrctaxonomy))).tree(Object.values(props.grameneTaxonomy));
6761
8347
  this.gene = props.geneDocs[props.searchResult.id];
6762
8348
  this.iframeRef = /*#__PURE__*/ (0, ($parcel$interopDefault($gXNCa$react))).createRef();
6763
8349
  // srcDoc is constant — the per-instance pathway/reaction state is
@@ -8012,22 +9598,55 @@ const $527ebc19dc92444d$var$buildId = (gene, geneSeq, up, down)=>{
8012
9598
  return `${geneSeq.genome}|${gene._id}|${gene.location.region}:${gs}..${ge} ${extras.join('|')}`;
8013
9599
  };
8014
9600
  const $527ebc19dc92444d$var$Detail = (props)=>{
8015
- const gene = props.geneDocs[props.searchResult.id];
8016
- const [tab, setTab] = (0, $gXNCa$react.useState)('dna');
8017
- const [upstream, setUpstream] = (0, $gXNCa$react.useState)(0);
8018
- const [downstream, setDownstream] = (0, $gXNCa$react.useState)(0);
8019
- const [tid, setTid] = (0, $gXNCa$react.useState)(gene.gene_structure.canonical_transcript);
9601
+ const geneId = props.searchResult.id;
9602
+ const gene = props.geneDocs[geneId];
9603
+ // Sub-tab + selected isoform + flanking lengths live in the uiViewState
9604
+ // bundle (keyed by geneId) so the shareable-views snapshot can round-trip
9605
+ // them. Defaults match the old local-state initial values.
9606
+ const slice = props.uiViewState && props.uiViewState.byGene[geneId] && props.uiViewState.byGene[geneId].sequences || {};
9607
+ const tab = slice.tab || 'dna';
9608
+ const tid = slice.tid || gene.gene_structure.canonical_transcript;
9609
+ const upstream = slice.upstream || 0;
9610
+ const downstream = slice.downstream || 0;
9611
+ const setTab = (k)=>props.doSetSequencesState({
9612
+ geneId: geneId,
9613
+ patch: {
9614
+ tab: k
9615
+ }
9616
+ });
9617
+ const setTid = (v)=>props.doSetSequencesState({
9618
+ geneId: geneId,
9619
+ patch: {
9620
+ tid: v
9621
+ }
9622
+ });
9623
+ const setUpstream = (v)=>props.doSetSequencesState({
9624
+ geneId: geneId,
9625
+ patch: {
9626
+ upstream: +v
9627
+ }
9628
+ });
9629
+ const setDownstream = (v)=>props.doSetSequencesState({
9630
+ geneId: geneId,
9631
+ patch: {
9632
+ downstream: +v
9633
+ }
9634
+ });
8020
9635
  let geneSeq;
8021
9636
  let rnaSeq;
8022
9637
  let pepSeq;
8023
- if (props.geneSequences && props.geneSequences[gene._id]) geneSeq = props.geneSequences[gene._id];
9638
+ // The *_SEQUENCE_REQUESTED reducer inserts a `{}` placeholder before the
9639
+ // fetch resolves, so a key-only existence check passes while the body is
9640
+ // still empty — leading to `seq.substring` of undefined downstream. Gate
9641
+ // on the actual payload field instead.
9642
+ if (props.geneSequences && props.geneSequences[gene._id] && props.geneSequences[gene._id].seq) geneSeq = props.geneSequences[gene._id];
8024
9643
  else {
8025
9644
  props.doRequestGeneSequence(gene);
8026
9645
  return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("pre", {
8027
9646
  children: "loading"
8028
9647
  });
8029
9648
  }
8030
- if (props.rnaSequences && props.rnaSequences[tid]) rnaSeq = props.rnaSequences[tid];
9649
+ if (props.rnaSequences && props.rnaSequences[tid] && props.rnaSequences[tid].seq) rnaSeq = props.rnaSequences[tid];
8031
9650
  else {
8032
9651
  props.doRequestRnaSequence(tid, gene);
8033
9652
  return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("pre", {
@@ -8040,7 +9659,7 @@ const $527ebc19dc92444d$var$Detail = (props)=>{
8040
9659
  let tl_id;
8041
9660
  if (transcript.translation) {
8042
9661
  tl_id = transcript.translation.id;
8043
- if (props.pepSequences && props.pepSequences[tl_id]) pepSeq = props.pepSequences[tl_id];
9662
+ if (props.pepSequences && props.pepSequences[tl_id] && props.pepSequences[tl_id].seq) pepSeq = props.pepSequences[tl_id];
8044
9663
  else {
8045
9664
  props.doRequestPepSequence(tl_id, gene);
8046
9665
  return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("pre", {
@@ -8257,7 +9876,7 @@ const $527ebc19dc92444d$var$Detail = (props)=>{
8257
9876
  ]
8258
9877
  });
8259
9878
  };
8260
- var $527ebc19dc92444d$export$2e2bcd8739ae039 = (0, $gXNCa$reduxbundlerreact.connect)('selectConfiguration', 'selectGeneSequences', 'selectRnaSequences', 'selectPepSequences', 'doRequestGeneSequence', 'doRequestRnaSequence', 'doRequestPepSequence', $527ebc19dc92444d$var$Detail);
9879
+ var $527ebc19dc92444d$export$2e2bcd8739ae039 = (0, $gXNCa$reduxbundlerreact.connect)('selectConfiguration', 'selectGeneSequences', 'selectRnaSequences', 'selectPepSequences', 'selectUiViewState', 'doRequestGeneSequence', 'doRequestRnaSequence', 'doRequestPepSequence', 'doSetSequencesState', $527ebc19dc92444d$var$Detail);
8261
9880
 
8262
9881
 
8263
9882
 
@@ -8447,12 +10066,12 @@ const $6c5c4f90059875bf$var$allDetails = [
8447
10066
  class $6c5c4f90059875bf$var$Gene extends (0, ($parcel$interopDefault($gXNCa$react))).Component {
8448
10067
  constructor(props){
8449
10068
  super(props);
10069
+ // `details` is derived per-render config (which tabs the site enables,
10070
+ // crossed with this row's `capabilities`) — keep it local.
8450
10071
  this.state = {
8451
10072
  details: $6c5c4f90059875bf$var$allDetails.map((o)=>({
8452
10073
  ...o
8453
- })).filter((d)=>props.config.details[d.id]),
8454
- expandedDetail: props.expandedDetail,
8455
- fullscreen: false
10074
+ })).filter((d)=>props.config.details[d.id])
8456
10075
  };
8457
10076
  let hasData = {};
8458
10077
  props.searchResult.capabilities.forEach((c)=>{
@@ -8461,28 +10080,46 @@ class $6c5c4f90059875bf$var$Gene extends (0, ($parcel$interopDefault($gXNCa$reac
8461
10080
  });
8462
10081
  this.state.details.forEach((d)=>d.available |= hasData.hasOwnProperty(d.id));
8463
10082
  }
10083
+ componentDidMount() {
10084
+ // Seed the auto-open-homology intent (from GeneList when numFound===1) the
10085
+ // first time this row mounts, only if the user hasn't already opened/closed
10086
+ // a tab here. Without this, the bundle would have no entry and the prior
10087
+ // "auto-expand homology on single-result" behavior would be lost.
10088
+ if (this.props.expandedDetail && this.expandedDetail === undefined) this.props.doExpandGeneDetail({
10089
+ geneId: this.props.searchResult.id,
10090
+ detail: this.props.expandedDetail
10091
+ });
10092
+ }
10093
+ get expandedDetail() {
10094
+ const slice = this.props.uiViewState && this.props.uiViewState.byGene[this.props.searchResult.id];
10095
+ return slice ? slice.expandedDetail : undefined;
10096
+ }
10097
+ get fullscreen() {
10098
+ const slice = this.props.uiViewState && this.props.uiViewState.byGene[this.props.searchResult.id];
10099
+ return !!(slice && slice.fullscreen);
10100
+ }
8464
10101
  getDetailStatus(d) {
8465
- if (this.state.expandedDetail === d.id) return 'expanded';
10102
+ if (this.expandedDetail === d.id) return 'expanded';
8466
10103
  if (d.available) return 'closed';
8467
10104
  return d.id === "pubs" ? 'empty' : 'disabled';
8468
10105
  }
8469
10106
  setExpanded(d) {
8470
10107
  if (d.available || d.id === "pubs") {
8471
- if (this.state.expandedDetail === d.id) this.setState({
8472
- expandedDetail: null,
8473
- fullscreen: false
10108
+ const geneId = this.props.searchResult.id;
10109
+ if (this.expandedDetail === d.id) this.props.doExpandGeneDetail({
10110
+ geneId: geneId,
10111
+ detail: null
8474
10112
  });
8475
10113
  else {
8476
- const geneId = this.props.searchResult.id;
8477
10114
  if (!(this.props.geneDocs && this.props.geneDocs.hasOwnProperty(geneId))) this.props.requestGene(geneId);
8478
10115
  (0, ($parcel$interopDefault($gXNCa$reactga4))).event({
8479
10116
  category: 'Search',
8480
10117
  action: 'Details',
8481
10118
  label: d.label
8482
10119
  });
8483
- this.setState({
8484
- expandedDetail: d.id,
8485
- fullscreen: false
10120
+ this.props.doExpandGeneDetail({
10121
+ geneId: geneId,
10122
+ detail: d.id
8486
10123
  });
8487
10124
  }
8488
10125
  }
@@ -8560,7 +10197,7 @@ class $6c5c4f90059875bf$var$Gene extends (0, ($parcel$interopDefault($gXNCa$reac
8560
10197
  /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
8561
10198
  className: "gene-detail-tabs",
8562
10199
  children: this.state.details.map((d, idx)=>{
8563
- const isExpanded = this.state.expandedDetail === d.id;
10200
+ const isExpanded = this.expandedDetail === d.id;
8564
10201
  return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.OverlayTrigger), {
8565
10202
  placement: 'bottom',
8566
10203
  overlay: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Tooltip), {
@@ -8586,7 +10223,8 @@ class $6c5c4f90059875bf$var$Gene extends (0, ($parcel$interopDefault($gXNCa$reac
8586
10223
  title: "View full screen",
8587
10224
  onClick: (e)=>{
8588
10225
  e.stopPropagation();
8589
- this.setState({
10226
+ this.props.doSetGeneFullscreen({
10227
+ geneId: searchResult.id,
8590
10228
  fullscreen: true
8591
10229
  });
8592
10230
  }
@@ -8596,19 +10234,24 @@ class $6c5c4f90059875bf$var$Gene extends (0, ($parcel$interopDefault($gXNCa$reac
8596
10234
  }, idx);
8597
10235
  })
8598
10236
  }),
8599
- this.state.expandedDetail && this.ensureGene(searchResult.id) && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $5c2c79352d3d7b81$export$ec6b54b688be1708), {
10237
+ this.expandedDetail && this.ensureGene(searchResult.id) && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $5c2c79352d3d7b81$export$ec6b54b688be1708), {
8600
10238
  className: "visible-detail",
8601
- fullscreen: this.state.fullscreen,
8602
- onExitFullscreen: ()=>this.setState({
10239
+ fullscreen: this.fullscreen,
10240
+ onExitFullscreen: ()=>this.props.doSetGeneFullscreen({
10241
+ geneId: searchResult.id,
8603
10242
  fullscreen: false
8604
10243
  }),
8605
- title: (this.state.details.find((d)=>d.id === this.state.expandedDetail) || {}).label,
8606
- children: /*#__PURE__*/ (0, ($parcel$interopDefault($gXNCa$react))).createElement($6c5c4f90059875bf$var$inventory[this.state.expandedDetail], this.props)
10244
+ title: (this.state.details.find((d)=>d.id === this.expandedDetail) || {}).label,
10245
+ children: /*#__PURE__*/ (0, ($parcel$interopDefault($gXNCa$react))).createElement($6c5c4f90059875bf$var$inventory[this.expandedDetail], this.props)
8607
10246
  })
8608
10247
  ]
8609
10248
  });
8610
10249
  }
8611
10250
  }
10251
+ // Wrap Gene so each row gets uiViewState + dispatchers. (We don't pull these
10252
+ // in at the GeneList level because the slice we care about is per-geneId; the
10253
+ // component-side getters read by row id.)
10254
+ const $6c5c4f90059875bf$var$ConnectedGene = (0, $gXNCa$reduxbundlerreact.connect)('selectUiViewState', 'doExpandGeneDetail', 'doSetGeneFullscreen', $6c5c4f90059875bf$var$Gene);
8612
10255
  const $6c5c4f90059875bf$var$GeneList = (props)=>{
8613
10256
  if (props.grameneSearch && props.grameneSearch.response && props.grameneTaxonomy) {
8614
10257
  let prev, page, next;
@@ -8650,7 +10293,7 @@ const $6c5c4f90059875bf$var$GeneList = (props)=>{
8650
10293
  next
8651
10294
  ]
8652
10295
  }),
8653
- props.grameneSearch.response.docs.map((g, idx)=>/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)($6c5c4f90059875bf$var$Gene, {
10296
+ props.grameneSearch.response.docs.map((g, idx)=>/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)($6c5c4f90059875bf$var$ConnectedGene, {
8654
10297
  searchResult: g,
8655
10298
  config: props.configuration,
8656
10299
  taxName: props.grameneTaxonomy[g.taxon_id].name,
@@ -12864,12 +14507,19 @@ function $caf32827df861c4e$var$logTickFormat(v) {
12864
14507
  if (a >= 0.01 && a < 10000) return $gXNCa$d3.format('~g')(v);
12865
14508
  return $gXNCa$d3.format('.0e')(v);
12866
14509
  }
12867
- const $caf32827df861c4e$var$ParallelCoordsPlot = ({ rows: rows, fields: fields, scale: scale = 'linear', onBrushChange: onBrushChange, onReorder: onReorder, clearVersion: clearVersion = 0, hoveredId: hoveredId = null, axisLabels: axisLabels = null })=>{
14510
+ const $caf32827df861c4e$var$ParallelCoordsPlot = ({ rows: rows, fields: fields, scale: scale = 'linear', initialSelections: initialSelections = null, onBrushChange: onBrushChange, onReorder: onReorder, clearVersion: clearVersion = 0, hoveredId: hoveredId = null, axisLabels: axisLabels = null })=>{
12868
14511
  const svgRef = (0, $gXNCa$react.useRef)(null);
12869
14512
  const containerRef = (0, $gXNCa$react.useRef)(null);
12870
14513
  // selections in data domain: { [field]: [lo, hi] }
12871
14514
  const selectionsRef = (0, $gXNCa$react.useRef)({});
12872
14515
  const lastClearRef = (0, $gXNCa$react.useRef)(0);
14516
+ // Seed brushes from a restored saved view exactly once (the first render
14517
+ // where `initialSelections` is non-empty). Read via a ref so a change to the
14518
+ // prop — which happens on every brush as it round-trips through the store —
14519
+ // doesn't force an SVG rebuild; the deps below already rerun on data changes.
14520
+ const initialSelectionsRef = (0, $gXNCa$react.useRef)(initialSelections);
14521
+ initialSelectionsRef.current = initialSelections;
14522
+ const seededRef = (0, $gXNCa$react.useRef)(false);
12873
14523
  // Track container size so the d3 render reruns when the user drags the
12874
14524
  // pane resizer (or when the window is resized). The values themselves
12875
14525
  // aren't read inside the effect — the effect always reads clientWidth/
@@ -12900,6 +14550,15 @@ const $caf32827df861c4e$var$ParallelCoordsPlot = ({ rows: rows, fields: fields,
12900
14550
  return ()=>ro.disconnect();
12901
14551
  }, []);
12902
14552
  (0, $gXNCa$react.useEffect)(()=>{
14553
+ if (!seededRef.current) {
14554
+ const init = initialSelectionsRef.current;
14555
+ if (init && Object.keys(init).length) {
14556
+ selectionsRef.current = {
14557
+ ...init
14558
+ };
14559
+ seededRef.current = true;
14560
+ }
14561
+ }
12903
14562
  if (clearVersion !== lastClearRef.current) {
12904
14563
  selectionsRef.current = {};
12905
14564
  lastClearRef.current = clearVersion;
@@ -13767,7 +15426,10 @@ const $1fd2507769d5bd00$var$ExprVizViewCmp = (props)=>{
13767
15426
  onOpenFields: ()=>doToggleExprVizFieldsModal(tid, true),
13768
15427
  onLoad: ()=>doFetchExprVizData(tid),
13769
15428
  onReorder: (next)=>props.doReorderExprVizFields(tid, next),
13770
- onAddRangeQuery: props.doAddGrameneRangeQuery
15429
+ onAddRangeQuery: props.doAddGrameneRangeQuery,
15430
+ onSetVizMode: (m)=>props.doSetExprVizVizMode(tid, m),
15431
+ onSetScale: (s)=>props.doSetExprVizScale(tid, s),
15432
+ onSetBrushes: (b)=>props.doSetExprVizBrushes(tid, b)
13771
15433
  })
13772
15434
  }, tid);
13773
15435
  })
@@ -13927,16 +15589,21 @@ function $1fd2507769d5bd00$var$downloadTsv(filename, rows, fields, studies, expr
13927
15589
  document.body.removeChild(a);
13928
15590
  URL.revokeObjectURL(url);
13929
15591
  }
13930
- const $1fd2507769d5bd00$var$TaxonPanel = ({ taxon: taxon, studies: studies, expressionSamples: expressionSamples, tabState: tabState, onOpenFields: onOpenFields, onLoad: onLoad, onReorder: onReorder, onAddRangeQuery: onAddRangeQuery })=>{
15592
+ const $1fd2507769d5bd00$var$TaxonPanel = ({ taxon: taxon, studies: studies, expressionSamples: expressionSamples, tabState: tabState, onOpenFields: onOpenFields, onLoad: onLoad, onReorder: onReorder, onAddRangeQuery: onAddRangeQuery, onSetVizMode: onSetVizMode, onSetScale: onSetScale, onSetBrushes: onSetBrushes })=>{
13931
15593
  const selected = tabState && tabState.selectedFields || [];
13932
15594
  const rows = tabState && tabState.rows || [];
13933
15595
  const fetchInfo = tabState && tabState.fetch || {
13934
15596
  status: 'idle',
13935
15597
  total: 0
13936
15598
  };
13937
- const [scale, setScale] = (0, $gXNCa$react.useState)('linear');
13938
- const [vizMode, setVizMode] = (0, $gXNCa$react.useState)('heatmap'); // 'parallel' | 'heatmap'
13939
- const [selections, setSelections] = (0, $gXNCa$react.useState)({});
15599
+ // View config (sub-tab, scale, brushes) is persisted in the exprViz bundle
15600
+ // per taxon so a saved view can round-trip it. Driven controlled from here.
15601
+ const scale = tabState && tabState.scale || 'linear';
15602
+ const vizMode = tabState && tabState.vizMode || 'heatmap'; // 'parallel' | 'heatmap'
15603
+ const selections = tabState && tabState.brushes || {};
15604
+ const setScale = (s)=>onSetScale && onSetScale(s);
15605
+ const setVizMode = (m)=>onSetVizMode && onSetVizMode(m);
15606
+ const setSelections = (b)=>onSetBrushes && onSetBrushes(b);
13940
15607
  const [clearVersion, setClearVersion] = (0, $gXNCa$react.useState)(0);
13941
15608
  const [hoveredId, setHoveredId] = (0, $gXNCa$react.useState)(null);
13942
15609
  const [plotHeight, setPlotHeight] = (0, $gXNCa$react.useState)(320);
@@ -14012,15 +15679,6 @@ const $1fd2507769d5bd00$var$TaxonPanel = ({ taxon: taxon, studies: studies, expr
14012
15679
  studies,
14013
15680
  expressionSamples
14014
15681
  ]);
14015
- (0, $gXNCa$react.useEffect)(()=>{
14016
- if (rows.length === 0 && hasBrush) {
14017
- setSelections({});
14018
- setClearVersion((v)=>v + 1);
14019
- }
14020
- }, [
14021
- rows.length,
14022
- hasBrush
14023
- ]);
14024
15682
  return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
14025
15683
  className: "exprviz-tab-panel",
14026
15684
  children: [
@@ -14161,6 +15819,7 @@ const $1fd2507769d5bd00$var$TaxonPanel = ({ taxon: taxon, studies: studies, expr
14161
15819
  rows: rows,
14162
15820
  fields: visibleFields,
14163
15821
  scale: scale,
15822
+ initialSelections: selections,
14164
15823
  onBrushChange: setSelections,
14165
15824
  onReorder: handleReorder,
14166
15825
  clearVersion: clearVersion,
@@ -14193,7 +15852,7 @@ const $1fd2507769d5bd00$var$TaxonPanel = ({ taxon: taxon, studies: studies, expr
14193
15852
  ]
14194
15853
  });
14195
15854
  };
14196
- var $1fd2507769d5bd00$export$2e2bcd8739ae039 = (0, $gXNCa$reduxbundlerreact.connect)('selectExprViz', 'selectExprVizPivot', 'selectExprVizActiveTaxon', 'selectGrameneMaps', 'selectExpressionStudies', 'selectExpressionSamples', 'doSetExprVizActiveTaxon', 'doToggleExprVizFieldsModal', 'doFetchExprVizData', 'doReorderExprVizFields', 'doAddGrameneRangeQuery', $1fd2507769d5bd00$var$ExprVizViewCmp);
15855
+ var $1fd2507769d5bd00$export$2e2bcd8739ae039 = (0, $gXNCa$reduxbundlerreact.connect)('selectExprViz', 'selectExprVizPivot', 'selectExprVizActiveTaxon', 'selectGrameneMaps', 'selectExpressionStudies', 'selectExpressionSamples', 'doSetExprVizActiveTaxon', 'doToggleExprVizFieldsModal', 'doFetchExprVizData', 'doReorderExprVizFields', 'doSetExprVizVizMode', 'doSetExprVizScale', 'doSetExprVizBrushes', 'doAddGrameneRangeQuery', $1fd2507769d5bd00$var$ExprVizViewCmp);
14197
15856
 
14198
15857
 
14199
15858
 
@@ -14682,7 +16341,7 @@ const $597fe213417ee6ca$var$SortableTh = ({ label: label, sortKey: sortKey, acti
14682
16341
  }) : inner
14683
16342
  });
14684
16343
  };
14685
- const $597fe213417ee6ca$var$OntologySection = ({ block: block, search: search, onAddFilter: onAddFilter })=>{
16344
+ const $597fe213417ee6ca$var$OntologySection = ({ block: block, search: search, sort: sort, onSortChange: onSortChange, onAddFilter: onAddFilter })=>{
14686
16345
  const filtered = (0, $gXNCa$react.useMemo)(()=>{
14687
16346
  if (!search) return block.rows;
14688
16347
  const needle = search.toLowerCase();
@@ -14692,14 +16351,13 @@ const $597fe213417ee6ca$var$OntologySection = ({ block: block, search: search, o
14692
16351
  search
14693
16352
  ]);
14694
16353
  const showType = $597fe213417ee6ca$var$ONTS_WITH_TYPE_COLUMN.has(block.ontology);
14695
- const [sortKey, setSortKey] = (0, $gXNCa$react.useState)('pAdj');
14696
- const [sortDir, setSortDir] = (0, $gXNCa$react.useState)('asc');
16354
+ // Sort state is persisted per section in the bundle ui so a saved view
16355
+ // restores it. Defaults match the historical local-state initials.
16356
+ const sortKey = sort && sort.key || 'pAdj';
16357
+ const sortDir = sort && sort.dir || 'asc';
14697
16358
  const handleSort = (key)=>{
14698
- if (key === sortKey) setSortDir((d)=>d === 'asc' ? 'desc' : 'asc');
14699
- else {
14700
- setSortKey(key);
14701
- setSortDir($597fe213417ee6ca$var$SORT_DEFAULT_DIR[key] || 'asc');
14702
- }
16359
+ if (key === sortKey) onSortChange(block.ontology, sortKey, sortDir === 'asc' ? 'desc' : 'asc');
16360
+ else onSortChange(block.ontology, key, $597fe213417ee6ca$var$SORT_DEFAULT_DIR[key] || 'asc');
14703
16361
  };
14704
16362
  const sorted = (0, $gXNCa$react.useMemo)(()=>{
14705
16363
  const accessor = $597fe213417ee6ca$var$SORT_ACCESSORS[sortKey];
@@ -14896,6 +16554,17 @@ const $597fe213417ee6ca$var$TaxonPanel = ({ taxon: taxon, ontologyEnrichment: on
14896
16554
  ] : [];
14897
16555
  // Hide ontologies that aren't used in this species at all.
14898
16556
  const blocks = allBlocks.filter((b)=>b.tested > 0);
16557
+ const handleSortChange = (sectionKey, key, dir)=>{
16558
+ onUiChange({
16559
+ sort: {
16560
+ ...ui.sort || {},
16561
+ [sectionKey]: {
16562
+ key: key,
16563
+ dir: dir
16564
+ }
16565
+ }
16566
+ });
16567
+ };
14899
16568
  return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
14900
16569
  className: "oe-panel",
14901
16570
  children: [
@@ -14923,6 +16592,8 @@ const $597fe213417ee6ca$var$TaxonPanel = ({ taxon: taxon, ontologyEnrichment: on
14923
16592
  children: blocks.map((b)=>/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)($597fe213417ee6ca$var$OntologySection, {
14924
16593
  block: b,
14925
16594
  search: ui.search,
16595
+ sort: ui.sort && ui.sort[b.ontology],
16596
+ onSortChange: handleSortChange,
14926
16597
  onAddFilter: onAddFilter
14927
16598
  }, b.ontology))
14928
16599
  })
@@ -15061,6 +16732,223 @@ var $597fe213417ee6ca$export$2e2bcd8739ae039 = (0, $gXNCa$reduxbundlerreact.conn
15061
16732
 
15062
16733
 
15063
16734
 
16735
+ // "Save this view" UI — sits inside the Auth sidebar panel.
16736
+ //
16737
+ // Sign-in-gated by Auth.js (we only mount this when `user` is truthy). Opens
16738
+ // a modal with label + description + visibility toggle; on save, swaps to a
16739
+ // post-save state that shows the shareable URL with a copy button.
16740
+ //
16741
+ // Talks to the savedViews bundle. The snapshot itself is built inside
16742
+ // doSaveView via selectViewSnapshot — this component doesn't know or care
16743
+ // what the snapshot looks like.
16744
+
16745
+
16746
+
16747
+
16748
+
16749
+ const $e63a3bee79bd1c7a$var$SaveViewButtonCmp = ({ user: user, savedViews: savedViews, doSaveView: doSaveView, doResetSavedViewState: doResetSavedViewState })=>{
16750
+ const [showModal, setShowModal] = (0, $gXNCa$react.useState)(false);
16751
+ const [label, setLabel] = (0, $gXNCa$react.useState)('');
16752
+ const [description, setDescription] = (0, $gXNCa$react.useState)('');
16753
+ const [isPublic, setIsPublic] = (0, $gXNCa$react.useState)(false);
16754
+ const [shareUrl, setShareUrl] = (0, $gXNCa$react.useState)(null);
16755
+ const [copied, setCopied] = (0, $gXNCa$react.useState)(false);
16756
+ const reset = (0, $gXNCa$react.useCallback)(()=>{
16757
+ setLabel('');
16758
+ setDescription('');
16759
+ setIsPublic(false);
16760
+ setShareUrl(null);
16761
+ setCopied(false);
16762
+ doResetSavedViewState();
16763
+ }, [
16764
+ doResetSavedViewState
16765
+ ]);
16766
+ const handleOpen = ()=>{
16767
+ reset();
16768
+ setShowModal(true);
16769
+ };
16770
+ const handleClose = ()=>{
16771
+ setShowModal(false);
16772
+ reset();
16773
+ };
16774
+ const handleSave = async (e)=>{
16775
+ e.preventDefault();
16776
+ if (!label.trim()) return;
16777
+ try {
16778
+ const { shareUrl: shareUrl } = await doSaveView({
16779
+ user: user,
16780
+ label: label.trim(),
16781
+ description: description.trim(),
16782
+ isPublic: isPublic
16783
+ });
16784
+ setShareUrl(shareUrl);
16785
+ } catch (_) {
16786
+ // savedViews.saveError is already set; modal renders it
16787
+ }
16788
+ };
16789
+ const handleCopy = async ()=>{
16790
+ if (!shareUrl) return;
16791
+ try {
16792
+ await navigator.clipboard.writeText(shareUrl);
16793
+ setCopied(true);
16794
+ setTimeout(()=>setCopied(false), 1500);
16795
+ } catch (_) {}
16796
+ };
16797
+ const saving = savedViews && savedViews.saving;
16798
+ const saveError = savedViews && savedViews.saveError;
16799
+ return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)((0, $gXNCa$reactjsxruntime.Fragment), {
16800
+ children: [
16801
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Button), {
16802
+ size: "sm",
16803
+ variant: "outline-primary",
16804
+ style: {
16805
+ marginTop: 8
16806
+ },
16807
+ onClick: handleOpen,
16808
+ title: "Save the current filters, views, and detail tabs as a shareable link",
16809
+ children: "Save this view"
16810
+ }),
16811
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)((0, $gXNCa$reactbootstrap.Modal), {
16812
+ show: showModal,
16813
+ onHide: handleClose,
16814
+ centered: true,
16815
+ children: [
16816
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Modal).Header, {
16817
+ closeButton: true,
16818
+ children: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Modal).Title, {
16819
+ children: shareUrl ? 'View saved' : 'Save this view'
16820
+ })
16821
+ }),
16822
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)((0, $gXNCa$reactbootstrap.Modal).Body, {
16823
+ children: [
16824
+ !shareUrl && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)((0, $gXNCa$reactbootstrap.Form), {
16825
+ onSubmit: handleSave,
16826
+ children: [
16827
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)((0, $gXNCa$reactbootstrap.Form).Group, {
16828
+ className: "mb-3",
16829
+ children: [
16830
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Form).Label, {
16831
+ children: "Name"
16832
+ }),
16833
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Form).Control, {
16834
+ autoFocus: true,
16835
+ type: "text",
16836
+ placeholder: "e.g. TAIR loci with TF binding sites",
16837
+ value: label,
16838
+ onChange: (e)=>setLabel(e.target.value),
16839
+ disabled: saving,
16840
+ required: true
16841
+ })
16842
+ ]
16843
+ }),
16844
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)((0, $gXNCa$reactbootstrap.Form).Group, {
16845
+ className: "mb-3",
16846
+ children: [
16847
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Form).Label, {
16848
+ children: "Description (optional)"
16849
+ }),
16850
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Form).Control, {
16851
+ as: "textarea",
16852
+ rows: 2,
16853
+ value: description,
16854
+ onChange: (e)=>setDescription(e.target.value),
16855
+ disabled: saving
16856
+ })
16857
+ ]
16858
+ }),
16859
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Form).Group, {
16860
+ className: "mb-3",
16861
+ children: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Form).Check, {
16862
+ type: "switch",
16863
+ id: "save-view-public",
16864
+ label: "Make this view public (visible to anyone with the link)",
16865
+ checked: isPublic,
16866
+ onChange: (e)=>setIsPublic(e.target.checked),
16867
+ disabled: saving
16868
+ })
16869
+ }),
16870
+ saveError && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Alert), {
16871
+ variant: "danger",
16872
+ children: saveError
16873
+ }),
16874
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
16875
+ className: "d-flex justify-content-end gap-2",
16876
+ children: [
16877
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Button), {
16878
+ variant: "secondary",
16879
+ onClick: handleClose,
16880
+ disabled: saving,
16881
+ children: "Cancel"
16882
+ }),
16883
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Button), {
16884
+ type: "submit",
16885
+ variant: "primary",
16886
+ disabled: saving || !label.trim(),
16887
+ children: saving ? /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)((0, $gXNCa$reactjsxruntime.Fragment), {
16888
+ children: [
16889
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Spinner), {
16890
+ as: "span",
16891
+ size: "sm",
16892
+ animation: "border"
16893
+ }),
16894
+ " Saving\u2026"
16895
+ ]
16896
+ }) : 'Save'
16897
+ })
16898
+ ]
16899
+ })
16900
+ ]
16901
+ }),
16902
+ shareUrl && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)((0, $gXNCa$reactjsxruntime.Fragment), {
16903
+ children: [
16904
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("p", {
16905
+ children: "Share this link to let others open the same search and detail state:"
16906
+ }),
16907
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)((0, $gXNCa$reactbootstrap.InputGroup), {
16908
+ children: [
16909
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Form).Control, {
16910
+ type: "text",
16911
+ value: shareUrl,
16912
+ readOnly: true,
16913
+ onFocus: (e)=>e.target.select()
16914
+ }),
16915
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Button), {
16916
+ variant: copied ? 'success' : 'outline-secondary',
16917
+ onClick: handleCopy,
16918
+ children: copied ? /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)((0, $gXNCa$reactjsxruntime.Fragment), {
16919
+ children: [
16920
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reacticonsbs.BsCheck2), {}),
16921
+ " Copied"
16922
+ ]
16923
+ }) : /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)((0, $gXNCa$reactjsxruntime.Fragment), {
16924
+ children: [
16925
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reacticonsbs.BsClipboard), {}),
16926
+ " Copy"
16927
+ ]
16928
+ })
16929
+ })
16930
+ ]
16931
+ }),
16932
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
16933
+ className: "mt-3 d-flex justify-content-end",
16934
+ children: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Button), {
16935
+ variant: "primary",
16936
+ onClick: handleClose,
16937
+ children: "Done"
16938
+ })
16939
+ })
16940
+ ]
16941
+ })
16942
+ ]
16943
+ })
16944
+ ]
16945
+ })
16946
+ ]
16947
+ });
16948
+ };
16949
+ var $e63a3bee79bd1c7a$export$2e2bcd8739ae039 = (0, $gXNCa$reduxbundlerreact.connect)('selectSavedViews', 'doSaveView', 'doResetSavedViewState', $e63a3bee79bd1c7a$var$SaveViewButtonCmp);
16950
+
16951
+
15064
16952
  const $c5d403787de8b05f$var$provider = new (0, $gXNCa$firebaseauth.GoogleAuthProvider)();
15065
16953
  const $c5d403787de8b05f$var$Auth = (props)=>{
15066
16954
  const firebaseConfig = props.configuration && props.configuration.firebaseConfig;
@@ -15070,10 +16958,25 @@ const $c5d403787de8b05f$var$Auth = (props)=>{
15070
16958
  }, [
15071
16959
  firebaseConfig
15072
16960
  ]);
15073
- const [user, setUser] = (0, $gXNCa$react.useState)({});
16961
+ const [user, setUser] = (0, $gXNCa$react.useState)(null);
15074
16962
  const [open, setOpen] = (0, $gXNCa$react.useState)(true);
16963
+ // Subscribe once, unsubscribe on unmount. On each auth-state emission,
16964
+ // also poke the shared-view boot path so a `?view=<hash>` for a private
16965
+ // view gets retried with the now-available Bearer token. bootViewFromUrl
16966
+ // no-ops when the param isn't present, so this is cheap.
16967
+ (0, $gXNCa$react.useEffect)(()=>{
16968
+ if (!auth) return undefined;
16969
+ const unsub = (0, $gXNCa$firebaseauth.onAuthStateChanged)(auth, (u)=>{
16970
+ setUser(u);
16971
+ props.doBootSharedView({
16972
+ user: u
16973
+ });
16974
+ });
16975
+ return unsub;
16976
+ }, [
16977
+ auth
16978
+ ]);
15075
16979
  if (!auth) return null;
15076
- (0, $gXNCa$firebaseauth.onAuthStateChanged)(auth, (user)=>setUser(user));
15077
16980
  function handleLogin() {
15078
16981
  (0, $gXNCa$firebaseauth.signInWithPopup)(auth, $c5d403787de8b05f$var$provider).then((result)=>{
15079
16982
  setUser(result.user);
@@ -15109,27 +17012,34 @@ const $c5d403787de8b05f$var$Auth = (props)=>{
15109
17012
  })
15110
17013
  ]
15111
17014
  }),
15112
- open && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
17015
+ open && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
15113
17016
  className: "sidebar-section-body",
15114
- children: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
15115
- children: user ? /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Button), {
15116
- size: "sm",
15117
- variant: "success",
15118
- onClick: handleLogout,
15119
- children: user.displayName
15120
- }) : /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Button), {
15121
- size: "sm",
15122
- variant: "success",
15123
- onClick: handleLogin,
15124
- children: "Login"
17017
+ children: [
17018
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
17019
+ children: user ? /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Button), {
17020
+ size: "sm",
17021
+ variant: "success",
17022
+ onClick: handleLogout,
17023
+ children: user.displayName
17024
+ }) : /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Button), {
17025
+ size: "sm",
17026
+ variant: "success",
17027
+ onClick: handleLogin,
17028
+ children: "Login"
17029
+ })
17030
+ }),
17031
+ user && user.uid && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
17032
+ children: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $e63a3bee79bd1c7a$export$2e2bcd8739ae039), {
17033
+ user: user
17034
+ })
15125
17035
  })
15126
- })
17036
+ ]
15127
17037
  })
15128
17038
  ]
15129
17039
  })
15130
17040
  });
15131
17041
  };
15132
- var $c5d403787de8b05f$export$2e2bcd8739ae039 = (0, $gXNCa$reduxbundlerreact.connect)('selectConfiguration', $c5d403787de8b05f$var$Auth);
17042
+ var $c5d403787de8b05f$export$2e2bcd8739ae039 = (0, $gXNCa$reduxbundlerreact.connect)('selectConfiguration', 'doBootSharedView', $c5d403787de8b05f$var$Auth);
15133
17043
 
15134
17044
 
15135
17045
 
@@ -15505,4 +17415,5 @@ const $693dd8c7a5607c3a$export$5cb791131c501f6a = (0, $gXNCa$reduxbundlerreact.c
15505
17415
 
15506
17416
 
15507
17417
 
17418
+
15508
17419
  //# sourceMappingURL=index.js.map