tfbpshiny 1.0.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.
- tfbpshiny/__init__.py +0 -0
- tfbpshiny/__main__.py +215 -0
- tfbpshiny/app.css +344 -0
- tfbpshiny/app.py +233 -0
- tfbpshiny/brentlab_yeast_collection.yaml +456 -0
- tfbpshiny/components.py +481 -0
- tfbpshiny/configure_logger.py +134 -0
- tfbpshiny/modules/__init__.py +0 -0
- tfbpshiny/modules/binding/__init__.py +13 -0
- tfbpshiny/modules/binding/queries.py +549 -0
- tfbpshiny/modules/binding/server/__init__.py +39 -0
- tfbpshiny/modules/binding/server/workspace.py +1233 -0
- tfbpshiny/modules/binding/ui.py +104 -0
- tfbpshiny/modules/comparison/__init__.py +0 -0
- tfbpshiny/modules/comparison/queries.py +769 -0
- tfbpshiny/modules/comparison/server/__init__.py +38 -0
- tfbpshiny/modules/comparison/server/workspace.py +1738 -0
- tfbpshiny/modules/comparison/ui.py +188 -0
- tfbpshiny/modules/home/__init__.py +0 -0
- tfbpshiny/modules/home/ui.py +109 -0
- tfbpshiny/modules/perturbation/__init__.py +15 -0
- tfbpshiny/modules/perturbation/queries.py +389 -0
- tfbpshiny/modules/perturbation/server/__init__.py +41 -0
- tfbpshiny/modules/perturbation/server/workspace.py +1208 -0
- tfbpshiny/modules/perturbation/ui.py +102 -0
- tfbpshiny/modules/select_datasets/__init__.py +0 -0
- tfbpshiny/modules/select_datasets/queries.py +330 -0
- tfbpshiny/modules/select_datasets/server/__init__.py +68 -0
- tfbpshiny/modules/select_datasets/server/dataset_row.py +314 -0
- tfbpshiny/modules/select_datasets/server/sidebar.py +867 -0
- tfbpshiny/modules/select_datasets/server/workspace.py +489 -0
- tfbpshiny/modules/select_datasets/ui.py +607 -0
- tfbpshiny/utils/correlation_matrix.py +125 -0
- tfbpshiny/utils/perf.py +109 -0
- tfbpshiny/utils/plot_formatter.py +41 -0
- tfbpshiny/utils/ratelimit.py +114 -0
- tfbpshiny/utils/sample_conditions.py +97 -0
- tfbpshiny/utils/topn_matrix.py +150 -0
- tfbpshiny/utils/vdb_init.py +397 -0
- tfbpshiny/utils/vdb_materialize.py +179 -0
- tfbpshiny/www/binding.png +0 -0
- tfbpshiny/www/perturbation.png +0 -0
- tfbpshiny-1.0.0.dist-info/METADATA +159 -0
- tfbpshiny-1.0.0.dist-info/RECORD +46 -0
- tfbpshiny-1.0.0.dist-info/WHEEL +4 -0
- tfbpshiny-1.0.0.dist-info/licenses/LICENSE +14 -0
tfbpshiny/__init__.py
ADDED
|
File without changes
|
tfbpshiny/__main__.py
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import logging
|
|
5
|
+
import os
|
|
6
|
+
import pathlib
|
|
7
|
+
import sys
|
|
8
|
+
from typing import Literal, cast
|
|
9
|
+
|
|
10
|
+
from shiny import run_app
|
|
11
|
+
|
|
12
|
+
from tfbpshiny.configure_logger import LogLevel, configure_logger
|
|
13
|
+
|
|
14
|
+
_DEFAULT_VIRTUALDB_CONFIG = str(
|
|
15
|
+
__import__("pathlib").Path(__file__).parent / "brentlab_yeast_collection.yaml"
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
_DEFAULT_CACHE_DIR = "./tfbpshiny_hf_cache"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _apply_cache_dir(cache_dir: str) -> None:
|
|
22
|
+
"""
|
|
23
|
+
Set ``HF_CACHE_DIR`` to the resolved absolute path before any HF imports.
|
|
24
|
+
|
|
25
|
+
Must be called before importing labretriever or huggingface_hub so that
|
|
26
|
+
``snapshot_download`` and ``VirtualDB`` see the overridden path.
|
|
27
|
+
|
|
28
|
+
:param cache_dir: Path to the HuggingFace cache directory.
|
|
29
|
+
|
|
30
|
+
"""
|
|
31
|
+
os.environ["HF_CACHE_DIR"] = str(pathlib.Path(cache_dir).resolve())
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _run_initialize(
|
|
35
|
+
virtualdb_config: str,
|
|
36
|
+
hf_token: str | None,
|
|
37
|
+
log_level: int,
|
|
38
|
+
log_handler: Literal["console", "file"],
|
|
39
|
+
) -> None:
|
|
40
|
+
"""
|
|
41
|
+
Download all dataset files into the local HuggingFace cache and verify views.
|
|
42
|
+
|
|
43
|
+
Exits the process with code 1 if any download or view-verification step fails.
|
|
44
|
+
|
|
45
|
+
:param virtualdb_config: Path to the VirtualDB YAML config file.
|
|
46
|
+
:param hf_token: Optional HuggingFace token for private repo access.
|
|
47
|
+
:param log_level: Numeric logging level (e.g. ``logging.INFO``).
|
|
48
|
+
:param log_handler: Handler type passed to :func:`configure_logger`.
|
|
49
|
+
|
|
50
|
+
"""
|
|
51
|
+
from tfbpshiny.utils.vdb_init import initialize_data
|
|
52
|
+
|
|
53
|
+
configure_logger("shiny", level=log_level, handler_type=log_handler)
|
|
54
|
+
logger = logging.getLogger("shiny")
|
|
55
|
+
|
|
56
|
+
cache_msg = os.environ.get("HF_CACHE_DIR", "(huggingface default)")
|
|
57
|
+
logger.info("Downloading all datasets into HuggingFace cache: %s", cache_msg)
|
|
58
|
+
try:
|
|
59
|
+
vdb, _ = initialize_data(virtualdb_config, hf_token, local_files_only=False)
|
|
60
|
+
except Exception:
|
|
61
|
+
logger.exception("Cache initialization failed.")
|
|
62
|
+
sys.exit(1)
|
|
63
|
+
|
|
64
|
+
logger.info("Verifying all dataset views are readable...")
|
|
65
|
+
views_df = vdb.query(
|
|
66
|
+
"SELECT view_name FROM duckdb_views()"
|
|
67
|
+
" WHERE schema_name = 'main'"
|
|
68
|
+
" AND view_name NOT LIKE 'duckdb_%'"
|
|
69
|
+
" AND view_name NOT LIKE 'sqlite_%'"
|
|
70
|
+
" AND view_name NOT LIKE 'pragma_%'"
|
|
71
|
+
" ORDER BY view_name"
|
|
72
|
+
)
|
|
73
|
+
view_names = views_df["view_name"].tolist()
|
|
74
|
+
failed: list[str] = []
|
|
75
|
+
for view_name in view_names:
|
|
76
|
+
try:
|
|
77
|
+
df = vdb.query(f'SELECT * FROM "{view_name}" LIMIT 1')
|
|
78
|
+
logger.info(" OK %-30s (%d col(s))", view_name, len(df.columns))
|
|
79
|
+
except Exception:
|
|
80
|
+
logger.exception(" FAIL %s", view_name)
|
|
81
|
+
failed.append(view_name)
|
|
82
|
+
|
|
83
|
+
if failed:
|
|
84
|
+
logger.error("Verification failed for: %s", ", ".join(failed))
|
|
85
|
+
sys.exit(1)
|
|
86
|
+
|
|
87
|
+
logger.info("Cache initialization complete.")
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def run_launch(args: argparse.Namespace) -> None:
|
|
91
|
+
"""
|
|
92
|
+
Download the dataset cache (unless ``--skip-initialize`` is set), then start the
|
|
93
|
+
app.
|
|
94
|
+
|
|
95
|
+
By default uses ``./tfbpshiny_hf_cache`` as the HuggingFace cache directory so
|
|
96
|
+
a plain ``python -m tfbpshiny launch`` is self-contained: it downloads data on
|
|
97
|
+
first run and serves it on subsequent runs from the same local directory.
|
|
98
|
+
|
|
99
|
+
"""
|
|
100
|
+
cache_dir: str = args.cache_dir
|
|
101
|
+
_apply_cache_dir(cache_dir)
|
|
102
|
+
|
|
103
|
+
log_level = LogLevel.from_string(args.log_level)
|
|
104
|
+
hf_token: str | None = os.getenv("HF_TOKEN")
|
|
105
|
+
|
|
106
|
+
if not args.skip_initialize:
|
|
107
|
+
_run_initialize(
|
|
108
|
+
virtualdb_config=args.virtualdb_config,
|
|
109
|
+
hf_token=hf_token,
|
|
110
|
+
log_level=log_level.value,
|
|
111
|
+
log_handler=cast(Literal["console", "file"], args.log_handler),
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
# Env vars are the only reliable way to pass config to uvicorn reload workers,
|
|
115
|
+
# which re-import app.py in a subprocess and cannot see in-process mutations.
|
|
116
|
+
os.environ["TFBPSHINY_LOG_LEVEL"] = str(log_level.value)
|
|
117
|
+
os.environ["TFBPSHINY_LOG_HANDLER"] = args.log_handler
|
|
118
|
+
os.environ["VIRTUALDB_CONFIG"] = args.virtualdb_config
|
|
119
|
+
os.environ["TFBPSHINY_MATERIALIZE"] = "0" if args.no_materialize else "1"
|
|
120
|
+
|
|
121
|
+
kwargs: dict[str, object] = {"port": args.port, "host": args.host}
|
|
122
|
+
if args.debug:
|
|
123
|
+
kwargs.update({"reload": True, "reload_dirs": ["tfbpshiny/shiny_app"]})
|
|
124
|
+
run_app("tfbpshiny.app:app", **kwargs) # type: ignore
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def make_parser() -> argparse.ArgumentParser:
|
|
128
|
+
parser = argparse.ArgumentParser(
|
|
129
|
+
prog="tfbpshiny",
|
|
130
|
+
description=(
|
|
131
|
+
"tfbpshiny — TF Binding and Perturbation Explorer."
|
|
132
|
+
" Use --help after any command."
|
|
133
|
+
),
|
|
134
|
+
epilog="Use 'tfbpshiny <command> --help' for more info on each command.",
|
|
135
|
+
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
|
136
|
+
)
|
|
137
|
+
parser.add_argument(
|
|
138
|
+
"--log-level",
|
|
139
|
+
type=str,
|
|
140
|
+
default="WARNING",
|
|
141
|
+
choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
|
|
142
|
+
help="Set logging level.",
|
|
143
|
+
)
|
|
144
|
+
parser.add_argument(
|
|
145
|
+
"--log-handler",
|
|
146
|
+
type=str,
|
|
147
|
+
default="console",
|
|
148
|
+
choices=["console", "file"],
|
|
149
|
+
help="Set log handler type.",
|
|
150
|
+
)
|
|
151
|
+
parser.add_argument(
|
|
152
|
+
"--virtualdb-config",
|
|
153
|
+
type=str,
|
|
154
|
+
default=_DEFAULT_VIRTUALDB_CONFIG,
|
|
155
|
+
help="Path to the VirtualDB YAML configuration file.",
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
159
|
+
|
|
160
|
+
launch_parser = subparsers.add_parser(
|
|
161
|
+
"launch",
|
|
162
|
+
help="Download the dataset cache (first run) and start the Shiny app.",
|
|
163
|
+
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
|
164
|
+
)
|
|
165
|
+
launch_parser.add_argument(
|
|
166
|
+
"--cache-dir",
|
|
167
|
+
type=str,
|
|
168
|
+
default=_DEFAULT_CACHE_DIR,
|
|
169
|
+
help=(
|
|
170
|
+
"HuggingFace cache directory. Datasets are downloaded here on first run "
|
|
171
|
+
"and read from here on subsequent runs. "
|
|
172
|
+
"Equivalent to setting HF_CACHE_DIR."
|
|
173
|
+
),
|
|
174
|
+
)
|
|
175
|
+
launch_parser.add_argument(
|
|
176
|
+
"--skip-initialize",
|
|
177
|
+
action="store_true",
|
|
178
|
+
default=False,
|
|
179
|
+
help=(
|
|
180
|
+
"Skip the dataset download and verification step. "
|
|
181
|
+
"Use when the cache is already populated and you want a faster startup."
|
|
182
|
+
),
|
|
183
|
+
)
|
|
184
|
+
launch_parser.add_argument(
|
|
185
|
+
"--no-materialize",
|
|
186
|
+
action="store_true",
|
|
187
|
+
default=False,
|
|
188
|
+
help=(
|
|
189
|
+
"Disable in-memory materialization of dataset views at startup. "
|
|
190
|
+
"Reduces startup memory at the cost of slower query performance. "
|
|
191
|
+
"Equivalent to setting TFBPSHINY_MATERIALIZE=0."
|
|
192
|
+
),
|
|
193
|
+
)
|
|
194
|
+
launch_parser.add_argument(
|
|
195
|
+
"--port", type=int, default=8000, help="Port to serve the Shiny app on."
|
|
196
|
+
)
|
|
197
|
+
launch_parser.add_argument(
|
|
198
|
+
"--host", type=str, default="127.0.0.1", help="Host to bind the Shiny app."
|
|
199
|
+
)
|
|
200
|
+
launch_parser.add_argument(
|
|
201
|
+
"--debug", action="store_true", help="Enable debug mode with auto-reload."
|
|
202
|
+
)
|
|
203
|
+
launch_parser.set_defaults(func=run_launch)
|
|
204
|
+
|
|
205
|
+
return parser
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def main() -> None:
|
|
209
|
+
parser = make_parser()
|
|
210
|
+
args = parser.parse_args()
|
|
211
|
+
args.func(args)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
if __name__ == "__main__":
|
|
215
|
+
main()
|
tfbpshiny/app.css
ADDED
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
/* --- Custom properties -------------------------------------------------- */
|
|
2
|
+
:root {
|
|
3
|
+
--color-primary: #2C7A7B;
|
|
4
|
+
--color-primary-dark: #1A5456;
|
|
5
|
+
--color-border: #E2E8F0;
|
|
6
|
+
--color-text: #1A202C;
|
|
7
|
+
|
|
8
|
+
--radius-sm: 6px;
|
|
9
|
+
|
|
10
|
+
--font-size-label: 0.875rem;
|
|
11
|
+
--transition-fast: 150ms ease;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/* --- Group headers ------------------------------------------------------ */
|
|
15
|
+
.group-header {
|
|
16
|
+
font-size: var(--font-size-label);
|
|
17
|
+
font-weight: 700;
|
|
18
|
+
letter-spacing: 0.04em;
|
|
19
|
+
color: var(--color-text);
|
|
20
|
+
padding: 12px 0 2px;
|
|
21
|
+
margin-top: 4px;
|
|
22
|
+
margin-bottom: 12px;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/* --- Empty / instruction state ------------------------------------------ */
|
|
26
|
+
.empty-state {
|
|
27
|
+
display: flex;
|
|
28
|
+
flex-direction: column;
|
|
29
|
+
align-items: center;
|
|
30
|
+
justify-content: center;
|
|
31
|
+
padding: 64px 24px;
|
|
32
|
+
text-align: center;
|
|
33
|
+
color: var(--color-text);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.empty-state.compact {
|
|
37
|
+
padding: 20px 12px;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/* --- Selection sidebar -------------------------------------------------- */
|
|
41
|
+
.sidebar-text {
|
|
42
|
+
color: var(--color-text);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/* Dataset row: grid with toggle+label taking remaining space, filter button
|
|
46
|
+
pinned to a fixed-width right column so all buttons align vertically. */
|
|
47
|
+
.selection-sidebar .dataset-row {
|
|
48
|
+
display: grid;
|
|
49
|
+
grid-template-columns: 1fr 52px;
|
|
50
|
+
align-items: center;
|
|
51
|
+
gap: 0.5rem;
|
|
52
|
+
padding: 0.125rem 0;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.selection-sidebar .dataset-row-label {
|
|
56
|
+
overflow: hidden;
|
|
57
|
+
text-overflow: ellipsis;
|
|
58
|
+
white-space: nowrap;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.selection-sidebar .btn-filter-dataset {
|
|
62
|
+
width: 100%;
|
|
63
|
+
font-size: var(--font-size-label);
|
|
64
|
+
padding: 0.125rem 0.4rem;
|
|
65
|
+
line-height: 1.4;
|
|
66
|
+
border-radius: 3px;
|
|
67
|
+
text-align: center;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.selection-sidebar .btn-filter-dataset.btn-filter-active {
|
|
71
|
+
color: #fff;
|
|
72
|
+
background-color: #0d6efd;
|
|
73
|
+
border-color: #0d6efd;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.selection-sidebar .dataset-list {
|
|
77
|
+
display: flex;
|
|
78
|
+
flex-direction: column;
|
|
79
|
+
gap: 2px;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.btn-apply-pending {
|
|
83
|
+
display: block;
|
|
84
|
+
width: calc(100% - 16px);
|
|
85
|
+
margin: 8px 8px 0 8px;
|
|
86
|
+
padding: 6px 12px;
|
|
87
|
+
font-size: 0.85rem;
|
|
88
|
+
font-weight: 600;
|
|
89
|
+
color: #fff;
|
|
90
|
+
background-color: #dc3545;
|
|
91
|
+
border: none;
|
|
92
|
+
border-radius: 4px;
|
|
93
|
+
cursor: pointer;
|
|
94
|
+
transition: opacity 0.15s ease, background-color 0.15s ease;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.btn-apply-pending:hover {
|
|
98
|
+
background-color: #bb2d3b;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.btn-apply-pending--idle {
|
|
102
|
+
opacity: 0.35;
|
|
103
|
+
pointer-events: none;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
/* Left cell: toggle + label as an inline flex row */
|
|
108
|
+
.selection-sidebar .dataset-row-left {
|
|
109
|
+
display: flex;
|
|
110
|
+
align-items: center;
|
|
111
|
+
gap: 0.5rem;
|
|
112
|
+
min-width: 0;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/* Tighten up Shiny's switch input inside dataset rows */
|
|
116
|
+
.selection-sidebar .dataset-row-left .shiny-input-container {
|
|
117
|
+
margin: 0;
|
|
118
|
+
width: auto;
|
|
119
|
+
flex: 0 0 auto;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.selection-sidebar .dataset-row-left .shiny-input-container .form-switch {
|
|
123
|
+
margin: 0;
|
|
124
|
+
min-height: 0;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.selection-sidebar .dataset-row-left .shiny-input-container .form-check-input {
|
|
128
|
+
margin-top: 0;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.selection-sidebar .dataset-row-left .shiny-input-container label {
|
|
132
|
+
display: none;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/* --- Intersection matrix ------------------------------------------------ */
|
|
136
|
+
.matrix-summary-table {
|
|
137
|
+
width: 100%;
|
|
138
|
+
table-layout: fixed;
|
|
139
|
+
border-spacing: 0;
|
|
140
|
+
border-collapse: collapse;
|
|
141
|
+
font-size: var(--font-size-label);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.matrix-summary-table th,
|
|
145
|
+
.matrix-summary-table td {
|
|
146
|
+
border-bottom: 1px solid #EDF2F7;
|
|
147
|
+
border-right: 1px solid #EDF2F7;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.matrix-summary-table .matrix-row-header {
|
|
151
|
+
position: sticky;
|
|
152
|
+
left: 0;
|
|
153
|
+
z-index: 2;
|
|
154
|
+
background: #F8FAFC;
|
|
155
|
+
text-align: left;
|
|
156
|
+
width: 20%;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.matrix-summary-table .matrix-col-header {
|
|
160
|
+
background: #F8FAFC;
|
|
161
|
+
text-align: center;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.matrix-summary-table .matrix-header-name {
|
|
165
|
+
font-size: var(--font-size-label);
|
|
166
|
+
font-weight: 700;
|
|
167
|
+
color: var(--color-text);
|
|
168
|
+
white-space: nowrap;
|
|
169
|
+
text-overflow: ellipsis;
|
|
170
|
+
overflow: hidden;
|
|
171
|
+
padding: 8px;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
.matrix-summary-table .matrix-row-label {
|
|
175
|
+
position: sticky;
|
|
176
|
+
left: 0;
|
|
177
|
+
z-index: 1;
|
|
178
|
+
background: #FFFFFF;
|
|
179
|
+
font-size: var(--font-size-label);
|
|
180
|
+
font-weight: 600;
|
|
181
|
+
color: var(--color-text);
|
|
182
|
+
text-align: left;
|
|
183
|
+
padding: 10px 12px;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
.matrix-summary-table .matrix-cell-empty {
|
|
187
|
+
background: #F8FAFC;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
.matrix-summary-table .matrix-cell-diagonal {
|
|
191
|
+
background: #EBF8FF;
|
|
192
|
+
padding: 0;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
.matrix-summary-table .matrix-cell-interactive {
|
|
196
|
+
padding: 0;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
.matrix-summary-table .matrix-cell-diagonal:hover,
|
|
200
|
+
.matrix-summary-table .matrix-cell-interactive:hover {
|
|
201
|
+
background: #4A5568;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
.matrix-summary-table .matrix-cell-active {
|
|
205
|
+
background: #2B4C7E;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
.matrix-summary-table .matrix-cell-active:hover {
|
|
209
|
+
background: #3A6098;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
.matrix-summary-table .matrix-cell-active .matrix-cell-button {
|
|
213
|
+
color: #FFFFFF;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
.matrix-summary-table .matrix-cell-pending {
|
|
217
|
+
background: #7B4F00;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
.matrix-summary-table .matrix-cell-pending:hover {
|
|
221
|
+
background: #9A6300;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
.matrix-summary-table .matrix-cell-pending .matrix-cell-button {
|
|
225
|
+
color: #FFFFFF;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/* --- Pending regulator filter banner ------------------------------------ */
|
|
229
|
+
.pending-regulator-banner {
|
|
230
|
+
border: 1px solid #9A6300;
|
|
231
|
+
border-radius: var(--radius-sm);
|
|
232
|
+
background: #FFF8E7;
|
|
233
|
+
padding: 10px 14px;
|
|
234
|
+
margin-bottom: 12px;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
.pending-regulator-banner-body {
|
|
238
|
+
display: flex;
|
|
239
|
+
align-items: center;
|
|
240
|
+
justify-content: space-between;
|
|
241
|
+
gap: 12px;
|
|
242
|
+
flex-wrap: wrap;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
.pending-regulator-banner-text {
|
|
246
|
+
font-size: var(--font-size-label);
|
|
247
|
+
color: #5A3800;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
.matrix-cell-button {
|
|
251
|
+
width: 100%;
|
|
252
|
+
height: 100%;
|
|
253
|
+
display: block;
|
|
254
|
+
padding: 10px 8px;
|
|
255
|
+
border: none;
|
|
256
|
+
background: transparent;
|
|
257
|
+
color: var(--color-text);
|
|
258
|
+
font-size: var(--font-size-label);
|
|
259
|
+
font-weight: 500;
|
|
260
|
+
font-variant-numeric: tabular-nums;
|
|
261
|
+
cursor: pointer;
|
|
262
|
+
text-align: center;
|
|
263
|
+
transition: color var(--transition-fast);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
.matrix-summary-table .matrix-cell-diagonal:hover .matrix-cell-button,
|
|
267
|
+
.matrix-summary-table .matrix-cell-interactive:hover .matrix-cell-button {
|
|
268
|
+
color: #FFFFFF;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/* --- GitHub repo badge -------------------------------------------------- */
|
|
272
|
+
.github-badge {
|
|
273
|
+
display: inline-flex;
|
|
274
|
+
align-items: center;
|
|
275
|
+
padding: 4px 6px;
|
|
276
|
+
color: #fff;
|
|
277
|
+
font-weight: 500;
|
|
278
|
+
text-decoration: none;
|
|
279
|
+
white-space: nowrap;
|
|
280
|
+
flex-shrink: 0;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
.github-badge:hover {
|
|
284
|
+
text-decoration: none;
|
|
285
|
+
color: #fff;
|
|
286
|
+
opacity: 0.85;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
.github-badge-version {
|
|
290
|
+
margin-left: 6px;
|
|
291
|
+
padding: 1px 6px;
|
|
292
|
+
border-radius: 10px;
|
|
293
|
+
background: rgba(255,255,255,0.25);
|
|
294
|
+
color: #fff;
|
|
295
|
+
font-size: var(--font-size-label);
|
|
296
|
+
font-weight: 600;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/* --- Navbar brand title -------------------------------------------------- */
|
|
300
|
+
.navbar-brand {
|
|
301
|
+
font-size: 1.15rem;
|
|
302
|
+
font-weight: 700;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/* --- Dataset tooltip ----------------------------------------------------- */
|
|
306
|
+
.tooltip-inner {
|
|
307
|
+
max-width: 300px;
|
|
308
|
+
text-align: left;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/* --- Progressive-loading placeholders for comparison cells/tables -------- */
|
|
312
|
+
.cd-cell-loading {
|
|
313
|
+
display: block;
|
|
314
|
+
width: 100%;
|
|
315
|
+
padding: 10px 8px;
|
|
316
|
+
text-align: center;
|
|
317
|
+
color: #A0AEC0;
|
|
318
|
+
font-size: var(--font-size-label);
|
|
319
|
+
animation: cd-pulse 1.4s ease-in-out infinite;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
.cp-table-loading,
|
|
323
|
+
.cm-table-loading {
|
|
324
|
+
padding: 12px 16px;
|
|
325
|
+
color: #A0AEC0;
|
|
326
|
+
font-size: var(--font-size-label);
|
|
327
|
+
animation: cd-pulse 1.4s ease-in-out infinite;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
@keyframes cd-pulse {
|
|
331
|
+
0%, 100% { opacity: 1; }
|
|
332
|
+
50% { opacity: 0.4; }
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/* --- Pending-changes banner in binding/perturbation sidebars ------------- */
|
|
336
|
+
.pending-banner {
|
|
337
|
+
background: #fff3cd;
|
|
338
|
+
border: 1px solid #ffc107;
|
|
339
|
+
border-radius: 4px;
|
|
340
|
+
padding: 0.5rem 0.75rem;
|
|
341
|
+
margin-bottom: 0.5rem;
|
|
342
|
+
font-size: 0.85rem;
|
|
343
|
+
color: #664d03;
|
|
344
|
+
}
|