react-bio-viz 0.1.0__py3-none-any.whl
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.
- react_bio_viz/__init__.py +44 -0
- react_bio_viz/_base.py +74 -0
- react_bio_viz/blasthitdistribution.py +44 -0
- react_bio_viz/distancematrix.py +40 -0
- react_bio_viz/genemodel.py +35 -0
- react_bio_viz/genomebrowser.py +31 -0
- react_bio_viz/io.py +133 -0
- react_bio_viz/msa.py +61 -0
- react_bio_viz/phylotree.py +80 -0
- react_bio_viz/static/widget.css +2 -0
- react_bio_viz/static/widget.js +100 -0
- react_bio_viz-0.1.0.dist-info/METADATA +141 -0
- react_bio_viz-0.1.0.dist-info/RECORD +14 -0
- react_bio_viz-0.1.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Jupyter widgets for biological data visualization.
|
|
2
|
+
|
|
3
|
+
Thin `anywidget <https://anywidget.dev>`_ bindings around the ``react-bio-viz`` React components.
|
|
4
|
+
Every piece of interactive state is a synced traitlet, so the kernel can both drive and observe it::
|
|
5
|
+
|
|
6
|
+
from react_bio_viz import MSA
|
|
7
|
+
|
|
8
|
+
widget = MSA(msa=[{"header": "seq1", "sequence": "ACGT"}])
|
|
9
|
+
widget.observe(lambda change: print(change["new"]), names="viewport")
|
|
10
|
+
widget # renders in the notebook
|
|
11
|
+
widget.viewport = {...} # drives the view from Python
|
|
12
|
+
|
|
13
|
+
Each trait is bridged to the component as a ``StoreController``, the same external-store seam a
|
|
14
|
+
JavaScript app fills with a Zustand store — the components themselves contain no Jupyter-specific
|
|
15
|
+
code.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from ._base import BioVizWidget, fit_to_extent
|
|
19
|
+
from .blasthitdistribution import BlastHitDistribution
|
|
20
|
+
from .distancematrix import DistanceMatrix
|
|
21
|
+
from .genemodel import GeneModel
|
|
22
|
+
from .genomebrowser import GenomeBrowser
|
|
23
|
+
from .io import parse_fasta, parse_newick, read_fasta, read_newick, to_newick
|
|
24
|
+
from .msa import MSA
|
|
25
|
+
from .phylotree import PhyloTree
|
|
26
|
+
|
|
27
|
+
__version__ = "0.1.0"
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
"BioVizWidget",
|
|
31
|
+
"BlastHitDistribution",
|
|
32
|
+
"DistanceMatrix",
|
|
33
|
+
"GeneModel",
|
|
34
|
+
"GenomeBrowser",
|
|
35
|
+
"MSA",
|
|
36
|
+
"PhyloTree",
|
|
37
|
+
"fit_to_extent",
|
|
38
|
+
"parse_fasta",
|
|
39
|
+
"parse_newick",
|
|
40
|
+
"read_fasta",
|
|
41
|
+
"read_newick",
|
|
42
|
+
"to_newick",
|
|
43
|
+
"__version__",
|
|
44
|
+
]
|
react_bio_viz/_base.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Shared plumbing for every react-bio-viz widget."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import pathlib
|
|
6
|
+
from typing import Any, Callable, Mapping
|
|
7
|
+
|
|
8
|
+
import anywidget
|
|
9
|
+
import traitlets
|
|
10
|
+
|
|
11
|
+
_STATIC = pathlib.Path(__file__).parent / "static"
|
|
12
|
+
|
|
13
|
+
#: One bundle serves every widget, dispatching on the ``_component`` trait, so React ships once.
|
|
14
|
+
_ESM = _STATIC / "widget.js"
|
|
15
|
+
_CSS = _STATIC / "widget.css"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def fit_to_extent(extent: Mapping[str, float]) -> dict[str, float]:
|
|
19
|
+
"""A viewport showing the full extent, i.e. fully zoomed out — ``fitToExtent`` in the JS core."""
|
|
20
|
+
return {
|
|
21
|
+
"x0": extent["xMin"],
|
|
22
|
+
"x1": extent["xMax"],
|
|
23
|
+
"y0": extent["yMin"],
|
|
24
|
+
"y1": extent["yMax"],
|
|
25
|
+
"xMin": extent["xMin"],
|
|
26
|
+
"xMax": extent["xMax"],
|
|
27
|
+
"yMin": extent["yMin"],
|
|
28
|
+
"yMax": extent["yMax"],
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class BioVizWidget(anywidget.AnyWidget):
|
|
33
|
+
"""Base class for the react-bio-viz Jupyter widgets.
|
|
34
|
+
|
|
35
|
+
Every piece of interactive state is a synced trait, observable and writable from the kernel::
|
|
36
|
+
|
|
37
|
+
widget = MSA(msa=records)
|
|
38
|
+
widget.observe(lambda change: print(change["new"]), names="viewport")
|
|
39
|
+
widget.viewport = {...} # drives the view from the kernel
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
_esm = _ESM
|
|
43
|
+
_css = _CSS
|
|
44
|
+
|
|
45
|
+
#: Selects which React component the shared bundle renders.
|
|
46
|
+
_component = traitlets.Unicode("").tag(sync=True)
|
|
47
|
+
|
|
48
|
+
#: The visible window, as a plain ``Viewport`` dict. Seeded on construction so it is readable
|
|
49
|
+
#: and observable before the user has interacted with the widget.
|
|
50
|
+
viewport = traitlets.Dict(allow_none=True, default_value=None).tag(sync=True)
|
|
51
|
+
|
|
52
|
+
def __init__(self, **kwargs: Any) -> None:
|
|
53
|
+
super().__init__(**kwargs)
|
|
54
|
+
if self.viewport is None:
|
|
55
|
+
extent = self._default_extent()
|
|
56
|
+
if extent is not None:
|
|
57
|
+
self.viewport = fit_to_extent(extent)
|
|
58
|
+
|
|
59
|
+
def _default_extent(self) -> dict[str, float] | None:
|
|
60
|
+
"""The data extent this widget's viewport spans, or ``None`` if it has no viewport."""
|
|
61
|
+
return None
|
|
62
|
+
|
|
63
|
+
def _on_event(self, event: str, callback: Callable[..., None], *fields: str) -> None:
|
|
64
|
+
"""Calls ``callback`` with ``fields`` of every ``event`` message the component sends.
|
|
65
|
+
|
|
66
|
+
One-off user actions (a rename, a click) arrive as custom messages, not traits: a trait would
|
|
67
|
+
drop the second of two identical events.
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
def handler(_widget: Any, content: Mapping[str, Any], _buffers: Any) -> None:
|
|
71
|
+
if isinstance(content, Mapping) and content.get("event") == event:
|
|
72
|
+
callback(*(content.get(field) for field in fields))
|
|
73
|
+
|
|
74
|
+
self.on_msg(handler)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""BLAST hit distribution widget."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import traitlets
|
|
8
|
+
|
|
9
|
+
from ._base import BioVizWidget
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class BlastHitDistribution(BioVizWidget):
|
|
13
|
+
"""BLAST hits along one query sequence, row-stacked and colored by a chosen metric.
|
|
14
|
+
|
|
15
|
+
:param hits: ``[{"id", "queryId", "subjectId", "queryStart", "queryEnd", "evalue",
|
|
16
|
+
"bitScore", "percentIdentity"}, ...]``.
|
|
17
|
+
:param selection: ``{"selectedHitIds": [...]}``; clicking a hit toggles it and writes this back.
|
|
18
|
+
:param metric: ``"evalue"``, ``"bitScore"`` or ``"percentIdentity"``; written back when the
|
|
19
|
+
user picks another in the widget's selector.
|
|
20
|
+
|
|
21
|
+
>>> w = BlastHitDistribution(hits=hits, query_length=2000)
|
|
22
|
+
>>> w.observe(lambda c: print(c["new"]["selectedHitIds"]), names="selection")
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
_component = traitlets.Unicode("blasthitdistribution").tag(sync=True)
|
|
26
|
+
|
|
27
|
+
hits = traitlets.List(traitlets.Dict(), default_value=[]).tag(sync=True)
|
|
28
|
+
query_length = traitlets.Int(0).tag(sync=True)
|
|
29
|
+
query_name = traitlets.Unicode("").tag(sync=True)
|
|
30
|
+
width = traitlets.Int(800).tag(sync=True)
|
|
31
|
+
show_scale = traitlets.Bool(True).tag(sync=True)
|
|
32
|
+
metric = traitlets.Enum(["evalue", "bitScore", "percentIdentity"], default_value="evalue").tag(sync=True)
|
|
33
|
+
|
|
34
|
+
selection = traitlets.Dict(allow_none=True, default_value=None).tag(sync=True)
|
|
35
|
+
|
|
36
|
+
def __init__(self, **kwargs: Any) -> None:
|
|
37
|
+
super().__init__(**kwargs)
|
|
38
|
+
if self.selection is None:
|
|
39
|
+
self.selection = {"selectedHitIds": []}
|
|
40
|
+
|
|
41
|
+
def _default_extent(self) -> dict[str, float] | None:
|
|
42
|
+
if not self.query_length:
|
|
43
|
+
return None
|
|
44
|
+
return {"xMin": 0, "xMax": self.query_length, "yMin": 0, "yMax": 1}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Pairwise distance matrix widget."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import traitlets
|
|
6
|
+
|
|
7
|
+
from ._base import BioVizWidget
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class DistanceMatrix(BioVizWidget):
|
|
11
|
+
"""A pairwise distance matrix as a heatmap, windowed so it scales to thousands of rows.
|
|
12
|
+
|
|
13
|
+
:param labels: Row/column ids, in ``matrix`` order.
|
|
14
|
+
:param matrix: Symmetric ``len(labels)`` × ``len(labels)`` list of lists.
|
|
15
|
+
:param label_names: Optional ``{id: display name}``.
|
|
16
|
+
:param options: ``{"showNumbers": bool, "colorScheme": "warm" | "cool" | "green" | "grayscale"}``.
|
|
17
|
+
:param row_order: Display order of rows (and columns), as ids; written back when labels are
|
|
18
|
+
dragged. Same shape as :class:`MSA`'s, so the two can be linked.
|
|
19
|
+
|
|
20
|
+
>>> w = DistanceMatrix(labels=["a", "b"], matrix=[[0, 0.1], [0.1, 0]])
|
|
21
|
+
>>> w.observe(print, names="row_order")
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
_component = traitlets.Unicode("distancematrix").tag(sync=True)
|
|
25
|
+
|
|
26
|
+
labels = traitlets.List(traitlets.Unicode(), default_value=[]).tag(sync=True)
|
|
27
|
+
matrix = traitlets.List(traitlets.List(traitlets.Float()), default_value=[]).tag(sync=True)
|
|
28
|
+
label_names = traitlets.Dict(allow_none=True, default_value=None).tag(sync=True)
|
|
29
|
+
width = traitlets.Int(650).tag(sync=True)
|
|
30
|
+
height = traitlets.Int(500).tag(sync=True)
|
|
31
|
+
options = traitlets.Dict(allow_none=True, default_value=None).tag(sync=True)
|
|
32
|
+
|
|
33
|
+
row_order = traitlets.List(traitlets.Unicode(), default_value=[]).tag(sync=True)
|
|
34
|
+
panel_sizes = traitlets.Dict(default_value={"labelWidth": 160}).tag(sync=True)
|
|
35
|
+
|
|
36
|
+
def _default_extent(self) -> dict[str, float] | None:
|
|
37
|
+
if not self.labels:
|
|
38
|
+
return None
|
|
39
|
+
n = len(self.labels)
|
|
40
|
+
return {"xMin": 0, "xMax": n, "yMin": 0, "yMax": n}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Gene model widget."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import traitlets
|
|
6
|
+
|
|
7
|
+
from ._base import BioVizWidget
|
|
8
|
+
|
|
9
|
+
#: Fraction of the gene's length padded onto each side of the default view, mirroring the
|
|
10
|
+
#: component's own ``VIEWPORT_PADDING_RATIO``.
|
|
11
|
+
_PADDING_RATIO = 0.1
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class GeneModel(BioVizWidget):
|
|
15
|
+
"""A (potentially spliced) gene model: transcripts with their exons and CDSs.
|
|
16
|
+
|
|
17
|
+
:param gene: A gff3-shaped ``SequenceInterval`` dict, with nested ``children``.
|
|
18
|
+
|
|
19
|
+
>>> w = GeneModel(gene=gff3_record)
|
|
20
|
+
>>> w.viewport = {**w.viewport, "x0": 35_040_000, "x1": 35_042_000} # zoom from the kernel
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
_component = traitlets.Unicode("genemodel").tag(sync=True)
|
|
24
|
+
|
|
25
|
+
gene = traitlets.Dict(default_value={}).tag(sync=True)
|
|
26
|
+
width = traitlets.Int(500).tag(sync=True)
|
|
27
|
+
color_seed = traitlets.Unicode("42").tag(sync=True)
|
|
28
|
+
show_scale = traitlets.Bool(True).tag(sync=True)
|
|
29
|
+
|
|
30
|
+
def _default_extent(self) -> dict[str, float] | None:
|
|
31
|
+
start, end = self.gene.get("start"), self.gene.get("end")
|
|
32
|
+
if start is None or end is None:
|
|
33
|
+
return None
|
|
34
|
+
padding = round(_PADDING_RATIO * (end - start))
|
|
35
|
+
return {"xMin": max(0, start - padding), "xMax": end + padding, "yMin": 0, "yMax": 1}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Multi-track genome browser widget."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import traitlets
|
|
6
|
+
|
|
7
|
+
from ._base import BioVizWidget
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class GenomeBrowser(BioVizWidget):
|
|
11
|
+
"""Stacked genome tracks sharing one genomic-coordinate viewport.
|
|
12
|
+
|
|
13
|
+
:param tracks: ``[{"id", "label", "kind", "data"}, ...]`` where ``kind`` is one of
|
|
14
|
+
``"feature"``, ``"coverage"`` or ``"genemodel"``.
|
|
15
|
+
:param reference_length: Length of the reference sequence; the x-axis extent.
|
|
16
|
+
|
|
17
|
+
>>> w = GenomeBrowser(tracks=tracks, reference_length=35_045_433)
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
_component = traitlets.Unicode("genomebrowser").tag(sync=True)
|
|
21
|
+
|
|
22
|
+
tracks = traitlets.List(traitlets.Dict(), default_value=[]).tag(sync=True)
|
|
23
|
+
reference_length = traitlets.Int(0).tag(sync=True)
|
|
24
|
+
reference_name = traitlets.Unicode("").tag(sync=True)
|
|
25
|
+
width = traitlets.Int(1000).tag(sync=True)
|
|
26
|
+
show_scale = traitlets.Bool(True).tag(sync=True)
|
|
27
|
+
|
|
28
|
+
def _default_extent(self) -> dict[str, float] | None:
|
|
29
|
+
if not self.reference_length:
|
|
30
|
+
return None
|
|
31
|
+
return {"xMin": 0, "xMax": self.reference_length, "yMin": 0, "yMax": 1}
|
react_bio_viz/io.py
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""Reading alignments and trees into the plain structures the widgets take.
|
|
2
|
+
|
|
3
|
+
The widgets take plain JSON-able data — a list of ``{"header", "sequence"}`` records for
|
|
4
|
+
:class:`~react_bio_viz.MSA`, nested ``{"name", "length", "children"}`` dicts for
|
|
5
|
+
:class:`~react_bio_viz.PhyloTree` — so any parser works. These cover the common formats without
|
|
6
|
+
extra dependencies, and parse Newick the same way the JavaScript ``parseNewick`` does.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
import re
|
|
13
|
+
from typing import Any, Union
|
|
14
|
+
|
|
15
|
+
PathLike = Union[str, "os.PathLike[str]"]
|
|
16
|
+
|
|
17
|
+
Record = dict[str, str]
|
|
18
|
+
TreeNode = dict[str, Any]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def parse_fasta(text: str) -> list[Record]:
|
|
22
|
+
"""Parses FASTA text into ``[{"header": ..., "sequence": ...}, ...]``.
|
|
23
|
+
|
|
24
|
+
The header is the whole line after ``>``; sequence lines are joined with whitespace removed.
|
|
25
|
+
"""
|
|
26
|
+
records: list[Record] = []
|
|
27
|
+
for line in text.splitlines():
|
|
28
|
+
line = line.strip()
|
|
29
|
+
if not line:
|
|
30
|
+
continue
|
|
31
|
+
if line.startswith(">"):
|
|
32
|
+
records.append({"header": line[1:].strip(), "sequence": ""})
|
|
33
|
+
elif records:
|
|
34
|
+
records[-1]["sequence"] += re.sub(r"\s+", "", line)
|
|
35
|
+
else:
|
|
36
|
+
raise ValueError("FASTA text must start with a '>' header line")
|
|
37
|
+
return records
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def read_fasta(path: PathLike) -> list[Record]:
|
|
41
|
+
"""Reads a FASTA file — see :func:`parse_fasta`."""
|
|
42
|
+
with open(path, encoding="utf-8") as handle:
|
|
43
|
+
return parse_fasta(handle.read())
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def parse_newick(text: str) -> TreeNode:
|
|
47
|
+
"""Parses a Newick string into nested ``{"name", "length", "children"}`` dicts.
|
|
48
|
+
|
|
49
|
+
Internal-node labels (typically bootstrap support) become the node's ``name``; ``:length``
|
|
50
|
+
suffixes become ``length`` (0 when absent). Quoted labels (``'a b'``, with ``''`` for a literal
|
|
51
|
+
quote) are unquoted, so FASTA-style names with spaces or ``|`` survive a round trip.
|
|
52
|
+
"""
|
|
53
|
+
position = 0
|
|
54
|
+
|
|
55
|
+
def skip_space() -> None:
|
|
56
|
+
nonlocal position
|
|
57
|
+
while position < len(text) and text[position].isspace():
|
|
58
|
+
position += 1
|
|
59
|
+
|
|
60
|
+
def parse_label() -> str:
|
|
61
|
+
nonlocal position
|
|
62
|
+
skip_space()
|
|
63
|
+
if position < len(text) and text[position] in "'\"":
|
|
64
|
+
quote = text[position]
|
|
65
|
+
position += 1
|
|
66
|
+
label = []
|
|
67
|
+
while position < len(text):
|
|
68
|
+
if text[position] == quote:
|
|
69
|
+
if position + 1 < len(text) and text[position + 1] == quote:
|
|
70
|
+
label.append(quote)
|
|
71
|
+
position += 2
|
|
72
|
+
continue
|
|
73
|
+
position += 1
|
|
74
|
+
break
|
|
75
|
+
label.append(text[position])
|
|
76
|
+
position += 1
|
|
77
|
+
return "".join(label)
|
|
78
|
+
start = position
|
|
79
|
+
while position < len(text) and text[position] not in ":,();":
|
|
80
|
+
position += 1
|
|
81
|
+
return text[start:position].strip()
|
|
82
|
+
|
|
83
|
+
def parse_node() -> TreeNode:
|
|
84
|
+
nonlocal position
|
|
85
|
+
node: TreeNode = {"name": "", "length": 0.0, "children": []}
|
|
86
|
+
skip_space()
|
|
87
|
+
if position < len(text) and text[position] == "(":
|
|
88
|
+
position += 1
|
|
89
|
+
node["children"].append(parse_node())
|
|
90
|
+
skip_space()
|
|
91
|
+
while position < len(text) and text[position] == ",":
|
|
92
|
+
position += 1
|
|
93
|
+
node["children"].append(parse_node())
|
|
94
|
+
skip_space()
|
|
95
|
+
if position < len(text) and text[position] == ")":
|
|
96
|
+
position += 1
|
|
97
|
+
node["name"] = parse_label()
|
|
98
|
+
skip_space()
|
|
99
|
+
if position < len(text) and text[position] == ":":
|
|
100
|
+
position += 1
|
|
101
|
+
start = position
|
|
102
|
+
while position < len(text) and text[position] not in ",);":
|
|
103
|
+
position += 1
|
|
104
|
+
try:
|
|
105
|
+
node["length"] = float(text[start:position])
|
|
106
|
+
except ValueError:
|
|
107
|
+
node["length"] = 0.0
|
|
108
|
+
return node
|
|
109
|
+
|
|
110
|
+
return parse_node()
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def read_newick(path: PathLike) -> TreeNode:
|
|
114
|
+
"""Reads a Newick file — see :func:`parse_newick`."""
|
|
115
|
+
with open(path, encoding="utf-8") as handle:
|
|
116
|
+
return parse_newick(handle.read())
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _quote(name: str) -> str:
|
|
120
|
+
return f"'{name.replace(chr(39), chr(39) * 2)}'" if re.search(r"[\s:,;()\[\]']", name) else name
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def to_newick(tree: TreeNode) -> str:
|
|
124
|
+
"""Serializes nested ``{"name", "length", "children"}`` dicts to Newick, quoting names as needed."""
|
|
125
|
+
|
|
126
|
+
def write(node: TreeNode, is_root: bool) -> str:
|
|
127
|
+
children = node.get("children") or []
|
|
128
|
+
inner = "(" + ",".join(write(child, False) for child in children) + ")" if children else ""
|
|
129
|
+
length = node.get("length") or 0
|
|
130
|
+
suffix = "" if is_root and not length else f":{length:g}"
|
|
131
|
+
return f"{inner}{_quote(node.get('name', ''))}{suffix}"
|
|
132
|
+
|
|
133
|
+
return write(tree, True) + ";"
|
react_bio_viz/msa.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Multiple sequence alignment widget."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Callable
|
|
6
|
+
|
|
7
|
+
import traitlets
|
|
8
|
+
|
|
9
|
+
from ._base import BioVizWidget
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class MSA(BioVizWidget):
|
|
13
|
+
"""A multiple sequence alignment, with pan/zoom, a column ruler, a minimap and a consensus row.
|
|
14
|
+
|
|
15
|
+
:param msa: The alignment, as ``[{"header": ..., "sequence": ...}, ...]``.
|
|
16
|
+
:param options: Rendering toggles, mirroring ``MSADrawOptions`` (camelCase keys, e.g.
|
|
17
|
+
``{"colorStyle": "AA Zappo", "tracks": ["conservation", "logo"]}``). Omitting
|
|
18
|
+
``colorStyle`` lets the component pick a scheme from the alignment's own alphabet.
|
|
19
|
+
:param selection: Selected rows (by id — a record's ``id``, else its ``header``) and columns,
|
|
20
|
+
``{"rows": [...], "columns": [...]}``; written back as the user selects.
|
|
21
|
+
:param row_order: Display order of the rows, as ids; written back when labels are dragged.
|
|
22
|
+
:param panel_sizes: ``{"labelWidth", "trackHeight", "minimapHeight"}`` in pixels.
|
|
23
|
+
|
|
24
|
+
The widget never edits ``msa`` itself: register :meth:`on_rename_row`,
|
|
25
|
+
:meth:`on_remove_rows` or :meth:`on_remove_columns` to apply edits (which also enables them).
|
|
26
|
+
|
|
27
|
+
>>> w = MSA(msa=[{"header": "a", "sequence": "ACGT"}])
|
|
28
|
+
>>> w.observe(print, names="viewport") # fires when the user pans or zooms
|
|
29
|
+
>>> w.observe(print, names="selection")
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
_component = traitlets.Unicode("msa").tag(sync=True)
|
|
33
|
+
|
|
34
|
+
msa = traitlets.List(traitlets.Dict(), default_value=[]).tag(sync=True)
|
|
35
|
+
width = traitlets.Int(650).tag(sync=True)
|
|
36
|
+
height = traitlets.Int(400).tag(sync=True)
|
|
37
|
+
options = traitlets.Dict(allow_none=True, default_value=None).tag(sync=True)
|
|
38
|
+
|
|
39
|
+
selection = traitlets.Dict(default_value={"rows": [], "columns": []}).tag(sync=True)
|
|
40
|
+
row_order = traitlets.List(traitlets.Unicode(), default_value=[]).tag(sync=True)
|
|
41
|
+
panel_sizes = traitlets.Dict(
|
|
42
|
+
default_value={"labelWidth": 150, "trackHeight": 48, "minimapHeight": 50}
|
|
43
|
+
).tag(sync=True)
|
|
44
|
+
|
|
45
|
+
def on_rename_row(self, callback: Callable[[str, str], Any]) -> None:
|
|
46
|
+
"""Calls ``callback(row_id, new_name)`` when the user renames a row label."""
|
|
47
|
+
self._on_event("rename_row", callback, "row_id", "name")
|
|
48
|
+
|
|
49
|
+
def on_remove_rows(self, callback: Callable[[list[str]], Any]) -> None:
|
|
50
|
+
"""Calls ``callback(row_ids)`` when the user removes rows (× on a label, or Delete)."""
|
|
51
|
+
self._on_event("remove_rows", callback, "row_ids")
|
|
52
|
+
|
|
53
|
+
def on_remove_columns(self, callback: Callable[[list[int]], Any]) -> None:
|
|
54
|
+
"""Calls ``callback(columns)`` when the user deletes selected columns."""
|
|
55
|
+
self._on_event("remove_columns", callback, "columns")
|
|
56
|
+
|
|
57
|
+
def _default_extent(self) -> dict[str, float] | None:
|
|
58
|
+
if not self.msa:
|
|
59
|
+
return None
|
|
60
|
+
columns = len(self.msa[0].get("sequence", ""))
|
|
61
|
+
return {"xMin": 0, "xMax": columns, "yMin": 0, "yMax": len(self.msa)}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Phylogenetic tree widget."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Callable
|
|
6
|
+
|
|
7
|
+
import traitlets
|
|
8
|
+
|
|
9
|
+
from ._base import BioVizWidget
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class PhyloTree(BioVizWidget):
|
|
13
|
+
"""A phylogenetic tree in rectangular, cladogram or radial layout.
|
|
14
|
+
|
|
15
|
+
:param tree: Recursively nested ``{"name": ..., "length": ..., "children": [...]}``.
|
|
16
|
+
:param selection: Where the tree is rooted and how it is arranged —
|
|
17
|
+
``{"rerootedAt": str | None, "rerootPosition": float, "collapsed": [str], "order": {...}}``.
|
|
18
|
+
Writable from the kernel and written back when the user collapses, drags or reroots.
|
|
19
|
+
:param node_styles: ``{node_id: {"color": ..., "bold": ...}}``.
|
|
20
|
+
:param branch_styles: ``{node_id: {"color": ...}}`` for the branch above each node.
|
|
21
|
+
:param leaf_order: Read-only: leaf names in display order, updated as the user rearranges.
|
|
22
|
+
|
|
23
|
+
>>> w = PhyloTree(tree=newick_dict, tree_layout="radial", interactive=True)
|
|
24
|
+
>>> w.observe(lambda c: print(c["new"]["collapsed"]), names="selection")
|
|
25
|
+
>>> w.on_node_click(lambda node: print(node["id"], node["leafNames"]))
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
_component = traitlets.Unicode("phylotree").tag(sync=True)
|
|
29
|
+
|
|
30
|
+
tree = traitlets.Dict(default_value={}).tag(sync=True)
|
|
31
|
+
width = traitlets.Int(1000).tag(sync=True)
|
|
32
|
+
height = traitlets.Int(900).tag(sync=True)
|
|
33
|
+
#: ``"rectangular"``, ``"cladogram"`` or ``"radial"``. Not called ``layout``: every ipywidgets
|
|
34
|
+
#: widget already has a ``layout`` trait (its CSS layout), and shadowing it breaks the view.
|
|
35
|
+
tree_layout = traitlets.Enum(
|
|
36
|
+
["rectangular", "cladogram", "radial"], default_value="rectangular"
|
|
37
|
+
).tag(sync=True)
|
|
38
|
+
show_support_values = traitlets.Bool(True).tag(sync=True)
|
|
39
|
+
shade_branch_by_support = traitlets.Bool(True).tag(sync=True)
|
|
40
|
+
font_size = traitlets.Int(10).tag(sync=True)
|
|
41
|
+
align_tips = traitlets.Bool(True).tag(sync=True)
|
|
42
|
+
interactive = traitlets.Bool(False).tag(sync=True)
|
|
43
|
+
search_query = traitlets.Unicode(allow_none=True, default_value=None).tag(sync=True)
|
|
44
|
+
search_use_regex = traitlets.Bool(False).tag(sync=True)
|
|
45
|
+
show_scale_bar = traitlets.Bool(True).tag(sync=True)
|
|
46
|
+
show_branch_lengths = traitlets.Bool(False).tag(sync=True)
|
|
47
|
+
support_threshold = traitlets.Float(0).tag(sync=True)
|
|
48
|
+
drag_enabled = traitlets.Bool(allow_none=True, default_value=None).tag(sync=True)
|
|
49
|
+
branch_width = traitlets.Float(0.75).tag(sync=True)
|
|
50
|
+
node_radius = traitlets.Float(4).tag(sync=True)
|
|
51
|
+
label_font_size = traitlets.Float(11).tag(sync=True)
|
|
52
|
+
leaf_spacing = traitlets.Float(allow_none=True, default_value=None).tag(sync=True)
|
|
53
|
+
leaf_marker_color = traitlets.Unicode(allow_none=True, default_value=None).tag(sync=True)
|
|
54
|
+
node_styles = traitlets.Dict(default_value={}).tag(sync=True)
|
|
55
|
+
branch_styles = traitlets.Dict(default_value={}).tag(sync=True)
|
|
56
|
+
active_node_id = traitlets.Unicode(allow_none=True, default_value=None).tag(sync=True)
|
|
57
|
+
leaf_order = traitlets.List(traitlets.Unicode(), default_value=[]).tag(sync=True)
|
|
58
|
+
|
|
59
|
+
selection = traitlets.Dict(allow_none=True, default_value=None).tag(sync=True)
|
|
60
|
+
|
|
61
|
+
def __init__(self, **kwargs: Any) -> None:
|
|
62
|
+
# Accept the React prop's name as a convenience; a string can only mean the tree layout
|
|
63
|
+
# (ipywidgets' own `layout` takes a Layout widget or a dict).
|
|
64
|
+
if isinstance(kwargs.get("layout"), str):
|
|
65
|
+
kwargs["tree_layout"] = kwargs.pop("layout")
|
|
66
|
+
super().__init__(**kwargs)
|
|
67
|
+
if self.selection is None:
|
|
68
|
+
self.selection = {"collapsed": []}
|
|
69
|
+
|
|
70
|
+
def on_node_click(self, callback: Callable[[dict[str, Any]], Any]) -> None:
|
|
71
|
+
"""Calls ``callback(node)`` when a node marker is clicked (see ``TreeNodeInfo``)."""
|
|
72
|
+
self._on_event("node_click", callback, "node")
|
|
73
|
+
|
|
74
|
+
def on_branch_click(self, callback: Callable[[dict[str, Any]], Any]) -> None:
|
|
75
|
+
"""Calls ``callback(node)`` — the node below the branch — when a branch is clicked."""
|
|
76
|
+
self._on_event("branch_click", callback, "node")
|
|
77
|
+
|
|
78
|
+
def _default_extent(self) -> dict[str, float] | None:
|
|
79
|
+
# The tree's pan/zoom is over the rendered pixel surface, not over data coordinates.
|
|
80
|
+
return {"xMin": 0, "xMax": self.width, "yMin": 0, "yMax": self.height}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-border-style:solid;--tw-font-weight:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-outline-style:solid;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-duration:initial;--tw-content:"";--tw-animation-delay:0s;--tw-animation-direction:normal;--tw-animation-duration:initial;--tw-animation-fill-mode:none;--tw-animation-iteration-count:1;--tw-enter-blur:0;--tw-enter-opacity:1;--tw-enter-rotate:0;--tw-enter-scale:1;--tw-enter-translate-x:0;--tw-enter-translate-y:0;--tw-exit-blur:0;--tw-exit-opacity:1;--tw-exit-rotate:0;--tw-exit-scale:1;--tw-exit-translate-x:0;--tw-exit-translate-y:0}}}@layer theme{:where(:root,:host){--font-sans:-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--color-white:#fff;--spacing:.25rem;--container-xs:20rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--font-weight-medium:500;--font-weight-bold:700;--radius-md:calc(var(--radius) * .8);--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1)}}@layer base{:where(:root){--background:oklch(100% 0 0);--foreground:oklch(14.5% 0 0);--card:oklch(100% 0 0);--card-foreground:oklch(14.5% 0 0);--popover:oklch(100% 0 0);--popover-foreground:oklch(14.5% 0 0);--primary:oklch(20.5% 0 0);--primary-foreground:oklch(98.5% 0 0);--secondary:oklch(97% 0 0);--secondary-foreground:oklch(20.5% 0 0);--muted:oklch(97% 0 0);--muted-foreground:oklch(55.6% 0 0);--accent:oklch(97% 0 0);--accent-foreground:oklch(20.5% 0 0);--destructive:oklch(57.7% .245 27.325);--border:oklch(92.2% 0 0);--input:oklch(92.2% 0 0);--ring:oklch(70.8% 0 0);--chart-1:oklch(87% 0 0);--chart-2:oklch(55.6% 0 0);--chart-3:oklch(43.9% 0 0);--chart-4:oklch(37.1% 0 0);--chart-5:oklch(26.9% 0 0);--radius:.625rem;--rbv-accent:#305cde;--rbv-accent-rgb:48, 92, 222}:where(.dark){--background:oklch(14.5% 0 0);--foreground:oklch(98.5% 0 0);--card:oklch(20.5% 0 0);--card-foreground:oklch(98.5% 0 0);--popover:oklch(20.5% 0 0);--popover-foreground:oklch(98.5% 0 0);--primary:oklch(92.2% 0 0);--primary-foreground:oklch(20.5% 0 0);--secondary:oklch(26.9% 0 0);--secondary-foreground:oklch(98.5% 0 0);--muted:oklch(26.9% 0 0);--muted-foreground:oklch(70.8% 0 0);--accent:oklch(26.9% 0 0);--accent-foreground:oklch(98.5% 0 0);--destructive:oklch(70.4% .191 22.216);--border:oklch(100% 0 0/.1);--input:oklch(100% 0 0/.15);--ring:oklch(55.6% 0 0);--chart-1:oklch(87% 0 0);--chart-2:oklch(55.6% 0 0);--chart-3:oklch(43.9% 0 0);--chart-4:oklch(37.1% 0 0);--chart-5:oklch(26.9% 0 0);--rbv-accent:#7da3ff;--rbv-accent-rgb:125, 163, 255}:where(.rbv,.rbv *){box-sizing:border-box;border:0 solid;border-color:var(--border);outline-color:var(--ring)}@supports (color:color-mix(in lab,red,red)){:where(.rbv,.rbv *){outline-color:color-mix(in oklab,var(--ring) 50%,transparent)}}:where(.rbv) :where(button,input,select,textarea){font:inherit;color:inherit;letter-spacing:inherit;background-color:#0000;margin:0;padding:0}:where(.rbv) :where(button){cursor:pointer}:where(.rbv) :where(ul,ol){margin:0;padding:0;list-style:none}:where(.rbv) :where(svg,canvas){display:block}}@layer components;@layer utilities{.pointer-events-none{pointer-events:none}.collapse{visibility:collapse}.invisible{visibility:hidden}.visible{visibility:visible}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.right-2{right:calc(var(--spacing) * 2)}.left-0{left:0}.z-10{z-index:10}.z-50{z-index:50}.container{width:100%}@media (min-width:40rem){.container{max-width:40rem}}@media (min-width:48rem){.container{max-width:48rem}}@media (min-width:64rem){.container{max-width:64rem}}@media (min-width:80rem){.container{max-width:80rem}}@media (min-width:96rem){.container{max-width:96rem}}.-mx-1{margin-inline:calc(var(--spacing) * -1)}.my-1{margin-block:var(--spacing)}.mb-2{margin-bottom:calc(var(--spacing) * 2)}.block{display:block}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.inline-flex{display:inline-flex}.size-2\.5{width:calc(var(--spacing) * 2.5);height:calc(var(--spacing) * 2.5)}.size-3{width:calc(var(--spacing) * 3);height:calc(var(--spacing) * 3)}.size-4{width:calc(var(--spacing) * 4);height:calc(var(--spacing) * 4)}.size-6{width:calc(var(--spacing) * 6);height:calc(var(--spacing) * 6)}.size-7{width:calc(var(--spacing) * 7);height:calc(var(--spacing) * 7)}.size-8{width:calc(var(--spacing) * 8);height:calc(var(--spacing) * 8)}.size-9{width:calc(var(--spacing) * 9);height:calc(var(--spacing) * 9)}.h-0\.5{height:calc(var(--spacing) * .5)}.h-6{height:calc(var(--spacing) * 6)}.h-7{height:calc(var(--spacing) * 7)}.h-8{height:calc(var(--spacing) * 8)}.h-9{height:calc(var(--spacing) * 9)}.h-px{height:1px}.max-h-\(--radix-select-content-available-height\){max-height:var(--radix-select-content-available-height)}.w-72{width:calc(var(--spacing) * 72)}.w-fit{width:fit-content}.w-full{width:100%}.w-px{width:1px}.max-w-xs{max-width:var(--container-xs)}.min-w-7{min-width:calc(var(--spacing) * 7)}.min-w-8{min-width:calc(var(--spacing) * 8)}.min-w-9{min-width:calc(var(--spacing) * 9)}.min-w-36{min-width:calc(var(--spacing) * 36)}.shrink-0{flex-shrink:0}.grow{flex-grow:1}.origin-\(--radix-popover-content-transform-origin\){transform-origin:var(--radix-popover-content-transform-origin)}.origin-\(--radix-select-content-transform-origin\){transform-origin:var(--radix-select-content-transform-origin)}.origin-\(--radix-tooltip-content-transform-origin\){transform-origin:var(--radix-tooltip-content-transform-origin)}.translate-y-\[calc\(-50\%_-_2px\)\]{--tw-translate-y: calc(-50% - 2px) ;translate:var(--tw-translate-x) var(--tw-translate-y)}.rotate-45{rotate:45deg}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.cursor-default{cursor:default}.cursor-pointer{cursor:pointer}.touch-none{touch-action:none}.resize{resize:both}.scroll-my-1{scroll-margin-block:var(--spacing)}.flex-col{flex-direction:column}.flex-row{flex-direction:row}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.items-end{align-items:flex-end}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.gap-0\.5{gap:calc(var(--spacing) * .5)}.gap-1{gap:var(--spacing)}.gap-1\.5{gap:calc(var(--spacing) * 1.5)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-2\.5{gap:calc(var(--spacing) * 2.5)}.gap-\[--spacing\(var\(--gap\)\)\]{gap:calc(var(--spacing) * var(--gap))}.self-stretch{align-self:stretch}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-hidden{overflow:hidden}.overflow-x-hidden{overflow-x:hidden}.overflow-y-auto{overflow-y:auto}.rounded{border-radius:.25rem}.rounded-\[2px\]{border-radius:2px}.rounded-\[min\(var\(--radius-md\)\,10px\)\]{border-radius:min(var(--radius-md),10px)}.rounded-\[min\(var\(--radius-md\)\,12px\)\]{border-radius:min(var(--radius-md),12px)}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius)}.rounded-md{border-radius:calc(var(--radius) * .8)}.rounded-sm{border-radius:calc(var(--radius) * .6)}.border{border-style:var(--tw-border-style);border-width:1px}.border-border{border-color:var(--border)}.border-input{border-color:var(--input)}.border-ring{border-color:var(--ring)}.border-transparent{border-color:#0000}.bg-background{background-color:var(--background)}.bg-border{background-color:var(--border)}.bg-current{background-color:currentColor}.bg-destructive\/10{background-color:var(--destructive)}@supports (color:color-mix(in lab,red,red)){.bg-destructive\/10{background-color:color-mix(in oklab,var(--destructive) 10%,transparent)}}.bg-foreground{background-color:var(--foreground)}.bg-muted{background-color:var(--muted)}.bg-popover{background-color:var(--popover)}.bg-primary{background-color:var(--primary)}.bg-secondary{background-color:var(--secondary)}.bg-transparent{background-color:#0000}.bg-white{background-color:var(--color-white)}.bg-clip-padding{background-clip:padding-box}.fill-foreground{fill:var(--foreground)}.stroke-1{stroke-width:1px}.stroke-\[1\.5px\]{stroke-width:1.5px}.p-1{padding:var(--spacing)}.p-2\.5{padding:calc(var(--spacing) * 2.5)}.px-1{padding-inline:var(--spacing)}.px-1\.5{padding-inline:calc(var(--spacing) * 1.5)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-2\.5{padding-inline:calc(var(--spacing) * 2.5)}.px-3{padding-inline:calc(var(--spacing) * 3)}.py-1{padding-block:var(--spacing)}.py-1\.5{padding-block:calc(var(--spacing) * 1.5)}.py-2{padding-block:calc(var(--spacing) * 2)}.pr-2{padding-right:calc(var(--spacing) * 2)}.pr-8{padding-right:calc(var(--spacing) * 8)}.pl-1\.5{padding-left:calc(var(--spacing) * 1.5)}.pl-2\.5{padding-left:calc(var(--spacing) * 2.5)}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\[0\.8rem\]{font-size:.8rem}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.text-ellipsis{text-overflow:ellipsis}.whitespace-nowrap{white-space:nowrap}.text-background{color:var(--background)}.text-destructive{color:var(--destructive)}.text-foreground{color:var(--foreground)}.text-muted-foreground{color:var(--muted-foreground)}.text-popover-foreground{color:var(--popover-foreground)}.text-primary{color:var(--primary)}.text-primary-foreground{color:var(--primary-foreground)}.text-secondary-foreground{color:var(--secondary-foreground)}.italic{font-style:italic}.underline-offset-4{text-underline-offset:4px}.opacity-50{opacity:.5}.opacity-60{opacity:.6}.shadow-md{--tw-shadow:0 4px 6px -1px var(--tw-shadow-color,#0000001a), 0 2px 4px -2px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ring-1{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ring-foreground\/10{--tw-ring-color:var(--foreground)}@supports (color:color-mix(in lab,red,red)){.ring-foreground\/10{--tw-ring-color:color-mix(in oklab, var(--foreground) 10%, transparent)}}.ring-ring\/50{--tw-ring-color:var(--ring)}@supports (color:color-mix(in lab,red,red)){.ring-ring\/50{--tw-ring-color:color-mix(in oklab, var(--ring) 50%, transparent)}}.outline-hidden{--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.outline-hidden{outline-offset:2px;outline:2px solid #0000}}.outline{outline-style:var(--tw-outline-style);outline-width:1px}.grayscale{--tw-grayscale:grayscale(100%);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.invert{--tw-invert:invert(100%);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-\[color\,box-shadow\]{transition-property:color,box-shadow;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-all{transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.duration-100{--tw-duration:.1s;transition-duration:.1s}.outline-none{--tw-outline-style:none;outline-style:none}.select-none{-webkit-user-select:none;user-select:none}.running{animation-play-state:running}@media (hover:hover){.group-hover\:bg-\(--rbv-accent\):is(:where(.group):hover *){background-color:var(--rbv-accent)}}.group-data-\[spacing\=0\]\/toggle-group\:rounded-none:is(:where(.group\/toggle-group)[data-spacing="0"] *){border-radius:0}.group-data-\[spacing\=0\]\/toggle-group\:px-2:is(:where(.group\/toggle-group)[data-spacing="0"] *){padding-inline:calc(var(--spacing) * 2)}.after\:absolute:after{content:var(--tw-content);position:absolute}.after\:-inset-2:after{content:var(--tw-content);inset:calc(var(--spacing) * -2)}@media (hover:hover){.hover\:bg-\[color-mix\(in_oklch\,var\(--secondary\)\,var\(--foreground\)_5\%\)\]:hover{background-color:var(--secondary)}@supports (color:color-mix(in lab,red,red)){.hover\:bg-\[color-mix\(in_oklch\,var\(--secondary\)\,var\(--foreground\)_5\%\)\]:hover{background-color:color-mix(in oklch,var(--secondary),var(--foreground) 5%)}}.hover\:bg-destructive\/20:hover{background-color:var(--destructive)}@supports (color:color-mix(in lab,red,red)){.hover\:bg-destructive\/20:hover{background-color:color-mix(in oklab,var(--destructive) 20%,transparent)}}.hover\:bg-muted:hover{background-color:var(--muted)}.hover\:bg-primary\/80:hover{background-color:var(--primary)}@supports (color:color-mix(in lab,red,red)){.hover\:bg-primary\/80:hover{background-color:color-mix(in oklab,var(--primary) 80%,transparent)}}.hover\:stroke-\(--rbv-accent\):hover{stroke:var(--rbv-accent)}.hover\:stroke-2:hover{stroke-width:2px}.hover\:stroke-\[3px\]:hover{stroke-width:3px}.hover\:text-foreground:hover{color:var(--foreground)}.hover\:underline:hover{text-decoration-line:underline}.hover\:opacity-100:hover{opacity:1}.hover\:ring-3:hover{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}}.focus\:z-10:focus{z-index:10}.focus\:bg-accent:focus{background-color:var(--accent)}.focus\:text-accent-foreground:focus,:is(.not-data-\[variant\=destructive\]\:focus\:\*\*\:text-accent-foreground:not([data-variant=destructive]):focus *){color:var(--accent-foreground)}.focus-visible\:z-10:focus-visible{z-index:10}.focus-visible\:border-destructive\/40:focus-visible{border-color:var(--destructive)}@supports (color:color-mix(in lab,red,red)){.focus-visible\:border-destructive\/40:focus-visible{border-color:color-mix(in oklab,var(--destructive) 40%,transparent)}}.focus-visible\:border-ring:focus-visible{border-color:var(--ring)}.focus-visible\:ring-3:focus-visible,.focus-visible\:ring-\[3px\]:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus-visible\:ring-destructive\/20:focus-visible{--tw-ring-color:var(--destructive)}@supports (color:color-mix(in lab,red,red)){.focus-visible\:ring-destructive\/20:focus-visible{--tw-ring-color:color-mix(in oklab, var(--destructive) 20%, transparent)}}.focus-visible\:ring-ring\/50:focus-visible{--tw-ring-color:var(--ring)}@supports (color:color-mix(in lab,red,red)){.focus-visible\:ring-ring\/50:focus-visible{--tw-ring-color:color-mix(in oklab, var(--ring) 50%, transparent)}}.focus-visible\:outline-hidden:focus-visible{--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.focus-visible\:outline-hidden:focus-visible{outline-offset:2px;outline:2px solid #0000}}.active\:ring-3:active{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.active\:not-aria-\[haspopup\]\:translate-y-px:active:not([aria-haspopup]){--tw-translate-y:1px;translate:var(--tw-translate-x) var(--tw-translate-y)}.disabled\:pointer-events-none:disabled{pointer-events:none}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-50:disabled{opacity:.5}:where([data-slot=button-group]) .in-data-\[slot\=button-group\]\:rounded-lg{border-radius:var(--radius)}.has-data-\[icon\=inline-end\]\:pr-1\.5:has([data-icon=inline-end]){padding-right:calc(var(--spacing) * 1.5)}.has-data-\[icon\=inline-end\]\:pr-2:has([data-icon=inline-end]){padding-right:calc(var(--spacing) * 2)}.group-data-\[spacing\=0\]\/toggle-group\:has-data-\[icon\=inline-end\]\:pr-1\.5:is(:where(.group\/toggle-group)[data-spacing="0"] *):has([data-icon=inline-end]){padding-right:calc(var(--spacing) * 1.5)}.has-data-\[icon\=inline-start\]\:pl-1\.5:has([data-icon=inline-start]){padding-left:calc(var(--spacing) * 1.5)}.has-data-\[icon\=inline-start\]\:pl-2:has([data-icon=inline-start]){padding-left:calc(var(--spacing) * 2)}.group-data-\[spacing\=0\]\/toggle-group\:has-data-\[icon\=inline-start\]\:pl-1\.5:is(:where(.group\/toggle-group)[data-spacing="0"] *):has([data-icon=inline-start]){padding-left:calc(var(--spacing) * 1.5)}.has-data-\[slot\=kbd\]\:pr-1\.5:has([data-slot=kbd]){padding-right:calc(var(--spacing) * 1.5)}.aria-expanded\:bg-muted[aria-expanded=true]{background-color:var(--muted)}.aria-expanded\:bg-secondary[aria-expanded=true]{background-color:var(--secondary)}.aria-expanded\:text-foreground[aria-expanded=true]{color:var(--foreground)}.aria-expanded\:text-secondary-foreground[aria-expanded=true]{color:var(--secondary-foreground)}.aria-invalid\:border-destructive[aria-invalid=true]{border-color:var(--destructive)}.aria-invalid\:ring-3[aria-invalid=true]{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.aria-invalid\:ring-destructive\/20[aria-invalid=true]{--tw-ring-color:var(--destructive)}@supports (color:color-mix(in lab,red,red)){.aria-invalid\:ring-destructive\/20[aria-invalid=true]{--tw-ring-color:color-mix(in oklab, var(--destructive) 20%, transparent)}}.aria-pressed\:bg-muted[aria-pressed=true]{background-color:var(--muted)}.data-placeholder\:text-muted-foreground[data-placeholder]{color:var(--muted-foreground)}.data-\[align-trigger\=true\]\:animate-none[data-align-trigger=true]{animation:none}.data-\[position\=popper\]\:h-\(--radix-select-trigger-height\)[data-position=popper]{height:var(--radix-select-trigger-height)}.data-\[position\=popper\]\:w-full[data-position=popper]{width:100%}.data-\[position\=popper\]\:min-w-\(--radix-select-trigger-width\)[data-position=popper]{min-width:var(--radix-select-trigger-width)}.data-\[side\=bottom\]\:translate-y-1[data-side=bottom]{--tw-translate-y:var(--spacing);translate:var(--tw-translate-x) var(--tw-translate-y)}.data-\[side\=bottom\]\:slide-in-from-top-2[data-side=bottom]{--tw-enter-translate-y:calc(2*var(--spacing)*-1)}.data-\[side\=left\]\:-translate-x-1[data-side=left]{--tw-translate-x:calc(var(--spacing) * -1);translate:var(--tw-translate-x) var(--tw-translate-y)}.data-\[side\=left\]\:slide-in-from-right-2[data-side=left]{--tw-enter-translate-x:calc(2*var(--spacing))}.data-\[side\=right\]\:translate-x-1[data-side=right]{--tw-translate-x:var(--spacing);translate:var(--tw-translate-x) var(--tw-translate-y)}.data-\[side\=right\]\:slide-in-from-left-2[data-side=right]{--tw-enter-translate-x:calc(2*var(--spacing)*-1)}.data-\[side\=top\]\:-translate-y-1[data-side=top]{--tw-translate-y:calc(var(--spacing) * -1);translate:var(--tw-translate-x) var(--tw-translate-y)}.data-\[side\=top\]\:slide-in-from-bottom-2[data-side=top]{--tw-enter-translate-y:calc(2*var(--spacing))}.data-\[size\=default\]\:h-8[data-size=default]{height:calc(var(--spacing) * 8)}.data-\[size\=sm\]\:h-7[data-size=sm]{height:calc(var(--spacing) * 7)}.data-\[size\=sm\]\:rounded-\[min\(var\(--radius-md\)\,10px\)\][data-size=sm]{border-radius:min(var(--radius-md),10px)}:is(.\*\*\:data-\[slot\=kbd\]\:relative *)[data-slot=kbd]{position:relative}:is(.\*\*\:data-\[slot\=kbd\]\:isolate *)[data-slot=kbd]{isolation:isolate}:is(.\*\*\:data-\[slot\=kbd\]\:z-50 *)[data-slot=kbd]{z-index:50}:is(.\*\*\:data-\[slot\=kbd\]\:rounded-sm *)[data-slot=kbd]{border-radius:calc(var(--radius) * .6)}:is(.\*\:data-\[slot\=select-value\]\:line-clamp-1>*)[data-slot=select-value]{-webkit-line-clamp:1;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}:is(.\*\:data-\[slot\=select-value\]\:flex>*)[data-slot=select-value]{display:flex}:is(.\*\:data-\[slot\=select-value\]\:items-center>*)[data-slot=select-value]{align-items:center}:is(.\*\:data-\[slot\=select-value\]\:gap-1\.5>*)[data-slot=select-value]{gap:calc(var(--spacing) * 1.5)}.group-data-horizontal\/toggle-group\:data-\[spacing\=0\]\:first\:rounded-l-lg:is(:where(.group\/toggle-group):where([data-orientation=horizontal]) *)[data-spacing="0"]:first-child{border-top-left-radius:var(--radius);border-bottom-left-radius:var(--radius)}.group-data-vertical\/toggle-group\:data-\[spacing\=0\]\:first\:rounded-t-lg:is(:where(.group\/toggle-group):where([data-orientation=vertical]) *)[data-spacing="0"]:first-child{border-top-left-radius:var(--radius);border-top-right-radius:var(--radius)}.group-data-horizontal\/toggle-group\:data-\[spacing\=0\]\:last\:rounded-r-lg:is(:where(.group\/toggle-group):where([data-orientation=horizontal]) *)[data-spacing="0"]:last-child{border-top-right-radius:var(--radius);border-bottom-right-radius:var(--radius)}.group-data-vertical\/toggle-group\:data-\[spacing\=0\]\:last\:rounded-b-lg:is(:where(.group\/toggle-group):where([data-orientation=vertical]) *)[data-spacing="0"]:last-child{border-bottom-right-radius:var(--radius);border-bottom-left-radius:var(--radius)}.data-\[state\=delayed-open\]\:animate-in[data-state=delayed-open]{animation:enter var(--tw-animation-duration,var(--tw-duration,.15s))var(--tw-ease,ease)var(--tw-animation-delay,0s)var(--tw-animation-iteration-count,1)var(--tw-animation-direction,normal)var(--tw-animation-fill-mode,none)}.data-\[state\=delayed-open\]\:fade-in-0[data-state=delayed-open]{--tw-enter-opacity:0}.data-\[state\=delayed-open\]\:zoom-in-95[data-state=delayed-open]{--tw-enter-scale:.95}.data-\[state\=on\]\:bg-muted[data-state=on]{background-color:var(--muted)}.group-data-horizontal\/toggle-group\:data-\[spacing\=0\]\:data-\[variant\=outline\]\:border-l-0:is(:where(.group\/toggle-group):where([data-orientation=horizontal]) *)[data-spacing="0"][data-variant=outline]{border-left-style:var(--tw-border-style);border-left-width:0}.group-data-vertical\/toggle-group\:data-\[spacing\=0\]\:data-\[variant\=outline\]\:border-t-0:is(:where(.group\/toggle-group):where([data-orientation=vertical]) *)[data-spacing="0"][data-variant=outline]{border-top-style:var(--tw-border-style);border-top-width:0}.group-data-horizontal\/toggle-group\:data-\[spacing\=0\]\:data-\[variant\=outline\]\:first\:border-l:is(:where(.group\/toggle-group):where([data-orientation=horizontal]) *)[data-spacing="0"][data-variant=outline]:first-child{border-left-style:var(--tw-border-style);border-left-width:1px}.group-data-vertical\/toggle-group\:data-\[spacing\=0\]\:data-\[variant\=outline\]\:first\:border-t:is(:where(.group\/toggle-group):where([data-orientation=vertical]) *)[data-spacing="0"][data-variant=outline]:first-child{border-top-style:var(--tw-border-style);border-top-width:1px}.dark\:border-input:is(.dark *){border-color:var(--input)}.dark\:bg-destructive\/20:is(.dark *){background-color:var(--destructive)}@supports (color:color-mix(in lab,red,red)){.dark\:bg-destructive\/20:is(.dark *){background-color:color-mix(in oklab,var(--destructive) 20%,transparent)}}.dark\:bg-input\/30:is(.dark *){background-color:var(--input)}@supports (color:color-mix(in lab,red,red)){.dark\:bg-input\/30:is(.dark *){background-color:color-mix(in oklab,var(--input) 30%,transparent)}}@media (hover:hover){.dark\:hover\:bg-destructive\/30:is(.dark *):hover{background-color:var(--destructive)}@supports (color:color-mix(in lab,red,red)){.dark\:hover\:bg-destructive\/30:is(.dark *):hover{background-color:color-mix(in oklab,var(--destructive) 30%,transparent)}}.dark\:hover\:bg-input\/50:is(.dark *):hover{background-color:var(--input)}@supports (color:color-mix(in lab,red,red)){.dark\:hover\:bg-input\/50:is(.dark *):hover{background-color:color-mix(in oklab,var(--input) 50%,transparent)}}.dark\:hover\:bg-muted\/50:is(.dark *):hover{background-color:var(--muted)}@supports (color:color-mix(in lab,red,red)){.dark\:hover\:bg-muted\/50:is(.dark *):hover{background-color:color-mix(in oklab,var(--muted) 50%,transparent)}}}.dark\:focus-visible\:ring-destructive\/40:is(.dark *):focus-visible{--tw-ring-color:var(--destructive)}@supports (color:color-mix(in lab,red,red)){.dark\:focus-visible\:ring-destructive\/40:is(.dark *):focus-visible{--tw-ring-color:color-mix(in oklab, var(--destructive) 40%, transparent)}}.dark\:aria-invalid\:border-destructive\/50:is(.dark *)[aria-invalid=true]{border-color:var(--destructive)}@supports (color:color-mix(in lab,red,red)){.dark\:aria-invalid\:border-destructive\/50:is(.dark *)[aria-invalid=true]{border-color:color-mix(in oklab,var(--destructive) 50%,transparent)}}.dark\:aria-invalid\:ring-destructive\/40:is(.dark *)[aria-invalid=true]{--tw-ring-color:var(--destructive)}@supports (color:color-mix(in lab,red,red)){.dark\:aria-invalid\:ring-destructive\/40:is(.dark *)[aria-invalid=true]{--tw-ring-color:color-mix(in oklab, var(--destructive) 40%, transparent)}}.data-open\:animate-in:where([data-state=open]),.data-open\:animate-in:where([data-open]:not([data-open=false])){animation:enter var(--tw-animation-duration,var(--tw-duration,.15s))var(--tw-ease,ease)var(--tw-animation-delay,0s)var(--tw-animation-iteration-count,1)var(--tw-animation-direction,normal)var(--tw-animation-fill-mode,none)}.data-open\:fade-in-0:where([data-state=open]),.data-open\:fade-in-0:where([data-open]:not([data-open=false])){--tw-enter-opacity:0}.data-open\:zoom-in-95:where([data-state=open]),.data-open\:zoom-in-95:where([data-open]:not([data-open=false])){--tw-enter-scale:.95}.data-closed\:animate-out:where([data-state=closed]),.data-closed\:animate-out:where([data-closed]:not([data-closed=false])){animation:exit var(--tw-animation-duration,var(--tw-duration,.15s))var(--tw-ease,ease)var(--tw-animation-delay,0s)var(--tw-animation-iteration-count,1)var(--tw-animation-direction,normal)var(--tw-animation-fill-mode,none)}.data-closed\:fade-out-0:where([data-state=closed]),.data-closed\:fade-out-0:where([data-closed]:not([data-closed=false])){--tw-exit-opacity:0}.data-closed\:zoom-out-95:where([data-state=closed]),.data-closed\:zoom-out-95:where([data-closed]:not([data-closed=false])){--tw-exit-scale:.95}.data-disabled\:pointer-events-none:where([data-disabled=true]),.data-disabled\:pointer-events-none:where([data-disabled]:not([data-disabled=false])){pointer-events:none}.data-disabled\:opacity-50:where([data-disabled=true]),.data-disabled\:opacity-50:where([data-disabled]:not([data-disabled=false])){opacity:.5}.data-horizontal\:h-1:where([data-orientation=horizontal]){height:var(--spacing)}.data-horizontal\:h-full:where([data-orientation=horizontal]){height:100%}.data-horizontal\:w-full:where([data-orientation=horizontal]){width:100%}.data-vertical\:h-full:where([data-orientation=vertical]){height:100%}.data-vertical\:min-h-40:where([data-orientation=vertical]){min-height:calc(var(--spacing) * 40)}.data-vertical\:w-1:where([data-orientation=vertical]){width:var(--spacing)}.data-vertical\:w-auto:where([data-orientation=vertical]){width:auto}.data-vertical\:w-full:where([data-orientation=vertical]){width:100%}.data-vertical\:flex-col:where([data-orientation=vertical]){flex-direction:column}.data-vertical\:items-stretch:where([data-orientation=vertical]){align-items:stretch}.\[\&_svg\]\:pointer-events-none svg{pointer-events:none}.\[\&_svg\]\:shrink-0 svg{flex-shrink:0}.\[\&_svg\:not\(\[class\*\=\'size-\'\]\)\]\:size-3 svg:not([class*=size-]){width:calc(var(--spacing) * 3);height:calc(var(--spacing) * 3)}.\[\&_svg\:not\(\[class\*\=\'size-\'\]\)\]\:size-3\.5 svg:not([class*=size-]){width:calc(var(--spacing) * 3.5);height:calc(var(--spacing) * 3.5)}.\[\&_svg\:not\(\[class\*\=\'size-\'\]\)\]\:size-4 svg:not([class*=size-]){width:calc(var(--spacing) * 4);height:calc(var(--spacing) * 4)}.\[\&\:hover\+path\]\:stroke-\(--rbv-accent\):hover+path{stroke:var(--rbv-accent)}:is(.\*\:\[span\]\:last\:flex>*):is(span):last-child{display:flex}:is(.\*\:\[span\]\:last\:items-center>*):is(span):last-child{align-items:center}:is(.\*\:\[span\]\:last\:gap-2>*):is(span):last-child{gap:calc(var(--spacing) * 2)}}@property --tw-animation-delay{syntax:"*";inherits:false;initial-value:0s}@property --tw-animation-direction{syntax:"*";inherits:false;initial-value:normal}@property --tw-animation-duration{syntax:"*";inherits:false}@property --tw-animation-fill-mode{syntax:"*";inherits:false;initial-value:none}@property --tw-animation-iteration-count{syntax:"*";inherits:false;initial-value:1}@property --tw-enter-blur{syntax:"*";inherits:false;initial-value:0}@property --tw-enter-opacity{syntax:"*";inherits:false;initial-value:1}@property --tw-enter-rotate{syntax:"*";inherits:false;initial-value:0}@property --tw-enter-scale{syntax:"*";inherits:false;initial-value:1}@property --tw-enter-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-enter-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-exit-blur{syntax:"*";inherits:false;initial-value:0}@property --tw-exit-opacity{syntax:"*";inherits:false;initial-value:1}@property --tw-exit-rotate{syntax:"*";inherits:false;initial-value:0}@property --tw-exit-scale{syntax:"*";inherits:false;initial-value:1}@property --tw-exit-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-exit-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-outline-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@property --tw-content{syntax:"*";inherits:false;initial-value:""}@keyframes enter{0%{opacity:var(--tw-enter-opacity,1);transform:translate3d(var(--tw-enter-translate-x,0),var(--tw-enter-translate-y,0),0)scale3d(var(--tw-enter-scale,1),var(--tw-enter-scale,1),var(--tw-enter-scale,1))rotate(var(--tw-enter-rotate,0));filter:blur(var(--tw-enter-blur,0))}}@keyframes exit{to{opacity:var(--tw-exit-opacity,1);transform:translate3d(var(--tw-exit-translate-x,0),var(--tw-exit-translate-y,0),0)scale3d(var(--tw-exit-scale,1),var(--tw-exit-scale,1),var(--tw-exit-scale,1))rotate(var(--tw-exit-rotate,0));filter:blur(var(--tw-exit-blur,0))}}
|
|
2
|
+
/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */
|