@czi-sds/data-viz 2.0.0 → 2.1.1

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