diff --git a/docs/stylesheets/landing.css b/docs/stylesheets/landing.css index 548984e..877b4c9 100644 --- a/docs/stylesheets/landing.css +++ b/docs/stylesheets/landing.css @@ -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 { diff --git a/quantui/app.py b/quantui/app.py index 3dd2762..46d5603 100644 --- a/quantui/app.py +++ b/quantui/app.py @@ -739,7 +739,8 @@ def _load_last_calibration_label() -> str: # ── Module-level constants ──────────────────────────────────────────────────── -_APP_CSS: str = """""" +) _LAYOUT_TRAITS: frozenset[str] = frozenset(widgets.Layout.class_trait_names()) diff --git a/quantui/app_visualization.py b/quantui/app_visualization.py index 41b28e5..dc4b36f 100644 --- a/quantui/app_visualization.py +++ b/quantui/app_visualization.py @@ -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() @@ -2710,7 +2713,7 @@ def _frame_stepper_controls( f'style="{btn}">▶ Play' f'' - f'' f"{ab_html}" diff --git a/quantui/theme.py b/quantui/theme.py index 4e72549..c295f5e 100644 --- a/quantui/theme.py +++ b/quantui/theme.py @@ -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)" @@ -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; " diff --git a/quantui/visualization_py3dmol.py b/quantui/visualization_py3dmol.py index aa61a44..5235b64 100644 --- a/quantui/visualization_py3dmol.py +++ b/quantui/visualization_py3dmol.py @@ -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 @@ -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) @@ -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 @@ -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] @@ -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 diff --git a/tests/test_metal_viewer_fallback.py b/tests/test_metal_viewer_fallback.py index 13b9c91..fdf63d0 100644 --- a/tests/test_metal_viewer_fallback.py +++ b/tests/test_metal_viewer_fallback.py @@ -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 diff --git a/tests/test_theme_contrast.py b/tests/test_theme_contrast.py index f2c00ab..3a22160 100644 --- a/tests/test_theme_contrast.py +++ b/tests/test_theme_contrast.py @@ -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") @@ -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