quantui 0.5.1__tar.gz → 0.5.2__tar.gz
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.
- {quantui-0.5.1 → quantui-0.5.2}/CHANGELOG.md +26 -1
- {quantui-0.5.1/quantui.egg-info → quantui-0.5.2}/PKG-INFO +2 -2
- {quantui-0.5.1 → quantui-0.5.2}/README.md +1 -1
- {quantui-0.5.1 → quantui-0.5.2}/pyproject.toml +1 -1
- {quantui-0.5.1 → quantui-0.5.2}/quantui/__init__.py +1 -1
- {quantui-0.5.1 → quantui-0.5.2}/quantui/app.py +29 -36
- {quantui-0.5.1 → quantui-0.5.2}/quantui/app_builders.py +35 -13
- {quantui-0.5.1 → quantui-0.5.2}/quantui/app_formatters.py +3 -1
- {quantui-0.5.1 → quantui-0.5.2}/quantui/app_visualization.py +27 -16
- {quantui-0.5.1 → quantui-0.5.2}/quantui/live_log.py +3 -1
- quantui-0.5.2/quantui/theme.py +125 -0
- {quantui-0.5.1 → quantui-0.5.2}/quantui/visualization_py3dmol.py +6 -1
- {quantui-0.5.1 → quantui-0.5.2/quantui.egg-info}/PKG-INFO +2 -2
- {quantui-0.5.1 → quantui-0.5.2}/quantui.egg-info/SOURCES.txt +1 -0
- {quantui-0.5.1 → quantui-0.5.2}/LICENSE +0 -0
- {quantui-0.5.1 → quantui-0.5.2}/MANIFEST.in +0 -0
- {quantui-0.5.1 → quantui-0.5.2}/SECURITY.md +0 -0
- {quantui-0.5.1 → quantui-0.5.2}/quantui/analytics.py +0 -0
- {quantui-0.5.1 → quantui-0.5.2}/quantui/app_analysis.py +0 -0
- {quantui-0.5.1 → quantui-0.5.2}/quantui/app_exports.py +0 -0
- {quantui-0.5.1 → quantui-0.5.2}/quantui/app_history.py +0 -0
- {quantui-0.5.1 → quantui-0.5.2}/quantui/app_runflow.py +0 -0
- {quantui-0.5.1 → quantui-0.5.2}/quantui/ase_bridge.py +0 -0
- {quantui-0.5.1 → quantui-0.5.2}/quantui/benchmarks.py +0 -0
- {quantui-0.5.1 → quantui-0.5.2}/quantui/c_stderr.py +0 -0
- {quantui-0.5.1 → quantui-0.5.2}/quantui/cactus.py +0 -0
- {quantui-0.5.1 → quantui-0.5.2}/quantui/calc_log.py +0 -0
- {quantui-0.5.1 → quantui-0.5.2}/quantui/calculator.py +0 -0
- {quantui-0.5.1 → quantui-0.5.2}/quantui/cancellation.py +0 -0
- {quantui-0.5.1 → quantui-0.5.2}/quantui/cli.py +0 -0
- {quantui-0.5.1 → quantui-0.5.2}/quantui/comparison.py +0 -0
- {quantui-0.5.1 → quantui-0.5.2}/quantui/config.py +0 -0
- {quantui-0.5.1 → quantui-0.5.2}/quantui/data/js/3Dmol-min.js +0 -0
- {quantui-0.5.1 → quantui-0.5.2}/quantui/data/js/3Dmol-min.js.LICENSE.txt +0 -0
- {quantui-0.5.1 → quantui-0.5.2}/quantui/data/library/library.sqlite +0 -0
- {quantui-0.5.1 → quantui-0.5.2}/quantui/data/manifests/bulk_qm9.json +0 -0
- {quantui-0.5.1 → quantui-0.5.2}/quantui/data/manifests/curated.json +0 -0
- {quantui-0.5.1 → quantui-0.5.2}/quantui/data/manifests/presets.json +0 -0
- {quantui-0.5.1 → quantui-0.5.2}/quantui/descriptor_cards.py +0 -0
- {quantui-0.5.1 → quantui-0.5.2}/quantui/freq_calc.py +0 -0
- {quantui-0.5.1 → quantui-0.5.2}/quantui/freq_ir_workers.py +0 -0
- {quantui-0.5.1 → quantui-0.5.2}/quantui/gpu_offload.py +0 -0
- {quantui-0.5.1 → quantui-0.5.2}/quantui/help_content.py +0 -0
- {quantui-0.5.1 → quantui-0.5.2}/quantui/ir_plot.py +0 -0
- {quantui-0.5.1 → quantui-0.5.2}/quantui/issue_tracker.py +0 -0
- {quantui-0.5.1 → quantui-0.5.2}/quantui/log_utils.py +0 -0
- {quantui-0.5.1 → quantui-0.5.2}/quantui/molecule.py +0 -0
- {quantui-0.5.1 → quantui-0.5.2}/quantui/molecule_library.py +0 -0
- {quantui-0.5.1 → quantui-0.5.2}/quantui/nmr_calc.py +0 -0
- {quantui-0.5.1 → quantui-0.5.2}/quantui/optimizer.py +0 -0
- {quantui-0.5.1 → quantui-0.5.2}/quantui/orbital_visualization.py +0 -0
- {quantui-0.5.1 → quantui-0.5.2}/quantui/pes_scan.py +0 -0
- {quantui-0.5.1 → quantui-0.5.2}/quantui/preopt.py +0 -0
- {quantui-0.5.1 → quantui-0.5.2}/quantui/progress.py +0 -0
- {quantui-0.5.1 → quantui-0.5.2}/quantui/pubchem.py +0 -0
- {quantui-0.5.1 → quantui-0.5.2}/quantui/reorganization_energy.py +0 -0
- {quantui-0.5.1 → quantui-0.5.2}/quantui/results_storage.py +0 -0
- {quantui-0.5.1 → quantui-0.5.2}/quantui/security.py +0 -0
- {quantui-0.5.1 → quantui-0.5.2}/quantui/session_calc.py +0 -0
- {quantui-0.5.1 → quantui-0.5.2}/quantui/structure_providers.py +0 -0
- {quantui-0.5.1 → quantui-0.5.2}/quantui/tddft_calc.py +0 -0
- {quantui-0.5.1 → quantui-0.5.2}/quantui/user_settings.py +0 -0
- {quantui-0.5.1 → quantui-0.5.2}/quantui/utils.py +0 -0
- {quantui-0.5.1 → quantui-0.5.2}/quantui/vib_cache.py +0 -0
- {quantui-0.5.1 → quantui-0.5.2}/quantui/viz_assets.py +0 -0
- {quantui-0.5.1 → quantui-0.5.2}/quantui/viz_backend_router.py +0 -0
- {quantui-0.5.1 → quantui-0.5.2}/quantui.egg-info/dependency_links.txt +0 -0
- {quantui-0.5.1 → quantui-0.5.2}/quantui.egg-info/entry_points.txt +0 -0
- {quantui-0.5.1 → quantui-0.5.2}/quantui.egg-info/requires.txt +0 -0
- {quantui-0.5.1 → quantui-0.5.2}/quantui.egg-info/top_level.txt +0 -0
- {quantui-0.5.1 → quantui-0.5.2}/setup.cfg +0 -0
|
@@ -7,6 +7,30 @@ and this project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.ht
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.5.2] - 2026-08-03
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **Dark mode now has visible panel edges.** Borders, cards, and dividers were
|
|
15
|
+
effectively invisible in Dark mode — measured at 1.14:1 against the panel
|
|
16
|
+
background, well under the 3:1 that WCAG asks of a UI component. Dark mode is
|
|
17
|
+
a whole-page colour inversion rather than a separate palette, so a light grey
|
|
18
|
+
border inverts to a near-black one on a near-black page and disappears. The
|
|
19
|
+
borders are now a mid-tone that stays visible in *both* modes (3.20:1 light,
|
|
20
|
+
4.30:1 dark). Text contrast was measured too and was never the problem — it
|
|
21
|
+
is 7.24:1 in light mode and slightly *better* inverted — so text colours are
|
|
22
|
+
deliberately unchanged.
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
|
|
26
|
+
- **Every 3-D viewer now has a border around it**, so it is clear where the
|
|
27
|
+
view ends and where the page can be scrolled without dragging the structure.
|
|
28
|
+
This covers the molecule preview, the results and analysis structure views,
|
|
29
|
+
the geometry-optimization trajectory, the vibrational-mode animation, and the
|
|
30
|
+
classical pre-optimization preview. The frame hugs the plot itself rather
|
|
31
|
+
than spanning the window width, and looks the same whichever rendering
|
|
32
|
+
backend (py3Dmol or plotlymol3d) is active.
|
|
33
|
+
|
|
10
34
|
## [0.5.1] - 2026-07-31
|
|
11
35
|
|
|
12
36
|
### Fixed
|
|
@@ -484,7 +508,8 @@ Initial public scaffolding of the QuantUI package: `quantui` package with
|
|
|
484
508
|
`calculator.py`, basic notebook launcher, Apptainer container definition,
|
|
485
509
|
MIT license, and project metadata.
|
|
486
510
|
|
|
487
|
-
[Unreleased]: https://github.com/The-Schultz-Lab/QuantUI/compare/v0.5.
|
|
511
|
+
[Unreleased]: https://github.com/The-Schultz-Lab/QuantUI/compare/v0.5.2...HEAD
|
|
512
|
+
[0.5.2]: https://github.com/The-Schultz-Lab/QuantUI/compare/v0.5.1...v0.5.2
|
|
488
513
|
[0.5.1]: https://github.com/The-Schultz-Lab/QuantUI/compare/v0.5.0...v0.5.1
|
|
489
514
|
[0.5.0]: https://github.com/The-Schultz-Lab/QuantUI/compare/v0.4.1...v0.5.0
|
|
490
515
|
[0.4.1]: https://github.com/The-Schultz-Lab/QuantUI/compare/v0.4.0...v0.4.1
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: quantui
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.2
|
|
4
4
|
Summary: An open-source frontend for DFT and post-HF quantum chemistry with PySCF
|
|
5
5
|
Author-email: Jonathan Schultz <nccu-schultz-lab@users.noreply.github.com>
|
|
6
6
|
License: MIT License
|
|
@@ -91,7 +91,7 @@ Dynamic: license-file
|
|
|
91
91
|
|
|
92
92
|
# QuantUI
|
|
93
93
|
|
|
94
|
-
[](https://pypi.org/project/quantui/)
|
|
94
|
+
[](https://pypi.org/project/quantui/)
|
|
95
95
|
[](https://github.com/The-Schultz-Lab/QuantUI/actions/workflows/ci.yml)
|
|
96
96
|
[](https://the-schultz-lab.github.io/QuantUI/)
|
|
97
97
|
[](https://github.com/The-Schultz-Lab/QuantUI/blob/main/LICENSE)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# QuantUI
|
|
2
2
|
|
|
3
|
-
[](https://pypi.org/project/quantui/)
|
|
3
|
+
[](https://pypi.org/project/quantui/)
|
|
4
4
|
[](https://github.com/The-Schultz-Lab/QuantUI/actions/workflows/ci.yml)
|
|
5
5
|
[](https://the-schultz-lab.github.io/QuantUI/)
|
|
6
6
|
[](https://github.com/The-Schultz-Lab/QuantUI/blob/main/LICENSE)
|
|
@@ -7,7 +7,7 @@ Calculations run locally in the Jupyter session — no cluster or SLURM required
|
|
|
7
7
|
PySCF requires Linux/macOS/WSL. Windows users should use the Apptainer container.
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
|
-
__version__ = "0.5.
|
|
10
|
+
__version__ = "0.5.2"
|
|
11
11
|
|
|
12
12
|
import logging
|
|
13
13
|
from typing import Any
|
|
@@ -32,6 +32,7 @@ import quantui
|
|
|
32
32
|
import quantui.calc_log as _calc_log
|
|
33
33
|
import quantui.issue_tracker as _issue_tracker
|
|
34
34
|
from quantui import molecule_library as _ml
|
|
35
|
+
from quantui import theme as _theme
|
|
35
36
|
from quantui.app_analysis import (
|
|
36
37
|
activate_ana_panel as _ana_activate_ana_panel,
|
|
37
38
|
)
|
|
@@ -450,9 +451,6 @@ try:
|
|
|
450
451
|
from quantui.visualization_py3dmol import (
|
|
451
452
|
VIZ_STYLE_OPTIONS as _VIZ_STYLE_OPTIONS,
|
|
452
453
|
)
|
|
453
|
-
from quantui.visualization_py3dmol import (
|
|
454
|
-
display_molecule as _display_molecule,
|
|
455
|
-
)
|
|
456
454
|
from quantui.visualization_py3dmol import (
|
|
457
455
|
render_molecule_html as _render_molecule_html,
|
|
458
456
|
)
|
|
@@ -460,7 +458,6 @@ try:
|
|
|
460
458
|
VISUALIZATION_AVAILABLE = True
|
|
461
459
|
except ImportError:
|
|
462
460
|
VISUALIZATION_AVAILABLE = False
|
|
463
|
-
_display_molecule = None # type: ignore[assignment]
|
|
464
461
|
_render_molecule_html = None # type: ignore[assignment]
|
|
465
462
|
_PLOTLYMOL_VIZ = False
|
|
466
463
|
_PY3DMOL_VIZ = False
|
|
@@ -616,7 +613,7 @@ h3 {
|
|
|
616
613
|
color: #64748b !important;
|
|
617
614
|
margin: 24px 0 10px !important;
|
|
618
615
|
padding-bottom: 5px !important;
|
|
619
|
-
border-bottom: 1px solid
|
|
616
|
+
border-bottom: 1px solid __Q_BORDER__ !important;
|
|
620
617
|
}
|
|
621
618
|
|
|
622
619
|
/* Rounded corners on inputs, dropdowns, and buttons -------------------- */
|
|
@@ -632,21 +629,6 @@ h3 {
|
|
|
632
629
|
background: transparent !important;
|
|
633
630
|
}
|
|
634
631
|
|
|
635
|
-
/* 3D molecule-viewer frames — a subtle bounding box so the viewer
|
|
636
|
-
extent reads clearly. The light border inverts to dark automatically under
|
|
637
|
-
the global dark-mode invert filter. */
|
|
638
|
-
.quantui-viewer-frame {
|
|
639
|
-
border: 1px solid #c0ccd8 !important;
|
|
640
|
-
border-radius: 6px !important;
|
|
641
|
-
overflow: hidden !important;
|
|
642
|
-
}
|
|
643
|
-
/* Collapse the frame to nothing when the viewer output is empty (e.g. before
|
|
644
|
-
a calc runs) so no hollow box shows. Degrades to an always-on border where
|
|
645
|
-
:has() is unsupported. */
|
|
646
|
-
.quantui-viewer-frame:not(:has(.jp-OutputArea-child)) {
|
|
647
|
-
border-color: transparent !important;
|
|
648
|
-
}
|
|
649
|
-
|
|
650
632
|
/* Inline "calculating" spinner — shown next to slow on-demand
|
|
651
633
|
controls (e.g. orbital-isosurface generation) while work is in flight. */
|
|
652
634
|
@keyframes quantui-spin { to { transform: rotate(360deg); } }
|
|
@@ -654,13 +636,20 @@ h3 {
|
|
|
654
636
|
display: inline-block;
|
|
655
637
|
width: 14px;
|
|
656
638
|
height: 14px;
|
|
657
|
-
border: 2px solid
|
|
639
|
+
border: 2px solid __Q_BORDER__;
|
|
658
640
|
border-top-color: #2563eb;
|
|
659
641
|
border-radius: 50%;
|
|
660
642
|
animation: quantui-spin 0.7s linear infinite;
|
|
661
643
|
vertical-align: middle;
|
|
662
644
|
}
|
|
663
|
-
</style>"""
|
|
645
|
+
</style>""".replace(
|
|
646
|
+
# Sentinel substitution rather than an f-string: this block is dense with
|
|
647
|
+
# CSS braces, every one of which would need doubling. If a second sentinel
|
|
648
|
+
# is ever added, substitute the LONGER name first — "__Q_BORDER__" matches
|
|
649
|
+
# inside "__Q_BORDER_STRONG__" and would leave a dangling "_STRONG__".
|
|
650
|
+
"__Q_BORDER__",
|
|
651
|
+
_theme.BORDER,
|
|
652
|
+
)
|
|
664
653
|
|
|
665
654
|
_LAYOUT_TRAITS: frozenset[str] = frozenset(widgets.Layout.class_trait_names())
|
|
666
655
|
|
|
@@ -2383,7 +2372,7 @@ class QuantUIApp:
|
|
|
2383
2372
|
body = rows[1:]
|
|
2384
2373
|
head_html = "".join(
|
|
2385
2374
|
f'<th style="padding:4px 10px;text-align:left;'
|
|
2386
|
-
f"border-bottom:1px solid
|
|
2375
|
+
f"border-bottom:1px solid {_theme.BORDER};font-size:12px;"
|
|
2387
2376
|
f'color:#1e293b">{_html.escape(str(c))}</th>'
|
|
2388
2377
|
for c in header
|
|
2389
2378
|
)
|
|
@@ -2427,7 +2416,7 @@ class QuantUIApp:
|
|
|
2427
2416
|
# reach the parent app.
|
|
2428
2417
|
iframe_html = (
|
|
2429
2418
|
'<iframe sandbox="allow-scripts" '
|
|
2430
|
-
'style="width:100%;height:400px;border:1px solid
|
|
2419
|
+
f'style="width:100%;height:400px;border:1px solid {_theme.BORDER};'
|
|
2431
2420
|
'border-radius:4px" '
|
|
2432
2421
|
f'srcdoc="{_html.escape(raw, quote=True)}"></iframe>'
|
|
2433
2422
|
)
|
|
@@ -2871,7 +2860,7 @@ class QuantUIApp:
|
|
|
2871
2860
|
def _rerender_3d_views(self) -> None:
|
|
2872
2861
|
"""Re-render visible 3D molecule viewers using the router to pick a
|
|
2873
2862
|
backend per task. Updates the "Rendering with: X" label widgets."""
|
|
2874
|
-
if
|
|
2863
|
+
if _render_molecule_html is None:
|
|
2875
2864
|
return
|
|
2876
2865
|
|
|
2877
2866
|
# Calculate-tab molecule preview (MOLECULE_PREVIEW task).
|
|
@@ -2884,15 +2873,19 @@ class QuantUIApp:
|
|
|
2884
2873
|
if self._analysis_displayed_molecule is not None:
|
|
2885
2874
|
chosen = self._resolve_backend(VizTask.ANALYSIS_STRUCTURE_VIEW)
|
|
2886
2875
|
if chosen is not None:
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2876
|
+
# Same render path as the first draw (app_visualization.
|
|
2877
|
+
# _show_result_3d), not display(): the fragment it returns
|
|
2878
|
+
# carries the viewer's own border. Going through display()
|
|
2879
|
+
# here would silently drop that border the moment a user
|
|
2880
|
+
# toggled backends on the Analysis tab.
|
|
2881
|
+
html = _render_molecule_html(
|
|
2882
|
+
self._analysis_displayed_molecule,
|
|
2883
|
+
backend=str(chosen),
|
|
2884
|
+
style=self._viz_style,
|
|
2885
|
+
lighting=self._viz_lighting,
|
|
2886
|
+
bgcolor=self._plotly_theme_colors()["scene_bgcolor"],
|
|
2887
|
+
)
|
|
2888
|
+
self._set_html_output(self._analysis_mol_output, html)
|
|
2896
2889
|
self._update_analysis_backend_label(chosen)
|
|
2897
2890
|
|
|
2898
2891
|
def _update_analysis_backend_label(self, chosen: VizBackend) -> None:
|
|
@@ -5514,7 +5507,7 @@ class QuantUIApp:
|
|
|
5514
5507
|
rows.append(f'<div style="{style}">{esc}</div>')
|
|
5515
5508
|
self._log_output_html.value = (
|
|
5516
5509
|
'<div style="font-family:monospace;font-size:12px;line-height:1.4;'
|
|
5517
|
-
"padding:8px 10px;background:#f8fafc;border:1px solid
|
|
5510
|
+
f"padding:8px 10px;background:#f8fafc;border:1px solid {_theme.BORDER};"
|
|
5518
5511
|
'border-radius:4px;overflow-x:auto;max-height:550px;overflow-y:auto">'
|
|
5519
5512
|
+ "".join(rows)
|
|
5520
5513
|
+ "</div>"
|
|
@@ -5530,7 +5523,7 @@ class QuantUIApp:
|
|
|
5530
5523
|
if key and key in HELP_TOPICS:
|
|
5531
5524
|
entry = HELP_TOPICS[key]
|
|
5532
5525
|
self.help_content_html.value = (
|
|
5533
|
-
f'<div style="border:1px solid
|
|
5526
|
+
f'<div style="border:1px solid {_theme.BORDER};border-radius:6px;'
|
|
5534
5527
|
f'padding:14px 18px;margin:8px 0;background:#f8fafc;max-width:700px">'
|
|
5535
5528
|
f'<h4 style="margin:0 0 10px;color:#1e293b;font-size:15px;font-weight:700">'
|
|
5536
5529
|
f'{entry["title"]}</h4>'
|
|
@@ -11,6 +11,7 @@ from IPython.display import HTML, display
|
|
|
11
11
|
|
|
12
12
|
import quantui
|
|
13
13
|
from quantui import molecule_library as _ml
|
|
14
|
+
from quantui import theme as _theme
|
|
14
15
|
from quantui.help_content import HELP_TOPICS
|
|
15
16
|
from quantui.live_log import LiveLog
|
|
16
17
|
|
|
@@ -153,7 +154,7 @@ def build_status_panel(
|
|
|
153
154
|
for k, v in items
|
|
154
155
|
)
|
|
155
156
|
return (
|
|
156
|
-
'<div style="background:#f8fafc;border:1px solid
|
|
157
|
+
f'<div style="background:#f8fafc;border:1px solid {_theme.BORDER};'
|
|
157
158
|
"border-left:4px solid #3b82f6;"
|
|
158
159
|
'padding:12px 16px;border-radius:6px;margin:4px 0 8px">'
|
|
159
160
|
'<div style="font-weight:600;font-size:14px;color:#1e293b">'
|
|
@@ -188,7 +189,7 @@ def build_status_panel(
|
|
|
188
189
|
],
|
|
189
190
|
)
|
|
190
191
|
settings_html = widgets.HTML(
|
|
191
|
-
'<div style="background:#f8fafc;border:1px solid
|
|
192
|
+
f'<div style="background:#f8fafc;border:1px solid {_theme.BORDER};'
|
|
192
193
|
"border-left:4px solid #94a3b8;padding:12px 16px;border-radius:6px;"
|
|
193
194
|
'margin:8px 0 4px">'
|
|
194
195
|
'<div style="font-weight:600;font-size:14px;color:#1e293b">Settings</div>'
|
|
@@ -645,8 +646,20 @@ def build_shared_widgets(
|
|
|
645
646
|
# overflow hidden (not auto): the 3D viewer is a fixed-size canvas, so it
|
|
646
647
|
# needs no scrollbar — clipping a few px of margin avoids an internal
|
|
647
648
|
# scrollbar that resets to the top on every backend/palette swap.
|
|
648
|
-
|
|
649
|
-
|
|
649
|
+
# min_height, not height: the fragment is the info box (~110px) PLUS the
|
|
650
|
+
# 500px canvas plus its border, so the old fixed 510px — sized for the
|
|
651
|
+
# canvas alone — clipped the bottom border off. A minimum still reserves
|
|
652
|
+
# space so the page doesn't jump when a molecule first renders, but lets
|
|
653
|
+
# the box grow if the info box wraps to more lines on a narrow window.
|
|
654
|
+
app.viz_output = widgets.Output(
|
|
655
|
+
layout=layout_fn(min_height="620px", overflow="hidden")
|
|
656
|
+
)
|
|
657
|
+
# No .quantui-viewer-frame here: this output renders via
|
|
658
|
+
# render_molecule_html, whose fragment now carries its own border sized
|
|
659
|
+
# to the viewer's exact pixel width. The class's border would sit on the
|
|
660
|
+
# Output widget, which CANNOT shrink-wrap (JupyterLab's Lumino layout
|
|
661
|
+
# pins its children to the full window width — measured 2026-08-03), so
|
|
662
|
+
# it would draw a second, full-width box around the tight one.
|
|
650
663
|
# Live calc log — a QuantUI-owned scroll container, not a widgets.Output
|
|
651
664
|
# (M-LOGSCROLL route C). An Output rebuilds its DOM subtree and resets
|
|
652
665
|
# scrollTop on every appended line, which made it impossible to scroll up
|
|
@@ -663,7 +676,9 @@ def build_shared_widgets(
|
|
|
663
676
|
app.run_output.add_class("quantui-run-output")
|
|
664
677
|
app.result_output = widgets.Output()
|
|
665
678
|
app.result_viz_output = widgets.Output()
|
|
666
|
-
|
|
679
|
+
# Same as viz_output above — bordered by the rendered fragment itself.
|
|
680
|
+
# overflow is set on the layout since the class no longer supplies it.
|
|
681
|
+
app.result_viz_output.layout.overflow = "hidden"
|
|
667
682
|
app.comparison_output = widgets.Output()
|
|
668
683
|
app._last_result_dir = None
|
|
669
684
|
|
|
@@ -808,10 +823,12 @@ def build_shared_widgets(
|
|
|
808
823
|
app.preopt_preview_output = widgets.Output(
|
|
809
824
|
layout=layout_fn(
|
|
810
825
|
# 290px viewer + stepper controls (slider / play / compare) below.
|
|
811
|
-
height
|
|
826
|
+
# min_height, not height: build_preopt_preview_html now returns a
|
|
827
|
+
# framed fragment, and a fixed height would clip its bottom border
|
|
828
|
+
# off under overflow:hidden.
|
|
829
|
+
min_height="380px",
|
|
812
830
|
width="100%",
|
|
813
831
|
max_width="480px",
|
|
814
|
-
border="1px solid #e2e8f0",
|
|
815
832
|
overflow="hidden",
|
|
816
833
|
)
|
|
817
834
|
)
|
|
@@ -1279,7 +1296,7 @@ def build_welcome_header(app: Any, *, layout_fn: Any = None) -> None:
|
|
|
1279
1296
|
justify_content="flex-start",
|
|
1280
1297
|
padding="22px 4px 18px",
|
|
1281
1298
|
margin="0 0 4px",
|
|
1282
|
-
border_bottom="1px solid
|
|
1299
|
+
border_bottom=f"1px solid {_theme.BORDER}",
|
|
1283
1300
|
),
|
|
1284
1301
|
)
|
|
1285
1302
|
|
|
@@ -1629,7 +1646,9 @@ def build_results_section(app: Any, *, layout_fn: Any) -> None:
|
|
|
1629
1646
|
# switch. Matches the trajectory frame_out fix pattern. 460+20=480
|
|
1630
1647
|
# accommodates the py3Dmol view (460px) plus a small horizontal pad;
|
|
1631
1648
|
# 420+20=440 likewise for the 420px view height.
|
|
1632
|
-
|
|
1649
|
+
# min_height: the vib renderers return a framed fragment, and a fixed
|
|
1650
|
+
# height clips its bottom border off under the tab's overflow rules.
|
|
1651
|
+
app.vib_output = widgets.Output(layout=layout_fn(min_height="450px", width="480px"))
|
|
1633
1652
|
|
|
1634
1653
|
# Vibration animation export: writes the current mode as a self-contained
|
|
1635
1654
|
# HTML file. Backend selection is independent of the user's default — see
|
|
@@ -2006,8 +2025,11 @@ def build_results_section(app: Any, *, layout_fn: Any) -> None:
|
|
|
2006
2025
|
)
|
|
2007
2026
|
app.results_panel = app.results_tab_panel
|
|
2008
2027
|
|
|
2028
|
+
# No .quantui-viewer-frame: this renders via render_molecule_html (see
|
|
2029
|
+
# app_visualization._show_result_3d), so the fragment carries its own
|
|
2030
|
+
# border fitted to the viewer. The class would add a second, full-width
|
|
2031
|
+
# box around it — the Output widget cannot shrink-wrap.
|
|
2009
2032
|
app._analysis_mol_output = widgets.Output()
|
|
2010
|
-
app._analysis_mol_output.add_class("quantui-viewer-frame")
|
|
2011
2033
|
|
|
2012
2034
|
# Analysis-tab backend toggle — mirrors the Calculate-tab `viz_backend_toggle`.
|
|
2013
2035
|
# Created only when both backends are available (matches Calculate-tab
|
|
@@ -2239,7 +2261,7 @@ def build_output_tab(app: Any, *, layout_fn: Any) -> None:
|
|
|
2239
2261
|
app._log_output_html,
|
|
2240
2262
|
app._result_log_accordion,
|
|
2241
2263
|
widgets.HTML(
|
|
2242
|
-
'<hr style="border:none;border-top:1px solid
|
|
2264
|
+
f'<hr style="border:none;border-top:1px solid {_theme.BORDER};margin:16px 0 10px"/>'
|
|
2243
2265
|
'<p style="color:#94a3b8;font-size:12px;margin:0 0 6px">'
|
|
2244
2266
|
"Session event log — records molecule loads, calculations, "
|
|
2245
2267
|
"and issue reports across this session.</p>"
|
|
@@ -2315,7 +2337,7 @@ def build_files_tab(app: Any, *, layout_fn: Any) -> None:
|
|
|
2315
2337
|
)
|
|
2316
2338
|
app._files_preview_output = widgets.Output(
|
|
2317
2339
|
layout=layout_fn(
|
|
2318
|
-
border="1px solid
|
|
2340
|
+
border=f"1px solid {_theme.BORDER}",
|
|
2319
2341
|
min_height="220px",
|
|
2320
2342
|
max_height="420px",
|
|
2321
2343
|
overflow="auto",
|
|
@@ -2403,7 +2425,7 @@ def build_help_section(app: Any, *, layout_fn: Any) -> None:
|
|
|
2403
2425
|
layout=layout_fn(
|
|
2404
2426
|
display="none",
|
|
2405
2427
|
padding="8px 0",
|
|
2406
|
-
border="1px solid
|
|
2428
|
+
border=f"1px solid {_theme.BORDER}",
|
|
2407
2429
|
border_radius="6px",
|
|
2408
2430
|
padding_left="12px",
|
|
2409
2431
|
margin="0 0 8px",
|
|
@@ -5,6 +5,8 @@ from __future__ import annotations
|
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
from typing import Any, Optional
|
|
7
7
|
|
|
8
|
+
from quantui import theme as _theme
|
|
9
|
+
|
|
8
10
|
|
|
9
11
|
def _result_extra_rows(get: Any) -> str:
|
|
10
12
|
"""Build the shared 'extra' result-card rows from an accessor.
|
|
@@ -478,7 +480,7 @@ def format_past_result(data: dict[str, Any], result_dir: Optional[Path] = None)
|
|
|
478
480
|
_thumb_html = (
|
|
479
481
|
f'<img src="data:image/png;base64,{_img_b64}" '
|
|
480
482
|
f'style="float:right;margin:0 0 6px 14px;border-radius:4px;'
|
|
481
|
-
f'border:1px solid
|
|
483
|
+
f'border:1px solid {_theme.BORDER}" width="173" height="108" />'
|
|
482
484
|
)
|
|
483
485
|
|
|
484
486
|
return (
|
|
@@ -11,6 +11,8 @@ from typing import Any, List
|
|
|
11
11
|
import ipywidgets as widgets
|
|
12
12
|
from IPython.display import HTML, display
|
|
13
13
|
|
|
14
|
+
from quantui import theme as _theme
|
|
15
|
+
|
|
14
16
|
|
|
15
17
|
@contextmanager
|
|
16
18
|
def _viz_render_event(app: Any, task: Any, backend: Any, **extras: Any):
|
|
@@ -306,9 +308,11 @@ def show_opt_trajectory(
|
|
|
306
308
|
return
|
|
307
309
|
|
|
308
310
|
# --- Single-viewer trajectory stepper (all frames preloaded) ---
|
|
311
|
+
# min_height, not height: build_trajectory_viewer_html returns a framed
|
|
312
|
+
# fragment, and a fixed height would clip its bottom border off.
|
|
309
313
|
viewer_output = widgets.Output(
|
|
310
314
|
layout=layout_fn(
|
|
311
|
-
|
|
315
|
+
min_height="420px", width="100%", max_width="500px", overflow="hidden"
|
|
312
316
|
)
|
|
313
317
|
)
|
|
314
318
|
try:
|
|
@@ -1689,7 +1693,8 @@ def _render_vib_mode_py3dmol(
|
|
|
1689
1693
|
from quantui.viz_assets import make_view
|
|
1690
1694
|
|
|
1691
1695
|
interval_ms = max(1, int(round(1000.0 / fps)))
|
|
1692
|
-
|
|
1696
|
+
vib_width = 460
|
|
1697
|
+
view = make_view(width=vib_width, height=420)
|
|
1693
1698
|
view.addModelsAsFrames(xyz_string, "xyz")
|
|
1694
1699
|
view.setStyle({"stick": {}, "sphere": {"scale": 0.3}})
|
|
1695
1700
|
bg = "white" if app.theme_btn.value == "Light" else "#1e1e1e"
|
|
@@ -1700,7 +1705,9 @@ def _render_vib_mode_py3dmol(
|
|
|
1700
1705
|
# pan/rotate state survives mode switches. The hook is idempotent
|
|
1701
1706
|
# (guarded by ``_quantuiVibCameraHookInstalled``) and ships inside
|
|
1702
1707
|
# the cached HTML too — so disk-cache hits also persist the camera.
|
|
1703
|
-
html_str =
|
|
1708
|
+
html_str = _theme.frame_viewer_html(
|
|
1709
|
+
_VIB_CAMERA_PERSISTENCE_JS + view._make_html(), width=vib_width
|
|
1710
|
+
)
|
|
1704
1711
|
except Exception as exc:
|
|
1705
1712
|
if not _is_vib_stale(app, render_token):
|
|
1706
1713
|
_vib_err(app, f"Vibrational animation render failed: {exc}")
|
|
@@ -1798,7 +1805,11 @@ def _render_vib_mode_plotlymol(
|
|
|
1798
1805
|
mode="ball+stick",
|
|
1799
1806
|
resolution=12,
|
|
1800
1807
|
)
|
|
1801
|
-
|
|
1808
|
+
# Explicit width, not just height: the frame is sized in pixels, and a
|
|
1809
|
+
# responsive-width figure would otherwise render this mode at a
|
|
1810
|
+
# different size than the py3Dmol path. 460 matches the viewer in
|
|
1811
|
+
# _render_vib_mode_py3dmol so the two backends look the same.
|
|
1812
|
+
anim_fig.update_layout(width=460, height=420)
|
|
1802
1813
|
except Exception as exc:
|
|
1803
1814
|
if not _is_vib_stale(app, render_token):
|
|
1804
1815
|
_vib_err(app, f"Animation generation failed: {exc}")
|
|
@@ -1814,7 +1825,7 @@ def _render_vib_mode_plotlymol(
|
|
|
1814
1825
|
)
|
|
1815
1826
|
if _is_vib_stale(app, render_token):
|
|
1816
1827
|
return
|
|
1817
|
-
_swap_vib_output(app, anim_html)
|
|
1828
|
+
_swap_vib_output(app, _theme.frame_viewer_html(anim_html, width=460))
|
|
1818
1829
|
|
|
1819
1830
|
|
|
1820
1831
|
def render_vib_mode(
|
|
@@ -1919,7 +1930,7 @@ def on_vib_mode_changed(app: Any, change: dict[str, Any]) -> None:
|
|
|
1919
1930
|
|
|
1920
1931
|
|
|
1921
1932
|
_STEPPER_BTN_STYLE = (
|
|
1922
|
-
"padding:2px 9px;border:1px solid
|
|
1933
|
+
f"padding:2px 9px;border:1px solid {_theme.BORDER};border-radius:4px;"
|
|
1923
1934
|
"background:#f8fafc;color:#334155;cursor:pointer;font-size:13px;line-height:1.4;"
|
|
1924
1935
|
)
|
|
1925
1936
|
|
|
@@ -2099,7 +2110,8 @@ def build_preopt_preview_html(
|
|
|
2099
2110
|
lines.append(f"{sym} {xyz[0]:.6f} {xyz[1]:.6f} {xyz[2]:.6f}")
|
|
2100
2111
|
xyz_string = "\n".join(lines) + "\n"
|
|
2101
2112
|
|
|
2102
|
-
|
|
2113
|
+
width = 460
|
|
2114
|
+
view = make_view(width=width, height=290)
|
|
2103
2115
|
view.addModelsAsFrames(xyz_string, "xyz")
|
|
2104
2116
|
view.setStyle({"stick": {}, "sphere": {"scale": 0.3}})
|
|
2105
2117
|
view.setBackgroundColor(bgcolor)
|
|
@@ -2109,7 +2121,7 @@ def build_preopt_preview_html(
|
|
|
2109
2121
|
n_frames = len(frames)
|
|
2110
2122
|
# Single frame (FF no-op / RDKit absent): nothing to step through.
|
|
2111
2123
|
if n_frames <= 1:
|
|
2112
|
-
return view_html
|
|
2124
|
+
return _theme.frame_viewer_html(view_html, width=width)
|
|
2113
2125
|
|
|
2114
2126
|
m = re.search(r"3dmolviewer_(\w+)", view_html)
|
|
2115
2127
|
if m is None:
|
|
@@ -2117,11 +2129,11 @@ def build_preopt_preview_html(
|
|
|
2117
2129
|
# plain auto-loop animation so the relaxation is still visible.
|
|
2118
2130
|
interval_ms = max(1, int(round(1000.0 / max(1, fps))))
|
|
2119
2131
|
view.animate({"loop": "forward", "interval": interval_ms, "reps": 0})
|
|
2120
|
-
return view._make_html()
|
|
2132
|
+
return _theme.frame_viewer_html(view._make_html(), width=width)
|
|
2121
2133
|
|
|
2122
2134
|
interval_ms = max(1, int(round(1000.0 / max(1, fps))))
|
|
2123
2135
|
controls = _preopt_controls_html(m.group(1), n_frames, interval_ms)
|
|
2124
|
-
return
|
|
2136
|
+
return _theme.frame_viewer_html(view_html, width=width, controls=controls)
|
|
2125
2137
|
|
|
2126
2138
|
|
|
2127
2139
|
def build_trajectory_viewer_html(
|
|
@@ -2162,11 +2174,12 @@ def build_trajectory_viewer_html(
|
|
|
2162
2174
|
view_html = view._make_html()
|
|
2163
2175
|
|
|
2164
2176
|
if n <= 1:
|
|
2165
|
-
return view_html
|
|
2177
|
+
return _theme.frame_viewer_html(view_html, width=width)
|
|
2166
2178
|
|
|
2167
2179
|
m = re.search(r"3dmolviewer_(\w+)", view_html)
|
|
2168
2180
|
if m is None:
|
|
2169
|
-
|
|
2181
|
+
# can't wire controls without the viewer id
|
|
2182
|
+
return _theme.frame_viewer_html(view_html, width=width)
|
|
2170
2183
|
|
|
2171
2184
|
interval_ms = max(1, int(round(1000.0 / max(1, fps))))
|
|
2172
2185
|
eabs = json.dumps([float(e) for e in energies]) if energies else "null"
|
|
@@ -2192,7 +2205,7 @@ def build_trajectory_viewer_html(
|
|
|
2192
2205
|
scrub_title="Scrub the optimization steps",
|
|
2193
2206
|
extra_decls=f"var EABS={eabs}; var EREL={erel};",
|
|
2194
2207
|
)
|
|
2195
|
-
return
|
|
2208
|
+
return _theme.frame_viewer_html(view_html, width=width, controls=controls)
|
|
2196
2209
|
|
|
2197
2210
|
|
|
2198
2211
|
# Single-viewer vibrational animation. ONE py3Dmol viewer holds every mode; the
|
|
@@ -2324,9 +2337,7 @@ def build_vib_viewer_html(
|
|
|
2324
2337
|
.replace("__BG__", json.dumps(bgcolor))
|
|
2325
2338
|
.replace("__INIT__", str(int(initial_mode)))
|
|
2326
2339
|
)
|
|
2327
|
-
return (
|
|
2328
|
-
f'<div style="max-width:{width + 20}px">{view_html}<script>{js}</script></div>'
|
|
2329
|
-
)
|
|
2340
|
+
return _theme.frame_viewer_html(f"{view_html}<script>{js}</script>", width=width)
|
|
2330
2341
|
|
|
2331
2342
|
|
|
2332
2343
|
def _vib_single_viewer_supported(app: Any, freq_result: Any) -> bool:
|
|
@@ -73,6 +73,8 @@ from typing import Any, Optional
|
|
|
73
73
|
import ipywidgets as widgets
|
|
74
74
|
from IPython.display import Javascript, display
|
|
75
75
|
|
|
76
|
+
from quantui import theme as _theme
|
|
77
|
+
|
|
76
78
|
_LOG = logging.getLogger(__name__)
|
|
77
79
|
|
|
78
80
|
# Coalescing window for appends. PySCF emits many lines per second; batching
|
|
@@ -88,7 +90,7 @@ _MAIL_CLASS = "quantui-live-mail"
|
|
|
88
90
|
# ASCII header exactly as .jp-OutputArea-output did (see GOTCHAS).
|
|
89
91
|
_CONTAINER_STYLE = (
|
|
90
92
|
"height:300px;overflow-y:auto;overflow-anchor:auto;"
|
|
91
|
-
"border:1px solid
|
|
93
|
+
f"border:1px solid {_theme.BORDER};border-radius:2px;padding:8px;"
|
|
92
94
|
"font-family:ui-monospace,SFMono-Regular,'SF Mono',Menlo,Consolas,"
|
|
93
95
|
"'Liberation Mono','Courier New',monospace;"
|
|
94
96
|
"font-size:12.5px;line-height:1.35;white-space:pre-wrap;"
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""Theme colour tokens (M-THEME).
|
|
2
|
+
|
|
3
|
+
Why this module exists
|
|
4
|
+
----------------------
|
|
5
|
+
QuantUI's dark mode is **not a palette swap** — it is a whole-page CSS filter::
|
|
6
|
+
|
|
7
|
+
html { filter: invert(1) hue-rotate(180deg) !important; }
|
|
8
|
+
canvas, img, iframe, video { filter: invert(1) hue-rotate(180deg) !important; }
|
|
9
|
+
|
|
10
|
+
(see ``app.QuantUIApp._theme_css``). Every colour in the UI is written once, for
|
|
11
|
+
light mode, and dark mode is that same colour mathematically inverted. The
|
|
12
|
+
counter-filter on ``canvas/img/iframe/video`` double-inverts embedded renderers
|
|
13
|
+
(3-D viewers, plots) back to their intended appearance.
|
|
14
|
+
|
|
15
|
+
**The consequence that matters, and the reason this module exists:** you cannot
|
|
16
|
+
tune light and dark independently. There is exactly one source value per colour,
|
|
17
|
+
and dark mode is whatever that value inverts to.
|
|
18
|
+
|
|
19
|
+
The mid-tone rule
|
|
20
|
+
-----------------
|
|
21
|
+
That constraint has a non-obvious implication for anything whose job is
|
|
22
|
+
*separation* rather than *legibility*:
|
|
23
|
+
|
|
24
|
+
- A **light** grey border (``#e2e8f0``) looks correct on a white page, but
|
|
25
|
+
inverts to a near-black border (``#121820``) on a near-black panel — invisible.
|
|
26
|
+
- A **dark** border has the mirror-image problem: fine in dark mode, invisible
|
|
27
|
+
in light.
|
|
28
|
+
- A **mid-tone** border stays mid-tone under inversion, so it is visible in
|
|
29
|
+
*both*. Mid-tones are the only values that survive.
|
|
30
|
+
|
|
31
|
+
Measured, not assumed (2026-07-31)
|
|
32
|
+
----------------------------------
|
|
33
|
+
Contrast ratios computed through the actual filter chain (invert, then the W3C
|
|
34
|
+
``hue-rotate`` matrix), against the panel background ``#f8fafc``:
|
|
35
|
+
|
|
36
|
+
=========== ============== ============= ==================================
|
|
37
|
+
candidate light vs panel dark vs panel verdict
|
|
38
|
+
=========== ============== ============= ==================================
|
|
39
|
+
``#e2e8f0`` 1.18:1 1.14:1 the old value — fails both
|
|
40
|
+
``#c0ccd8`` 1.56:1 1.65:1 fails both
|
|
41
|
+
``#94a3b8`` 2.45:1 3.12:1 passes dark only
|
|
42
|
+
``#7d8ea3`` 3.20:1 4.30:1 **passes both** — chosen
|
|
43
|
+
``#64748b`` 4.55:1 6.17:1 passes both, but heavy in light
|
|
44
|
+
=========== ============== ============= ==================================
|
|
45
|
+
|
|
46
|
+
``BORDER`` is the *lightest* value clearing WCAG 1.4.11's **3:1** bar for
|
|
47
|
+
non-text UI components in both modes. ``#64748b`` clears it more comfortably but
|
|
48
|
+
reads as a heavy rule in light mode, which is a real cost for a subtle panel
|
|
49
|
+
divider.
|
|
50
|
+
|
|
51
|
+
What was NOT the problem
|
|
52
|
+
------------------------
|
|
53
|
+
Text contrast was measured too, and it was never broken — body text on a panel
|
|
54
|
+
is 7.24:1 in light and *improves* to 8.91:1 inverted. The user-reported "fix
|
|
55
|
+
dark mode contrasts" was, on measurement, entirely about **structural
|
|
56
|
+
separation**: panels and their borders were invisible against the page, which is
|
|
57
|
+
why the same request also asked to "add borders". Text colours are deliberately
|
|
58
|
+
left alone here.
|
|
59
|
+
|
|
60
|
+
Scope of this module today
|
|
61
|
+
--------------------------
|
|
62
|
+
Only the tokens needed for the THEME.5 fix. It is intentionally not a
|
|
63
|
+
full-palette migration: the codebase has ~390 hardcoded hex literals across 17
|
|
64
|
+
files, most of them *semantic* accents (error red, success green, link blue)
|
|
65
|
+
whose hue survives ``hue-rotate(180)`` and which look correct in both modes
|
|
66
|
+
already. Migrating those wholesale would be a large, visually-unverifiable
|
|
67
|
+
change for no user-visible gain.
|
|
68
|
+
|
|
69
|
+
When THEME.6 (customisable palettes) lands, the invert filter has to go — a
|
|
70
|
+
palette system cannot work when dark mode is a derived inversion rather than an
|
|
71
|
+
independent set of values. At that point these tokens become the seam: they
|
|
72
|
+
grow light/dark variants and the ``_theme_css`` filter is replaced. Keeping them
|
|
73
|
+
named here means that change edits this file plus the CSS, not 19 call sites
|
|
74
|
+
again.
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
from __future__ import annotations
|
|
78
|
+
|
|
79
|
+
# ── Structural / chrome ──────────────────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
#: Panel, card, and viewer borders. Mid-tone so it survives inversion — see the
|
|
82
|
+
#: module docstring's table. Replaced ``#e2e8f0`` / ``#e5e7eb`` / ``#cbd5e1`` /
|
|
83
|
+
#: ``#c0ccd8``, all of which were invisible in dark mode (1.14-1.65:1).
|
|
84
|
+
BORDER = "#7d8ea3"
|
|
85
|
+
|
|
86
|
+
#: Emphasised border for elements that should read as framed even at a glance
|
|
87
|
+
#: (the 3-D viewer frame, which sits on its own rather than in a card stack).
|
|
88
|
+
BORDER_STRONG = "#64748b"
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def frame_viewer_html(view_html: str, *, width: int, controls: str = "") -> str:
|
|
92
|
+
"""Wrap a 3-D viewer fragment in the standard frame, sized to the viewer.
|
|
93
|
+
|
|
94
|
+
Every 3-D viewer in the app — static molecule, optimization trajectory,
|
|
95
|
+
vibrational mode, classical pre-opt preview — goes through here so they all
|
|
96
|
+
frame identically and a token change lands everywhere at once.
|
|
97
|
+
|
|
98
|
+
``width`` must be the viewer's own pixel width. The frame is built at that
|
|
99
|
+
width **here**, in the code that knows it, rather than as a CSS class on the
|
|
100
|
+
hosting ``widgets.Output``. Measured in the browser 2026-08-03: an Output's
|
|
101
|
+
children are Lumino widgets that JupyterLab sizes with explicit pixel widths
|
|
102
|
+
tracking the window, so a class-borne ``fit-content`` always resolves to the
|
|
103
|
+
full page width. The border then enclosed a wide strip of dead space beside
|
|
104
|
+
the plot, which is actively misleading — it implies the whole strip is
|
|
105
|
+
interactive and hides where the page can be scrolled without dragging the
|
|
106
|
+
3-D view.
|
|
107
|
+
|
|
108
|
+
``controls`` is optional stepper/player markup rendered below the viewer.
|
|
109
|
+
It is inset so the buttons don't sit flush against the frame; the viewer
|
|
110
|
+
itself stays flush, since its canvas is its own edge.
|
|
111
|
+
|
|
112
|
+
Note for callers: the hosting Output must not pin a fixed ``height``, or the
|
|
113
|
+
frame's bottom edge is clipped off by ``overflow: hidden``. Use
|
|
114
|
+
``min_height`` — see ``app_builders.build_shared_widgets``.
|
|
115
|
+
"""
|
|
116
|
+
if controls:
|
|
117
|
+
controls = f'<div style="padding:0 8px 6px">{controls}</div>'
|
|
118
|
+
return (
|
|
119
|
+
f'<div style="width:{width}px;max-width:100%;'
|
|
120
|
+
f"border:1px solid {BORDER_STRONG};border-radius:6px;"
|
|
121
|
+
f'overflow:hidden">{view_html}{controls}</div>'
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
__all__ = ["BORDER", "BORDER_STRONG", "frame_viewer_html"]
|
|
@@ -14,6 +14,8 @@ import os
|
|
|
14
14
|
import tempfile
|
|
15
15
|
from typing import Literal, cast
|
|
16
16
|
|
|
17
|
+
from quantui import theme as _theme
|
|
18
|
+
|
|
17
19
|
logger = logging.getLogger(__name__)
|
|
18
20
|
|
|
19
21
|
Py3DmolStyle = Literal["ball+stick", "stick", "sphere", "line", "cartoon"]
|
|
@@ -469,7 +471,10 @@ def render_molecule_html(
|
|
|
469
471
|
'<div style="color:#b91c1c;padding:8px;">'
|
|
470
472
|
f"❌ Visualization failed: {e}</div>"
|
|
471
473
|
)
|
|
472
|
-
|
|
474
|
+
# Frame the fragment at the viewer's own width — see theme.frame_viewer_html
|
|
475
|
+
# for why the border cannot live on the hosting Output widget's CSS class.
|
|
476
|
+
# The info box is INSIDE the frame so it aligns with the canvas.
|
|
477
|
+
return _theme.frame_viewer_html("\n".join(parts), width=width)
|
|
473
478
|
|
|
474
479
|
|
|
475
480
|
def display_molecule(
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: quantui
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.2
|
|
4
4
|
Summary: An open-source frontend for DFT and post-HF quantum chemistry with PySCF
|
|
5
5
|
Author-email: Jonathan Schultz <nccu-schultz-lab@users.noreply.github.com>
|
|
6
6
|
License: MIT License
|
|
@@ -91,7 +91,7 @@ Dynamic: license-file
|
|
|
91
91
|
|
|
92
92
|
# QuantUI
|
|
93
93
|
|
|
94
|
-
[](https://pypi.org/project/quantui/)
|
|
94
|
+
[](https://pypi.org/project/quantui/)
|
|
95
95
|
[](https://github.com/The-Schultz-Lab/QuantUI/actions/workflows/ci.yml)
|
|
96
96
|
[](https://the-schultz-lab.github.io/QuantUI/)
|
|
97
97
|
[](https://github.com/The-Schultz-Lab/QuantUI/blob/main/LICENSE)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|