Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 27 additions & 1 deletion docs/stylesheets/landing.css
Original file line number Diff line number Diff line change
Expand Up @@ -164,9 +164,35 @@

/* ── Hero ─────────────────────────────────────────────────────────── */
.quantui-landing .hero {
background: var(--quantui-hero-bg);
position: relative;
background: transparent;
padding: 4rem 0 4.5rem;
overflow: hidden;
isolation: isolate;
}

/* Gradient sits behind content and fades to transparent at the edges so the
full-bleed hero does not show a hard rectangular cutoff against the page. */
.quantui-landing .hero::before {
content: "";
position: absolute;
inset: 0;
z-index: -1;
background: var(--quantui-hero-bg);
pointer-events: none;
-webkit-mask-image:
linear-gradient(to right, transparent 0%, #000 10%, #000 90%, transparent 100%),
linear-gradient(to bottom, transparent 0%, #000 8%, #000 92%, transparent 100%);
-webkit-mask-composite: source-in;
mask-image:
linear-gradient(to right, transparent 0%, #000 10%, #000 90%, transparent 100%),
linear-gradient(to bottom, transparent 0%, #000 8%, #000 92%, transparent 100%);
mask-composite: intersect;
}

.quantui-landing .hero > .container {
position: relative;
z-index: 1;
}

.quantui-landing .hero__inner {
Expand Down
39 changes: 6 additions & 33 deletions quantui/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -739,7 +739,8 @@ def _load_last_calibration_label() -> str:

# ── Module-level constants ────────────────────────────────────────────────────

_APP_CSS: str = """<style>
_APP_CSS: str = (
"""<style>
/* System font stack ---------------------------------------------------- */
body, p, span, li, td, th, label, input, select, textarea, blockquote,
.jp-OutputArea-output, .widget-html-content, .widget-label-basic,
Expand Down Expand Up @@ -864,38 +865,9 @@ class carries a per-app uid suffix. LiveLog also sets the stack inline — this
.quantui-result-card table {
color: var(--q-text-body) !important;
}
.jupyter-widgets input[type=range],
.widget-slider input[type=range] {
height: 6px !important;
border-radius: 3px !important;
background: var(--q-border-strong) !important;
accent-color: var(--q-accent-info) !important;
}
.jupyter-widgets input[type=range]::-webkit-slider-thumb,
.widget-slider input[type=range]::-webkit-slider-thumb {
-webkit-appearance: none !important;
width: 16px !important;
height: 16px !important;
border-radius: 50% !important;
background: var(--q-text-strong) !important;
border: 2px solid var(--q-accent-info) !important;
cursor: pointer !important;
}
.jupyter-widgets input[type=range]::-moz-range-track,
.widget-slider input[type=range]::-moz-range-track {
background: var(--q-border-strong) !important;
height: 6px !important;
border-radius: 3px !important;
}
.jupyter-widgets input[type=range]::-moz-range-thumb,
.widget-slider input[type=range]::-moz-range-thumb {
width: 16px !important;
height: 16px !important;
border-radius: 50% !important;
background: var(--q-text-strong) !important;
border: 2px solid var(--q-accent-info) !important;
cursor: pointer !important;
}
"""
+ _theme.slider_chrome_css()
+ """
.quantui-completion-banner {
padding: 10px 12px;
border: 1px solid var(--q-accent-success);
Expand Down Expand Up @@ -923,6 +895,7 @@ class carries a per-app uid suffix. LiveLog also sets the stack inline — this
vertical-align: middle;
}
</style>"""
)

_LAYOUT_TRAITS: frozenset[str] = frozenset(widgets.Layout.class_trait_names())

Expand Down
5 changes: 4 additions & 1 deletion quantui/app_visualization.py
Original file line number Diff line number Diff line change
Expand Up @@ -539,6 +539,9 @@ def render_traj_frame(app: Any, molecule: Any, output_widget: Any) -> None:
view = make_view(width=460, height=340)
view.addModel(xyz, "xyz")
view.setStyle({"stick": {}, "sphere": {"scale": 0.3}})
from quantui.visualization_py3dmol import decorate_py3dmol_coordination_bonds

decorate_py3dmol_coordination_bonds(view, molecule.atoms, molecule.coordinates)
view.setBackgroundColor("white")
view.zoomTo()
output_widget.clear_output()
Expand Down Expand Up @@ -2710,7 +2713,7 @@ def _frame_stepper_controls(
f'style="{btn}">&#9654; Play</button>'
f'<button id="st_next_{uid}" type="button" title="Next frame" '
f'style="{btn}">&#9654;</button>'
f'<input id="st_slider_{uid}" type="range" min="0" max="{n - 1}" '
f'<input id="st_slider_{uid}" class="quantui-range" type="range" min="0" max="{n - 1}" '
f'value="{start}" step="1" title="{scrub_title}" '
'style="flex:1;min-width:110px;vertical-align:middle;">'
f"{ab_html}"
Expand Down
79 changes: 57 additions & 22 deletions quantui/theme.py
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,62 @@ def _neutral_button_css() -> str:
)


def slider_chrome_css() -> str:
"""High-contrast range-slider styling for ipywidgets and in-viewer scrubbers.

WebKit requires ``-webkit-slider-runnable-track`` / ``-webkit-slider-thumb``;
styling ``input[type=range]`` alone is often ignored. Thumb uses the accent
colour with a page-bg ring so it stays visible on every palette.
"""
selectors = (
".jupyter-widgets input[type=range], "
".widget-slider input[type=range], "
".widget-hslider input[type=range], "
"input.quantui-range[type=range]"
)
thumb = (
f"{selectors}::-webkit-slider-thumb, "
".jupyter-widgets input[type=range]::-moz-range-thumb, "
".widget-slider input[type=range]::-moz-range-thumb, "
".widget-hslider input[type=range]::-moz-range-thumb, "
"input.quantui-range[type=range]::-moz-range-thumb"
)
track = (
f"{selectors}::-webkit-slider-runnable-track, "
".jupyter-widgets input[type=range]::-moz-range-track, "
".widget-slider input[type=range]::-moz-range-track, "
".widget-hslider input[type=range]::-moz-range-track, "
"input.quantui-range[type=range]::-moz-range-track"
)
return (
f"{selectors} "
"{ -webkit-appearance: none !important; appearance: none !important; "
"width: 100% !important; height: 20px !important; "
"background: transparent !important; "
"accent-color: var(--q-accent-info) !important; "
"cursor: pointer !important; }\n"
f"{track} "
"{ height: 10px !important; border-radius: 5px !important; "
"background: var(--q-border-strong) !important; "
"border: 1px solid var(--q-border) !important; "
"box-shadow: inset 0 0 0 1px var(--q-page-bg) !important; }\n"
f"{thumb} "
"{ -webkit-appearance: none !important; appearance: none !important; "
"width: 20px !important; height: 20px !important; "
"border-radius: 50% !important; "
"background: var(--q-accent-info) !important; "
"border: 2px solid var(--q-page-bg) !important; "
"box-shadow: 0 0 0 1px var(--q-accent-info), "
"0 1px 4px rgba(0,0,0,0.45) !important; "
"cursor: pointer !important; margin-top: -6px !important; }\n"
".widget-hslider .widget-readout, "
".widget-slider .widget-readout, "
".widget-float-slider .widget-readout "
"{ color: var(--q-text-strong) !important; "
"font-weight: 600 !important; min-width: 2.5em !important; }\n"
)


def _widget_chrome_css() -> str:
"""CSS rules for ipywidgets/Lumino chrome (palette-agnostic — uses vars)."""
surface = "var(--q-bg-panel)"
Expand Down Expand Up @@ -266,28 +322,7 @@ def _widget_chrome_css() -> str:
"border-color: var(--q-border) !important; }}\n"
f"{_neutral_button_css()}"
f"{_accent_button_css()}"
".jupyter-widgets input[type=range], "
".widget-slider input[type=range] "
"{ height: 6px !important; border-radius: 3px !important; "
"background: var(--q-border-strong) !important; "
"accent-color: var(--q-accent-info) !important; }\n"
".jupyter-widgets input[type=range]::-webkit-slider-thumb, "
".widget-slider input[type=range]::-webkit-slider-thumb "
"{ -webkit-appearance: none !important; width: 16px !important; "
"height: 16px !important; border-radius: 50% !important; "
"background: var(--q-text-strong) !important; "
"border: 2px solid var(--q-accent-info) !important; "
"cursor: pointer !important; }\n"
".jupyter-widgets input[type=range]::-moz-range-track, "
".widget-slider input[type=range]::-moz-range-track "
"{ background: var(--q-border-strong) !important; "
"height: 6px !important; border-radius: 3px !important; }\n"
".jupyter-widgets input[type=range]::-moz-range-thumb, "
".widget-slider input[type=range]::-moz-range-thumb "
"{ width: 16px !important; height: 16px !important; "
"border-radius: 50% !important; background: var(--q-text-strong) !important; "
"border: 2px solid var(--q-accent-info) !important; "
"cursor: pointer !important; }\n"
f"{slider_chrome_css()}"
".quantui-info-box "
f"{{ background: {surface} !important; "
"color: var(--q-text-body) !important; "
Expand Down
54 changes: 41 additions & 13 deletions quantui/visualization_py3dmol.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
import logging
import os
import tempfile
from typing import Literal, cast
from typing import Literal, Sequence, cast

from quantui import theme as _theme

Expand Down Expand Up @@ -200,11 +200,10 @@ def visualize_molecule_py3dmol(
else:
view.setStyle({style: {}})

# MET.6: 3Dmol.js's own bond perception leaves a coordination metal as a lone
# dot — it draws no bonds to the centre. Draw the metal↔donor bonds ourselves,
# dashed (GaussView convention), from the same distance-based connectivity the
# salt-warning uses. No-op for purely organic molecules.
_add_coordination_bonds(view, molecule)
# MET.6: 3Dmol.js now infers some metal-ligand contacts as covalent sticks,
# which render as thick tapered spokes (ferrocene). Hide those and draw
# dashed coordination bonds ourselves (GaussView convention).
decorate_py3dmol_coordination_bonds(view, molecule.atoms, molecule.coordinates)

# Set background
view.setBackgroundColor(bgcolor)
Expand All @@ -220,11 +219,41 @@ def visualize_molecule_py3dmol(

# Dashed coordination-bond styling (py3Dmol addCylinder): a thin gray dashed
# cylinder from the metal centre to each donor atom.
_COORD_BOND_RADIUS = 0.06
_COORD_BOND_COLOR = "#777777"
_COORD_BOND_RADIUS = 0.05
_COORD_BOND_COLOR = "#888888"


def _add_coordination_bonds(view, molecule) -> None:
def _suppress_metal_coordination_sticks(
view,
atoms: Sequence[str],
coords: Sequence[Sequence[float]],
) -> None:
"""Hide 3Dmol's default sticks on metal↔donor pairs before overlaying dashes."""
try:
from quantui.connectivity import metal_coordination_bonds

for i, j in metal_coordination_bonds(atoms, coords):
view.setStyle({"atom": i, "bond": j}, {"stick": {"hidden": True}})
view.setStyle({"atom": j, "bond": i}, {"stick": {"hidden": True}})
except Exception: # noqa: BLE001 — bond decoration must never break the viewer
logger.debug("coordination-stick suppression skipped", exc_info=True)


def decorate_py3dmol_coordination_bonds(
view,
atoms: Sequence[str],
coords: Sequence[Sequence[float]],
) -> None:
"""Hide metal-ligand sticks and draw dashed coordination bonds (MET.6)."""
_suppress_metal_coordination_sticks(view, atoms, coords)
_add_coordination_bonds(view, atoms, coords)


def _add_coordination_bonds(
view,
atoms: Sequence[str],
coords: Sequence[Sequence[float]],
) -> None:
"""Draw dashed metal↔donor cylinders so a metal centre isn't a lone dot.

Uses the distance-based, metal-aware connectivity finder. Best-effort: any
Expand All @@ -233,8 +262,7 @@ def _add_coordination_bonds(view, molecule) -> None:
try:
from quantui.connectivity import metal_coordination_bonds

coords = molecule.coordinates
bonds = metal_coordination_bonds(molecule.atoms, coords)
bonds = metal_coordination_bonds(atoms, coords)
for i, j in bonds:
xi, yi, zi = coords[i]
xj, yj, zj = coords[j]
Expand All @@ -245,8 +273,8 @@ def _add_coordination_bonds(view, molecule) -> None:
"radius": _COORD_BOND_RADIUS,
"color": _COORD_BOND_COLOR,
"dashed": True,
"fromCap": 1,
"toCap": 1,
"fromCap": 0,
"toCap": 0,
}
)
except Exception: # noqa: BLE001 — bond decoration must never break the viewer
Expand Down
10 changes: 10 additions & 0 deletions tests/test_metal_viewer_fallback.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,16 @@ def test_metal_html_has_dashed_cylinders(self):
assert "addCylinder" in html # coordination bonds drawn
assert "dashed" in html

def test_ferrocene_hides_default_metal_sticks(self):
pytest.importorskip("py3Dmol")
from quantui.visualization_py3dmol import render_molecule_html

html = render_molecule_html(
_mol("inorganic-ferrocene"), backend="py3dmol", width=300, height=250
)
assert '"hidden": true' in html
assert html.count("addCylinder") == 10

def test_organic_html_has_no_cylinders(self):
pytest.importorskip("py3Dmol")
from quantui.visualization_py3dmol import render_molecule_html
Expand Down
7 changes: 7 additions & 0 deletions tests/test_theme_contrast.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ def test_app_css_uses_css_variables(self):
assert "var(--q-accent-info)" in _APP_CSS
assert "var(--q-page-bg)" in _APP_CSS
assert ".widget-dropdown select" in _APP_CSS
assert "-webkit-slider-runnable-track" in _APP_CSS

def test_theme_css_block_styles_widget_tabs(self):
block = theme.theme_css_block("Dark")
Expand All @@ -145,6 +146,12 @@ def test_theme_css_block_styles_widget_tabs(self):
assert "var(--q-bg-panel)" in block
assert ":not(.mod-danger)" in block

def test_theme_css_block_styles_sliders_with_webkit_track(self):
block = theme.theme_css_block("Dark")
assert "-webkit-slider-runnable-track" in block
assert "quantui-range" in block
assert "var(--q-accent-info)" in block

def test_app_css_preserves_accent_buttons(self):
from quantui.app import _APP_CSS

Expand Down