docs: a standalone front page with the three programs and the downloads, the Sphinx tree one page below it

docs/landing/ is a self-contained page (HTML, CSS, a little JS, no framework): a hero, a card per
program, the download section with the archives of the newest release and the reader's own
platform marked, the licence in one paragraph, light and dark. The jfjoch_downloads extension
renders it into the HTML output root as index.html at the end of every Sphinx build, so it is
served wherever the documentation is - Read the Docs, the gitea-pages branch and the broker's
frontend bundle - and its download buttons are the same ones as on the Sphinx download pages,
built from the configured release and repointed at the newest published one by the shared script.

The Sphinx root page moves to documentation.html (root_doc), and the frontend's documentation
frame opens that page.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015CeEGgpKdfkCTJb3YNMJcu
(cherry picked from commit 64cde61f0b941b3e8c99eaa9b74afeaa75aefc78)
This commit is contained in:
leonarski_f committed 2026-10-25 07:20:29 +01:00
1 parent 9c543f60b6
commit 8ff2abf3aa
7 files changed
+615 -7

No files matched your search

+26 -1
View File
@@ -12,11 +12,18 @@ script matches against the release's asset list, written loosely enough to survi
bump.
"""
import pathlib
import shutil
from docutils import nodes
from docutils.parsers.rst import Directive
RELEASES = "https://gitea.psi.ch/mx/jungfraujoch/releases"
# docs/landing/: the front page, rendered from its template into the output root as index.html at
# the end of the build (write_landing below), with the same download blocks as the Sphinx pages.
LANDING_FILES = ("landing.css", "landing.js")
# program -> [(platform, requirements, [(label, asset name with {v}, regex for the live lookup)])]
# The first button of a platform is the recommended one and is drawn filled.
PROGRAMS = {
@@ -69,7 +76,8 @@ def render(program, version):
' &middot; <a href="%s">all releases</a></p>' % (version, RELEASES, version, RELEASES),
'<div class="jfj-dl__grid">']
for platform, requirements, archives in PROGRAMS[program]:
out.append('<div class="jfj-dl__card"><h3 class="jfj-dl__platform">%s</h3>' % platform)
out.append('<div class="jfj-dl__card" data-platform="%s"><h3 class="jfj-dl__platform">%s</h3>'
% (platform.lower(), platform))
for i, (label, pattern, regex) in enumerate(archives):
name = pattern.format(v=version)
cls = "jfj-dl__btn" if i == 0 else "jfj-dl__btn jfj-dl__btn--secondary"
@@ -93,6 +101,23 @@ class JfjochDownloads(Directive):
return [nodes.raw("", render(program, version), format="html")]
def write_landing(app, exception):
"""Render docs/landing/index.html into the HTML output root, next to the Sphinx pages."""
if exception is not None or app.builder.name != "html":
return
src = pathlib.Path(app.confdir) / "landing"
out = pathlib.Path(app.outdir)
version = app.config.release
html = (src / "index.html").read_text(encoding="utf-8")
html = (html.replace("{{release}}", version)
.replace("{{downloads:rugnux}}", render("rugnux", version))
.replace("{{downloads:viewer}}", render("viewer", version)))
(out / "index.html").write_text(html, encoding="utf-8")
for name in LANDING_FILES:
shutil.copy(src / name, out / name)
def setup(app):
app.add_directive("jfjoch-downloads", JfjochDownloads)
app.connect("build-finished", write_landing)
return {"parallel_read_safe": True, "parallel_write_safe": True}
+4
View File
@@ -26,6 +26,10 @@ extensions = [
'jfjoch_downloads',
]
# The site's front page is docs/landing/index.html, a standalone page the jfjoch_downloads
# extension renders into the output root as index.html; the Sphinx tree starts one page below it.
root_doc = 'documentation'
templates_path = ['_templates']
# docs/review/ is gitignored working material (review reports, work plans); Sphinx renders
# whatever is on disk, so without this a local review file would become a published page.
+6 -5
View File
@@ -1,11 +1,12 @@
PSI Jungfraujoch
================
Documentation
=============
.. raw:: html
<p class="jfj-lead">Open-source software from the <a href="https://www.psi.ch">Paul Scherrer
Institute</a> for X-ray diffraction data, from the detector to the merged reflections. Three
programs, each usable on its own.</p>
<p class="jfj-lead">The manuals and reference of Jungfraujoch and Rugnux, open-source software
from the <a href="https://www.psi.ch">Paul Scherrer Institute</a> for X-ray diffraction data.
The <a href="index.html">front page</a> has the overview and the downloads; this page is the
map of the documentation.</p>
<div class="jfj-cards">
+149
View File
@@ -0,0 +1,149 @@
<!doctype html>
<!-- The front page of the documentation site. docs/_ext/jfjoch_downloads.py renders this template
at the end of a Sphinx build (the {{...}} placeholders) into the output root as index.html,
next to the Sphinx pages, so it is served wherever the documentation is: Read the Docs, the
gitea-pages branch and the broker's frontend bundle. The Sphinx home page is documentation.html. -->
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Jungfraujoch and Rugnux: X-ray diffraction data, from the detector to the merged reflections</title>
<meta name="description" content="Open-source software from the Paul Scherrer Institute: Rugnux processes macromolecular diffraction data with one command, the Jungfraujoch Viewer shows it, and Jungfraujoch acquires it from JUNGFRAU and EIGER detectors at full speed.">
<link rel="icon" href="_static/jfjoch.png">
<link rel="stylesheet" href="landing.css">
<script>
// Apply the saved theme before the first paint, so a dark page does not flash light.
try { var t = localStorage.getItem("jfj-theme"); if (t) document.documentElement.setAttribute("data-theme", t); } catch (e) {}
</script>
</head>
<body>
<header class="top">
<a class="brand" href="index.html"><img src="_static/jfjoch.png" alt="" width="36" height="33"><span>Jungfraujoch</span></a>
<nav class="top__nav" aria-label="Sections">
<a href="#programs">Programs</a>
<a href="#download">Download</a>
<a href="documentation.html">Documentation</a>
<a href="https://gitea.psi.ch/mx/jungfraujoch">Source</a>
</nav>
<button class="theme-toggle" type="button" aria-label="Switch between light and dark theme" title="Light / dark">
<svg class="theme-toggle__sun" viewBox="0 0 24 24" width="20" height="20" aria-hidden="true"><circle cx="12" cy="12" r="4.5" fill="none" stroke="currentColor" stroke-width="2"/><path d="M12 2v3M12 19v3M2 12h3M19 12h3M4.9 4.9l2.1 2.1M17 17l2.1 2.1M4.9 19.1 7 17M17 7l2.1-2.1" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
<svg class="theme-toggle__moon" viewBox="0 0 24 24" width="20" height="20" aria-hidden="true"><path d="M20 15.5A8.5 8.5 0 0 1 8.5 4a8.5 8.5 0 1 0 11.5 11.5z" fill="none" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/></svg>
</button>
</header>
<main>
<section class="hero">
<svg class="hero__art" aria-hidden="true" viewBox="0 0 600 600"></svg>
<div class="hero__text">
<p class="eyebrow">Paul Scherrer Institute &middot; open source</p>
<h1>From the detector to the merged reflections.</h1>
<p class="lede">Software for X-ray diffraction data: <strong>Rugnux</strong> processes a stored
dataset with one command, the <strong>Viewer</strong> shows what is in it, and
<strong>Jungfraujoch</strong> acquires it from the detector at full speed.</p>
<div class="hero__actions">
<a class="btn btn--primary" href="#download">Download</a>
<a class="btn btn--ghost" href="documentation.html">Read the documentation</a>
</div>
<p class="hero__run"><code>rugnux &lt;diffraction image file&gt;</code><span>is the whole command: cell, space group and resolution come out of the data.</span></p>
</div>
</section>
<section class="programs" id="programs">
<div class="programs__grid">
<article class="program">
<p class="program__kicker">Data processing</p>
<h2>Rugnux</h2>
<p>Takes a macromolecular diffraction dataset, a rotation sweep or serial stills, from the
images to merged reflections: spot finding, indexing, integration, scaling and merging, with
the space group, cell and resolution decided from the data and a report that says on what
evidence. Writes <code>.mtz</code>, <code>.cif</code> and <code>.hkl</code> for the programs
that come next.</p>
<p class="program__meta">Linux &middot; macOS &middot; Windows &middot; command line &middot; fast on an NVIDIA GPU, works without one</p>
<p class="program__links"><a href="RUGNUX.html">Get started</a><a href="RUGNUX_OVERVIEW.html">What it does</a><a href="RUGNUX_TUTORIAL.html">Manual</a><a href="#dl-rugnux">Download</a></p>
</article>
<article class="program">
<p class="program__kicker">Desktop application</p>
<h2>Jungfraujoch Viewer</h2>
<p>Shows what is in a dataset: every image with its spots, predictions and resolution rings,
per-image statistics over the sweep, grid-scan maps, and the same processing pipeline as
Rugnux, run from a settings panel on the open file. Follows a live collection from a running
Jungfraujoch broker.</p>
<p class="program__meta">Linux &middot; macOS &middot; Windows &middot; Qt 6</p>
<p class="program__links"><a href="JFJOCH_VIEWER.html">Get started</a><a href="JFJOCH_VIEWER.html#a-guided-tour">Guided tour</a><a href="#dl-viewer">Download</a></p>
</article>
<article class="program">
<p class="program__kicker">Data acquisition</p>
<h2>Jungfraujoch</h2>
<p>The acquisition and real-time analysis system for the PSI JUNGFRAU and EIGER detectors,
and for DECTRIS detectors through their control unit: an FPGA and GPU pipeline that takes
the detector's frames at full rate, analyses them on the fly, streams them to the NXmx HDF5
writer and serves a web interface and REST API to the beamline.</p>
<p class="program__meta">Linux servers &middot; AMD Alveo U55C FPGA &middot; NVIDIA GPUs</p>
<p class="program__links"><a href="JUNGFRAUJOCH.html">Get started</a><a href="DEPLOYMENT.html">Install</a><a href="JFJOCH_BROKER.html">Broker</a><a href="JFJOCH_WRITER.html">Writer</a></p>
</article>
</div>
</section>
<section class="download" id="download">
<div class="download__head">
<h2>Download</h2>
<p>Pre-built, signed where the platform asks for it, nothing to install beside them.
<span data-os-note></span></p>
</div>
<div class="download__program" id="dl-rugnux">
<h3>Rugnux</h3>
<p>One self-contained executable. Unpack the archive into a directory of its own and run
<code>bin/rugnux</code>; no CUDA toolkit, no Qt, no service. On a machine with an NVIDIA GPU
take the GPU build, which also runs without one. <a href="RUGNUX_DOWNLOAD.html">Details, packages and building from source</a>.</p>
{{downloads:rugnux}}
</div>
<div class="download__program" id="dl-viewer">
<h3>Jungfraujoch Viewer</h3>
<p>A desktop application with the Qt runtime included: a portable archive on Linux, a
notarized disk image on macOS, an installer on Windows. <a href="VIEWER_DOWNLOAD.html">Details and building from source</a>.</p>
{{downloads:viewer}}
</div>
<div class="download__program download__program--text">
<h3>Jungfraujoch, the acquisition system</h3>
<p>Installed on the beamline's Linux servers from the <a href="REPOSITORIES.html">RPM and APT
repositories</a> (<code>jfjoch</code>, <code>jfjoch-writer</code>, <code>jfjoch-driver-dkms</code>,
<code>jfjoch-viewer</code>, <code>rugnux</code>), with the FPGA firmware and the writer's
packages attached to every <a href="https://gitea.psi.ch/mx/jungfraujoch/releases">release</a>.
<a href="DEPLOYMENT.html">Deployment</a> is the step-by-step guide; <a href="RELEASE_CONTENTS.html">Release contents</a> says what each package needs on the machine.</p>
</div>
</section>
<section class="licence">
<h2>Free to use, in academia and industry</h2>
<p>Rugnux, the viewer and the Jungfraujoch services are free software under the
<a href="LICENSE.html">GNU General Public License v3</a>; the FPGA design is released under the
<a href="FPGA_LICENSE.html">CERN Open Hardware Licence v2, strongly reciprocal</a>. Both let
anyone use, study and modify the software and the hardware design at no charge, commercial use
included; what they ask is that a modified version which is passed on is shared under the same
terms. The released binaries bundle third-party components under GPL-compatible licences, and the
CUDA builds the NVIDIA CUDA runtime under NVIDIA's EULA: see the
<a href="THIRD_PARTY_NOTICES.html">third-party notices</a>.</p>
</section>
</main>
<footer class="foot">
<p>Documentation for Jungfraujoch {{release}} &middot; <a href="CHANGELOG.html">changelog</a> &middot;
<a href="documentation.html">documentation</a> &middot; <a href="NAMING.html">why the names</a> &middot;
<a href="ACKNOWLEDGEMENT.html">acknowledgements</a> &middot; <a href="https://gitea.psi.ch/mx/jungfraujoch">source</a></p>
<p>&copy; Paul Scherrer Institute. Jungfraujoch streams, Rugnux processes: two Swiss mountains, two railways, one idea of throughput.</p>
</footer>
<script src="_static/jfjoch_downloads.js"></script>
<script src="landing.js"></script>
</body>
</html>
+369
View File
@@ -0,0 +1,369 @@
/* The front page (docs/landing/index.html). Self-contained: system fonts, no framework, one
palette per theme. The download blocks use the same jfj-dl markup as the Sphinx download pages
(docs/_ext/jfjoch_downloads.py) and are styled here for this page. */
:root {
--bg: #fbfbfd;
--bg-raised: #ffffff;
--bg-tint: #eef0fa;
--fg: #1b1d2a;
--fg-muted: #5b6072;
--line: rgba(27, 29, 42, 0.12);
--primary: #3949ab;
--primary-strong: #2c3a8f;
--primary-soft: rgba(57, 73, 171, 0.1);
--on-primary: #ffffff;
--accent: #e9a23b;
--accent-soft: rgba(233, 162, 59, 0.16);
--shadow: 0 1px 2px rgba(27, 29, 42, 0.06), 0 8px 24px rgba(27, 29, 42, 0.06);
--ring: rgba(57, 73, 171, 0.22);
--spot: rgba(57, 73, 171, 0.55);
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
--bg: #11131c;
--bg-raised: #181b27;
--bg-tint: #1d2130;
--fg: #e8e9f1;
--fg-muted: #9ea3b8;
--line: rgba(232, 233, 241, 0.12);
--primary: #8e9dff;
--primary-strong: #aab5ff;
--primary-soft: rgba(142, 157, 255, 0.14);
--on-primary: #0d1030;
--accent: #f0b65b;
--accent-soft: rgba(240, 182, 91, 0.16);
--shadow: 0 1px 2px rgba(0, 0, 0, 0.4), 0 8px 24px rgba(0, 0, 0, 0.35);
--ring: rgba(142, 157, 255, 0.24);
--spot: rgba(170, 181, 255, 0.7);
}
}
:root[data-theme="dark"] {
--bg: #11131c;
--bg-raised: #181b27;
--bg-tint: #1d2130;
--fg: #e8e9f1;
--fg-muted: #9ea3b8;
--line: rgba(232, 233, 241, 0.12);
--primary: #8e9dff;
--primary-strong: #aab5ff;
--primary-soft: rgba(142, 157, 255, 0.14);
--on-primary: #0d1030;
--accent: #f0b65b;
--accent-soft: rgba(240, 182, 91, 0.16);
--shadow: 0 1px 2px rgba(0, 0, 0, 0.4), 0 8px 24px rgba(0, 0, 0, 0.35);
--ring: rgba(142, 157, 255, 0.24);
--spot: rgba(170, 181, 255, 0.7);
}
* { box-sizing: border-box; }
html { scroll-behavior: smooth; }
body {
margin: 0;
background: var(--bg);
color: var(--fg);
font: 16px/1.55 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
-webkit-font-smoothing: antialiased;
}
a { color: var(--primary); text-decoration: none; }
a:hover { color: var(--primary-strong); text-decoration: underline; }
code {
font-family: "SFMono-Regular", Menlo, Consolas, "Roboto Mono", monospace;
font-size: 0.92em;
background: var(--primary-soft);
padding: 0.1em 0.35em;
border-radius: 4px;
}
h1, h2, h3 { line-height: 1.2; letter-spacing: -0.01em; margin: 0; }
main > section, .top, .foot {
max-width: 1120px;
margin: 0 auto;
padding: 0 24px;
}
/* ---- header ---------------------------------------------------------------------------------- */
.top {
display: flex;
align-items: center;
gap: 24px;
padding-top: 18px;
padding-bottom: 18px;
}
.brand {
display: inline-flex;
align-items: center;
gap: 10px;
color: var(--fg);
font-weight: 600;
font-size: 1.05em;
}
.brand:hover { text-decoration: none; }
.brand img { display: block; }
.top__nav { display: flex; gap: 22px; margin-left: auto; font-weight: 500; }
.top__nav a { color: var(--fg-muted); }
.top__nav a:hover { color: var(--primary); text-decoration: none; }
.theme-toggle {
display: inline-flex;
align-items: center;
justify-content: center;
width: 38px; height: 38px;
border: 1px solid var(--line);
border-radius: 50%;
background: var(--bg-raised);
color: var(--fg-muted);
cursor: pointer;
}
.theme-toggle:hover { color: var(--primary); border-color: var(--primary); }
.theme-toggle__moon { display: none; }
:root[data-theme="dark"] .theme-toggle__sun { display: none; }
:root[data-theme="dark"] .theme-toggle__moon { display: block; }
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) .theme-toggle__sun { display: none; }
:root:not([data-theme="light"]) .theme-toggle__moon { display: block; }
}
/* ---- hero ------------------------------------------------------------------------------------ */
.hero {
position: relative;
display: grid;
grid-template-columns: minmax(0, 1.2fr) minmax(0, 1fr);
align-items: center;
gap: 32px;
padding-top: 56px;
padding-bottom: 64px;
}
.hero__art {
grid-column: 2;
grid-row: 1;
width: 100%;
max-width: 460px;
justify-self: end;
aspect-ratio: 1;
}
.hero__art .ring { fill: none; stroke: var(--ring); stroke-width: 1; }
.hero__art .ring--strong { stroke-width: 1.6; }
.hero__art .spot { fill: var(--spot); }
.hero__art .beam { stroke: var(--accent); stroke-width: 2; }
.hero__text { grid-column: 1; grid-row: 1; max-width: 600px; }
.eyebrow {
margin: 0 0 14px;
font-size: 0.78em;
font-weight: 700;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--primary);
}
.hero h1 {
font-size: clamp(2.1rem, 4.6vw, 3.4rem);
font-weight: 700;
letter-spacing: -0.025em;
margin-bottom: 18px;
}
.lede { font-size: 1.2em; color: var(--fg-muted); margin: 0 0 26px; }
.lede strong { color: var(--fg); font-weight: 600; }
.hero__actions { display: flex; flex-wrap: wrap; gap: 12px; margin-bottom: 26px; }
.btn {
display: inline-block;
padding: 11px 20px;
border-radius: 8px;
font-weight: 600;
border: 1px solid transparent;
transition: transform 0.12s, box-shadow 0.12s, background 0.12s;
}
.btn:hover { text-decoration: none; transform: translateY(-1px); }
.btn--primary { background: var(--primary); color: var(--on-primary); box-shadow: var(--shadow); }
.btn--primary:hover { background: var(--primary-strong); color: var(--on-primary); }
.btn--ghost { border-color: var(--line); color: var(--fg); background: var(--bg-raised); }
.btn--ghost:hover { border-color: var(--primary); color: var(--primary); }
.hero__run {
display: flex;
flex-wrap: wrap;
align-items: baseline;
gap: 10px 14px;
margin: 0;
color: var(--fg-muted);
font-size: 0.95em;
}
.hero__run code { font-size: 1em; padding: 0.35em 0.7em; }
/* ---- programs -------------------------------------------------------------------------------- */
.programs { padding-bottom: 56px; }
.programs__grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 20px;
}
.program {
display: flex;
flex-direction: column;
padding: 24px 24px 20px;
border: 1px solid var(--line);
border-radius: 14px;
background: var(--bg-raised);
box-shadow: var(--shadow);
}
.program__kicker {
margin: 0 0 8px;
font-size: 0.74em;
font-weight: 700;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--primary);
}
.program h2 { font-size: 1.45em; font-weight: 650; margin-bottom: 12px; }
.program > p:not([class]) { margin: 0 0 14px; color: var(--fg); flex: 1 1 auto; }
.program__meta { margin: 0 0 14px; font-size: 0.82em; color: var(--fg-muted); }
.program__links {
margin: 0;
padding-top: 14px;
border-top: 1px solid var(--line);
display: flex;
flex-wrap: wrap;
gap: 6px 18px;
font-weight: 500;
}
.program__links a:first-child { font-weight: 700; }
/* ---- download -------------------------------------------------------------------------------- */
.download {
padding-top: 48px;
padding-bottom: 24px;
border-top: 1px solid var(--line);
}
.download__head { max-width: 640px; margin-bottom: 28px; }
.download__head h2 { font-size: 2em; font-weight: 700; margin-bottom: 10px; }
.download__head p { margin: 0; color: var(--fg-muted); font-size: 1.05em; }
.download__head [data-os-note]:not(:empty) { color: var(--fg); font-weight: 500; }
.download__program { margin-bottom: 36px; }
.download__program h3 { font-size: 1.3em; font-weight: 650; margin-bottom: 8px; }
.download__program > p { margin: 0 0 16px; max-width: 720px; color: var(--fg-muted); }
.download__program--text > p { color: var(--fg); }
.jfj-dl__release { margin: 0 0 14px; font-size: 0.95em; color: var(--fg-muted); }
.jfj-dl__release strong { color: var(--fg); }
.jfj-dl__grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 16px;
}
.jfj-dl__card {
position: relative;
padding: 18px 18px 8px;
border: 1px solid var(--line);
border-radius: 12px;
background: var(--bg-raised);
}
.jfj-dl__card.is-yours {
border-color: var(--accent);
box-shadow: 0 0 0 3px var(--accent-soft), var(--shadow);
}
.jfj-dl__card.is-yours::after {
content: "Your system";
position: absolute;
top: -11px; right: 14px;
padding: 2px 10px;
border-radius: 999px;
background: var(--accent);
color: #1b1d2a;
font-size: 0.72em;
font-weight: 700;
letter-spacing: 0.04em;
}
.jfj-dl__platform { font-size: 1.1em; font-weight: 650; margin-bottom: 12px; }
a.jfj-dl__btn {
display: block;
margin-bottom: 10px;
padding: 10px 14px;
border: 1px solid var(--primary);
border-radius: 8px;
background: var(--primary);
color: var(--on-primary);
font-weight: 600;
line-height: 1.3;
transition: background 0.12s, box-shadow 0.12s;
}
a.jfj-dl__btn:hover { text-decoration: none; background: var(--primary-strong); box-shadow: var(--shadow); color: var(--on-primary); }
a.jfj-dl__btn--secondary { background: transparent; color: var(--primary); }
a.jfj-dl__btn--secondary:hover { background: var(--primary-soft); color: var(--primary-strong); }
a.jfj-dl__btn--missing, a.jfj-dl__btn--missing:hover {
background: transparent;
border-style: dashed;
border-color: var(--line);
color: var(--fg-muted);
box-shadow: none;
cursor: default;
}
.jfj-dl__file {
display: block;
margin-top: 2px;
font-family: "SFMono-Regular", Menlo, Consolas, "Roboto Mono", monospace;
font-size: 0.74em;
font-weight: 400;
opacity: 0.85;
word-break: break-all;
}
.jfj-dl__req { margin: 6px 0 8px; font-size: 0.82em; line-height: 1.45; color: var(--fg-muted); }
.jfj-dl__note { margin: 10px 0 0; font-size: 0.82em; color: var(--fg-muted); }
.jfj-dl__note:empty { display: none; }
/* ---- licence, footer ------------------------------------------------------------------------- */
.licence {
padding-top: 40px;
padding-bottom: 48px;
border-top: 1px solid var(--line);
}
.licence h2 { font-size: 1.5em; font-weight: 650; margin-bottom: 12px; }
.licence p { margin: 0; max-width: 760px; color: var(--fg-muted); }
.foot {
padding-top: 24px;
padding-bottom: 40px;
border-top: 1px solid var(--line);
font-size: 0.88em;
color: var(--fg-muted);
}
.foot p { margin: 0 0 6px; }
/* ---- small screens --------------------------------------------------------------------------- */
@media (max-width: 760px) {
body { font-size: 15px; }
.top { gap: 14px; flex-wrap: wrap; }
.top__nav { gap: 14px; margin-left: 0; order: 3; width: 100%; font-size: 0.95em; }
.theme-toggle { margin-left: auto; }
.hero { grid-template-columns: 1fr; padding-top: 28px; padding-bottom: 40px; }
.hero__art { grid-column: 1; grid-row: 1; max-width: 220px; justify-self: center; opacity: 0.9; }
.hero__text { grid-row: 2; }
.programs__grid { grid-template-columns: 1fr; }
}
+60
View File
@@ -0,0 +1,60 @@
// The front page: theme toggle, the hero's diffraction-pattern drawing, and the download section's
// "your system" highlight. The download buttons themselves are repointed at the newest release by
// _static/jfjoch_downloads.js, shared with the Sphinx download pages.
(function () {
// ---- theme: follows the system until the toggle is used, then remembered -----------------
var root = document.documentElement;
var toggle = document.querySelector(".theme-toggle");
function isDark() {
var set = root.getAttribute("data-theme");
if (set) return set === "dark";
return window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches;
}
if (toggle) toggle.addEventListener("click", function () {
var next = isDark() ? "light" : "dark";
root.setAttribute("data-theme", next);
try { localStorage.setItem("jfj-theme", next); } catch (e) {}
});
// ---- hero art: resolution rings and a scatter of spots, the same every time --------------
var art = document.querySelector(".hero__art");
if (art) {
var seed = 7;
function rand() { seed = (seed * 16807) % 2147483647; return seed / 2147483647; }
var ns = "http://www.w3.org/2000/svg", out = [];
function el(name, attrs) {
var e = document.createElementNS(ns, name);
for (var k in attrs) e.setAttribute(k, attrs[k]);
return e;
}
var cx = 300, cy = 300;
[60, 110, 160, 210, 260].forEach(function (r, i) {
art.appendChild(el("circle", { cx: cx, cy: cy, r: r, "class": "ring" + (i === 2 ? " ring--strong" : "") }));
});
// beam centre
art.appendChild(el("path", { d: "M" + (cx - 9) + " " + cy + "h18M" + cx + " " + (cy - 9) + "v18", "class": "beam" }));
// spots: denser near the middle, fainter and smaller towards the edge, a few bright ones
for (var i = 0; i < 230; i++) {
var a = rand() * Math.PI * 2;
var r = Math.sqrt(rand()) * 285;
if (r < 25) continue;
var x = cx + r * Math.cos(a), y = cy + r * Math.sin(a);
var fade = 1 - r / 320;
var size = 1.2 + fade * 2.2 * rand() + (rand() > 0.94 ? 2.5 : 0);
var c = el("circle", { cx: x.toFixed(1), cy: y.toFixed(1), r: size.toFixed(1), "class": "spot" });
c.style.opacity = (0.25 + 0.75 * fade * rand()).toFixed(2);
art.appendChild(c);
}
}
// ---- "your system": highlight the platform the browser runs on ----------------------------
var ua = navigator.userAgent || "", platform = navigator.platform || "";
var os = /Windows/.test(ua) ? "windows" : /Mac/.test(platform) || /Macintosh/.test(ua) ? "macos" :
/Linux|X11/.test(platform + ua) && !/Android/.test(ua) ? "linux" : null;
if (os) {
document.querySelectorAll('.jfj-dl__card[data-platform="' + os + '"]').forEach(function (c) { c.classList.add("is-yours"); });
var note = document.querySelector("[data-os-note]");
var names = { windows: "Windows", macos: "macOS", linux: "Linux" };
if (note) note.textContent = "The " + names[os] + " builds are marked for you.";
}
})();
+1 -1
View File
@@ -254,7 +254,7 @@ function App() {
<DocFrame title="API reference" src="/frontend/openapi.html"/>
)},
{ id: 'documentation', label: 'Documentation', icon: <MenuBookIcon/>, render: () => (
<DocFrame title="Documentation" src="/frontend/docs/index.html"/>
<DocFrame title="Documentation" src="/frontend/docs/documentation.html"/>
)},
{ id: 'changelog', label: 'Changelog', icon: <HistoryIcon/>, render: () => (
<MarkdownDoc md={changelogMd}/>