diff --git a/site/content/_index.md b/site/content/_index.md
@@ -50,9 +50,6 @@ keys_body_2 = "The mouse works too, when your hand is already there: click a tab
# ── 6. Install ───────────────────────────────────────────────────────────
install_title = "Install it."
-install_lede = "About 7 MB installed, linked against alsa-lib, glibc and gcc-libs and nothing else, and a 5,000-track library opens in about 18 ms once its tags are cached."
-install_note_1 = "Two things to set while you are here. ommp draws its interface with <a href=\"https://www.nerdfonts.com/\">Nerd Font</a> glyphs, so point your terminal at a Nerd Font or the icons arrive as empty boxes. And album art comes through as a real image in terminals that speak Kitty graphics, Sixel or the iTerm2 protocol; ommp works out which at startup and there is nothing for you to configure. Everywhere else the cover is drawn in blocks."
-install_note_2 = "Linux only, said plainly: it is untested anywhere else and does not compile on Windows. It plays what is on your disk, so there is no streaming, no catalogue and no account, and it does not resume the track you were playing last time."
install_close = "Then put some music in <code>~/Music</code> and run <code>ommp</code>."
splash_note = "The notes behind the top of this page are the ones ommp scatters across its splash screen, generated from the same hash the program uses."
diff --git a/site/static/css/main.css b/site/static/css/main.css
@@ -151,25 +151,39 @@ a:hover { color: var(--cyan); }
color: var(--ink-2); font-size: 1.0625rem;
}
-.installs { display: flex; flex-wrap: wrap; gap: 1rem; justify-content: center; }
+.installs { display: flex; flex-wrap: wrap; gap: 1rem 1.5rem; justify-content: center; }
+.installs-left { justify-content: flex-start; margin-bottom: 2.5rem; }
.install { text-align: left; }
.install-label {
display: block; font-size: .75rem; letter-spacing: .14em; text-transform: uppercase;
color: var(--ink-2); margin-bottom: .4rem;
}
+/* The box is sized by the command, and the button sits outside it, so nothing
+ moves when you copy — only the icon changes. */
+.cmdline { display: flex; align-items: stretch; gap: .5rem; }
.cmd {
- display: block;
font-family: var(--mono); font-size: .9375rem;
background: var(--panel); border: 1px solid var(--line);
padding: .7rem 1rem; border-radius: 4px;
color: var(--ink); white-space: nowrap; overflow-x: auto;
}
+/* The prompt lives in a pseudo-element, which never reaches the clipboard. */
.cmd::before { content: "$ "; color: var(--dim); }
-.cmd[data-copy] { cursor: pointer; }
-.cmd[data-copy]:hover { border-color: var(--sky); }
-.cmd.is-copied { border-color: var(--playing); }
-.cmd.is-copied::after { content: " copied"; color: var(--playing); }
+
+.cmd-copy {
+ flex: none; display: grid; place-items: center;
+ width: 2.5rem; padding: 0;
+ background: none; border: 1px solid var(--line); border-radius: 4px;
+ color: var(--ink-2); cursor: pointer;
+ transition: color .15s, border-color .15s, background .15s;
+}
+.cmd-copy:hover { color: var(--ink); border-color: var(--sky); background: #ffffff0a; }
+.cmd-copy svg { width: 15px; height: 15px; }
+.cmd-copy svg:last-child { display: none; }
+.cmd-copy.is-copied { color: var(--playing); border-color: var(--playing); }
+.cmd-copy.is-copied svg:first-child { display: none; }
+.cmd-copy.is-copied svg:last-child { display: block; }
.hero-meta { margin-top: 2rem; color: var(--ink-2); font-size: .875rem; }
@@ -243,10 +257,24 @@ a:hover { color: var(--cyan); }
.setup { font-size: .9375rem; }
.setup-skipped { list-style: none; margin: 0 0 1.5rem; padding: 0; display: grid; gap: .7rem; }
.setup-skipped li {
+ position: relative; width: max-content; max-width: 100%;
color: var(--dim);
- text-decoration: line-through;
- text-decoration-color: #FF329699;
- text-decoration-thickness: 1px;
+}
+.setup-skipped li::after {
+ content: ""; position: absolute; inset-inline: 0; top: .62em;
+ height: 1px; background: var(--pink);
+ transform-origin: left;
+}
+
+/* Once the script is running the lines arrive unstruck and are crossed out one
+ at a time as the section comes into view. Without it they are simply already
+ crossed out, which is the state that makes the point. */
+.setup-armed .setup-skipped li { color: var(--ink-2); transition: color .4s ease; }
+.setup-armed .setup-skipped li::after { transform: scaleX(0); transition: transform .45s ease; }
+.setup-armed .setup-skipped li.is-struck { color: var(--dim); }
+.setup-armed .setup-skipped li.is-struck::after { transform: scaleX(1); }
+@media (prefers-reduced-motion: reduce) {
+ .setup-armed .setup-skipped li::after { transition: none; }
}
.setup-real {
margin: 0; font-weight: 700; font-size: 1.5rem;
@@ -292,25 +320,9 @@ kbd {
display: inline-block; text-align: center;
}
-/* ── Install grid ─────────────────────────────────────────────────── */
-
-.install-grid {
- display: grid; gap: 1rem; margin-bottom: 2rem;
- grid-template-columns: repeat(auto-fit, minmax(17rem, 1fr));
-}
-/* The clone URL is longer than a third of the wrap. Scrolling it inside the
- card hides the end behind no affordance at all, so wrap it instead — the
- whole string is what gets copied either way. Wrapping makes that one box two
- lines tall, so the boxes are stretched to a common height rather than left
- with one hanging 24px below its neighbours. */
-.install-card { display: grid; grid-template-rows: auto 1fr; }
-.install-card .cmd { white-space: normal; overflow-wrap: anywhere; height: 100%; }
+/* ── Install ──────────────────────────────────────────────────────── */
-.install-close { margin: 0 0 2.5rem; font-size: 1.0625rem; }
-/* Everything a person about to type the command needs to know, at the size that
- says it is not the reason they came. */
-.fine { max-width: 46rem; margin: 0 0 1rem; color: var(--ink-2); font-size: .875rem; }
-.fine:last-child { margin-bottom: 0; }
+.install-close { margin: 0; font-size: 1.0625rem; }
/* ── Footer ───────────────────────────────────────────────────────── */
diff --git a/site/static/js/status.js b/site/static/js/status.js
@@ -48,29 +48,40 @@
addEventListener("resize", onScroll);
update();
- // Click a command to copy it — the page is mostly commands. The button role
- // and the tab stop are set here rather than in the markup: copying is only
- // possible once this file has run, so without it these are plain text and
- // should not claim to be anything else.
- document.querySelectorAll("[data-copy]").forEach(function (el) {
- el.tabIndex = 0;
- el.setAttribute("role", "button");
-
- function copy() {
+ // Copy a command. The button is its own element beside the box, so the box
+ // never resizes — the icon swaps and that is the whole feedback.
+ document.querySelectorAll(".cmd-copy").forEach(function (btn) {
+ var cmd = btn.parentNode.querySelector(".cmd");
+ if (!cmd) return;
+ btn.addEventListener("click", function () {
if (!navigator.clipboard) return;
- navigator.clipboard.writeText(el.textContent.trim()).then(function () {
- el.classList.add("is-copied");
- setTimeout(function () { el.classList.remove("is-copied"); }, 1400);
+ // textContent, so the "$ " prompt drawn by CSS is left behind.
+ navigator.clipboard.writeText(cmd.textContent.trim()).then(function () {
+ btn.classList.add("is-copied");
+ setTimeout(function () { btn.classList.remove("is-copied"); }, 1600);
});
- }
-
- el.addEventListener("click", copy);
- el.addEventListener("keydown", function (e) {
- // What a real button answers to.
- if (e.key === "Enter" || e.key === " " || e.key === "Spacebar") {
- e.preventDefault();
- copy();
- }
});
});
+
+ // The setup you skip crosses itself out as you reach it. The markup ships
+ // already struck, so this first un-strikes it and then puts the lines back
+ // one at a time; if the script never runs, the finished state is what shows.
+ var setup = document.querySelector(".setup-skipped");
+ if (setup && "IntersectionObserver" in window) {
+ var lines = Array.prototype.slice.call(setup.children);
+ setup.parentNode.classList.add("setup-armed");
+
+ var still = matchMedia("(prefers-reduced-motion: reduce)").matches;
+ var strike = new IntersectionObserver(function (entries) {
+ entries.forEach(function (e) {
+ if (!e.isIntersecting) return;
+ strike.unobserve(e.target);
+ lines.forEach(function (li, i) {
+ setTimeout(function () { li.classList.add("is-struck"); }, still ? 0 : i * 260);
+ });
+ });
+ }, { threshold: 0.6 });
+
+ strike.observe(setup);
+ }
})();
diff --git a/site/templates/landing.html b/site/templates/landing.html
@@ -1,4 +1,18 @@
{%- set x = page.extra -%}
+{#- A command and a button that copies it. The button is separate so the box
+ never changes size: the icon swaps, the command does not move. -#}
+{%- macro command(label, text) -%}
+ <div class="install">
+ <span class="install-label">{{ label }}</span>
+ <div class="cmdline">
+ <code class="cmd">{{ text }}</code>
+ <button class="cmd-copy" type="button" aria-label="Copy {{ text }}">
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="12" height="12" rx="2.5"/><path d="M5 15H4.5A1.5 1.5 0 0 1 3 13.5v-9A1.5 1.5 0 0 1 4.5 3h9A1.5 1.5 0 0 1 15 4.5V5"/></svg>
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m5 12.5 5 5 9-11"/></svg>
+ </button>
+ </div>
+ </div>
+{%- endmacro -%}
<!doctype html>
<html lang="en">
<head>
@@ -40,14 +54,8 @@
<p class="lede">{{ x.lede }}</p>
<div class="installs">
- <div class="install">
- <span class="install-label">{{ x.install_primary_label }}</span>
- <code class="cmd" data-copy>{{ x.install_primary }}</code>
- </div>
- <div class="install">
- <span class="install-label">{{ x.install_secondary_label }}</span>
- <code class="cmd" data-copy>{{ x.install_secondary }}</code>
- </div>
+{{ command(x.install_primary_label, x.install_primary) }}
+{{ command(x.install_secondary_label, x.install_secondary) }}
</div>
<p class="hero-meta">
@@ -162,21 +170,12 @@
<div class="wrap">
<h2 class="sec-title">{{ x.install_title }}</h2>
- <div class="install-grid">
- <div class="install-card">
- <span class="install-label">{{ x.install_primary_label }}</span>
- <code class="cmd" data-copy>{{ x.install_primary }}</code>
- </div>
- <div class="install-card">
- <span class="install-label">{{ x.install_secondary_label }}</span>
- <code class="cmd" data-copy>{{ x.install_secondary }}</code>
- </div>
+ <div class="installs installs-left">
+{{ command(x.install_primary_label, x.install_primary) }}
+{{ command(x.install_secondary_label, x.install_secondary) }}
</div>
<p class="install-close">{{ x.install_close | safe }}</p>
- <p class="fine">{{ x.install_lede }}</p>
- <p class="fine">{{ x.install_note_1 | safe }}</p>
- <p class="fine">{{ x.install_note_2 }}</p>
</div>
</section>
|