@czi-sds/data-viz 1.0.1 → 2.1.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.
package/dist/index.esm.js CHANGED
@@ -1,11 +1,138 @@
1
1
  "use client";
2
2
  import { init } from "echarts";
3
- import { cloneElement, forwardRef, memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
3
+ import { Fragment, cloneElement, forwardRef, memo, useCallback, useContext, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState } from "react";
4
4
  import { throttle } from "lodash";
5
5
  import styled from "@emotion/styled";
6
- import { jsx, jsxs } from "react/jsx-runtime";
7
- import { Legend, Tooltip, TooltipTable, fontHeaderS, fontTabularSemiboldXxxxs, generateDiscreteColors, getCorners, getMode, getSemanticColors, getSpaces } from "@czi-sds/components";
8
- import { useTheme } from "@mui/material";
6
+ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
7
+ import { Button, Legend, Theme, Tooltip, TooltipTable, defaultTheme, fontBodyXs, fontBodyXxxs, fontCodeXs, fontHeaderM, fontHeaderS, fontTabularSemiboldXxxxs, generateDiscreteColors, getCorners, getMode, getSemanticColors, getSpaces } from "@czi-sds/components";
8
+ import { ThemeProvider, useTheme } from "@mui/material/styles";
9
+ import { ArrowCounterClockwiseIcon, CheckIcon, CopyIcon, QuestionIcon } from "@phosphor-icons/react";
10
+ import { Structure, StructureElement, StructureProperties, Unit } from "molstar/lib/mol-model/structure";
11
+ import { createPluginUI } from "molstar/lib/mol-plugin-ui";
12
+ import { renderReact18 } from "molstar/lib/mol-plugin-ui/react18";
13
+ import { DefaultPluginUISpec } from "molstar/lib/mol-plugin-ui/spec";
14
+ import { PluginBehaviors } from "molstar/lib/mol-plugin/behavior";
15
+ import { PluginConfig } from "molstar/lib/mol-plugin/config";
16
+ import { Representation } from "molstar/lib/mol-repr/representation";
17
+ import { BehaviorSubject, Subject } from "rxjs";
18
+ import { EveryLoci, Loci } from "molstar/lib/mol-model/loci";
19
+ import { PluginReactContext } from "molstar/lib/mol-plugin-ui/base";
20
+ import { ButtonsType, getButton, getButtons, getModifiers } from "molstar/lib/mol-util/input/input-observer";
21
+ import { throttleTime } from "rxjs/operators";
22
+ import { OrderedSet } from "molstar/lib/mol-data/int";
23
+ import { useTheme as useTheme$1 } from "@mui/material";
24
+ import { PluginStateObject } from "molstar/lib/mol-plugin-state/objects";
25
+ import { getChainOptions, getModelEntityOptions, getOperatorOptions, getSequenceWrapper, getStructureOptions } from "molstar/lib/mol-plugin-ui/sequence";
26
+ import { StateSelection } from "molstar/lib/mol-state/state/selection";
27
+ import { arrayEqual } from "molstar/lib/mol-util";
28
+ import { ParamDefinition } from "molstar/lib/mol-util/param-definition";
29
+ import { Toasts } from "molstar/lib/mol-plugin-ui/toast";
30
+ import { Viewport } from "molstar/lib/mol-plugin-ui/viewport";
31
+ import { PluginCommands } from "molstar/lib/mol-plugin/commands";
32
+ import { Vec3 } from "molstar/lib/mol-math/linear-algebra";
33
+ import { CustomElementProperty } from "molstar/lib/mol-model-props/common/custom-element-property";
34
+ import { Color } from "molstar/lib/mol-util/color";
35
+ //#region src/common/colorScales.ts
36
+ /**
37
+ * Perceptually uniform sequential scale (matplotlib's plasma), the default for
38
+ * value overlays.
39
+ */
40
+ const PLASMA_COLOR_SCALE = {
41
+ kind: "continuous",
42
+ stops: [
43
+ {
44
+ color: "#0D0887",
45
+ t: 0
46
+ },
47
+ {
48
+ color: "#7E03A8",
49
+ t: .25
50
+ },
51
+ {
52
+ color: "#CC4778",
53
+ t: .5
54
+ },
55
+ {
56
+ color: "#F89441",
57
+ t: .75
58
+ },
59
+ {
60
+ color: "#F0F921",
61
+ t: 1
62
+ }
63
+ ]
64
+ };
65
+ /** Parses `#RRGGBB` (or `RRGGBB`) into an RGB triplet. */
66
+ function hexToRgb(hex) {
67
+ const h = hex.replace("#", "");
68
+ return [
69
+ parseInt(h.substring(0, 2), 16),
70
+ parseInt(h.substring(2, 4), 16),
71
+ parseInt(h.substring(4, 6), 16)
72
+ ];
73
+ }
74
+ /** Linearly interpolates a continuous scale at `t`, clamped to 0-1. */
75
+ function sampleContinuousScale(stops, t) {
76
+ if (stops.length === 0) return [
77
+ 0,
78
+ 0,
79
+ 0
80
+ ];
81
+ const first = stops[0];
82
+ const last = stops[stops.length - 1];
83
+ const clamped = Math.min(1, Math.max(0, t));
84
+ let lo = first;
85
+ let hi = last;
86
+ for (let i = 0; i < stops.length - 1; i++) {
87
+ const current = stops[i];
88
+ const next = stops[i + 1];
89
+ if (clamped >= current.t && clamped <= next.t) {
90
+ lo = current;
91
+ hi = next;
92
+ break;
93
+ }
94
+ }
95
+ const loRgb = hexToRgb(lo.color);
96
+ const hiRgb = hexToRgb(hi.color);
97
+ const f = hi.t === lo.t ? 0 : (clamped - lo.t) / (hi.t - lo.t);
98
+ return [
99
+ Math.round(loRgb[0] + f * (hiRgb[0] - loRgb[0])),
100
+ Math.round(loRgb[1] + f * (hiRgb[1] - loRgb[1])),
101
+ Math.round(loRgb[2] + f * (hiRgb[2] - loRgb[2]))
102
+ ];
103
+ }
104
+ /**
105
+ * Picks the band a normalized value falls into on a stepped scale.
106
+ *
107
+ * A value lands in the first band whose threshold it falls short of, which puts
108
+ * each threshold at the bottom of the band above it: against thresholds of 0.5,
109
+ * 0.7 and 0.9, a value of exactly 0.7 reads as the 0.7-0.9 band, not the one
110
+ * below.
111
+ */
112
+ function sampleSteppedScale(stops, t) {
113
+ for (const stop of stops) if (t < stop.threshold) return hexToRgb(stop.color);
114
+ const last = stops[stops.length - 1];
115
+ return last ? hexToRgb(last.color) : [
116
+ 0,
117
+ 0,
118
+ 0
119
+ ];
120
+ }
121
+ /**
122
+ * Colors a raw value against a scale, normalizing it into `min`-`max` first.
123
+ * Returns null when the value sits at or below `min`, or when the range is
124
+ * degenerate, so callers can fall back to a neutral color.
125
+ */
126
+ function sampleColorScale(scale, value, max, min = 0) {
127
+ if (max <= min || value <= min) return null;
128
+ const t = Math.min(1, (value - min) / (max - min));
129
+ return scale.kind === "continuous" ? sampleContinuousScale(scale.stops, t) : sampleSteppedScale(scale.stops, t);
130
+ }
131
+ /** CSS `linear-gradient` spanning a continuous scale, for the legend bar. */
132
+ function toCssGradient(stops) {
133
+ return `linear-gradient(to right, ${stops.map((stop) => `${stop.color} ${stop.t * 100}%`).join(", ")})`;
134
+ }
135
+ //#endregion
9
136
  //#region src/common/utils.ts
10
137
  const EMPTY_OBJECT = {};
11
138
  //#endregion
@@ -93,7 +220,7 @@ function _objectSpread2(e) {
93
220
  }
94
221
  //#endregion
95
222
  //#region src/core/HeatmapChart/hooks/utils.ts
96
- const _excluded$2 = [
223
+ const _excluded$3 = [
97
224
  "axisPointer",
98
225
  "dataZoom",
99
226
  "series",
@@ -107,7 +234,7 @@ function createChartOptions(props) {
107
234
  const { axisPointer, camera, data, dataZoom, emphasis, encode, grid: gridProp, itemStyle = DEFAULT_ITEM_STYLE, options, symbolSize, symbol = "rect" } = props;
108
235
  const { defaultAxisPointer, defaultDataZoom, defaultEmphasis, defaultGrid, defaultXAxis, defaultYAxis } = generateDefaultValues(props);
109
236
  const customGrid = typeof gridProp === "function" ? gridProp(defaultGrid) : gridProp;
110
- const _ref = options || {}, { axisPointer: optionsAxisPointer, dataZoom: optionsDataZoom, series: optionsSeries, xAxis: optionsXAxis, yAxis: optionsYAxis } = _ref, optionsRest = _objectWithoutProperties(_ref, _excluded$2);
237
+ const _ref = options || {}, { axisPointer: optionsAxisPointer, dataZoom: optionsDataZoom, series: optionsSeries, xAxis: optionsXAxis, yAxis: optionsYAxis } = _ref, optionsRest = _objectWithoutProperties(_ref, _excluded$3);
111
238
  return _objectSpread2({
112
239
  animation: false,
113
240
  axisPointer: mergeAxisPointer(defaultAxisPointer, axisPointer, optionsAxisPointer),
@@ -221,7 +348,7 @@ function generateDefaultValues(props) {
221
348
  //#endregion
222
349
  //#region src/core/HeatmapChart/hooks/useUpdateChart.ts
223
350
  const UPDATE_THROTTLE_MS = 100;
224
- function useUpdateChart({ axisPointer, camera, chart, data, emphasis, xAxisData, yAxisData, width, height, encode, itemStyle, symbol, symbolSize, grid, options, onEvents }) {
351
+ function useUpdateChart({ axisPointer, camera, chart, data, dataZoom, emphasis, xAxisData, yAxisData, width, height, encode, itemStyle, symbol, symbolSize, grid, options, onEvents }) {
225
352
  const throttledUpdateChart = useMemo(() => {
226
353
  return throttle(() => {
227
354
  if (!chart || !data || !xAxisData || !yAxisData) return;
@@ -230,6 +357,7 @@ function useUpdateChart({ axisPointer, camera, chart, data, emphasis, xAxisData,
230
357
  axisPointer,
231
358
  camera,
232
359
  data,
360
+ dataZoom,
233
361
  emphasis,
234
362
  encode,
235
363
  grid,
@@ -261,6 +389,7 @@ function useUpdateChart({ axisPointer, camera, chart, data, emphasis, xAxisData,
261
389
  camera,
262
390
  chart,
263
391
  data,
392
+ dataZoom,
264
393
  emphasis,
265
394
  xAxisData,
266
395
  yAxisData,
@@ -284,6 +413,7 @@ function useUpdateChart({ axisPointer, camera, chart, data, emphasis, xAxisData,
284
413
  camera,
285
414
  chart,
286
415
  data,
416
+ dataZoom,
287
417
  emphasis,
288
418
  xAxisData,
289
419
  yAxisData,
@@ -312,12 +442,13 @@ function getWidthAndHeight({ width, height }) {
312
442
  }
313
443
  //#endregion
314
444
  //#region src/core/HeatmapChart/index.tsx
315
- const _excluded$1 = [
445
+ const _excluded$2 = [
316
446
  "axisPointer",
317
447
  "width",
318
448
  "height",
319
449
  "echartsRendererMode",
320
450
  "camera",
451
+ "dataZoom",
321
452
  "onEvents",
322
453
  "xAxisData",
323
454
  "yAxisData",
@@ -331,7 +462,7 @@ const _excluded$1 = [
331
462
  "options"
332
463
  ];
333
464
  var HeatmapChart_default = memo(forwardRef((props, ref) => {
334
- const { axisPointer, width, height, echartsRendererMode = "svg", camera, onEvents, xAxisData, yAxisData, data, encode, emphasis, itemStyle, symbol, symbolSize = 5, grid, options } = props, rest = _objectWithoutProperties(props, _excluded$1);
465
+ const { axisPointer, width, height, echartsRendererMode = "svg", camera, dataZoom, onEvents, xAxisData, yAxisData, data, encode, emphasis, itemStyle, symbol, symbolSize = 5, grid, options } = props, rest = _objectWithoutProperties(props, _excluded$2);
335
466
  if (!width || !height) throw Error("Heatmap must have width and height bigger than Zero!");
336
467
  const innerRef = useRef(null);
337
468
  /**
@@ -384,6 +515,7 @@ var HeatmapChart_default = memo(forwardRef((props, ref) => {
384
515
  camera,
385
516
  chart,
386
517
  data,
518
+ dataZoom,
387
519
  emphasis,
388
520
  encode,
389
521
  grid,
@@ -417,27 +549,294 @@ var HeatmapChart_default = memo(forwardRef((props, ref) => {
417
549
  else ref.current = element;
418
550
  }
419
551
  }));
420
- //#endregion
421
- //#region src/core/StackedBarChart/style.ts
422
- const ChartWrapper = styled("div")`
552
+ /** Height of the color bar itself. */
553
+ const BAR_HEIGHT = 8;
554
+ /**
555
+ * Inset applied to the outer tick labels on a stepped scale. Labels sit on the
556
+ * boundaries *between* bands, so the row is padded by half a band to line each
557
+ * label up with its boundary rather than with a band's center.
558
+ */
559
+ const STEPPED_LABEL_INSET = 5;
560
+ const LegendWrapper = styled("div")`
561
+ width: ${142}px;
562
+ `;
563
+ const LegendLabels = styled("div")`
564
+ ${fontBodyXxxs}
565
+
423
566
  display: flex;
424
- flex-direction: column;
425
- width: ${(props) => typeof props.width === "number" ? `${props.width}px` : props.width};
567
+ justify-content: space-between;
568
+ width: 100%;
569
+
570
+ ${(props) => {
571
+ var _semanticColors$base;
572
+ const semanticColors = getSemanticColors(props);
573
+ return `
574
+ color: ${semanticColors === null || semanticColors === void 0 || (_semanticColors$base = semanticColors.base) === null || _semanticColors$base === void 0 ? void 0 : _semanticColors$base.textTertiaryOnDark};
575
+ `;
576
+ }}
426
577
  `;
427
- const TitleContainer = styled("div")`
578
+ const SteppedLabels = styled(LegendLabels)`
579
+ &::before,
580
+ &::after {
581
+ content: "";
582
+ width: ${STEPPED_LABEL_INSET}px;
583
+ }
584
+ `;
585
+ const LegendBar = styled("div")`
428
586
  display: flex;
429
- align-items: center;
587
+ height: ${BAR_HEIGHT}px;
588
+ width: 100%;
589
+ `;
590
+ const LegendBand = styled("div")`
591
+ flex: 1;
592
+ background-color: ${(props) => props.color};
593
+ `;
594
+ const LegendGradient = styled("div")`
595
+ height: ${BAR_HEIGHT}px;
596
+ width: 100%;
597
+ background: ${(props) => props.gradient};
598
+ `;
599
+ //#endregion
600
+ //#region src/core/ProteinStructureViewer/components/ColorScaleLegend/index.tsx
601
+ /** Decimal places used for the ticks on a continuous scale. */
602
+ const TICK_PRECISION = 2;
603
+ /**
604
+ * The color key beneath the structure viewer. A stepped scale renders as
605
+ * discrete bands with a tick at each boundary; a continuous scale renders as a
606
+ * gradient running from `min` to `max`, the range its values are normalized
607
+ * into before they are sampled.
608
+ */
609
+ function ColorScaleLegend({ max = null, min = 0, scale }) {
610
+ if (scale.kind === "stepped") return /* @__PURE__ */ jsxs(LegendWrapper, { children: [/* @__PURE__ */ jsx(SteppedLabels, { children: scale.stops.slice(0, -1).map((stop) => /* @__PURE__ */ jsx("div", { children: stop.label }, stop.label)) }), /* @__PURE__ */ jsx(LegendBar, { children: scale.stops.map((stop) => /* @__PURE__ */ jsx(LegendBand, { color: stop.color }, stop.label)) })] });
611
+ return /* @__PURE__ */ jsxs(LegendWrapper, { children: [/* @__PURE__ */ jsxs(LegendLabels, { children: [/* @__PURE__ */ jsx("div", { children: min.toFixed(TICK_PRECISION) }), /* @__PURE__ */ jsx("div", { children: max !== null ? max.toFixed(TICK_PRECISION) : "" })] }), /* @__PURE__ */ jsx(LegendGradient, { gradient: toCssGradient(scale.stops) })] });
612
+ }
613
+ //#endregion
614
+ //#region src/core/ProteinStructureViewer/style.ts
615
+ /** Smallest box in which Mol* can still lay out a usable viewport. */
616
+ const MIN_HEIGHT = 190;
617
+ const MIN_WIDTH = 200;
618
+ /**
619
+ * Name of the size container the root declares below, which is what lets the
620
+ * sequence panel and the legend reflow to the viewer's own width rather than
621
+ * the page's.
622
+ */
623
+ const VIEWER_CONTAINER_NAME = "sds-structure-viewer";
624
+ /**
625
+ * Height reserved for the sequence panel along the bottom, and the viewer
626
+ * width above which it grows. Measured against the container above rather than
627
+ * the page, so a viewer embedded in a wider page sizes to the room it has.
628
+ *
629
+ * The legend sits directly on top of the panel and offsets itself by these
630
+ * same three values, so it reads them from here rather than restating them.
631
+ * Copies that drifted would leave the legend floating off the panel.
632
+ */
633
+ const SEQUENCE_HEIGHT = "max(104px, 30%)";
634
+ const SEQUENCE_HEIGHT_WIDE = "max(134px, 32%)";
635
+ /**
636
+ * The element Mol* mounts into.
637
+ *
638
+ * This has to be its own node rather than the root: Mol* takes ownership of its
639
+ * target and replaces whatever is in it, so anything the viewer renders
640
+ * alongside the plugin (the legend) has to be a sibling of this, not a child.
641
+ */
642
+ const PluginMount = styled("div")`
643
+ position: absolute;
644
+ inset: 0;
645
+ `;
646
+ /**
647
+ * Root container for the viewer.
648
+ *
649
+ * Mol* ships no stylesheet of its own here: the viewport and sequence panel are
650
+ * replaced with custom components and the remaining chrome is hidden, so all
651
+ * that is left to style is the layout scaffolding. Those overrides are nested
652
+ * below rather than declared globally, so consumers do not have to import
653
+ * anything extra and two viewers on a page cannot fight over the same
654
+ * selectors.
655
+ *
656
+ * The element also declares itself a size container, which is what lets the
657
+ * legend reflow to the viewer's own width instead of the page's.
658
+ */
659
+ const ViewerRoot = styled("div")`
660
+ position: relative;
661
+ overflow: hidden;
662
+ width: 100%;
663
+ height: 100%;
664
+ min-width: ${MIN_WIDTH}px;
665
+ min-height: ${MIN_HEIGHT}px;
666
+ container-type: inline-size;
667
+ container-name: ${VIEWER_CONTAINER_NAME};
668
+
669
+ .msp-plugin {
670
+ position: absolute !important;
671
+ inset: 0 !important;
672
+ width: 100% !important;
673
+ height: 100% !important;
674
+ }
675
+
676
+ /* Mol* chrome that the custom viewport and sequence panel replace. */
677
+ .msp-plugin .msp-viewport-controls-buttons,
678
+ .msp-plugin .msp-layout-expanded,
679
+ .msp-plugin .msp-log,
680
+ .msp-plugin .msp-left-panel-controls,
681
+ .msp-plugin .msp-layout-region.msp-layout-left,
682
+ .msp-plugin .msp-layout-region.msp-layout-right,
683
+ .msp-plugin .msp-layout-region.msp-layout-bottom {
684
+ display: none !important;
685
+ }
686
+
687
+ .msp-plugin canvas {
688
+ display: block !important;
689
+ }
690
+
691
+ .msp-plugin .msp-layout-static {
692
+ height: 100%;
693
+ }
694
+
695
+ .msp-plugin .msp-sequence-wrapper-non-empty {
696
+ overflow-y: auto;
697
+ }
698
+
699
+ /*
700
+ * Mol* puts the sequence panel in its "top" region; move it to the bottom and
701
+ * shrink the main viewport region to match, so the structure sits above the
702
+ * sequence rather than behind it. The panel grows on wider viewers, and the
703
+ * viewport region's offset follows it.
704
+ */
705
+ ${(props) => {
706
+ const { showSequenceViewer } = props;
707
+ return `
708
+ .msp-plugin .msp-layout-region.msp-layout-top {
709
+ display: ${showSequenceViewer ? "block" : "none"} !important;
710
+ position: absolute !important;
711
+ inset: auto 0 0 0 !important;
712
+ height: ${SEQUENCE_HEIGHT} !important;
713
+ }
714
+
715
+ .msp-plugin .msp-layout-region.msp-layout-main {
716
+ position: absolute !important;
717
+ top: 0 !important;
718
+ left: 0 !important;
719
+ right: 0 !important;
720
+ bottom: ${showSequenceViewer ? SEQUENCE_HEIGHT : "0"} !important;
721
+ }
722
+
723
+ @container ${VIEWER_CONTAINER_NAME} (min-width: 880px) {
724
+ .msp-plugin .msp-layout-region.msp-layout-top {
725
+ height: ${SEQUENCE_HEIGHT_WIDE} !important;
726
+ }
727
+
728
+ .msp-plugin .msp-layout-region.msp-layout-main {
729
+ bottom: ${showSequenceViewer ? SEQUENCE_HEIGHT_WIDE : "0"} !important;
730
+ }
731
+ }
732
+ `;
733
+ }}
734
+
735
+ /*
736
+ * Toast slot rendered by the custom viewport. No loci label providers are
737
+ * registered (residue info goes to the legend instead), so this only carries
738
+ * Mol*'s own transient messages.
739
+ */
740
+ .msp-plugin .msp-highlight-toast-wrapper {
741
+ position: absolute !important;
742
+ left: 10px !important;
743
+ max-width: 95% !important;
744
+ z-index: 10000 !important;
745
+ pointer-events: none;
746
+ }
747
+ `;
748
+ //#endregion
749
+ //#region src/core/ProteinStructureViewer/components/StructureLegend/style.ts
750
+ /** Width below which the legend switches to its stacked layout. */
751
+ const NARROW_BREAKPOINT = 480;
752
+ /** Fixed stat column width, so labels swapping on hover never shift the grid. */
753
+ const STAT_COLUMN_WIDTH = "4.5rem";
754
+ /**
755
+ * Overlaid along the bottom of the viewer, above the sequence panel. Its bottom
756
+ * offset is the panel's own height, taken from the root style so the two cannot
757
+ * disagree; with no panel it drops to the bottom edge, where its own padding
758
+ * provides the gap. Pointer events are off so the legend never intercepts drags
759
+ * meant for the structure; the help tooltip re-enables them on itself.
760
+ */
761
+ const LegendOverlay = styled("div")`
762
+ position: absolute;
763
+ z-index: 1;
764
+ display: flex;
765
+ align-items: flex-end;
766
+ width: 100%;
767
+ pointer-events: none;
430
768
 
431
769
  ${(props) => {
432
770
  const spaces = getSpaces(props);
771
+ const { showSequenceViewer } = props;
433
772
  return `
434
- gap: ${spaces === null || spaces === void 0 ? void 0 : spaces.s}px;
435
- margin-bottom: ${spaces === null || spaces === void 0 ? void 0 : spaces.l}px;
773
+ right: ${spaces === null || spaces === void 0 ? void 0 : spaces.m}px;
774
+ gap: ${spaces === null || spaces === void 0 ? void 0 : spaces.xs}px;
775
+ padding-bottom: ${spaces === null || spaces === void 0 ? void 0 : spaces.m}px;
776
+ bottom: ${showSequenceViewer ? SEQUENCE_HEIGHT : "0"};
777
+
778
+ /*
779
+ * Track the sequence panel as it grows on wider viewers. The panel is
780
+ * sized against this same container, so answering to the page instead
781
+ * would part the legend from it on a viewer narrower than its page.
782
+ */
783
+ @container ${VIEWER_CONTAINER_NAME} (min-width: 880px) {
784
+ bottom: ${showSequenceViewer ? SEQUENCE_HEIGHT_WIDE : "0"};
785
+ }
786
+
787
+ /* Spacer balancing the axes widget pinned in the opposite corner. */
788
+ &::before {
789
+ content: "";
790
+ display: block;
791
+ width: ${spaces === null || spaces === void 0 ? void 0 : spaces.xxl}px;
792
+ }
793
+
794
+ @container ${VIEWER_CONTAINER_NAME} (max-width: ${NARROW_BREAKPOINT}px) {
795
+ width: auto;
796
+ flex-direction: column;
797
+ align-items: flex-end;
798
+ gap: ${spaces === null || spaces === void 0 ? void 0 : spaces.l}px;
799
+
800
+ /* The horizontal spacer only makes sense in the bottom-bar layout. */
801
+ &::before {
802
+ display: none;
803
+ }
804
+ }
436
805
  `;
437
806
  }}
438
807
  `;
439
- const ChartTitle = styled("div")`
440
- ${fontHeaderS}
808
+ /**
809
+ * Fills the space between the axes widget on the left and the color legend on
810
+ * the right. Three fixed-width columns distributed with space-evenly, so the
811
+ * side padding equals the inter-column gap and both flex with the viewer width.
812
+ */
813
+ const StatsGrid = styled("div")`
814
+ display: grid;
815
+ flex: 1;
816
+ grid-template-columns: repeat(3, ${STAT_COLUMN_WIDTH});
817
+ align-items: end;
818
+ justify-content: space-evenly;
819
+ white-space: nowrap;
820
+
821
+ ${(props) => {
822
+ const spaces = getSpaces(props);
823
+ return `
824
+ @container ${VIEWER_CONTAINER_NAME} (max-width: ${NARROW_BREAKPOINT}px) {
825
+ flex: none;
826
+ grid-template-columns: minmax(0, 1fr);
827
+ justify-items: end;
828
+ text-align: right;
829
+ row-gap: ${spaces === null || spaces === void 0 ? void 0 : spaces.xl}px;
830
+ }
831
+ `;
832
+ }}
833
+ `;
834
+ const StatColumnWrapper = styled("div")`
835
+ display: flex;
836
+ flex-direction: column;
837
+ `;
838
+ const StatValue = styled("span")`
839
+ ${fontBodyXxxs}
441
840
 
442
841
  ${(props) => {
443
842
  var _semanticColors$base;
@@ -447,140 +846,2483 @@ const ChartTitle = styled("div")`
447
846
  `;
448
847
  }}
449
848
  `;
450
- const StyledBadge = styled("div")`
451
- ${fontTabularSemiboldXxxxs}
849
+ const StatLabel = styled("span")`
850
+ ${fontBodyXxxs}
452
851
 
453
852
  ${(props) => {
454
- var _semanticColors$base2, _semanticColors$base3;
455
- const spaces = getSpaces(props);
456
- const corners = getCorners(props);
853
+ var _semanticColors$base2;
457
854
  const semanticColors = getSemanticColors(props);
458
855
  return `
459
- padding: ${spaces === null || spaces === void 0 ? void 0 : spaces.xxxs}px ${spaces === null || spaces === void 0 ? void 0 : spaces.xxs}px;
460
- border-radius: ${corners === null || corners === void 0 ? void 0 : corners.m}px;
461
- background-color: ${semanticColors === null || semanticColors === void 0 || (_semanticColors$base2 = semanticColors.base) === null || _semanticColors$base2 === void 0 ? void 0 : _semanticColors$base2.backgroundTertiary};
462
- color: ${semanticColors === null || semanticColors === void 0 || (_semanticColors$base3 = semanticColors.base) === null || _semanticColors$base3 === void 0 ? void 0 : _semanticColors$base3.textSecondary};
856
+ color: ${semanticColors === null || semanticColors === void 0 || (_semanticColors$base2 = semanticColors.base) === null || _semanticColors$base2 === void 0 ? void 0 : _semanticColors$base2.textTertiaryOnDark};
463
857
  `;
464
858
  }}
465
859
  `;
466
- const BarContainer = styled("div")`
860
+ const ScaleColumn = styled("div")`
861
+ display: flex;
862
+ flex-direction: column;
863
+
467
864
  ${(props) => {
468
- var _semanticColors$base4;
469
- const { width, barHeight, isEmpty } = props;
470
- const corners = getCorners(props);
471
- const semanticColors = getSemanticColors(props);
472
- const finalWidth = typeof width === "number" ? `${width}px` : width;
865
+ const spaces = getSpaces(props);
473
866
  return `
474
- display: flex;
475
- border-radius: ${corners === null || corners === void 0 ? void 0 : corners.s}px;
476
- width: ${finalWidth};
477
- height: ${barHeight}px;
478
- background-color: ${isEmpty ? semanticColors === null || semanticColors === void 0 || (_semanticColors$base4 = semanticColors.base) === null || _semanticColors$base4 === void 0 ? void 0 : _semanticColors$base4.backgroundTertiary : "transparent"};
479
- overflow: hidden;
867
+ gap: ${spaces === null || spaces === void 0 ? void 0 : spaces.xxs}px;
480
868
  `;
481
869
  }}
482
870
  `;
483
- const BarSegment = styled("div")`
871
+ const ScaleCaption = styled("span")`
872
+ ${fontBodyXxxs}
873
+
874
+ text-align: left;
875
+ display: flex;
876
+ align-items: center;
877
+
484
878
  ${(props) => {
485
- const { color, height, percentage, opacity, disabled, isLast } = props;
486
- const spaces = getSpaces(props);
487
- const gap = (spaces === null || spaces === void 0 ? void 0 : spaces.xxxs) || 0;
879
+ var _semanticColors$base3;
880
+ const semanticColors = getSemanticColors(props);
488
881
  return `
489
- background-color: ${color};
490
- height: ${height}px;
491
- flex-grow: ${percentage};
492
- flex-basis: 0;
493
- flex-shrink: 1;
494
- margin-right: ${percentage > 0 && !isLast ? `${gap}px` : 0};
495
- opacity: ${opacity};
496
- transition: opacity 0.2s ease-in-out, flex-grow 0.25s ease-in-out, margin-right 0.25s ease-out;
497
- cursor: ${disabled ? "default" : "pointer"};
498
- pointer-events: ${disabled ? "none" : "auto"};
882
+ color: ${semanticColors === null || semanticColors === void 0 || (_semanticColors$base3 = semanticColors.base) === null || _semanticColors$base3 === void 0 ? void 0 : _semanticColors$base3.textTertiaryOnDark};
499
883
  `;
500
884
  }}
501
885
  `;
502
- const StyledStackedBarChartWrapper = styled("div")`
886
+ /** Re-enables pointer events for the one interactive element in the overlay. */
887
+ const TooltipAnchor = styled("span")`
888
+ pointer-events: auto;
889
+
503
890
  ${(props) => {
891
+ var _semanticColors$base4;
504
892
  const spaces = getSpaces(props);
893
+ const semanticColors = getSemanticColors(props);
505
894
  return `
506
- display: flex;
507
- flex-direction: column;
508
- gap: ${spaces === null || spaces === void 0 ? void 0 : spaces.m}px;
895
+ padding-left: ${spaces === null || spaces === void 0 ? void 0 : spaces.xxxs}px;
896
+ margin-bottom: -${spaces === null || spaces === void 0 ? void 0 : spaces.xxxs}px;
897
+
898
+ &:hover {
899
+ color: ${semanticColors === null || semanticColors === void 0 || (_semanticColors$base4 = semanticColors.base) === null || _semanticColors$base4 === void 0 ? void 0 : _semanticColors$base4.ornamentSecondary};
900
+ }
509
901
  `;
510
902
  }}
511
903
  `;
512
904
  //#endregion
513
- //#region src/core/StackedBarChart/index.tsx
514
- const _excluded = [
515
- "title",
516
- "badge",
517
- "hideBadge",
518
- "data",
519
- "width",
520
- "barHeight",
521
- "showLegend",
522
- "showLegendValues",
523
- "legendValueFormat",
524
- "selectedIndices",
525
- "onSelectionChange",
526
- "onSegmentMouseEnter",
527
- "onSegmentMouseLeave",
528
- "onLegendItemMouseEnter",
529
- "onLegendItemMouseLeave",
530
- "onSegmentClick",
531
- "onLegendItemClick",
532
- "selectionBehavior",
533
- "mode",
534
- "maxAmount",
535
- "remainingLabel",
536
- "remainingUnit",
537
- "unit",
538
- "colorGeneratorOptions"
539
- ];
540
- const calculateBadgeText = (totalItems, selectedCount) => {
541
- if (selectedCount === 0 || selectedCount === totalItems) return `${totalItems}`;
542
- return `${selectedCount} of ${totalItems}`;
543
- };
544
- const calculateLegendValue = (item, showLegendValues, legendValueFormat, globalUnit) => {
545
- if (!showLegendValues) return void 0;
546
- if (legendValueFormat === "percentage") return `${Math.round(item.percentage)}%`;
547
- const effectiveUnit = item.unit || globalUnit;
548
- return effectiveUnit ? `${item.value} ${effectiveUnit}` : `${item.value}`;
549
- };
550
- const getSegmentOpacity = (index, hoveredIndex, selectedIndicesSet, selectionBehavior) => {
551
- const hasSelection = selectedIndicesSet.size > 0;
552
- if (selectionBehavior === "hide") return 1;
553
- if (hoveredIndex !== null) return hoveredIndex === index || selectedIndicesSet.has(index) ? 1 : .2;
554
- if (hasSelection) return selectedIndicesSet.has(index) ? 1 : .2;
555
- return 1;
556
- };
557
- const applyCumulativeHideStyling = (items, selectedIndicesSet, selectedTotal, hasRemaining, effectiveMaxAmount) => {
558
- const newRemainingValue = hasRemaining ? effectiveMaxAmount - selectedTotal : 0;
559
- const newRemainingPercentage = hasRemaining ? newRemainingValue / effectiveMaxAmount * 100 : 0;
905
+ //#region src/core/ProteinStructureViewer/components/StructureLegend/index.tsx
906
+ /** Number of stat columns, fixed so the grid tracks never move. */
907
+ const SLOT_COUNT = 3;
908
+ /** Decimal places for stat and readout values. */
909
+ const VALUE_PRECISION = 3;
910
+ /** Shown in place of a number the residue has none of. */
911
+ const MISSING_VALUE = "–";
912
+ function StatColumn({ label, value }) {
913
+ return /* @__PURE__ */ jsxs(StatColumnWrapper, { children: [/* @__PURE__ */ jsx(StatValue, { children: value }), /* @__PURE__ */ jsx(StatLabel, { children: label })] });
914
+ }
915
+ /**
916
+ * Stats and color key overlaid on the structure viewer.
917
+ *
918
+ * When a residue is selected or hovered, the whole-structure stats are replaced
919
+ * in place by that residue's readout: slot 0 becomes the residue label, slot 1
920
+ * the overlay value (only when an overlay is active, otherwise the stat stays),
921
+ * and slot 2 the residue pLDDT. A selection pins the readout; hovering another
922
+ * residue takes priority so the user can inspect others without losing their
923
+ * selection.
924
+ */
925
+ function StructureLegend({ hoveredResidue = null, scale, scaleLabel, scaleMax = null, scaleMin = 0, scaleTooltip, selectedResidue = null, showSequenceViewer, stats, valueLabel }) {
926
+ var _stats$, _stats$2, _stats$3;
927
+ const activeResidue = hoveredResidue !== null && hoveredResidue !== void 0 ? hoveredResidue : selectedResidue;
928
+ const showResidue = activeResidue !== null;
929
+ const showValue = showResidue && valueLabel !== void 0;
930
+ const slots = [
931
+ showResidue ? {
932
+ label: "Residue",
933
+ value: activeResidue.label
934
+ } : (_stats$ = stats[0]) !== null && _stats$ !== void 0 ? _stats$ : null,
935
+ showValue ? {
936
+ label: valueLabel,
937
+ value: activeResidue.value !== null ? activeResidue.value.toFixed(VALUE_PRECISION) : MISSING_VALUE
938
+ } : (_stats$2 = stats[1]) !== null && _stats$2 !== void 0 ? _stats$2 : null,
939
+ showResidue ? {
940
+ label: "pLDDT",
941
+ value: activeResidue.plddt !== null ? activeResidue.plddt.toFixed(VALUE_PRECISION) : MISSING_VALUE
942
+ } : (_stats$3 = stats[2]) !== null && _stats$3 !== void 0 ? _stats$3 : null
943
+ ];
944
+ return /* @__PURE__ */ jsxs(LegendOverlay, {
945
+ showSequenceViewer,
946
+ children: [/* @__PURE__ */ jsx(StatsGrid, { children: Array.from({ length: SLOT_COUNT }, (_, i) => {
947
+ var _slots$i;
948
+ const slot = (_slots$i = slots[i]) !== null && _slots$i !== void 0 ? _slots$i : null;
949
+ return slot ? /* @__PURE__ */ jsx(StatColumn, {
950
+ label: slot.label,
951
+ value: slot.value
952
+ }, i) : /* @__PURE__ */ jsx("div", {}, i);
953
+ }) }), scale && /* @__PURE__ */ jsxs(ScaleColumn, { children: [/* @__PURE__ */ jsx(ColorScaleLegend, {
954
+ max: scaleMax,
955
+ min: scaleMin,
956
+ scale
957
+ }), /* @__PURE__ */ jsxs(ScaleCaption, { children: [scaleLabel, scaleTooltip !== void 0 && /* @__PURE__ */ jsx(Tooltip, {
958
+ arrow: true,
959
+ placement: "bottom",
960
+ title: scaleTooltip,
961
+ children: /* @__PURE__ */ jsx(TooltipAnchor, { children: /* @__PURE__ */ jsx(QuestionIcon, {
962
+ size: 10,
963
+ weight: "bold"
964
+ }) })
965
+ })] })] })]
966
+ });
967
+ }
968
+ //#endregion
969
+ //#region src/core/ProteinStructureViewer/utils/theme.ts
970
+ const themeCache = /* @__PURE__ */ new Map();
971
+ /**
972
+ * The MUI theme for a mode, built at most once. Mol*'s view components rebuild
973
+ * their `ThemeProvider` on every render, and `Theme()` is not cheap.
974
+ */
975
+ function themeForMode(mode) {
976
+ const cached = themeCache.get(mode);
977
+ if (cached) return cached;
978
+ const theme = Theme(mode);
979
+ themeCache.set(mode, theme);
980
+ return theme;
981
+ }
982
+ const residueColorCache = /* @__PURE__ */ new Map();
983
+ function residueColorsForMode(mode) {
984
+ var _getSemanticColors, _base$fillSecondaryIn, _base$textPrimary, _base$textSecondary, _base$textTertiary;
985
+ const cached = residueColorCache.get(mode);
986
+ if (cached) return cached;
987
+ const base = (_getSemanticColors = getSemanticColors({ theme: themeForMode(mode) })) === null || _getSemanticColors === void 0 ? void 0 : _getSemanticColors.base;
988
+ const colors = {
989
+ activeBackground: (_base$fillSecondaryIn = base === null || base === void 0 ? void 0 : base.fillSecondaryInteraction) !== null && _base$fillSecondaryIn !== void 0 ? _base$fillSecondaryIn : "transparent",
990
+ activeText: (_base$textPrimary = base === null || base === void 0 ? void 0 : base.textPrimary) !== null && _base$textPrimary !== void 0 ? _base$textPrimary : "inherit",
991
+ defaultText: (_base$textSecondary = base === null || base === void 0 ? void 0 : base.textSecondary) !== null && _base$textSecondary !== void 0 ? _base$textSecondary : "inherit",
992
+ inactiveText: (_base$textTertiary = base === null || base === void 0 ? void 0 : base.textTertiary) !== null && _base$textTertiary !== void 0 ? _base$textTertiary : "inherit"
993
+ };
994
+ residueColorCache.set(mode, colors);
995
+ return colors;
996
+ }
997
+ //#endregion
998
+ //#region src/core/ProteinStructureViewer/hooks/useSubscribe.ts
999
+ /**
1000
+ * Subscribes to an observable for as long as the component is mounted, the
1001
+ * hook equivalent of `PluginUIComponent`'s `subscribe`.
1002
+ *
1003
+ * The handler is read through a ref so that passing a fresh closure on every
1004
+ * render - which is the normal case, since these handlers read props and state
1005
+ * - does not tear down and rebuild the subscription.
1006
+ */
1007
+ function useSubscribe(observable, handler) {
1008
+ const handlerRef = useRef(handler);
1009
+ handlerRef.current = handler;
1010
+ useEffect(() => {
1011
+ if (!observable) return;
1012
+ const subscription = observable.subscribe((value) => handlerRef.current(value));
1013
+ return () => subscription.unsubscribe();
1014
+ }, [observable]);
1015
+ }
1016
+ //#endregion
1017
+ //#region src/core/ProteinStructureViewer/hooks/useViewSetting.ts
1018
+ /**
1019
+ * One field of the settings Mol*'s own React root cannot read from context.
1020
+ *
1021
+ * The subject emits a whole new settings object on every push, so each field is
1022
+ * selected out separately: React's equality bailout then absorbs the pushes
1023
+ * that left this particular value alone, and a component only re-renders for
1024
+ * the settings it actually reads.
1025
+ */
1026
+ function useViewSetting(settings, select) {
1027
+ const [value, setValue] = useState(() => select(settings.value));
1028
+ useSubscribe(settings, (next) => setValue(select(next)));
1029
+ return value;
1030
+ }
1031
+ //#endregion
1032
+ //#region src/core/ProteinStructureViewer/hooks/useMolstarTheme.ts
1033
+ /**
1034
+ * The active theme for a component Mol* renders in its own React root.
1035
+ *
1036
+ * Those components sit outside the consumer's provider tree, so they cannot
1037
+ * read the theme from context; the mode reaches them through the settings
1038
+ * subject instead. Callers re-supply the theme to their subtree via a local
1039
+ * `ThemeProvider` so Emotion styles resolve SDS tokens correctly.
1040
+ */
1041
+ function useMolstarTheme(settings) {
1042
+ const mode = useViewSetting(settings, (s) => s.mode);
560
1043
  return {
561
- styledItems: items.map((item) => {
562
- const isSelected = selectedIndicesSet.has(item.originalIndex);
563
- return _objectSpread2(_objectSpread2({}, item), {}, { percentage: isSelected ? item.percentage : 0 });
564
- }),
565
- updatedRemainingPercentage: newRemainingPercentage,
566
- updatedRemainingValue: newRemainingValue
1044
+ mode,
1045
+ theme: themeForMode(mode)
567
1046
  };
568
- };
569
- const applyProportionalHideStyling = (items, selectedIndicesSet, selectedTotal) => {
570
- if (selectedTotal === 0) return {
571
- styledItems: items.map((item) => _objectSpread2(_objectSpread2({}, item), {}, { percentage: 0 })),
572
- updatedRemainingPercentage: 0,
573
- updatedRemainingValue: 0
1047
+ }
1048
+ //#endregion
1049
+ //#region src/core/ProteinStructureViewer/components/SequenceView/constants.ts
1050
+ /** Attribute carrying a residue's sequence index, used for hit testing. */
1051
+ const SEQ_ID_ATTR = "data-seqid";
1052
+ /** Hover updates are coalesced to roughly one every three frames. */
1053
+ const HOVER_THROTTLE_MS = 3 * 16.666;
1054
+ /** How long the copy button shows its confirmation state. */
1055
+ const COPIED_FEEDBACK_MS = 2e3;
1056
+ //#endregion
1057
+ //#region src/core/ProteinStructureViewer/hooks/usePlugin.ts
1058
+ /**
1059
+ * The plugin owning the React root this component renders in.
1060
+ *
1061
+ * Mol* renders its view components inside its own root and provides the plugin
1062
+ * through this context, which is what `PluginUIComponent` reads to populate
1063
+ * `this.plugin`.
1064
+ */
1065
+ function usePlugin() {
1066
+ return useContext(PluginReactContext);
1067
+ }
1068
+ //#endregion
1069
+ //#region src/core/ProteinStructureViewer/components/SequenceView/utils/residue.ts
1070
+ /** Reads the sequence index off a residue span, or undefined if it is not one. */
1071
+ function readSeqIdx(el) {
1072
+ if (!el || !el.getAttribute) return void 0;
1073
+ return el.hasAttribute("data-seqid") ? +el.getAttribute(SEQ_ID_ATTR) : void 0;
1074
+ }
1075
+ /**
1076
+ * Nearest ancestor that scrolls vertically, or null if none. Used to scope the
1077
+ * residue tooltip's hide-on-scroll listener to the sequence's own overflow
1078
+ * container instead of a global window listener.
1079
+ */
1080
+ function getScrollParent(node) {
1081
+ var _node$parentElement;
1082
+ let el = (_node$parentElement = node === null || node === void 0 ? void 0 : node.parentElement) !== null && _node$parentElement !== void 0 ? _node$parentElement : null;
1083
+ while (el) {
1084
+ const overflowY = getComputedStyle(el).overflowY;
1085
+ if (overflowY === "auto" || overflowY === "scroll") return el;
1086
+ el = el.parentElement;
1087
+ }
1088
+ return null;
1089
+ }
1090
+ function getResidueClass(sequenceWrapper, seqIdx) {
1091
+ return sequenceWrapper.isHighlighted(seqIdx) || sequenceWrapper.isSelected(seqIdx) || sequenceWrapper.isFocused(seqIdx) ? "msp-sequence-present msp-sequence-residue-active" : "msp-sequence-present msp-sequence-residue";
1092
+ }
1093
+ /**
1094
+ * The label shown above a residue: its author sequence id for atomic units,
1095
+ * or a 1-based position for coarse ones.
1096
+ *
1097
+ * `location` is a scratch object the caller reuses across residues, since this
1098
+ * runs once per numbered residue on every render.
1099
+ */
1100
+ function getSequenceNumber(sequenceWrapper, seqIdx, location) {
1101
+ let seqNum = "";
1102
+ const loci = sequenceWrapper.getLoci(seqIdx);
1103
+ const l = StructureElement.Loci.getFirstLocation(loci, location);
1104
+ if (l) {
1105
+ if (Unit.isAtomic(l.unit)) {
1106
+ const seqId = StructureProperties.residue.auth_seq_id(l);
1107
+ const insCode = StructureProperties.residue.pdbx_PDB_ins_code(l);
1108
+ seqNum = `${seqId}${insCode ? insCode : ""}`;
1109
+ } else if (Unit.isCoarse(l.unit)) seqNum = `${seqIdx + 1}`;
1110
+ }
1111
+ return seqNum;
1112
+ }
1113
+ /**
1114
+ * How often a sequence number is drawn above the residues.
1115
+ *
1116
+ * Long sequences get one every ten residues. Short ones number every residue,
1117
+ * unless their numbers run to more than one digit, which would crowd them.
1118
+ */
1119
+ function resolveSequenceNumberPeriod(sequenceWrapper, location, sequenceNumberPeriod) {
1120
+ if (sequenceNumberPeriod !== void 0) return sequenceNumberPeriod;
1121
+ if (sequenceWrapper.length > 10) return 10;
1122
+ if (getSequenceNumber(sequenceWrapper, sequenceWrapper.length - 1, location).length > 1) return 5;
1123
+ return 1;
1124
+ }
1125
+ /**
1126
+ * The grid's accessible name.
1127
+ *
1128
+ * The grid is a rendering of the structure rather than a control of its own,
1129
+ * so it is exposed as an image; that makes this label the only thing an
1130
+ * assistive technology gets, which is why it carries the residues themselves
1131
+ * rather than just how many there are. They are spaced apart so they are read
1132
+ * as individual letters instead of running together into a word.
1133
+ *
1134
+ * A complex draws one grid per chain, and the caption naming each is a visual
1135
+ * sibling rather than a label, so the chain is named here too. Without it every
1136
+ * grid announces the same name and which chain a reader is on is apparent only
1137
+ * by sight.
1138
+ */
1139
+ function buildSequenceLabel(sequenceWrapper, chainLabel) {
1140
+ const letters = [];
1141
+ for (let i = 0; i < sequenceWrapper.length; i++) letters.push(sequenceWrapper.residueLabel(i));
1142
+ const name = chainLabel ? `Chain ${chainLabel}, residue sequence, ${sequenceWrapper.length} residues` : `Residue sequence, ${sequenceWrapper.length} residues`;
1143
+ return letters.length > 0 ? `${name}: ${letters.join(" ")}` : name;
1144
+ }
1145
+ //#endregion
1146
+ //#region src/core/ProteinStructureViewer/components/SequenceView/hooks/useResidueMarkers.ts
1147
+ /**
1148
+ * Keeps the residue grid's hover and selection colors in sync with the 3D
1149
+ * view.
1150
+ *
1151
+ * The colors are written straight to the residue spans rather than rendered,
1152
+ * because a marker change would otherwise re-render a grid that can hold
1153
+ * thousands of nodes.
1154
+ */
1155
+ function useResidueMarkers({ containerRef, residueColors, sequenceWrapper }) {
1156
+ const plugin = usePlugin();
1157
+ /**
1158
+ * Residues sit at the default color until something is active. A hovered,
1159
+ * selected, or focused residue brightens and takes a translucent fill; while
1160
+ * any residue is selected, the rest dim to draw attention to the selection.
1161
+ */
1162
+ const paintMarkers = useCallback(() => {
1163
+ const container = containerRef.current;
1164
+ if (!container) return;
1165
+ const { activeBackground, activeText, defaultText, inactiveText } = residueColors;
1166
+ const spans = container.querySelectorAll(`[${SEQ_ID_ATTR}]`);
1167
+ let anySelected = false;
1168
+ for (let i = 0; i < sequenceWrapper.length; i++) if (sequenceWrapper.isSelected(i)) {
1169
+ anySelected = true;
1170
+ break;
1171
+ }
1172
+ const inactiveColor = anySelected ? inactiveText : defaultText;
1173
+ for (let i = 0; i < sequenceWrapper.length; i++) {
1174
+ const span = spans[i];
1175
+ if (!span) return;
1176
+ const className = getResidueClass(sequenceWrapper, i);
1177
+ if (span.className !== className) span.className = className;
1178
+ const active = sequenceWrapper.isSelected(i) || sequenceWrapper.isFocused(i) || sequenceWrapper.isHighlighted(i);
1179
+ span.style.color = active ? activeText : inactiveColor;
1180
+ span.style.backgroundColor = active ? activeBackground : "";
1181
+ }
1182
+ }, [
1183
+ containerRef,
1184
+ residueColors,
1185
+ sequenceWrapper
1186
+ ]);
1187
+ const markFocus = useCallback((loci) => {
1188
+ sequenceWrapper.markResidue(EveryLoci, "unfocus");
1189
+ if (loci) sequenceWrapper.markResidue(loci, "focus");
1190
+ }, [sequenceWrapper]);
1191
+ const paintMarkersRef = useRef(paintMarkers);
1192
+ paintMarkersRef.current = paintMarkers;
1193
+ const sequenceWrapperRef = useRef(sequenceWrapper);
1194
+ sequenceWrapperRef.current = sequenceWrapper;
1195
+ useEffect(() => {
1196
+ const markProvider = (loci, action) => {
1197
+ if (sequenceWrapperRef.current.markResidue(loci.loci, action)) paintMarkersRef.current();
1198
+ };
1199
+ const { lociHighlights, lociSelects } = plugin.managers.interactivity;
1200
+ lociHighlights.addProvider(markProvider);
1201
+ lociSelects.addProvider(markProvider);
1202
+ return () => {
1203
+ lociHighlights.removeProvider(markProvider);
1204
+ lociSelects.removeProvider(markProvider);
1205
+ };
1206
+ }, [plugin]);
1207
+ useSubscribe(plugin.managers.structure.focus.behaviors.current, (focus) => {
1208
+ markFocus(focus === null || focus === void 0 ? void 0 : focus.loci);
1209
+ paintMarkers();
1210
+ });
1211
+ useLayoutEffect(() => {
1212
+ var _plugin$managers$stru;
1213
+ markFocus((_plugin$managers$stru = plugin.managers.structure.focus.behaviors.current.value) === null || _plugin$managers$stru === void 0 ? void 0 : _plugin$managers$stru.loci);
1214
+ paintMarkers();
1215
+ });
1216
+ }
1217
+ //#endregion
1218
+ //#region src/core/ProteinStructureViewer/components/SequenceView/utils/loci.ts
1219
+ /** The loci for a residue, or undefined when there is no residue there. */
1220
+ function getLoci(sequenceWrapper, seqIdx) {
1221
+ if (seqIdx !== void 0) {
1222
+ const loci = sequenceWrapper.getLoci(seqIdx);
1223
+ if (!StructureElement.Loci.isEmpty(loci)) return loci;
1224
+ }
1225
+ }
1226
+ /**
1227
+ * Extends a loci to the range between the mouse-down anchor and the current
1228
+ * residue, so dragging across the sequence selects a contiguous span.
1229
+ *
1230
+ * Element indices are only comparable within a single unit, and the range below
1231
+ * is built on the anchor's unit alone. So a drag is only extended when both
1232
+ * endpoints are a single element of the same unit; anything else -- different
1233
+ * units, or an endpoint spanning several because the chain has more than one
1234
+ * symmetry operator -- is left unextended rather than collapsed onto one unit
1235
+ * and silently reinterpreted as a different range.
1236
+ */
1237
+ function extendToRange(loci, anchor) {
1238
+ if (anchor.elements.length !== 1 || loci.elements.length !== 1) return loci;
1239
+ const ref = anchor.elements[0];
1240
+ const ext = loci.elements[0];
1241
+ if (!ref || !ext) return loci;
1242
+ if (ref.unit !== ext.unit) return loci;
1243
+ const min = Math.min(OrderedSet.min(ref.indices), OrderedSet.min(ext.indices));
1244
+ const max = Math.max(OrderedSet.max(ref.indices), OrderedSet.max(ext.indices));
1245
+ return StructureElement.Loci(loci.structure, [{
1246
+ indices: OrderedSet.ofRange(min, max),
1247
+ unit: ref.unit
1248
+ }]);
1249
+ }
1250
+ //#endregion
1251
+ //#region src/core/ProteinStructureViewer/components/SequenceView/hooks/useHoveredResidue.ts
1252
+ /**
1253
+ * Tracks which residue the pointer is over and keeps the tooltip pinned to it.
1254
+ *
1255
+ * The tracked index also deduplicates pointer moves: the grid sees a move for
1256
+ * every pixel, but only the ones that cross into a different residue matter.
1257
+ */
1258
+ function useHoveredResidue({ containerRef, sequenceWrapper }) {
1259
+ const tooltipRef = useRef(null);
1260
+ /** Index of the tracked residue, or -1 when the pointer is off the grid. */
1261
+ const seqIdxRef = useRef(-1);
1262
+ const hide = useCallback(() => {
1263
+ var _tooltipRef$current;
1264
+ return (_tooltipRef$current = tooltipRef.current) === null || _tooltipRef$current === void 0 ? void 0 : _tooltipRef$current.hide();
1265
+ }, []);
1266
+ const clear = useCallback(() => {
1267
+ var _tooltipRef$current2;
1268
+ if (seqIdxRef.current === -1) return false;
1269
+ seqIdxRef.current = -1;
1270
+ (_tooltipRef$current2 = tooltipRef.current) === null || _tooltipRef$current2 === void 0 || _tooltipRef$current2.hide();
1271
+ return true;
1272
+ }, []);
1273
+ const moveTo = useCallback((seqIdx, el) => {
1274
+ var _tooltipRef$current3, _tooltipRef$current4;
1275
+ if (seqIdxRef.current === seqIdx) return false;
1276
+ seqIdxRef.current = seqIdx;
1277
+ if (seqIdx < 0) (_tooltipRef$current3 = tooltipRef.current) === null || _tooltipRef$current3 === void 0 || _tooltipRef$current3.hide();
1278
+ else (_tooltipRef$current4 = tooltipRef.current) === null || _tooltipRef$current4 === void 0 || _tooltipRef$current4.show(el, sequenceWrapper.residueLabel(seqIdx));
1279
+ return true;
1280
+ }, [sequenceWrapper]);
1281
+ useEffect(() => {
1282
+ const scrollTarget = getScrollParent(containerRef.current);
1283
+ if (!scrollTarget) return;
1284
+ const handleScroll = () => clear();
1285
+ scrollTarget.addEventListener("scroll", handleScroll, { passive: true });
1286
+ return () => scrollTarget.removeEventListener("scroll", handleScroll);
1287
+ }, [clear, containerRef]);
1288
+ return useMemo(() => ({
1289
+ clear,
1290
+ hide,
1291
+ moveTo,
1292
+ tooltipRef
1293
+ }), [
1294
+ clear,
1295
+ hide,
1296
+ moveTo
1297
+ ]);
1298
+ }
1299
+ //#endregion
1300
+ //#region src/core/ProteinStructureViewer/components/SequenceView/hooks/useLociDispatch.ts
1301
+ /**
1302
+ * Wraps a residue loci and the mouse state into a Mol* interaction event. An
1303
+ * `anchor` extends the loci to the range between it and `loci`, which is how a
1304
+ * drag across the sequence comes out as a contiguous span.
1305
+ */
1306
+ function buildEvent(loci, buttons, button, modifiers, anchor) {
1307
+ const ev = {
1308
+ button,
1309
+ buttons,
1310
+ current: Representation.Loci.Empty,
1311
+ modifiers
1312
+ };
1313
+ if (loci === void 0 || StructureElement.Loci.isEmpty(loci)) return ev;
1314
+ ev.current = { loci: anchor ? extendToRange(loci, anchor) : loci };
1315
+ return ev;
1316
+ }
1317
+ /**
1318
+ * Reports the grid's hover and click to Mol*, on the same behaviors the 3D
1319
+ * view publishes to, so an interaction in either surface reaches the other.
1320
+ */
1321
+ function useLociDispatch() {
1322
+ const plugin = usePlugin();
1323
+ const hover = useCallback((loci, buttons, button, modifiers, anchor) => {
1324
+ plugin.behaviors.interaction.hover.next(buildEvent(loci, buttons, button, modifiers, anchor));
1325
+ }, [plugin]);
1326
+ const click = useCallback((loci, buttons, button, modifiers, anchor) => {
1327
+ plugin.behaviors.interaction.click.next(buildEvent(loci, buttons, button, modifiers, anchor));
1328
+ }, [plugin]);
1329
+ return useMemo(() => ({
1330
+ click,
1331
+ hover
1332
+ }), [click, hover]);
1333
+ }
1334
+ //#endregion
1335
+ //#region src/core/ProteinStructureViewer/components/SequenceView/hooks/useResidueSelection.ts
1336
+ /**
1337
+ * Turns a press and release on the grid into a selection: one residue when
1338
+ * both land on the same one, otherwise the contiguous range dragged across.
1339
+ */
1340
+ function useResidueSelection({ sequenceWrapper }) {
1341
+ const anchorRef = useRef(void 0);
1342
+ const { click } = useLociDispatch();
1343
+ const onMouseDown = (e) => {
1344
+ e.stopPropagation();
1345
+ anchorRef.current = getLoci(sequenceWrapper, readSeqIdx(e.target));
1346
+ };
1347
+ const onMouseUp = (e) => {
1348
+ e.stopPropagation();
1349
+ const anchor = anchorRef.current;
1350
+ if (anchor === void 0) return;
1351
+ const loci = getLoci(sequenceWrapper, readSeqIdx(e.target));
1352
+ if (loci) {
1353
+ const dragged = !StructureElement.Loci.areEqual(anchor, loci);
1354
+ click(loci, getButtons(e.nativeEvent), getButton(e.nativeEvent), getModifiers(e.nativeEvent), dragged ? anchor : void 0);
1355
+ }
1356
+ anchorRef.current = void 0;
574
1357
  };
575
1358
  return {
576
- styledItems: items.map((item) => {
577
- const newPercentage = selectedIndicesSet.has(item.originalIndex) ? item.value / selectedTotal * 100 : 0;
578
- return _objectSpread2(_objectSpread2({}, item), {}, { percentage: newPercentage });
579
- }),
580
- updatedRemainingPercentage: 0,
581
- updatedRemainingValue: 0
1359
+ clearAnchor: () => {
1360
+ anchorRef.current = void 0;
1361
+ },
1362
+ getAnchor: () => anchorRef.current,
1363
+ onMouseDown,
1364
+ onMouseUp
582
1365
  };
583
- };
1366
+ }
1367
+ //#endregion
1368
+ //#region src/core/ProteinStructureViewer/components/SequenceView/hooks/useResiduePointer.ts
1369
+ /**
1370
+ * Everything the grid does with a pointer: hover on move, and the drag-to-range
1371
+ * selection delegated to `useResidueSelection`.
1372
+ *
1373
+ * Hover reaches the 3D view through a throttled queue, so that sweeping the
1374
+ * pointer across the grid does not ask Mol* to re-mark on every residue
1375
+ * crossed. A drag is exempt: its hover carries the growing range, so it goes
1376
+ * out immediately to keep the preview under the pointer.
1377
+ */
1378
+ function useResiduePointer({ containerRef, sequenceWrapper }) {
1379
+ const { hover } = useLociDispatch();
1380
+ const { clear, hide, moveTo, tooltipRef } = useHoveredResidue({
1381
+ containerRef,
1382
+ sequenceWrapper
1383
+ });
1384
+ const { clearAnchor, getAnchor, onMouseDown, onMouseUp } = useResidueSelection({ sequenceWrapper });
1385
+ const queueRef = useRef(null);
1386
+ if (queueRef.current === null) queueRef.current = new Subject();
1387
+ const queue = queueRef.current;
1388
+ useSubscribe(useMemo(() => queue.pipe(throttleTime(HOVER_THROTTLE_MS, void 0, {
1389
+ leading: true,
1390
+ trailing: true
1391
+ })), [queue]), (e) => {
1392
+ hover(getLoci(sequenceWrapper, e.seqIdx < 0 ? void 0 : e.seqIdx), e.buttons, e.button, e.modifiers, getAnchor());
1393
+ });
1394
+ const queueHover = (e, seqIdx) => queue.next({
1395
+ button: getButton(e.nativeEvent),
1396
+ buttons: getButtons(e.nativeEvent),
1397
+ modifiers: getModifiers(e.nativeEvent),
1398
+ seqIdx
1399
+ });
1400
+ /** Asks the 3D view to drop its hover, once, if the grid still holds one. */
1401
+ const releaseHover = (e) => {
1402
+ if (clear()) queueHover(e, -1);
1403
+ };
1404
+ const onContextMenu = (e) => {
1405
+ e.preventDefault();
1406
+ };
1407
+ const onMouseMove = (e) => {
1408
+ var _readSeqIdx;
1409
+ e.stopPropagation();
1410
+ const el = e.target;
1411
+ if (!el || !el.getAttribute) {
1412
+ releaseHover(e);
1413
+ return;
1414
+ }
1415
+ const seqIdx = (_readSeqIdx = readSeqIdx(el)) !== null && _readSeqIdx !== void 0 ? _readSeqIdx : -1;
1416
+ if (!moveTo(seqIdx, el)) return;
1417
+ const anchor = getAnchor();
1418
+ if (anchor === void 0) {
1419
+ queueHover(e, seqIdx);
1420
+ return;
1421
+ }
1422
+ hover(getLoci(sequenceWrapper, seqIdx), ButtonsType.Flag.None, ButtonsType.Flag.None, getModifiers(e.nativeEvent), anchor);
1423
+ };
1424
+ const onMouseLeave = (e) => {
1425
+ e.stopPropagation();
1426
+ clearAnchor();
1427
+ hide();
1428
+ releaseHover(e);
1429
+ };
1430
+ return {
1431
+ handlers: {
1432
+ onContextMenu,
1433
+ onMouseDown,
1434
+ onMouseLeave,
1435
+ onMouseMove,
1436
+ onMouseUp
1437
+ },
1438
+ tooltipRef
1439
+ };
1440
+ }
1441
+ //#endregion
1442
+ //#region src/core/ProteinStructureViewer/components/SequenceView/style.ts
1443
+ /** Height of the fade that masks residues scrolling under the panel header. */
1444
+ const SCROLL_FADE_HEIGHT = 8;
1445
+ const SequencePanel = styled("div")`
1446
+ display: flex;
1447
+ flex-direction: column;
1448
+ height: 100%;
1449
+
1450
+ ${(props) => {
1451
+ var _semanticColors$base, _props$backgroundColo, _semanticColors$base2;
1452
+ const spaces = getSpaces(props);
1453
+ const semanticColors = getSemanticColors(props);
1454
+ return `
1455
+ padding: ${spaces === null || spaces === void 0 ? void 0 : spaces.m}px ${spaces === null || spaces === void 0 ? void 0 : spaces.m}px 0;
1456
+ border-top: 1px solid ${semanticColors === null || semanticColors === void 0 || (_semanticColors$base = semanticColors.base) === null || _semanticColors$base === void 0 ? void 0 : _semanticColors$base.divider};
1457
+ background-color: ${(_props$backgroundColo = props.backgroundColor) !== null && _props$backgroundColo !== void 0 ? _props$backgroundColo : semanticColors === null || semanticColors === void 0 || (_semanticColors$base2 = semanticColors.base) === null || _semanticColors$base2 === void 0 ? void 0 : _semanticColors$base2.surfacePrimary};
1458
+ `;
1459
+ }}
1460
+ `;
1461
+ const EmptyState = styled("div")`
1462
+ ${fontBodyXs}
1463
+
1464
+ display: flex;
1465
+ align-items: center;
1466
+ justify-content: center;
1467
+ height: 100%;
1468
+
1469
+ ${(props) => {
1470
+ var _props$backgroundColo2, _semanticColors$base3, _semanticColors$base4;
1471
+ const spaces = getSpaces(props);
1472
+ const semanticColors = getSemanticColors(props);
1473
+ return `
1474
+ padding: ${spaces === null || spaces === void 0 ? void 0 : spaces.m}px;
1475
+ background-color: ${(_props$backgroundColo2 = props.backgroundColor) !== null && _props$backgroundColo2 !== void 0 ? _props$backgroundColo2 : semanticColors === null || semanticColors === void 0 || (_semanticColors$base3 = semanticColors.base) === null || _semanticColors$base3 === void 0 ? void 0 : _semanticColors$base3.surfaceSecondary};
1476
+ color: ${semanticColors === null || semanticColors === void 0 || (_semanticColors$base4 = semanticColors.base) === null || _semanticColors$base4 === void 0 ? void 0 : _semanticColors$base4.textTertiaryOnDark};
1477
+ `;
1478
+ }}
1479
+ `;
1480
+ const PanelHeader = styled("div")`
1481
+ display: flex;
1482
+ align-items: center;
1483
+
1484
+ ${(props) => {
1485
+ const spaces = getSpaces(props);
1486
+ return `
1487
+ gap: ${spaces === null || spaces === void 0 ? void 0 : spaces.xs}px;
1488
+ `;
1489
+ }}
1490
+ `;
1491
+ const PanelTitle = styled("span")`
1492
+ ${fontHeaderM}
1493
+
1494
+ ${(props) => {
1495
+ var _semanticColors$base5;
1496
+ const semanticColors = getSemanticColors(props);
1497
+ return `
1498
+ color: ${semanticColors === null || semanticColors === void 0 || (_semanticColors$base5 = semanticColors.base) === null || _semanticColors$base5 === void 0 ? void 0 : _semanticColors$base5.textPrimary};
1499
+ `;
1500
+ }}
1501
+ `;
1502
+ const CopyButton = styled("button")`
1503
+ display: flex;
1504
+ align-items: center;
1505
+ padding: 0;
1506
+ border: none;
1507
+ background: none;
1508
+ cursor: default;
1509
+
1510
+ ${(props) => {
1511
+ var _semanticColors$base6, _semanticColors$base7;
1512
+ const semanticColors = getSemanticColors(props);
1513
+ return `
1514
+ color: ${semanticColors === null || semanticColors === void 0 || (_semanticColors$base6 = semanticColors.base) === null || _semanticColors$base6 === void 0 ? void 0 : _semanticColors$base6.ornamentSecondary};
1515
+
1516
+ &:hover {
1517
+ color: ${semanticColors === null || semanticColors === void 0 || (_semanticColors$base7 = semanticColors.base) === null || _semanticColors$base7 === void 0 ? void 0 : _semanticColors$base7.textPrimary};
1518
+ }
1519
+ `;
1520
+ }}
1521
+ `;
1522
+ const ResidueCount = styled("div")`
1523
+ display: flex;
1524
+ align-items: center;
1525
+ margin-left: auto;
1526
+
1527
+ ${(props) => {
1528
+ const spaces = getSpaces(props);
1529
+ return `
1530
+ gap: ${spaces === null || spaces === void 0 ? void 0 : spaces.xxs}px;
1531
+ `;
1532
+ }}
1533
+ `;
1534
+ const ResidueCountLabel = styled("span")`
1535
+ ${fontBodyXs}
1536
+
1537
+ ${(props) => {
1538
+ var _semanticColors$base8;
1539
+ const semanticColors = getSemanticColors(props);
1540
+ return `
1541
+ color: ${semanticColors === null || semanticColors === void 0 || (_semanticColors$base8 = semanticColors.base) === null || _semanticColors$base8 === void 0 ? void 0 : _semanticColors$base8.textTertiary};
1542
+ `;
1543
+ }}
1544
+ `;
1545
+ const ResidueCountValue = styled("span")`
1546
+ ${fontBodyXs}
1547
+
1548
+ ${(props) => {
1549
+ var _semanticColors$base9;
1550
+ const semanticColors = getSemanticColors(props);
1551
+ return `
1552
+ color: ${semanticColors === null || semanticColors === void 0 || (_semanticColors$base9 = semanticColors.base) === null || _semanticColors$base9 === void 0 ? void 0 : _semanticColors$base9.textPrimary};
1553
+ `;
1554
+ }}
1555
+ `;
1556
+ /**
1557
+ * Scroll region for the residue grid. The `::before` fade masks residues as
1558
+ * they scroll up under the header, so it has to start from whatever the panel
1559
+ * is painted with or it leaves a mismatched strip.
1560
+ */
1561
+ const SequenceScroller = styled("div")`
1562
+ position: relative;
1563
+ flex: 1;
1564
+ min-height: 0;
1565
+
1566
+ ${(props) => {
1567
+ var _props$backgroundColo3, _semanticColors$base10;
1568
+ const semanticColors = getSemanticColors(props);
1569
+ return `
1570
+ &::before {
1571
+ content: "";
1572
+ position: absolute;
1573
+ inset-inline: 0;
1574
+ top: 0;
1575
+ z-index: 10;
1576
+ height: ${SCROLL_FADE_HEIGHT}px;
1577
+ pointer-events: none;
1578
+ background: linear-gradient(to bottom, ${(_props$backgroundColo3 = props.backgroundColor) !== null && _props$backgroundColo3 !== void 0 ? _props$backgroundColo3 : semanticColors === null || semanticColors === void 0 || (_semanticColors$base10 = semanticColors.base) === null || _semanticColors$base10 === void 0 ? void 0 : _semanticColors$base10.surfacePrimary}, transparent);
1579
+ }
1580
+ `;
1581
+ }}
1582
+ `;
1583
+ /**
1584
+ * Residue spans are written to directly by `useResidueMarkers` rather than
1585
+ * re-rendered, so their hover and selection colors are set inline. Everything
1586
+ * here is the static styling underneath that: the monospace type, the pointer
1587
+ * affordance, and the superscript sequence numbers (which need their own color
1588
+ * rule, since they are nested inside residue spans and would otherwise inherit
1589
+ * the inline color).
1590
+ */
1591
+ const SequenceScrollArea = styled("div")`
1592
+ height: 100%;
1593
+ overflow-y: auto;
1594
+
1595
+ .msp-sequence-wrapper {
1596
+ ${fontCodeXs};
1597
+ font-size: 11px;
1598
+ line-height: 1.2;
1599
+ }
1600
+
1601
+ .msp-sequence-present {
1602
+ cursor: pointer;
1603
+ }
1604
+
1605
+ ${(props) => {
1606
+ var _semanticColors$base11;
1607
+ const semanticColors = getSemanticColors(props);
1608
+ return `
1609
+ .msp-sequence-number {
1610
+ font-size: 8px;
1611
+ color: ${semanticColors === null || semanticColors === void 0 || (_semanticColors$base11 = semanticColors.base) === null || _semanticColors$base11 === void 0 ? void 0 : _semanticColors$base11.textTertiaryOnDark};
1612
+ }
1613
+ `;
1614
+ }}
1615
+ `;
1616
+ /**
1617
+ * Layout for one chain's residues.
1618
+ *
1619
+ * Residues are grouped into fixed-size sections, each one grid cell. The
1620
+ * equal-width auto-fill tracks and uniform gap keep the spacing between
1621
+ * sections identical on every row - including a partial last row, which keeps
1622
+ * the same column width (rather than stretching) and stacks from the leading
1623
+ * edge.
1624
+ */
1625
+ const ResidueGrid = styled("div")`
1626
+ user-select: none;
1627
+ display: grid;
1628
+ grid-template-columns: repeat(
1629
+ auto-fill,
1630
+ minmax(${10}ch, 1fr)
1631
+ );
1632
+ gap: 8px;
1633
+ justify-items: start;
1634
+ padding-top: 6px;
1635
+
1636
+ /* Section wrappers, so a section's letters never split across lines. */
1637
+ & > span {
1638
+ white-space: nowrap;
1639
+ }
1640
+
1641
+ .msp-sequence-present {
1642
+ position: relative;
1643
+ display: inline-block;
1644
+ width: 1ch;
1645
+ /*
1646
+ * Reserves room above the letter for its superscript sequence number.
1647
+ * content-box clipping keeps the active fill off that gutter, so a
1648
+ * highlighted residue does not tint the number.
1649
+ */
1650
+ padding-top: 8px;
1651
+ background-clip: content-box;
1652
+ text-align: center;
1653
+ }
1654
+
1655
+ .msp-sequence-number {
1656
+ position: absolute;
1657
+ top: 0;
1658
+ line-height: 1.2;
1659
+ white-space: nowrap;
1660
+ pointer-events: none;
1661
+ }
1662
+ `;
1663
+ /**
1664
+ * Caption naming the chain a sequence grid belongs to. A structure with more
1665
+ * than one chain renders one grid per chain, and without a caption between them
1666
+ * the grids read as a single continuous protein.
1667
+ */
1668
+ const ChainLabel = styled("div")`
1669
+ ${fontBodyXs}
1670
+
1671
+ ${(props) => {
1672
+ var _semanticColors$base12;
1673
+ const spaces = getSpaces(props);
1674
+ const semanticColors = getSemanticColors(props);
1675
+ return `
1676
+ margin-top: ${spaces === null || spaces === void 0 ? void 0 : spaces.xs}px;
1677
+ margin-bottom: ${spaces === null || spaces === void 0 ? void 0 : spaces.xxxs}px;
1678
+ color: ${semanticColors === null || semanticColors === void 0 || (_semanticColors$base12 = semanticColors.base) === null || _semanticColors$base12 === void 0 ? void 0 : _semanticColors$base12.textSecondary};
1679
+ `;
1680
+ }}
1681
+ `;
1682
+ //#endregion
1683
+ //#region src/core/ProteinStructureViewer/components/SequenceView/components/ResidueTooltip/index.tsx
1684
+ var _getSpaces$m, _getSpaces;
1685
+ /**
1686
+ * Pull the tooltip toward the residue by the SDS `m` spacing token. Spacing
1687
+ * tokens are theme-invariant, so reading from the light theme is fine; popper's
1688
+ * offset modifier needs a number, hence deriving it from the token.
1689
+ */
1690
+ const MAIN_AXIS_OFFSET = -((_getSpaces$m = (_getSpaces = getSpaces({ theme: defaultTheme })) === null || _getSpaces === void 0 ? void 0 : _getSpaces.m) !== null && _getSpaces$m !== void 0 ? _getSpaces$m : 12);
1691
+ /**
1692
+ * A single controlled tooltip anchored to the residue span currently under the
1693
+ * cursor. Driven imperatively through its ref so that hover updates re-render
1694
+ * only the tooltip, not the residue grid.
1695
+ */
1696
+ const ResidueTooltip = forwardRef((_props, ref) => {
1697
+ var _getSemanticColors;
1698
+ const [{ anchorEl, label }, setState] = useState({
1699
+ anchorEl: null,
1700
+ label: ""
1701
+ });
1702
+ const residueTooltipBackgroundColor = (_getSemanticColors = getSemanticColors({ theme: useTheme$1() })) === null || _getSemanticColors === void 0 || (_getSemanticColors = _getSemanticColors.base) === null || _getSemanticColors === void 0 ? void 0 : _getSemanticColors.backgroundPrimary;
1703
+ useImperativeHandle(ref, () => ({
1704
+ hide: () => setState((prev) => prev.anchorEl === null ? prev : _objectSpread2(_objectSpread2({}, prev), {}, { anchorEl: null })),
1705
+ show: (nextAnchorEl, nextLabel) => setState({
1706
+ anchorEl: nextAnchorEl,
1707
+ label: nextLabel
1708
+ })
1709
+ }), []);
1710
+ return /* @__PURE__ */ jsx(Tooltip, {
1711
+ arrow: true,
1712
+ disableInteractive: true,
1713
+ open: anchorEl !== null,
1714
+ placement: "top",
1715
+ slotProps: {
1716
+ popper: {
1717
+ anchorEl,
1718
+ modifiers: [{
1719
+ name: "offset",
1720
+ options: { offset: [0, MAIN_AXIS_OFFSET] }
1721
+ }]
1722
+ },
1723
+ tooltip: { style: { backgroundColor: residueTooltipBackgroundColor } },
1724
+ arrow: { style: { color: residueTooltipBackgroundColor } }
1725
+ },
1726
+ title: label,
1727
+ children: /* @__PURE__ */ jsx("span", { style: {
1728
+ height: 0,
1729
+ position: "fixed",
1730
+ width: 0
1731
+ } })
1732
+ });
1733
+ });
1734
+ //#endregion
1735
+ //#region src/core/ProteinStructureViewer/components/SequenceView/components/Sequence/index.tsx
1736
+ /**
1737
+ * One residue. Not a component: the grid can hold thousands of these, and
1738
+ * giving each its own fiber costs more than it saves, since the markers that
1739
+ * actually change bypass React anyway.
1740
+ */
1741
+ function renderResidue(sequenceWrapper, seqIdx, seqNum) {
1742
+ return /* @__PURE__ */ jsxs("span", {
1743
+ className: getResidueClass(sequenceWrapper, seqIdx),
1744
+ "data-seqid": seqIdx,
1745
+ children: [seqNum && /* @__PURE__ */ jsx("span", {
1746
+ className: "msp-sequence-number",
1747
+ children: seqNum
1748
+ }), sequenceWrapper.residueLabel(seqIdx)]
1749
+ }, seqIdx);
1750
+ }
1751
+ /** Interactive residue grid, kept in sync with the 3D view's hover and selection. */
1752
+ const Sequence = ({ chainLabel, hideSequenceNumbers, residueColors, sequenceNumberPeriod, sequenceWrapper }) => {
1753
+ const containerRef = useRef(null);
1754
+ /** Scratch location reused by every sequence-number lookup below. */
1755
+ const locationRef = useRef(StructureElement.Location.create(void 0));
1756
+ useResidueMarkers({
1757
+ containerRef,
1758
+ residueColors,
1759
+ sequenceWrapper
1760
+ });
1761
+ const { handlers, tooltipRef } = useResiduePointer({
1762
+ containerRef,
1763
+ sequenceWrapper
1764
+ });
1765
+ const period = resolveSequenceNumberPeriod(sequenceWrapper, locationRef.current, sequenceNumberPeriod);
1766
+ const groups = [];
1767
+ for (let start = 0; start < sequenceWrapper.length; start += 10) {
1768
+ const end = Math.min(start + 10, sequenceWrapper.length);
1769
+ const residues = [];
1770
+ for (let i = start; i < end; i++) {
1771
+ const showNum = !hideSequenceNumbers && i % period === 0;
1772
+ residues.push(renderResidue(sequenceWrapper, i, showNum ? getSequenceNumber(sequenceWrapper, i, locationRef.current) : null));
1773
+ }
1774
+ groups.push(/* @__PURE__ */ jsx("span", { children: residues }, `group-${start}`));
1775
+ }
1776
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(ResidueGrid, _objectSpread2(_objectSpread2({
1777
+ "aria-label": buildSequenceLabel(sequenceWrapper, chainLabel),
1778
+ className: "msp-sequence-wrapper",
1779
+ ref: containerRef,
1780
+ role: "img"
1781
+ }, handlers), {}, { children: groups })), /* @__PURE__ */ jsx(ResidueTooltip, { ref: tooltipRef })] });
1782
+ };
1783
+ //#endregion
1784
+ //#region src/core/ProteinStructureViewer/components/SequenceView/utils/sequenceText.ts
1785
+ /**
1786
+ * The structure's residues as text, one section per chain separated by
1787
+ * `CHAIN_SEPARATOR`. Placeholder entries contribute nothing, since they have no
1788
+ * residues to write.
1789
+ */
1790
+ function sequenceTextFromEntries(entries) {
1791
+ return entries.filter((entry) => typeof entry.wrapper !== "string").map((entry) => {
1792
+ const wrapper = entry.wrapper;
1793
+ return Array.from({ length: wrapper.length }, (_, i) => wrapper.residueLabel(i)).join("");
1794
+ }).join("|");
1795
+ }
1796
+ //#endregion
1797
+ //#region src/core/ProteinStructureViewer/components/SequenceView/hooks/useCopySequence.ts
1798
+ /**
1799
+ * Copies the structure's residues to the clipboard, chains separated so a
1800
+ * complex does not paste as one sequence, and holds a confirmation flag for a
1801
+ * moment afterwards.
1802
+ */
1803
+ function useCopySequence(entries) {
1804
+ const [copied, setCopied] = useState(false);
1805
+ const timeoutRef = useRef(null);
1806
+ useEffect(() => () => {
1807
+ if (timeoutRef.current) clearTimeout(timeoutRef.current);
1808
+ }, []);
1809
+ return {
1810
+ copied,
1811
+ copySequence: useCallback(() => {
1812
+ const sequence = sequenceTextFromEntries(entries);
1813
+ navigator.clipboard.writeText(sequence).then(() => {
1814
+ setCopied(true);
1815
+ if (timeoutRef.current) clearTimeout(timeoutRef.current);
1816
+ timeoutRef.current = setTimeout(() => setCopied(false), COPIED_FEEDBACK_MS);
1817
+ });
1818
+ }, [entries])
1819
+ };
1820
+ }
1821
+ //#endregion
1822
+ //#region src/core/ProteinStructureViewer/components/SequenceView/hooks/useSequenceWrappers.ts
1823
+ const EMPTY_STATE = {
1824
+ chainGroupId: -1,
1825
+ modelEntityId: "",
1826
+ operatorKey: "",
1827
+ structure: Structure.Empty,
1828
+ structureOptions: {
1829
+ all: [],
1830
+ options: []
1831
+ },
1832
+ structureRef: ""
1833
+ };
1834
+ function readStructure(plugin, ref) {
1835
+ const cell = plugin.state.data.select(ref)[0];
1836
+ if (!ref || !cell || !cell.obj) return Structure.Empty;
1837
+ return cell.obj.data;
1838
+ }
1839
+ /** Picks the first structure, entity, chain, and operator Mol* offers. */
1840
+ function readInitialState(plugin) {
1841
+ const structureOptions = getStructureOptions(plugin.state.data);
1842
+ const structureRef = structureOptions.options[0][0];
1843
+ const structure = readStructure(plugin, structureRef);
1844
+ const modelEntityId = getModelEntityOptions(structure)[0][0];
1845
+ const chainGroupId = getChainOptions(structure, modelEntityId)[0][0];
1846
+ return {
1847
+ chainGroupId,
1848
+ modelEntityId,
1849
+ operatorKey: getOperatorOptions(structure, modelEntityId, chainGroupId)[0][0],
1850
+ structure,
1851
+ structureOptions,
1852
+ structureRef
1853
+ };
1854
+ }
1855
+ function buildParams(state) {
1856
+ const { modelEntityId, structure, structureOptions } = state;
1857
+ const chainOptions = getChainOptions(structure, modelEntityId);
1858
+ return {
1859
+ chain: ParamDefinition.Select(chainOptions[0][0], chainOptions, { shortLabel: true }),
1860
+ structure: ParamDefinition.Select(structureOptions.options[0][0], structureOptions.options, { shortLabel: true })
1861
+ };
1862
+ }
1863
+ /** One entry per chain, in Mol*'s entity / chain / operator order. */
1864
+ function buildEntries(structure, selection, _params) {
1865
+ const entries = [];
1866
+ for (const [modelEntityId] of getModelEntityOptions(structure)) for (const [chainGroupId, cLabel] of getChainOptions(structure, modelEntityId)) for (const [operatorKey] of getOperatorOptions(structure, modelEntityId, chainGroupId)) entries.push({
1867
+ id: `${modelEntityId}:${chainGroupId}:${operatorKey}`,
1868
+ label: cLabel,
1869
+ wrapper: getSequenceWrapper({
1870
+ chainGroupId,
1871
+ modelEntityId,
1872
+ operatorKey,
1873
+ structure
1874
+ }, selection)
1875
+ });
1876
+ return entries;
1877
+ }
1878
+ function countResidues(entries) {
1879
+ return entries.reduce((sum, entry) => sum + (typeof entry.wrapper === "string" ? 0 : entry.wrapper.length), 0);
1880
+ }
1881
+ /**
1882
+ * The residue wrappers for every chain of the loaded structure, resynced
1883
+ * whenever Mol* creates, updates, or removes a structure object.
1884
+ *
1885
+ * The wrappers read the live selection, so they are rebuilt on each render
1886
+ * rather than memoized; that is what keeps the grid's dimming in step with a
1887
+ * selection made in the 3D view.
1888
+ */
1889
+ function useSequenceWrappers() {
1890
+ const plugin = usePlugin();
1891
+ const [state, setState] = useState(EMPTY_STATE);
1892
+ const sync = useCallback(() => {
1893
+ if (arrayEqual(getStructureOptions(plugin.state.data).all, state.structureOptions.all)) return;
1894
+ setState(readInitialState(plugin));
1895
+ }, [plugin, state.structureOptions.all]);
1896
+ useEffect(() => {
1897
+ if (plugin.state.data.select(StateSelection.Generators.rootsOfType(PluginStateObject.Molecule.Structure)).length > 0) setState(readInitialState(plugin));
1898
+ }, []);
1899
+ useSubscribe(plugin.state.events.object.updated, ({ ref, obj }) => {
1900
+ if (ref === state.structureRef && obj && obj.type === PluginStateObject.Molecule.Structure.type && obj.data !== state.structure) sync();
1901
+ });
1902
+ useSubscribe(plugin.state.events.object.created, ({ obj }) => {
1903
+ if (obj && obj.type === PluginStateObject.Molecule.Structure.type) sync();
1904
+ });
1905
+ useSubscribe(plugin.state.events.object.removed, ({ obj }) => {
1906
+ if (obj && obj.type === PluginStateObject.Molecule.Structure.type) sync();
1907
+ });
1908
+ const structure = readStructure(plugin, state.structureRef);
1909
+ if (structure === Structure.Empty) return {
1910
+ entries: [],
1911
+ isEmpty: true,
1912
+ residueCount: 0
1913
+ };
1914
+ const entries = buildEntries(structure, plugin.managers.structure.selection, buildParams(state));
1915
+ return {
1916
+ entries,
1917
+ isEmpty: false,
1918
+ residueCount: countResidues(entries)
1919
+ };
1920
+ }
1921
+ //#endregion
1922
+ //#region src/core/ProteinStructureViewer/components/SequenceView/index.tsx
1923
+ /**
1924
+ * Builds the Mol* sequence panel view.
1925
+ *
1926
+ * Like the viewport, Mol* renders this inside its own React root, so the theme
1927
+ * mode and the panel's background arrive through `viewSettings` and are watched
1928
+ * for changes rather than read once. The theme is re-supplied through a local
1929
+ * `ThemeProvider` for the Emotion styles and the SDS `Tooltip` below.
1930
+ */
1931
+ function createSequenceView(viewSettings) {
1932
+ return function SequenceView() {
1933
+ const { mode, theme } = useMolstarTheme(viewSettings);
1934
+ const backgroundColor = useViewSetting(viewSettings, (s) => s.sequenceViewerBackgroundColor);
1935
+ const { entries, isEmpty, residueCount } = useSequenceWrappers();
1936
+ const { copied, copySequence } = useCopySequence(entries);
1937
+ if (isEmpty) return /* @__PURE__ */ jsx(ThemeProvider, {
1938
+ theme,
1939
+ children: /* @__PURE__ */ jsx(EmptyState, {
1940
+ backgroundColor,
1941
+ className: "msp-sequence",
1942
+ children: "No structure available"
1943
+ })
1944
+ });
1945
+ const residueColors = residueColorsForMode(mode);
1946
+ const showChainLabels = entries.length > 1;
1947
+ return /* @__PURE__ */ jsx(ThemeProvider, {
1948
+ theme,
1949
+ children: /* @__PURE__ */ jsxs(SequencePanel, {
1950
+ backgroundColor,
1951
+ children: [/* @__PURE__ */ jsxs(PanelHeader, { children: [
1952
+ /* @__PURE__ */ jsx(PanelTitle, { children: "Sequence" }),
1953
+ /* @__PURE__ */ jsx(Tooltip, {
1954
+ arrow: true,
1955
+ placement: "top",
1956
+ title: copied ? "Copied!" : "Copy",
1957
+ children: /* @__PURE__ */ jsx(CopyButton, {
1958
+ "aria-label": "Copy sequence to clipboard",
1959
+ onClick: copySequence,
1960
+ type: "button",
1961
+ children: copied ? /* @__PURE__ */ jsx(CheckIcon, { size: 16 }) : /* @__PURE__ */ jsx(CopyIcon, { size: 16 })
1962
+ })
1963
+ }),
1964
+ /* @__PURE__ */ jsxs(ResidueCount, { children: [/* @__PURE__ */ jsx(ResidueCountLabel, { children: "Residues" }), /* @__PURE__ */ jsx(ResidueCountValue, { children: residueCount })] })
1965
+ ] }), /* @__PURE__ */ jsx(SequenceScroller, {
1966
+ backgroundColor,
1967
+ children: /* @__PURE__ */ jsx(SequenceScrollArea, {
1968
+ className: "msp-sequence msp-sequence-wrapper-non-empty",
1969
+ children: entries.map((s) => /* @__PURE__ */ jsxs(Fragment, { children: [showChainLabels && /* @__PURE__ */ jsx(ChainLabel, { children: s.label }), typeof s.wrapper === "string" ? /* @__PURE__ */ jsx("div", {
1970
+ className: "msp-sequence-wrapper",
1971
+ children: s.wrapper
1972
+ }) : /* @__PURE__ */ jsx(Sequence, {
1973
+ chainLabel: showChainLabels ? s.label : void 0,
1974
+ residueColors,
1975
+ sequenceWrapper: s.wrapper
1976
+ })] }, s.id))
1977
+ })
1978
+ })]
1979
+ })
1980
+ });
1981
+ };
1982
+ }
1983
+ //#endregion
1984
+ //#region src/core/ProteinStructureViewer/components/Viewport/constants.ts
1985
+ /**
1986
+ * Fraction of the bounding sphere radius within which the camera still counts
1987
+ * as sitting at the default view, absorbing easing and float error.
1988
+ */
1989
+ const DEFAULT_VIEW_TOLERANCE = .02;
1990
+ //#endregion
1991
+ //#region src/core/ProteinStructureViewer/components/Viewport/utils/camera.ts
1992
+ /** True when the camera's zoom/center differs from the default fit. */
1993
+ function isZoomedOffDefault(canvas3d) {
1994
+ const { center, radius } = canvas3d.boundingSphereVisible;
1995
+ if (radius <= 0) return false;
1996
+ const { state } = canvas3d.camera;
1997
+ const tolerance = radius * DEFAULT_VIEW_TOLERANCE;
1998
+ return Math.abs(state.radius - radius) > tolerance || Vec3.distance(state.target, center) > tolerance;
1999
+ }
2000
+ //#endregion
2001
+ //#region \0@oxc-project+runtime@0.134.0/helpers/esm/asyncToGenerator.js
2002
+ function asyncGeneratorStep(n, t, e, r, o, a, c) {
2003
+ try {
2004
+ var i = n[a](c), u = i.value;
2005
+ } catch (n) {
2006
+ e(n);
2007
+ return;
2008
+ }
2009
+ i.done ? t(u) : Promise.resolve(u).then(r, o);
2010
+ }
2011
+ function _asyncToGenerator(n) {
2012
+ return function() {
2013
+ var t = this, e = arguments;
2014
+ return new Promise(function(r, o) {
2015
+ var a = n.apply(t, e);
2016
+ function _next(n) {
2017
+ asyncGeneratorStep(a, r, o, _next, _throw, "next", n);
2018
+ }
2019
+ function _throw(n) {
2020
+ asyncGeneratorStep(a, r, o, _next, _throw, "throw", n);
2021
+ }
2022
+ _next(void 0);
2023
+ });
2024
+ };
2025
+ }
2026
+ //#endregion
2027
+ //#region src/core/ProteinStructureViewer/components/Viewport/hooks/useCameraReset.ts
2028
+ /**
2029
+ * Tracks whether the camera has moved off the default view, and puts it back.
2030
+ *
2031
+ * The default view is the auto-fit to the structure's bounding sphere, so
2032
+ * "moved" means either the zoom/center has drifted off that fit or a manual
2033
+ * drag has rotated it - rotation has to be tracked separately, since it leaves
2034
+ * the zoom and center at the default and the bounding-sphere check cannot see
2035
+ * it.
2036
+ */
2037
+ function useCameraReset() {
2038
+ const plugin = usePlugin();
2039
+ const [cameraChanged, setCameraChanged] = useState(false);
2040
+ /**
2041
+ * Mirrors the state above so the per-frame handler below can skip redundant
2042
+ * updates without re-reading state through a stale closure.
2043
+ */
2044
+ const cameraChangedRef = useRef(false);
2045
+ const updateCameraChanged = useCallback((changed) => {
2046
+ if (changed === cameraChangedRef.current) return;
2047
+ cameraChangedRef.current = changed;
2048
+ setCameraChanged(changed);
2049
+ }, []);
2050
+ /**
2051
+ * A manual gesture (rotate via drag) moves the camera off the default view.
2052
+ * Tracked separately from zoom because rotation leaves the zoom/center at
2053
+ * the default fit, so the bounding-sphere check cannot detect it.
2054
+ */
2055
+ const gesturedRef = useRef(false);
2056
+ /**
2057
+ * A (re)loaded structure auto-fits to the default view. Ignore camera
2058
+ * changes until that fit transition settles so the button does not flash
2059
+ * during load.
2060
+ */
2061
+ const awaitingFitRef = useRef(true);
2062
+ const fitStartedRef = useRef(false);
2063
+ /**
2064
+ * Reconciles the button visibility with the current camera. The button
2065
+ * shows when the camera differs from the default view: either the
2066
+ * zoom/center has moved off the bounding-sphere fit, or a manual gesture
2067
+ * rotated the view.
2068
+ */
2069
+ const syncCameraChanged = useCallback(() => {
2070
+ const canvas3d = plugin.canvas3d;
2071
+ if (!canvas3d) return;
2072
+ const inTransition = canvas3d.camera.transition.inTransition;
2073
+ if (awaitingFitRef.current) {
2074
+ if (inTransition) {
2075
+ fitStartedRef.current = true;
2076
+ return;
2077
+ }
2078
+ if (!fitStartedRef.current && isZoomedOffDefault(canvas3d)) return;
2079
+ awaitingFitRef.current = false;
2080
+ }
2081
+ if (inTransition) return;
2082
+ updateCameraChanged(gesturedRef.current || isZoomedOffDefault(canvas3d));
2083
+ }, [plugin, updateCameraChanged]);
2084
+ useEffect(() => {
2085
+ const subscriptions = [];
2086
+ let cameraBound = false;
2087
+ const bindCamera = () => {
2088
+ const canvas3d = plugin.canvas3d;
2089
+ if (!canvas3d || cameraBound) return;
2090
+ cameraBound = true;
2091
+ const setGestured = () => {
2092
+ gesturedRef.current = true;
2093
+ syncCameraChanged();
2094
+ };
2095
+ subscriptions.push(canvas3d.input.drag.subscribe(setGestured), canvas3d.input.wheel.subscribe(setGestured), plugin.state.data.events.object.created.subscribe(() => {
2096
+ gesturedRef.current = false;
2097
+ awaitingFitRef.current = true;
2098
+ fitStartedRef.current = false;
2099
+ updateCameraChanged(false);
2100
+ }), canvas3d.didDraw.subscribe(syncCameraChanged));
2101
+ };
2102
+ subscriptions.push(plugin.behaviors.canvas3d.initialized.subscribe(bindCamera));
2103
+ bindCamera();
2104
+ return () => subscriptions.forEach((s) => s.unsubscribe());
2105
+ }, [
2106
+ plugin,
2107
+ syncCameraChanged,
2108
+ updateCameraChanged
2109
+ ]);
2110
+ return {
2111
+ cameraChanged,
2112
+ resetCamera: useCallback(_asyncToGenerator(function* () {
2113
+ yield PluginCommands.Camera.Reset(plugin, { durationMs: 250 });
2114
+ yield new Promise((resolve) => setTimeout(resolve, 250));
2115
+ yield PluginCommands.Camera.ResetAxes(plugin, { durationMs: 250 });
2116
+ yield new Promise((resolve) => setTimeout(resolve, 250));
2117
+ gesturedRef.current = false;
2118
+ updateCameraChanged(false);
2119
+ }), [plugin, updateCameraChanged])
2120
+ };
2121
+ }
2122
+ //#endregion
2123
+ //#region src/core/ProteinStructureViewer/components/Viewport/style.ts
2124
+ /**
2125
+ * Pinned to the lower-left of the viewport, clear of the sequence panel below
2126
+ * and the axes widget in the corner.
2127
+ */
2128
+ const ResetCameraSlot = styled("div")`
2129
+ position: absolute;
2130
+ bottom: 48px;
2131
+ z-index: 10;
2132
+
2133
+ ${(props) => {
2134
+ const spaces = getSpaces(props);
2135
+ return `
2136
+ left: ${spaces === null || spaces === void 0 ? void 0 : spaces.m}px;
2137
+ `;
2138
+ }}
2139
+ `;
2140
+ //#endregion
2141
+ //#region src/core/ProteinStructureViewer/components/Viewport/index.tsx
2142
+ /**
2143
+ * Builds the Mol* viewport view.
2144
+ *
2145
+ * Mol* instantiates view components inside its own React root, so they render
2146
+ * outside the consumer's provider tree and cannot read the theme from context.
2147
+ * `viewSettings` carries the mode in instead, and is watched rather than read
2148
+ * once so that a theme change after the plugin was created still lands here;
2149
+ * the mode is re-supplied via a local `ThemeProvider` so the Emotion styles
2150
+ * below resolve SDS tokens correctly.
2151
+ */
2152
+ function createViewportView(viewSettings) {
2153
+ return function ViewportWithReset() {
2154
+ const { theme } = useMolstarTheme(viewSettings);
2155
+ const { cameraChanged, resetCamera } = useCameraReset();
2156
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [
2157
+ /* @__PURE__ */ jsx(Viewport, {}),
2158
+ /* @__PURE__ */ jsx("div", {
2159
+ className: "msp-highlight-toast-wrapper",
2160
+ children: /* @__PURE__ */ jsx(Toasts, {})
2161
+ }),
2162
+ /* @__PURE__ */ jsx(ThemeProvider, {
2163
+ theme,
2164
+ children: /* @__PURE__ */ jsx(ResetCameraSlot, {
2165
+ hidden: !cameraChanged,
2166
+ children: /* @__PURE__ */ jsx(Button, {
2167
+ "aria-label": "Reset camera",
2168
+ onClick: resetCamera,
2169
+ title: "Reset camera",
2170
+ sdsStyle: "minimal",
2171
+ sdsType: "secondary",
2172
+ size: "medium",
2173
+ children: /* @__PURE__ */ jsx(ArrowCounterClockwiseIcon, {})
2174
+ })
2175
+ })
2176
+ })
2177
+ ] });
2178
+ };
2179
+ }
2180
+ //#endregion
2181
+ //#region src/core/ProteinStructureViewer/utils/cameraFocus.ts
2182
+ /**
2183
+ * Margin (angstroms) added around a residue's bounding sphere when focusing,
2184
+ * matching Mol*'s default `FocusLoci` extra radius. Used both for the framing
2185
+ * radius that sets the zoom-in distance (floored at `FOCUS_MIN_RADIUS`) and for
2186
+ * the depth-clip anchor, so the focus crop matches Mol*'s default depth slab.
2187
+ */
2188
+ const FOCUS_MIN_RADIUS = 8;
2189
+ const FOCUS_EXTRA_RADIUS = 4;
2190
+ /**
2191
+ * Never let the clip window collapse below this, so the focused residue and its
2192
+ * immediate neighborhood stay visible even at extreme zoom-in.
2193
+ */
2194
+ const MIN_CLIP_RADIUS = 4;
2195
+ /** Camera transition length (ms) when zooming in on a residue. */
2196
+ const FOCUS_DURATION_MS = 250;
2197
+ /** Clip radius has to drift this far before it is worth redrawing. */
2198
+ const CLIP_EPSILON = .5;
2199
+ /**
2200
+ * Depth-clip radius at a given camera distance. The clip scales linearly with
2201
+ * distance, `ratio` being the clip radius per unit distance (fixed at focus so
2202
+ * the clip hugs the residue there), clamped to the scene. This keeps clipping
2203
+ * tied to the zoom:
2204
+ * - at the focus zoom the clip hugs the residue, peeling the structure so the
2205
+ * selected residue reads clearly;
2206
+ * - zooming in shrinks the clip further, peeling deeper toward the residue;
2207
+ * - zooming out grows the clip, revealing more of the structure, up to the
2208
+ * whole scene (`radiusMax`) - the point at which nothing is cropped.
2209
+ */
2210
+ function clipRadiusForDistance(distance, ratio, radiusMax) {
2211
+ return Math.max(MIN_CLIP_RADIUS, Math.min(ratio * distance, radiusMax));
2212
+ }
2213
+ /**
2214
+ * Zooms the camera in on a residue. Uses Mol*'s focus framing for the camera
2215
+ * *position* (so the zoom level matches the built-in focus) but anchors the
2216
+ * clip tightly on the residue itself, and returns the clip-radius-per-distance
2217
+ * ratio used to keep clipping in sync with the zoom afterwards.
2218
+ */
2219
+ function focusResidue(plugin, loci) {
2220
+ var _plugin$canvas3d;
2221
+ const camera = (_plugin$canvas3d = plugin.canvas3d) === null || _plugin$canvas3d === void 0 ? void 0 : _plugin$canvas3d.camera;
2222
+ if (!camera) return null;
2223
+ const sphere = Loci.getBoundingSphere(loci);
2224
+ if (!sphere) return null;
2225
+ const framingRadius = Math.max(sphere.radius + FOCUS_EXTRA_RADIUS, FOCUS_MIN_RADIUS);
2226
+ const snapshot = camera.getFocus(sphere.center, framingRadius);
2227
+ const focusDistance = camera.getTargetDistance(framingRadius);
2228
+ const clipRatio = (sphere.radius + FOCUS_EXTRA_RADIUS) / focusDistance;
2229
+ const radius = clipRadiusForDistance(focusDistance, clipRatio, camera.state.radiusMax);
2230
+ camera.setState(_objectSpread2(_objectSpread2({}, snapshot), {}, { radius }), FOCUS_DURATION_MS);
2231
+ return clipRatio;
2232
+ }
2233
+ /**
2234
+ * Keeps the depth clip in sync with the zoom while a residue is focused.
2235
+ *
2236
+ * Mol*'s trackball zoom moves the camera without touching the clip radius, so
2237
+ * this recomputes it after each draw: zooming in tightens the clip (peeling
2238
+ * toward the residue), zooming out widens it toward the whole structure. It is
2239
+ * a no-op when `getClipRatio` returns null (nothing focused) and while the
2240
+ * camera is mid-transition, so it does not fight the focus animation.
2241
+ */
2242
+ function syncClipToZoom(plugin, getClipRatio) {
2243
+ var _plugin$canvas3d2;
2244
+ const camera = (_plugin$canvas3d2 = plugin.canvas3d) === null || _plugin$canvas3d2 === void 0 ? void 0 : _plugin$canvas3d2.camera;
2245
+ const clipRatio = getClipRatio();
2246
+ if (!camera || clipRatio === null || camera.transition.inTransition) return;
2247
+ const radius = clipRadiusForDistance(Vec3.distance(camera.state.position, camera.state.target), clipRatio, camera.state.radiusMax);
2248
+ if (Math.abs(camera.state.radius - radius) > CLIP_EPSILON) {
2249
+ var _plugin$canvas3d3;
2250
+ camera.setState({ radius });
2251
+ (_plugin$canvas3d3 = plugin.canvas3d) === null || _plugin$canvas3d3 === void 0 || _plugin$canvas3d3.requestDraw();
2252
+ }
2253
+ }
2254
+ //#endregion
2255
+ //#region src/core/ProteinStructureViewer/utils/residueRef.ts
2256
+ /**
2257
+ * Reads the residue a loci points at, or null when it points at nothing the
2258
+ * viewer can address.
2259
+ *
2260
+ * `index` comes from Mol*'s residue key, which counts residues in file order
2261
+ * from zero and is the same counter `injectPlddtIntoPdb` walks. Deriving it
2262
+ * from the residue's *number* instead would tie it to how the file happens to
2263
+ * be numbered: a crop beginning at residue 200 would index `plddt` at 199, and
2264
+ * a file numbering each chain from 1 would give two residues the same index.
2265
+ */
2266
+ function residueRefFromLoci(loci) {
2267
+ var _StructureProperties$;
2268
+ const location = StructureElement.Location.create(void 0);
2269
+ const firstLoc = StructureElement.Loci.getFirstLocation(loci, location);
2270
+ if (!firstLoc) return null;
2271
+ return {
2272
+ chainId: StructureProperties.chain.auth_asym_id(firstLoc),
2273
+ compId: StructureProperties.atom.label_comp_id(firstLoc),
2274
+ insCode: (_StructureProperties$ = StructureProperties.residue.pdbx_PDB_ins_code(firstLoc)) !== null && _StructureProperties$ !== void 0 ? _StructureProperties$ : "",
2275
+ index: StructureProperties.residue.key(firstLoc),
2276
+ seqId: StructureProperties.residue.auth_seq_id(firstLoc)
2277
+ };
2278
+ }
2279
+ /**
2280
+ * How a residue is named in the readout, e.g. `"LYS 111"`. Takes the number and
2281
+ * the insertion code from the file so that the readout and the sequence panel
2282
+ * beneath it agree on what to call a residue.
2283
+ */
2284
+ function residueLabel(residue) {
2285
+ return `${residue.compId} ${residue.seqId}${residue.insCode}`;
2286
+ }
2287
+ //#endregion
2288
+ //#region src/core/ProteinStructureViewer/utils/plddt.ts
2289
+ /** Mol* color theme name under which pLDDT coloring is registered. */
2290
+ const PLDDT_THEME_NAME = "plddt-bfactor";
2291
+ /**
2292
+ * AlphaFold's pLDDT confidence bands. Thresholds are on the 0-1 scale that
2293
+ * `plddt` values use, ascending from lowest to highest confidence.
2294
+ */
2295
+ const PLDDT_COLOR_SCALE = {
2296
+ kind: "stepped",
2297
+ stops: [
2298
+ {
2299
+ color: "#FF7C45",
2300
+ label: "0.5",
2301
+ threshold: .5
2302
+ },
2303
+ {
2304
+ color: "#FFDB11",
2305
+ label: "0.7",
2306
+ threshold: .7
2307
+ },
2308
+ {
2309
+ color: "#64CBF3",
2310
+ label: "0.9",
2311
+ threshold: .9
2312
+ },
2313
+ {
2314
+ color: "#0053D5",
2315
+ label: "1.0",
2316
+ threshold: 1
2317
+ }
2318
+ ]
2319
+ };
2320
+ const PLDDT_BANDS = PLDDT_COLOR_SCALE.kind === "stepped" ? PLDDT_COLOR_SCALE.stops : [];
2321
+ const DEFAULT_PLDDT_COLOR = Color.fromRgb(128, 128, 128);
2322
+ /**
2323
+ * Maps a pLDDT confidence score on the 0-100 B-factor scale to its band color.
2324
+ * `PLDDT_COLOR_SCALE` thresholds are on the 0-1 scale, so the score is
2325
+ * normalized before it is sampled.
2326
+ */
2327
+ function plddtToColor(value) {
2328
+ const [r, g, b] = sampleSteppedScale(PLDDT_BANDS, value / 100);
2329
+ return Color.fromRgb(r, g, b);
2330
+ }
2331
+ /**
2332
+ * Rewrites a PDB file's B-factor column with pLDDT scores so Mol* can read them
2333
+ * off the parsed model. B-factors occupy columns 60-66 of ATOM/HETATM lines;
2334
+ * scores arrive on a 0-1 scale and are stored on the conventional 0-100 one.
2335
+ * Residues past the end of `plddtValues` fall back to a mid-confidence 50.
2336
+ *
2337
+ * A residue is identified by chain, sequence number and insertion code. The
2338
+ * code has to be part of that: `10` and `10A` are two residues, Mol* counts
2339
+ * them as two, and treating them as one here would hand every residue after
2340
+ * them its neighbour's score.
2341
+ */
2342
+ function injectPlddtIntoPdb(pdbData, plddtValues) {
2343
+ const lines = pdbData.split("\n");
2344
+ let residueIndex = -1;
2345
+ let lastResidue = "";
2346
+ return lines.map((line) => {
2347
+ var _plddtValues$residueI;
2348
+ if (!line.startsWith("ATOM") && !line.startsWith("HETATM")) return line;
2349
+ const residue = line.substring(21, 27);
2350
+ if (residue !== lastResidue) {
2351
+ residueIndex++;
2352
+ lastResidue = residue;
2353
+ }
2354
+ const bfactorStr = (residueIndex < plddtValues.length ? ((_plddtValues$residueI = plddtValues[residueIndex]) !== null && _plddtValues$residueI !== void 0 ? _plddtValues$residueI : 0) * 100 : 50).toFixed(2).padStart(6, " ");
2355
+ const paddedLine = line.padEnd(66, " ");
2356
+ return paddedLine.substring(0, 60) + bfactorStr + paddedLine.substring(66);
2357
+ }).join("\n");
2358
+ }
2359
+ /**
2360
+ * Mol* color theme that reads pLDDT scores out of the B-factor column, paired
2361
+ * with `injectPlddtIntoPdb`. Registered per plugin instance.
2362
+ */
2363
+ const PlddtColoring = CustomElementProperty.create({
2364
+ coloring: {
2365
+ defaultColor: DEFAULT_PLDDT_COLOR,
2366
+ getColor(e) {
2367
+ return plddtToColor(e);
2368
+ }
2369
+ },
2370
+ getData(model) {
2371
+ const map = /* @__PURE__ */ new Map();
2372
+ const atomCount = model.atomicHierarchy.atoms._rowCount;
2373
+ for (let i = 0; i < atomCount; i++) map.set(i, model.atomicConformation.B_iso_or_equiv.value(i));
2374
+ return { value: map };
2375
+ },
2376
+ label: "pLDDT",
2377
+ name: PLDDT_THEME_NAME
2378
+ });
2379
+ //#endregion
2380
+ //#region src/core/ProteinStructureViewer/utils/color.ts
2381
+ /** Parses `#RRGGBB` (or `RRGGBB`) into a Mol* `Color`. */
2382
+ function parseHexColor(hex) {
2383
+ const h = hex.replace("#", "");
2384
+ return Color.fromRgb(parseInt(h.substring(0, 2), 16), parseInt(h.substring(2, 4), 16), parseInt(h.substring(4, 6), 16));
2385
+ }
2386
+ /**
2387
+ * Parses a theme color into a Mol* `Color`, falling back when the token is
2388
+ * missing or is a format Mol* cannot consume (theme values are hex in practice,
2389
+ * but the palette types allow any CSS color string).
2390
+ */
2391
+ function themeColor(value, fallback) {
2392
+ const candidate = value === null || value === void 0 ? void 0 : value.trim();
2393
+ return parseHexColor(candidate !== void 0 && /^#?[0-9a-f]{6}$/i.test(candidate) ? candidate : fallback);
2394
+ }
2395
+ /**
2396
+ * Neutral fill for residues with no overlay value. Sits between the light and
2397
+ * dark backgrounds so unhighlighted geometry stays legible in both modes.
2398
+ */
2399
+ function neutralResidueColor(mode) {
2400
+ const g = mode === "dark" ? 100 : 180;
2401
+ return Color.fromRgb(g, g, g);
2402
+ }
2403
+ //#endregion
2404
+ //#region src/core/ProteinStructureViewer/utils/residueValueTheme.ts
2405
+ /** Mol* color theme name under which residue value overlays are registered. */
2406
+ const RESIDUE_VALUE_THEME_NAME = "residue-value";
2407
+ /**
2408
+ * Builds a Mol* color theme that paints residues by an arbitrary per-residue
2409
+ * value.
2410
+ *
2411
+ * The theme is created per plugin instance rather than shared at module scope,
2412
+ * so several viewers can render different overlays on the same page. Its state
2413
+ * lives in the closure below and is read live inside `color()`, which is what
2414
+ * lets an overlay update recolor the structure without rebuilding it.
2415
+ */
2416
+ function createResidueValueTheme(mode) {
2417
+ const state = {
2418
+ colorScale: PLASMA_COLOR_SCALE,
2419
+ max: 1,
2420
+ min: 0,
2421
+ mode,
2422
+ values: /* @__PURE__ */ new Map(),
2423
+ version: 0
2424
+ };
2425
+ const provider = {
2426
+ category: "residue property",
2427
+ defaultValues: {},
2428
+ factory() {
2429
+ const { colorScale, max, min, mode: currentMode, values } = state;
2430
+ const neutral = neutralResidueColor(currentMode);
2431
+ return {
2432
+ color(location) {
2433
+ if (!StructureElement.Location.is(location)) return neutral;
2434
+ const value = values.get(StructureProperties.residue.key(location));
2435
+ if (value === void 0) return neutral;
2436
+ const rgb = sampleColorScale(colorScale, value, max, min);
2437
+ return rgb ? Color.fromRgb(rgb[0], rgb[1], rgb[2]) : neutral;
2438
+ },
2439
+ contextHash: state.version,
2440
+ description: "Color residues by a per-residue value",
2441
+ factory: provider.factory,
2442
+ granularity: "group",
2443
+ props: {}
2444
+ };
2445
+ },
2446
+ getParams: () => ({}),
2447
+ isApplicable: () => true,
2448
+ label: "Residue Value",
2449
+ name: RESIDUE_VALUE_THEME_NAME
2450
+ };
2451
+ return {
2452
+ provider,
2453
+ setState(next) {
2454
+ Object.assign(state, next);
2455
+ state.version++;
2456
+ }
2457
+ };
2458
+ }
2459
+ //#endregion
2460
+ //#region src/core/ProteinStructureViewer/hooks/useMolstarPlugin.ts
2461
+ /** Structure representation Mol* builds for the loaded model. */
2462
+ const REPRESENTATION_PRESET = "polymer-cartoon";
2463
+ /** Theme used when no pLDDT scores are supplied. */
2464
+ const FALLBACK_THEME_NAME = "chain-id";
2465
+ /** Delay before retrying initialization while the container has no size. */
2466
+ const LAYOUT_RETRY_MS = 100;
2467
+ /** How long to wait for a frame before starting without one. */
2468
+ const FRAME_WAIT_MS = 100;
2469
+ /**
2470
+ * Yield once so the container can be laid out before it is measured.
2471
+ *
2472
+ * `requestAnimationFrame` does not fire while the document is hidden -- a
2473
+ * background tab, a collapsed pane, or a headless capture that never paints --
2474
+ * and awaiting it alone leaves the viewer parked forever on a page that has in
2475
+ * fact been laid out. Racing it against a timeout keeps the fast path on a
2476
+ * visible page and still starts on a hidden one, where the size check below is
2477
+ * what actually guards against measuring too early.
2478
+ *
2479
+ * The loser of the race is cancelled rather than left to fire: on a visible
2480
+ * page this runs once per viewer, and a grid of them would otherwise each keep
2481
+ * a stray timer alive past the frame that already resolved.
2482
+ */
2483
+ function nextFrame() {
2484
+ return new Promise((resolve) => {
2485
+ const done = () => {
2486
+ cancelAnimationFrame(frame);
2487
+ clearTimeout(timer);
2488
+ resolve();
2489
+ };
2490
+ const frame = requestAnimationFrame(done);
2491
+ const timer = setTimeout(done, FRAME_WAIT_MS);
2492
+ });
2493
+ }
2494
+ /**
2495
+ * Hover fills the geometry with a tint; selection stays outline-only. Mol*'s
2496
+ * `colorMarker` is a global toggle for the per-fragment marker tint, so it is
2497
+ * scoped to hover via the per-state strengths below.
2498
+ */
2499
+ const HIGHLIGHT_STRENGTH = .2;
2500
+ const SELECT_STRENGTH = 0;
2501
+ const AXES_ON = {
2502
+ name: "on",
2503
+ params: {
2504
+ alpha: .51,
2505
+ colorX: 16711680,
2506
+ colorY: 32768,
2507
+ colorZ: 255,
2508
+ labelColorX: 8421504,
2509
+ labelColorY: 8421504,
2510
+ labelColorZ: 8421504,
2511
+ labelOpacity: 1,
2512
+ labelScale: .25,
2513
+ labelX: "X",
2514
+ labelY: "Y",
2515
+ labelZ: "Z",
2516
+ location: "bottom-left",
2517
+ locationOffsetX: 0,
2518
+ locationOffsetY: 0,
2519
+ originColor: 8421504,
2520
+ planeColorXY: 8421504,
2521
+ planeColorXZ: 8421504,
2522
+ planeColorYZ: 8421504,
2523
+ radiusScale: .075,
2524
+ scale: .15,
2525
+ showLabels: false,
2526
+ showPlanes: true
2527
+ }
2528
+ };
2529
+ const AXES_OFF = {
2530
+ name: "off",
2531
+ params: {}
2532
+ };
2533
+ /**
2534
+ * Turns the screenshot controls off, on the versions of Mol* that have them.
2535
+ *
2536
+ * `ShowScreenshotControls` arrived in Mol* 5 and this package's peer range
2537
+ * reaches back to 4, so the key is read through an index rather than named
2538
+ * directly: naming it would not compile against 4, and passing the resulting
2539
+ * `undefined` would ask Mol* to set a config item it has no name for. On 4
2540
+ * there are no screenshot controls to hide in the first place.
2541
+ */
2542
+ const SCREENSHOT_CONTROLS_CONFIG = (() => {
2543
+ const item = PluginConfig.Viewport.ShowScreenshotControls;
2544
+ return item ? [[item, false]] : [];
2545
+ })();
2546
+ function setAxes(plugin, enabled) {
2547
+ if (!plugin.canvas3d) return;
2548
+ try {
2549
+ plugin.canvas3d.setProps({ camera: { helper: { axes: enabled ? AXES_ON : AXES_OFF } } });
2550
+ } catch (e) {
2551
+ console.warn("Could not set axes:", e);
2552
+ }
2553
+ }
2554
+ function createViewer(_x) {
2555
+ return _createViewer.apply(this, arguments);
2556
+ }
2557
+ function _createViewer() {
2558
+ _createViewer = _asyncToGenerator(function* ({ backgroundColor, edgeColor, highlightColor, residueValueTheme, root, showAxes, showSequenceViewer, viewSettings }) {
2559
+ var _spec$config;
2560
+ const spec = DefaultPluginUISpec();
2561
+ const behaviors = spec.behaviors.filter((b) => b.transformer !== PluginBehaviors.Camera.FocusLoci);
2562
+ const plugin = yield createPluginUI({
2563
+ render: renderReact18,
2564
+ spec: _objectSpread2(_objectSpread2({}, spec), {}, {
2565
+ behaviors,
2566
+ canvas3d: {
2567
+ camera: { helper: { axes: showAxes ? AXES_ON : AXES_OFF } },
2568
+ marking: {
2569
+ highlightEdgeColor: edgeColor,
2570
+ selectEdgeColor: edgeColor
2571
+ },
2572
+ renderer: {
2573
+ backgroundColor,
2574
+ colorMarker: true,
2575
+ highlightColor,
2576
+ highlightStrength: HIGHLIGHT_STRENGTH,
2577
+ selectStrength: SELECT_STRENGTH
2578
+ }
2579
+ },
2580
+ components: _objectSpread2({
2581
+ remoteState: "none",
2582
+ sequenceViewer: { view: createSequenceView(viewSettings) }
2583
+ }, showAxes && { viewport: { view: createViewportView(viewSettings) } }),
2584
+ config: [
2585
+ ...(_spec$config = spec.config) !== null && _spec$config !== void 0 ? _spec$config : [],
2586
+ [PluginConfig.Viewport.ShowExpand, false],
2587
+ [PluginConfig.Viewport.ShowControls, false],
2588
+ [PluginConfig.Viewport.ShowSettings, false],
2589
+ [PluginConfig.Viewport.ShowSelectionMode, false],
2590
+ [PluginConfig.Viewport.ShowAnimation, false],
2591
+ [PluginConfig.Viewport.ShowTrajectoryControls, false],
2592
+ ...SCREENSHOT_CONTROLS_CONFIG
2593
+ ],
2594
+ layout: { initial: {
2595
+ controlsDisplay: "reactive",
2596
+ isExpanded: false,
2597
+ regionState: {
2598
+ bottom: "hidden",
2599
+ left: "hidden",
2600
+ right: "hidden",
2601
+ top: showSequenceViewer ? "full" : "hidden"
2602
+ }
2603
+ } }
2604
+ }),
2605
+ target: root
2606
+ });
2607
+ const registry = plugin.representation.structure.themes.colorThemeRegistry;
2608
+ if (PlddtColoring.colorThemeProvider) registry.add(PlddtColoring.colorThemeProvider);
2609
+ registry.add(residueValueTheme.provider);
2610
+ for (const p of [...plugin.managers.lociLabels.providers]) plugin.managers.lociLabels.removeProvider(p);
2611
+ plugin.managers.structure.focus.behaviors.current.subscribe((entry) => {
2612
+ const select = plugin.managers.interactivity.lociSelects;
2613
+ if (entry === null || entry === void 0 ? void 0 : entry.loci) select.selectOnly({ loci: entry.loci });
2614
+ else select.deselectAll();
2615
+ });
2616
+ return plugin;
2617
+ });
2618
+ return _createViewer.apply(this, arguments);
2619
+ }
2620
+ function loadStructure(_x2, _x3, _x4, _x5) {
2621
+ return _loadStructure.apply(this, arguments);
2622
+ }
2623
+ function _loadStructure() {
2624
+ _loadStructure = _asyncToGenerator(function* (plugin, pdbData, usePlddtColoring, showAxes) {
2625
+ try {
2626
+ var _plugin$canvas3d;
2627
+ yield plugin.clear();
2628
+ const data = yield plugin.builders.data.rawData({
2629
+ data: pdbData,
2630
+ label: "Structure"
2631
+ });
2632
+ const trajectory = yield plugin.builders.structure.parseTrajectory(data, "pdb");
2633
+ yield plugin.builders.structure.hierarchy.applyPreset(trajectory, "default", {
2634
+ representationPreset: REPRESENTATION_PRESET,
2635
+ showUnitcell: false,
2636
+ structure: {
2637
+ name: "model",
2638
+ params: {}
2639
+ }
2640
+ });
2641
+ if (usePlddtColoring) yield applyColorTheme(plugin, PLDDT_THEME_NAME);
2642
+ (_plugin$canvas3d = plugin.canvas3d) === null || _plugin$canvas3d === void 0 || _plugin$canvas3d.requestCameraReset();
2643
+ setAxes(plugin, showAxes);
2644
+ } catch (error) {
2645
+ console.error("Failed to load structure:", error);
2646
+ }
2647
+ });
2648
+ return _loadStructure.apply(this, arguments);
2649
+ }
2650
+ /** Recolors every loaded structure's representations with the named theme. */
2651
+ function applyColorTheme(_x6, _x7) {
2652
+ return _applyColorTheme.apply(this, arguments);
2653
+ }
2654
+ function _applyColorTheme() {
2655
+ _applyColorTheme = _asyncToGenerator(function* (plugin, colorTheme) {
2656
+ yield plugin.dataTransaction(_asyncToGenerator(function* () {
2657
+ for (const s of plugin.managers.structure.hierarchy.current.structures) yield plugin.managers.structure.component.updateRepresentationsTheme(s.components, { color: colorTheme });
2658
+ }));
2659
+ });
2660
+ return _applyColorTheme.apply(this, arguments);
2661
+ }
2662
+ /**
2663
+ * Owns the Mol* plugin for one viewer: creates it, loads the structure, wires
2664
+ * interaction callbacks, and disposes of it on unmount.
2665
+ *
2666
+ * The plugin is built once and then mutated in place. Creating it is expensive
2667
+ * and destroys the camera, so prop changes that Mol* can absorb (background,
2668
+ * axes, coloring, new PDB) are pushed in through the effects below and in the
2669
+ * sibling hooks rather than by rebuilding.
2670
+ */
2671
+ function useMolstarPlugin({ backgroundColor, containerRef, edgeColor, hasPlddt, highlightColor, mode, onResidueClick, onResidueHover, onSelectionClear, pdb, sequenceViewerBackgroundColor, showAxes, showSequenceViewer }) {
2672
+ const pluginRef = useRef(null);
2673
+ const residueValueThemeRef = useRef(null);
2674
+ const currentPdbRef = useRef(null);
2675
+ const [isReady, setIsReady] = useState(false);
2676
+ /**
2677
+ * Carries the theme-dependent props into the views Mol* renders in its own
2678
+ * React root. They cannot read them from context, and rebuilding the plugin
2679
+ * would throw away the camera, so the values are pushed to them instead.
2680
+ */
2681
+ const viewSettingsRef = useRef(null);
2682
+ if (viewSettingsRef.current === null) viewSettingsRef.current = new BehaviorSubject({
2683
+ mode,
2684
+ sequenceViewerBackgroundColor
2685
+ });
2686
+ const viewSettings = viewSettingsRef.current;
2687
+ /**
2688
+ * Clip-radius-per-camera-distance ratio recorded at the last residue focus,
2689
+ * or null when no residue is focused. Anchors the adaptive depth clipping.
2690
+ */
2691
+ const clipRatioRef = useRef(null);
2692
+ const setClipRatio = useCallback((ratio) => {
2693
+ clipRatioRef.current = ratio;
2694
+ }, []);
2695
+ const onResidueClickRef = useRef(onResidueClick);
2696
+ onResidueClickRef.current = onResidueClick;
2697
+ const onSelectionClearRef = useRef(onSelectionClear);
2698
+ onSelectionClearRef.current = onSelectionClear;
2699
+ const onResidueHoverRef = useRef(onResidueHover);
2700
+ onResidueHoverRef.current = onResidueHover;
2701
+ /** Last residue reported to `onResidueHover`, to suppress repeats. */
2702
+ const lastHoverRef = useRef(null);
2703
+ const initialPropsRef = useRef({
2704
+ backgroundColor,
2705
+ edgeColor,
2706
+ hasPlddt,
2707
+ highlightColor,
2708
+ mode,
2709
+ pdb,
2710
+ showAxes,
2711
+ showSequenceViewer
2712
+ });
2713
+ initialPropsRef.current = {
2714
+ backgroundColor,
2715
+ edgeColor,
2716
+ hasPlddt,
2717
+ highlightColor,
2718
+ mode,
2719
+ pdb,
2720
+ showAxes,
2721
+ showSequenceViewer
2722
+ };
2723
+ useEffect(() => {
2724
+ const container = containerRef.current;
2725
+ if (!container) return;
2726
+ let cancelled = false;
2727
+ let clipSubscription;
2728
+ const init = function() {
2729
+ var _ref = _asyncToGenerator(function* () {
2730
+ yield nextFrame();
2731
+ if (cancelled) return;
2732
+ const rect = container.getBoundingClientRect();
2733
+ if (rect.width === 0 || rect.height === 0) {
2734
+ setTimeout(init, LAYOUT_RETRY_MS);
2735
+ return;
2736
+ }
2737
+ const initial = initialPropsRef.current;
2738
+ try {
2739
+ var _plugin$canvas3d2;
2740
+ const residueValueTheme = createResidueValueTheme(initial.mode);
2741
+ const plugin = yield createViewer({
2742
+ backgroundColor: initial.backgroundColor,
2743
+ edgeColor: initial.edgeColor,
2744
+ highlightColor: initial.highlightColor,
2745
+ residueValueTheme,
2746
+ root: container,
2747
+ showAxes: initial.showAxes,
2748
+ showSequenceViewer: initial.showSequenceViewer,
2749
+ viewSettings
2750
+ });
2751
+ if (cancelled) {
2752
+ plugin.dispose();
2753
+ return;
2754
+ }
2755
+ pluginRef.current = plugin;
2756
+ residueValueThemeRef.current = residueValueTheme;
2757
+ const latest = initialPropsRef.current;
2758
+ yield loadStructure(plugin, latest.pdb, latest.hasPlddt, latest.showAxes);
2759
+ currentPdbRef.current = latest.pdb;
2760
+ plugin.behaviors.interaction.click.subscribe((e) => {
2761
+ var _onResidueClickRef$cu;
2762
+ if (Representation.Loci.isEmpty(e.current)) {
2763
+ var _onSelectionClearRef$;
2764
+ (_onSelectionClearRef$ = onSelectionClearRef.current) === null || _onSelectionClearRef$ === void 0 || _onSelectionClearRef$.call(onSelectionClearRef);
2765
+ return;
2766
+ }
2767
+ const loci = e.current.loci;
2768
+ if (!StructureElement.Loci.is(loci)) return;
2769
+ const residue = residueRefFromLoci(loci);
2770
+ if (!residue) return;
2771
+ (_onResidueClickRef$cu = onResidueClickRef.current) === null || _onResidueClickRef$cu === void 0 || _onResidueClickRef$cu.call(onResidueClickRef, residue);
2772
+ });
2773
+ plugin.behaviors.interaction.hover.subscribe((e) => {
2774
+ var _lastHoverRef$current, _onResidueHoverRef$cu;
2775
+ const loci = e.current.loci;
2776
+ const residue = StructureElement.Loci.is(loci) ? residueRefFromLoci(loci) : null;
2777
+ if ((residue === null || residue === void 0 ? void 0 : residue.index) === ((_lastHoverRef$current = lastHoverRef.current) === null || _lastHoverRef$current === void 0 ? void 0 : _lastHoverRef$current.index)) return;
2778
+ lastHoverRef.current = residue;
2779
+ (_onResidueHoverRef$cu = onResidueHoverRef.current) === null || _onResidueHoverRef$cu === void 0 || _onResidueHoverRef$cu.call(onResidueHoverRef, residue);
2780
+ });
2781
+ clipSubscription = (_plugin$canvas3d2 = plugin.canvas3d) === null || _plugin$canvas3d2 === void 0 ? void 0 : _plugin$canvas3d2.didDraw.subscribe(() => {
2782
+ syncClipToZoom(plugin, () => clipRatioRef.current);
2783
+ });
2784
+ if (!cancelled) setIsReady(true);
2785
+ } catch (error) {
2786
+ console.error("Failed to initialize Mol* viewer:", error);
2787
+ }
2788
+ });
2789
+ return function init() {
2790
+ return _ref.apply(this, arguments);
2791
+ };
2792
+ }();
2793
+ init();
2794
+ return () => {
2795
+ var _pluginRef$current;
2796
+ cancelled = true;
2797
+ clipSubscription === null || clipSubscription === void 0 || clipSubscription.unsubscribe();
2798
+ (_pluginRef$current = pluginRef.current) === null || _pluginRef$current === void 0 || _pluginRef$current.dispose();
2799
+ pluginRef.current = null;
2800
+ residueValueThemeRef.current = null;
2801
+ currentPdbRef.current = null;
2802
+ clipRatioRef.current = null;
2803
+ setIsReady(false);
2804
+ };
2805
+ }, []);
2806
+ useEffect(() => {
2807
+ var _pluginRef$current2;
2808
+ const canvas3d = (_pluginRef$current2 = pluginRef.current) === null || _pluginRef$current2 === void 0 ? void 0 : _pluginRef$current2.canvas3d;
2809
+ if (!canvas3d || !isReady) return;
2810
+ canvas3d.setProps({
2811
+ marking: {
2812
+ highlightEdgeColor: edgeColor,
2813
+ selectEdgeColor: edgeColor
2814
+ },
2815
+ renderer: {
2816
+ backgroundColor,
2817
+ highlightColor
2818
+ }
2819
+ });
2820
+ }, [
2821
+ backgroundColor,
2822
+ edgeColor,
2823
+ highlightColor,
2824
+ isReady
2825
+ ]);
2826
+ useEffect(() => {
2827
+ viewSettings.next({
2828
+ mode,
2829
+ sequenceViewerBackgroundColor
2830
+ });
2831
+ }, [
2832
+ mode,
2833
+ sequenceViewerBackgroundColor,
2834
+ viewSettings
2835
+ ]);
2836
+ useEffect(() => {
2837
+ if (!isReady || !pluginRef.current) return;
2838
+ setAxes(pluginRef.current, showAxes);
2839
+ }, [showAxes, isReady]);
2840
+ useEffect(() => {
2841
+ const plugin = pluginRef.current;
2842
+ if (!plugin || !isReady) return;
2843
+ if (pdb === currentPdbRef.current) return;
2844
+ currentPdbRef.current = pdb;
2845
+ clipRatioRef.current = null;
2846
+ lastHoverRef.current = null;
2847
+ loadStructure(plugin, pdb, hasPlddt, showAxes);
2848
+ }, [
2849
+ pdb,
2850
+ hasPlddt,
2851
+ isReady
2852
+ ]);
2853
+ return {
2854
+ isReady,
2855
+ pluginRef,
2856
+ residueValueThemeRef,
2857
+ setClipRatio
2858
+ };
2859
+ }
2860
+ //#endregion
2861
+ //#region src/core/ProteinStructureViewer/utils/residueLoci.ts
2862
+ /**
2863
+ * Every atom of the residue at a 0-based index, as geometry Mol* can focus and
2864
+ * frame, or undefined when the structure has no such residue.
2865
+ *
2866
+ * The inverse of `residueRefFromLoci`, and keyed on the same residue ordinal,
2867
+ * so a `selectedResidue` echoed back from a click resolves to the residue that
2868
+ * was clicked - on a complex as much as on a single chain.
2869
+ *
2870
+ * The whole residue is collected rather than one atom, since the caller frames
2871
+ * the camera on the loci's bounding sphere and a single atom would zoom far
2872
+ * closer than the residue warrants.
2873
+ */
2874
+ function lociForResidueIndex(plugin, residueIndex) {
2875
+ for (const entry of plugin.managers.structure.hierarchy.current.structures) {
2876
+ var _entry$cell$obj;
2877
+ const structure = (_entry$cell$obj = entry.cell.obj) === null || _entry$cell$obj === void 0 ? void 0 : _entry$cell$obj.data;
2878
+ if (!structure) continue;
2879
+ const location = StructureElement.Location.create(structure);
2880
+ for (const unit of structure.units) {
2881
+ location.unit = unit;
2882
+ const indices = [];
2883
+ for (let i = 0; i < unit.elements.length; i++) {
2884
+ location.element = unit.elements[i];
2885
+ if (StructureProperties.residue.key(location) === residueIndex) indices.push(i);
2886
+ }
2887
+ if (indices.length > 0) return StructureElement.Loci(structure, [{
2888
+ indices: OrderedSet.ofSortedArray(indices),
2889
+ unit
2890
+ }]);
2891
+ }
2892
+ }
2893
+ }
2894
+ //#endregion
2895
+ //#region src/core/ProteinStructureViewer/hooks/useResidueFocus.ts
2896
+ /**
2897
+ * Makes `selectedResidue` the one thing that decides where the camera sits.
2898
+ *
2899
+ * Setting the prop zooms in on that residue and marks it; clearing it drops the
2900
+ * focus and zooms back out. The click subscription only reports the residue it
2901
+ * was given, so a click moves the camera only once the consumer accepts it and
2902
+ * echoes it back - which is what the prop being controlled is supposed to mean.
2903
+ *
2904
+ * Clicking still marks the residue immediately, through Mol*'s own focus
2905
+ * behavior, so the structure responds to a click whether or not the consumer
2906
+ * wires the prop up.
2907
+ */
2908
+ function useResidueFocus({ isReady, pluginRef, selectedResidue, setClipRatio }) {
2909
+ /**
2910
+ * The residue this hook last pointed the camera at. Tracked rather than
2911
+ * compared against the previous prop value so that a selection made before
2912
+ * the plugin was ready is still applied once it is, and so that a plugin
2913
+ * rebuilt underneath us - which comes up focused on nothing - is not assumed
2914
+ * to still be holding the old one.
2915
+ */
2916
+ const focusedRef = useRef(null);
2917
+ useEffect(() => {
2918
+ const plugin = pluginRef.current;
2919
+ if (!plugin || !isReady) {
2920
+ focusedRef.current = null;
2921
+ return;
2922
+ }
2923
+ const residue = selectedResidue !== null && selectedResidue !== void 0 ? selectedResidue : null;
2924
+ if (residue === focusedRef.current) return;
2925
+ focusedRef.current = residue;
2926
+ if (residue === null) {
2927
+ var _plugin$canvas3d;
2928
+ setClipRatio(null);
2929
+ plugin.managers.structure.focus.clear();
2930
+ (_plugin$canvas3d = plugin.canvas3d) === null || _plugin$canvas3d === void 0 || _plugin$canvas3d.requestCameraReset();
2931
+ return;
2932
+ }
2933
+ const loci = lociForResidueIndex(plugin, residue);
2934
+ if (!loci) return;
2935
+ const focused = plugin.managers.structure.focus.behaviors.current.value;
2936
+ if (!focused || !StructureElement.Loci.areEqual(focused.loci, loci)) plugin.managers.structure.focus.setFromLoci(loci);
2937
+ setClipRatio(focusResidue(plugin, loci));
2938
+ }, [
2939
+ selectedResidue,
2940
+ isReady,
2941
+ setClipRatio
2942
+ ]);
2943
+ }
2944
+ //#endregion
2945
+ //#region src/core/ProteinStructureViewer/hooks/useResidueOverlay.ts
2946
+ /**
2947
+ * Switches the structure's coloring between the residue value overlay and the
2948
+ * default (pLDDT, or chain-id when no scores were supplied).
2949
+ *
2950
+ * Recoloring happens in place: the camera is left where it is, so selecting or
2951
+ * switching an overlay does not zoom back out to the default view.
2952
+ */
2953
+ function useResidueOverlay({ hasPlddt, isReady, mode, overlay, pluginRef, residueValueThemeRef }) {
2954
+ useEffect(() => {
2955
+ const plugin = pluginRef.current;
2956
+ const theme = residueValueThemeRef.current;
2957
+ if (!plugin || !theme || !isReady) return;
2958
+ if (overlay) {
2959
+ var _overlay$colorScale, _overlay$min;
2960
+ theme.setState({
2961
+ colorScale: (_overlay$colorScale = overlay.colorScale) !== null && _overlay$colorScale !== void 0 ? _overlay$colorScale : PLASMA_COLOR_SCALE,
2962
+ max: overlay.max,
2963
+ min: (_overlay$min = overlay.min) !== null && _overlay$min !== void 0 ? _overlay$min : 0,
2964
+ mode,
2965
+ values: overlay.values
2966
+ });
2967
+ applyColorTheme(plugin, RESIDUE_VALUE_THEME_NAME);
2968
+ } else {
2969
+ theme.setState({
2970
+ mode,
2971
+ values: /* @__PURE__ */ new Map()
2972
+ });
2973
+ applyColorTheme(plugin, hasPlddt ? PLDDT_THEME_NAME : FALLBACK_THEME_NAME);
2974
+ }
2975
+ }, [
2976
+ overlay,
2977
+ hasPlddt,
2978
+ isReady,
2979
+ mode
2980
+ ]);
2981
+ }
2982
+ //#endregion
2983
+ //#region src/core/ProteinStructureViewer/index.tsx
2984
+ const _excluded$1 = [
2985
+ "backgroundColor",
2986
+ "onResidueClick",
2987
+ "onResidueHover",
2988
+ "onSelectionClear",
2989
+ "pdb",
2990
+ "plddt",
2991
+ "residueOverlay",
2992
+ "selectedResidue",
2993
+ "sequenceViewerBackgroundColor",
2994
+ "showAxes",
2995
+ "showLegend",
2996
+ "showSequenceViewer",
2997
+ "stats"
2998
+ ];
2999
+ /** Fallbacks for theme tokens Mol* needs as concrete hex colors. */
3000
+ const FALLBACK_EDGE_COLOR = "#6c6c6c";
3001
+ const FALLBACK_HIGHLIGHT_COLOR = "#1b1b1b";
3002
+ /**
3003
+ * Keyed by mode: unlike the two above, the canvas has to invert with it. Only
3004
+ * reached when the viewer renders outside an SDS theme; inside one the
3005
+ * `base.backgroundPrimary` token wins.
3006
+ */
3007
+ const FALLBACK_BACKGROUND_COLOR = {
3008
+ dark: "#000000",
3009
+ light: "#ffffff"
3010
+ };
3011
+ /** Caption shown beneath the color key when no overlay is active. */
3012
+ const PLDDT_SCALE_LABEL = "pLDDT";
3013
+ /** Caption used when an overlay does not name itself. */
3014
+ const DEFAULT_OVERLAY_LABEL = "Value";
3015
+ /** Readout slot label used when an overlay does not name one. */
3016
+ const DEFAULT_READOUT_LABEL = "Value";
3017
+ /**
3018
+ * Chooses what the legend describes, following whatever is actually coloring
3019
+ * the structure: the overlay when one is set, the pLDDT bands when scores were
3020
+ * supplied, and nothing at all otherwise - Mol* falls back to chain coloring
3021
+ * there, which no per-residue scale describes, so a key would be labelling
3022
+ * colors that are not on screen.
3023
+ */
3024
+ function resolveScaleProps(overlay, hasPlddt) {
3025
+ if (overlay) {
3026
+ var _overlay$colorScale, _overlay$label, _overlay$min, _overlay$readoutLabel;
3027
+ return {
3028
+ scale: (_overlay$colorScale = overlay.colorScale) !== null && _overlay$colorScale !== void 0 ? _overlay$colorScale : PLASMA_COLOR_SCALE,
3029
+ scaleLabel: (_overlay$label = overlay.label) !== null && _overlay$label !== void 0 ? _overlay$label : DEFAULT_OVERLAY_LABEL,
3030
+ scaleMax: overlay.max,
3031
+ scaleMin: (_overlay$min = overlay.min) !== null && _overlay$min !== void 0 ? _overlay$min : 0,
3032
+ scaleTooltip: overlay.tooltip,
3033
+ valueLabel: (_overlay$readoutLabel = overlay.readoutLabel) !== null && _overlay$readoutLabel !== void 0 ? _overlay$readoutLabel : DEFAULT_READOUT_LABEL
3034
+ };
3035
+ }
3036
+ if (hasPlddt) return {
3037
+ scale: PLDDT_COLOR_SCALE,
3038
+ scaleLabel: PLDDT_SCALE_LABEL,
3039
+ scaleMax: null,
3040
+ scaleTooltip: void 0,
3041
+ valueLabel: void 0
3042
+ };
3043
+ return {
3044
+ scale: null,
3045
+ scaleLabel: void 0,
3046
+ scaleMax: null,
3047
+ scaleTooltip: void 0,
3048
+ valueLabel: void 0
3049
+ };
3050
+ }
3051
+ var ProteinStructureViewer_default = memo(forwardRef((props, ref) => {
3052
+ const { backgroundColor, onResidueClick, onResidueHover, onSelectionClear, pdb, plddt, residueOverlay, selectedResidue = null, sequenceViewerBackgroundColor, showAxes = true, showLegend = true, showSequenceViewer = true, stats } = props, rest = _objectWithoutProperties(props, _excluded$1);
3053
+ const theme = useTheme();
3054
+ const mode = getMode({ theme });
3055
+ const semanticColors = getSemanticColors({ theme });
3056
+ const pluginMountRef = useRef(null);
3057
+ const [hoveredResidue, setHoveredResidue] = useState(null);
3058
+ /**
3059
+ * Label for the selected residue, read back off the structure rather than
3060
+ * remembered from the click that selected it, so the readout follows the
3061
+ * prop however the selection was made.
3062
+ */
3063
+ const [selectedLabel, setSelectedLabel] = useState(null);
3064
+ const hasPlddt = Boolean(plddt && plddt.length > 0);
3065
+ const processedPdb = useMemo(() => hasPlddt ? injectPlddtIntoPdb(pdb, plddt) : pdb, [
3066
+ pdb,
3067
+ plddt,
3068
+ hasPlddt
3069
+ ]);
3070
+ const { isReady, pluginRef, residueValueThemeRef, setClipRatio } = useMolstarPlugin({
3071
+ backgroundColor: useMemo(() => {
3072
+ var _semanticColors$base;
3073
+ return themeColor(backgroundColor !== null && backgroundColor !== void 0 ? backgroundColor : semanticColors === null || semanticColors === void 0 || (_semanticColors$base = semanticColors.base) === null || _semanticColors$base === void 0 ? void 0 : _semanticColors$base.backgroundPrimary, FALLBACK_BACKGROUND_COLOR[mode]);
3074
+ }, [
3075
+ backgroundColor,
3076
+ mode,
3077
+ semanticColors
3078
+ ]),
3079
+ containerRef: pluginMountRef,
3080
+ edgeColor: useMemo(() => {
3081
+ var _semanticColors$base2;
3082
+ return themeColor(semanticColors === null || semanticColors === void 0 || (_semanticColors$base2 = semanticColors.base) === null || _semanticColors$base2 === void 0 ? void 0 : _semanticColors$base2.borderPrimary, FALLBACK_EDGE_COLOR);
3083
+ }, [semanticColors]),
3084
+ hasPlddt,
3085
+ highlightColor: useMemo(() => {
3086
+ var _semanticColors$base3;
3087
+ return themeColor(semanticColors === null || semanticColors === void 0 || (_semanticColors$base3 = semanticColors.base) === null || _semanticColors$base3 === void 0 ? void 0 : _semanticColors$base3.fillSelected, FALLBACK_HIGHLIGHT_COLOR);
3088
+ }, [semanticColors]),
3089
+ mode,
3090
+ onResidueClick,
3091
+ onResidueHover: useCallback((residue) => {
3092
+ setHoveredResidue((prev) => {
3093
+ if (residue === null) return prev === null ? prev : null;
3094
+ const label = residueLabel(residue);
3095
+ if (prev !== null && prev.index === residue.index && prev.label === label) return prev;
3096
+ return {
3097
+ index: residue.index,
3098
+ label
3099
+ };
3100
+ });
3101
+ onResidueHover === null || onResidueHover === void 0 || onResidueHover(residue);
3102
+ }, [onResidueHover]),
3103
+ onSelectionClear,
3104
+ pdb: processedPdb,
3105
+ sequenceViewerBackgroundColor,
3106
+ showAxes,
3107
+ showSequenceViewer
3108
+ });
3109
+ useResidueOverlay({
3110
+ hasPlddt,
3111
+ isReady,
3112
+ mode,
3113
+ overlay: residueOverlay,
3114
+ pluginRef,
3115
+ residueValueThemeRef
3116
+ });
3117
+ useResidueFocus({
3118
+ isReady,
3119
+ pluginRef,
3120
+ selectedResidue,
3121
+ setClipRatio
3122
+ });
3123
+ useEffect(() => {
3124
+ const plugin = pluginRef.current;
3125
+ if (!plugin || !isReady || selectedResidue === null) {
3126
+ setSelectedLabel(null);
3127
+ return;
3128
+ }
3129
+ const loci = lociForResidueIndex(plugin, selectedResidue);
3130
+ const residue = loci && residueRefFromLoci(loci);
3131
+ setSelectedLabel(residue ? residueLabel(residue) : null);
3132
+ }, [
3133
+ isReady,
3134
+ pluginRef,
3135
+ selectedResidue
3136
+ ]);
3137
+ const buildReadout = useCallback((label, index) => {
3138
+ var _plddt$index, _residueOverlay$value;
3139
+ return {
3140
+ label,
3141
+ plddt: (_plddt$index = plddt === null || plddt === void 0 ? void 0 : plddt[index]) !== null && _plddt$index !== void 0 ? _plddt$index : null,
3142
+ value: (_residueOverlay$value = residueOverlay === null || residueOverlay === void 0 ? void 0 : residueOverlay.values.get(index)) !== null && _residueOverlay$value !== void 0 ? _residueOverlay$value : null
3143
+ };
3144
+ }, [plddt, residueOverlay]);
3145
+ const hoveredReadout = useMemo(() => hoveredResidue === null ? null : buildReadout(hoveredResidue.label, hoveredResidue.index), [hoveredResidue, buildReadout]);
3146
+ const selectedReadout = useMemo(() => selectedResidue === null || selectedLabel === null ? null : buildReadout(selectedLabel, selectedResidue), [
3147
+ selectedResidue,
3148
+ selectedLabel,
3149
+ buildReadout
3150
+ ]);
3151
+ const scaleProps = useMemo(() => resolveScaleProps(residueOverlay, hasPlddt), [residueOverlay, hasPlddt]);
3152
+ return /* @__PURE__ */ jsxs(ViewerRoot, _objectSpread2(_objectSpread2({
3153
+ ref,
3154
+ showSequenceViewer
3155
+ }, rest), {}, { children: [/* @__PURE__ */ jsx(PluginMount, { ref: pluginMountRef }), showLegend && /* @__PURE__ */ jsx(StructureLegend, _objectSpread2({
3156
+ hoveredResidue: hoveredReadout,
3157
+ selectedResidue: selectedReadout,
3158
+ showSequenceViewer,
3159
+ stats: stats !== null && stats !== void 0 ? stats : []
3160
+ }, scaleProps))] }));
3161
+ }));
3162
+ //#endregion
3163
+ //#region src/core/StackedBarChart/style.ts
3164
+ const ChartWrapper = styled("div")`
3165
+ display: flex;
3166
+ flex-direction: column;
3167
+ width: ${(props) => typeof props.width === "number" ? `${props.width}px` : props.width};
3168
+ `;
3169
+ const TitleContainer = styled("div")`
3170
+ display: flex;
3171
+ align-items: center;
3172
+
3173
+ ${(props) => {
3174
+ const spaces = getSpaces(props);
3175
+ return `
3176
+ gap: ${spaces === null || spaces === void 0 ? void 0 : spaces.s}px;
3177
+ margin-bottom: ${spaces === null || spaces === void 0 ? void 0 : spaces.l}px;
3178
+ `;
3179
+ }}
3180
+ `;
3181
+ const ChartTitle = styled("div")`
3182
+ ${fontHeaderS}
3183
+
3184
+ ${(props) => {
3185
+ var _semanticColors$base;
3186
+ const semanticColors = getSemanticColors(props);
3187
+ return `
3188
+ color: ${semanticColors === null || semanticColors === void 0 || (_semanticColors$base = semanticColors.base) === null || _semanticColors$base === void 0 ? void 0 : _semanticColors$base.textPrimary};
3189
+ `;
3190
+ }}
3191
+ `;
3192
+ const StyledBadge = styled("div")`
3193
+ ${fontTabularSemiboldXxxxs}
3194
+
3195
+ ${(props) => {
3196
+ var _semanticColors$base2, _semanticColors$base3;
3197
+ const spaces = getSpaces(props);
3198
+ const corners = getCorners(props);
3199
+ const semanticColors = getSemanticColors(props);
3200
+ return `
3201
+ padding: ${spaces === null || spaces === void 0 ? void 0 : spaces.xxxs}px ${spaces === null || spaces === void 0 ? void 0 : spaces.xxs}px;
3202
+ border-radius: ${corners === null || corners === void 0 ? void 0 : corners.m}px;
3203
+ background-color: ${semanticColors === null || semanticColors === void 0 || (_semanticColors$base2 = semanticColors.base) === null || _semanticColors$base2 === void 0 ? void 0 : _semanticColors$base2.backgroundTertiary};
3204
+ color: ${semanticColors === null || semanticColors === void 0 || (_semanticColors$base3 = semanticColors.base) === null || _semanticColors$base3 === void 0 ? void 0 : _semanticColors$base3.textSecondary};
3205
+ `;
3206
+ }}
3207
+ `;
3208
+ const BarContainer = styled("div")`
3209
+ ${(props) => {
3210
+ var _semanticColors$base4;
3211
+ const { width, barHeight, isEmpty } = props;
3212
+ const corners = getCorners(props);
3213
+ const semanticColors = getSemanticColors(props);
3214
+ const finalWidth = typeof width === "number" ? `${width}px` : width;
3215
+ return `
3216
+ display: flex;
3217
+ border-radius: ${corners === null || corners === void 0 ? void 0 : corners.s}px;
3218
+ width: ${finalWidth};
3219
+ height: ${barHeight}px;
3220
+ background-color: ${isEmpty ? semanticColors === null || semanticColors === void 0 || (_semanticColors$base4 = semanticColors.base) === null || _semanticColors$base4 === void 0 ? void 0 : _semanticColors$base4.backgroundTertiary : "transparent"};
3221
+ overflow: hidden;
3222
+ `;
3223
+ }}
3224
+ `;
3225
+ const BarSegment = styled("div")`
3226
+ ${(props) => {
3227
+ const { color, height, percentage, opacity, disabled, isLast } = props;
3228
+ const spaces = getSpaces(props);
3229
+ const gap = (spaces === null || spaces === void 0 ? void 0 : spaces.xxxs) || 0;
3230
+ return `
3231
+ background-color: ${color};
3232
+ height: ${height}px;
3233
+ flex-grow: ${percentage};
3234
+ flex-basis: 0;
3235
+ flex-shrink: 1;
3236
+ margin-right: ${percentage > 0 && !isLast ? `${gap}px` : 0};
3237
+ opacity: ${opacity};
3238
+ transition: opacity 0.2s ease-in-out, flex-grow 0.25s ease-in-out, margin-right 0.25s ease-out;
3239
+ cursor: ${disabled ? "default" : "pointer"};
3240
+ pointer-events: ${disabled ? "none" : "auto"};
3241
+ `;
3242
+ }}
3243
+ `;
3244
+ const StyledStackedBarChartWrapper = styled("div")`
3245
+ ${(props) => {
3246
+ const spaces = getSpaces(props);
3247
+ return `
3248
+ display: flex;
3249
+ flex-direction: column;
3250
+ gap: ${spaces === null || spaces === void 0 ? void 0 : spaces.m}px;
3251
+ `;
3252
+ }}
3253
+ `;
3254
+ //#endregion
3255
+ //#region src/core/StackedBarChart/index.tsx
3256
+ const _excluded = [
3257
+ "title",
3258
+ "badge",
3259
+ "hideBadge",
3260
+ "data",
3261
+ "width",
3262
+ "barHeight",
3263
+ "showLegend",
3264
+ "showLegendValues",
3265
+ "legendValueFormat",
3266
+ "selectedIndices",
3267
+ "onSelectionChange",
3268
+ "onSegmentMouseEnter",
3269
+ "onSegmentMouseLeave",
3270
+ "onLegendItemMouseEnter",
3271
+ "onLegendItemMouseLeave",
3272
+ "onSegmentClick",
3273
+ "onLegendItemClick",
3274
+ "selectionBehavior",
3275
+ "mode",
3276
+ "maxAmount",
3277
+ "remainingLabel",
3278
+ "remainingUnit",
3279
+ "unit",
3280
+ "colorGeneratorOptions"
3281
+ ];
3282
+ const calculateBadgeText = (totalItems, selectedCount) => {
3283
+ if (selectedCount === 0 || selectedCount === totalItems) return `${totalItems}`;
3284
+ return `${selectedCount} of ${totalItems}`;
3285
+ };
3286
+ const calculateLegendValue = (item, showLegendValues, legendValueFormat, globalUnit) => {
3287
+ if (!showLegendValues) return void 0;
3288
+ if (legendValueFormat === "percentage") return `${Math.round(item.percentage)}%`;
3289
+ const effectiveUnit = item.unit || globalUnit;
3290
+ return effectiveUnit ? `${item.value} ${effectiveUnit}` : `${item.value}`;
3291
+ };
3292
+ const getSegmentOpacity = (index, hoveredIndex, selectedIndicesSet, selectionBehavior) => {
3293
+ const hasSelection = selectedIndicesSet.size > 0;
3294
+ if (selectionBehavior === "hide") return 1;
3295
+ if (hoveredIndex !== null) return hoveredIndex === index || selectedIndicesSet.has(index) ? 1 : .2;
3296
+ if (hasSelection) return selectedIndicesSet.has(index) ? 1 : .2;
3297
+ return 1;
3298
+ };
3299
+ const applyCumulativeHideStyling = (items, selectedIndicesSet, selectedTotal, hasRemaining, effectiveMaxAmount) => {
3300
+ const newRemainingValue = hasRemaining ? effectiveMaxAmount - selectedTotal : 0;
3301
+ const newRemainingPercentage = hasRemaining ? newRemainingValue / effectiveMaxAmount * 100 : 0;
3302
+ return {
3303
+ styledItems: items.map((item) => {
3304
+ const isSelected = selectedIndicesSet.has(item.originalIndex);
3305
+ return _objectSpread2(_objectSpread2({}, item), {}, { percentage: isSelected ? item.percentage : 0 });
3306
+ }),
3307
+ updatedRemainingPercentage: newRemainingPercentage,
3308
+ updatedRemainingValue: newRemainingValue
3309
+ };
3310
+ };
3311
+ const applyProportionalHideStyling = (items, selectedIndicesSet, selectedTotal) => {
3312
+ if (selectedTotal === 0) return {
3313
+ styledItems: items.map((item) => _objectSpread2(_objectSpread2({}, item), {}, { percentage: 0 })),
3314
+ updatedRemainingPercentage: 0,
3315
+ updatedRemainingValue: 0
3316
+ };
3317
+ return {
3318
+ styledItems: items.map((item) => {
3319
+ const newPercentage = selectedIndicesSet.has(item.originalIndex) ? item.value / selectedTotal * 100 : 0;
3320
+ return _objectSpread2(_objectSpread2({}, item), {}, { percentage: newPercentage });
3321
+ }),
3322
+ updatedRemainingPercentage: 0,
3323
+ updatedRemainingValue: 0
3324
+ };
3325
+ };
584
3326
  const applySelectionStyling = (items, selectedIndicesSet, selectionBehavior, mode, hasRemaining, effectiveMaxAmount, baseRemainingPercentage, baseRemainingValue) => {
585
3327
  const hasSelection = selectedIndicesSet.size > 0;
586
3328
  if (selectionBehavior !== "hide" || !hasSelection) return {
@@ -635,7 +3377,6 @@ const renderBarSegment = (item, options) => {
635
3377
  title: null,
636
3378
  componentSlot: /* @__PURE__ */ jsx(TooltipTable, _objectSpread2({}, item.tooltip)),
637
3379
  placement: "top",
638
- hasInvertedStyle: false,
639
3380
  open: hoveredIndex === item.originalIndex && !isHidden,
640
3381
  disableInteractive: true,
641
3382
  slotProps: { popper: {
@@ -652,7 +3393,7 @@ const StackedBarChart = (props) => {
652
3393
  var _theme$palette4;
653
3394
  const { title, badge, hideBadge = false, data, width = "100%", barHeight: barHeightProp = 16, showLegend = true, showLegendValues = true, legendValueFormat = "percentage", selectedIndices = [], onSelectionChange, onSegmentMouseEnter, onSegmentMouseLeave, onLegendItemMouseEnter, onLegendItemMouseLeave, onSegmentClick, onLegendItemClick, selectionBehavior = "dim", mode = "proportional", maxAmount, remainingLabel = "Remaining", remainingUnit, unit, colorGeneratorOptions } = props, rest = _objectWithoutProperties(props, _excluded);
654
3395
  const barHeight = Math.max(1, barHeightProp);
655
- const theme = useTheme();
3396
+ const theme = useTheme$1();
656
3397
  const [hoveredIndex, setHoveredIndex] = useState(null);
657
3398
  const [animatedItems, setAnimatedItems] = useState([]);
658
3399
  const [autoColorMap, setAutoColorMap] = useState(/* @__PURE__ */ new Map());
@@ -894,4 +3635,4 @@ const StackedBarChart = (props) => {
894
3635
  return /* @__PURE__ */ jsxs(ChartWrapper, _objectSpread2(_objectSpread2({ width }, rest), {}, { children: [/* @__PURE__ */ jsxs(TitleContainer, { children: [/* @__PURE__ */ jsx(ChartTitle, { children: title }), !hideBadge && displayBadge && /* @__PURE__ */ jsx(StyledBadge, { children: displayBadge })] }), chartContent] }));
895
3636
  };
896
3637
  //#endregion
897
- export { HeatmapChart_default as HeatmapChart, StackedBarChart };
3638
+ export { HeatmapChart_default as HeatmapChart, PLASMA_COLOR_SCALE, PLDDT_COLOR_SCALE, ProteinStructureViewer_default as ProteinStructureViewer, StackedBarChart, injectPlddtIntoPdb, sampleColorScale, toCssGradient };