feat(web): settings helptext behind a "?" popup instead of inline paragraphs

The Qobuz/Monitor/Discovery help I just added was always-visible and verbose.
Replace each with a compact "?" button (new reusable HelpButton) that opens the
same content in a popup (reuses the Modal), keeping the settings tabs clean.

web tsc + build clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jonathan
2026-07-14 12:21:17 +02:00
parent 24d4ba6098
commit 1a612f1808
3 changed files with 72 additions and 31 deletions
+25
View File
@@ -0,0 +1,25 @@
"use client";
import { useState, type ReactNode } from "react";
import { Modal } from "./modal";
/** A small "?" affordance that opens its help content in a popup, keeping settings pages
* uncluttered. */
export function HelpButton({ title, children }: { title: string; children: ReactNode }) {
const [open, setOpen] = useState(false);
return (
<>
<button
type="button"
className="help-btn"
aria-label={`Help: ${title}`}
onClick={() => setOpen(true)}
>
?
</button>
<Modal open={open} onClose={() => setOpen(false)} title={title}>
<div className="help-body">{children}</div>
</Modal>
</>
);
}
+11 -1
View File
@@ -259,7 +259,7 @@ nav.contents .sep { flex: 1; }
.field.wide { grid-column: 1 / -1; }
.subhead {
font-family: var(--mono); font-size: 0.66rem; letter-spacing: 0.16em; text-transform: uppercase;
color: var(--graphite); margin: 26px 0 2px;
color: var(--graphite); margin: 26px 0 2px; display: flex; align-items: center; gap: 10px;
}
.field .set { color: var(--accent); }
.info-panel {
@@ -276,6 +276,16 @@ nav.contents .sep { flex: 1; }
.help { font-size: 0.8rem; color: var(--graphite); line-height: 1.5; margin: 4px 0 14px; max-width: 62ch; }
.help b { color: var(--ink); font-weight: 600; }
.help code { font-family: var(--mono); font-size: 0.85em; }
/* compact "?" trigger that opens help in a popup */
.help-btn {
font-family: var(--mono); font-size: 0.72rem; line-height: 1; width: 20px; height: 20px;
border: 1.5px solid var(--rule-2); border-radius: 50%; background: transparent; color: var(--graphite);
cursor: pointer; display: inline-flex; align-items: center; justify-content: center; padding: 0;
}
.help-btn:hover { border-color: var(--accent); color: var(--accent); }
.help-body { font-size: 0.9rem; color: var(--ink); line-height: 1.6; }
.help-body b { font-weight: 600; }
.help-body code { font-family: var(--mono); font-size: 0.85em; }
.info-panel + .info-panel { margin-top: 14px; }
/* ── Modal ────────────────────────────────────────────── */
+36 -30
View File
@@ -2,6 +2,7 @@
import { useEffect, useState } from "react";
import { PageHead } from "../_ui/page-head";
import { HelpButton } from "../_ui/help-button";
type Loaded = {
qobuzEmail: string;
@@ -192,12 +193,14 @@ export function SettingsForm() {
{tab === "Qobuz" ? (
<form className="settings-section" onSubmit={saveCredentials}>
<p className="help">
Lyra signs in to Qobuz with a <b>user ID + auth token</b> the download engine
treats these as more reliable than email/password, which Qobuz often rejects. Find
them in your logged-in Qobuz web session (bundle/app secret + user auth token).
</p>
<div className="subhead">Auth token</div>
<div className="subhead">
Auth token
<HelpButton title="Qobuz authentication">
Lyra signs in to Qobuz with a <b>user ID + auth token</b> the download engine
treats these as more reliable than email/password, which Qobuz often rejects. Find
them in your logged-in Qobuz web session (bundle/app secret + user auth token).
</HelpButton>
</div>
<div className="field-grid">
<label className="field">
<span>Qobuz user ID</span>
@@ -339,20 +342,22 @@ export function SettingsForm() {
/>
Automatic monitoring (grab &amp; upgrade)
</label>
<HelpButton title="Monitoring">
When on, the worker periodically checks your followed artists: it <b>grabs new
releases</b> as they appear and <b>upgrades</b> owned albums that fall below the
quality cutoff.
<br />
<br />
<b>Quality cutoff</b> the minimum acceptable quality class: <code>1</code> = lossy,{" "}
<code>2</code> = CD-lossless (FLAC 16/44.1), <code>3</code> = hi-res. An owned album is
kept as-is when its the cutoff; otherwise a better copy is chased. Range 13.
<br />
<br />
<b>Poll interval</b> hours between new-release checks. <b>Retry interval</b> hours
before re-attempting a release that couldnt be found yet. <b>Upgrade window</b> how
many days after first acquiring an album to keep looking for a better copy.
</HelpButton>
</div>
<p className="help">
When on, the worker periodically checks your followed artists: it <b>grabs new
releases</b> as they appear and <b>upgrades</b> owned albums that fall below the
quality cutoff.
<br />
<b>Quality cutoff</b> the minimum acceptable quality class: <code>1</code> = lossy,{" "}
<code>2</code> = CD-lossless (FLAC 16/44.1), <code>3</code> = hi-res. An owned album is
kept as-is when its the cutoff; otherwise a better copy is chased. Range 13.
<br />
<b>Poll interval</b> hours between new-release checks. <b>Retry interval</b> hours
before re-attempting a release that couldnt be found yet. <b>Upgrade window</b> how
many days after first acquiring an album to keep looking for a better copy.
</p>
<div className="field-grid">
{MONITOR_FIELDS.map(([key, label]) => (
<label key={key} className="field">
@@ -379,18 +384,19 @@ export function SettingsForm() {
/>
Scheduled discovery sweep
</label>
<HelpButton title="Discovery">
When on, the worker periodically sweeps your followed artists for similar artists +
their new releases (via ListenBrainz and, if configured, Last.fm) and lists them on{" "}
<b>Discover</b>. Nothing is downloaded automatically you Follow/Want from there.
<br />
<br />
<b>Interval</b> hours between sweeps. <b>Seeds per chunk</b> how many followed
artists are processed per worker cycle (keeps job-claiming responsive on big rosters).
<b> Similar per seed</b> candidates pulled per artist. <b>Albums per artist</b>
releases surfaced per suggested artist. <b>Min score</b> drops weak suggestions.{" "}
<b>ListenBrainz base URL</b> leave blank for the default endpoint.
</HelpButton>
</div>
<p className="help">
When on, the worker periodically sweeps your followed artists for similar artists +
their new releases (via ListenBrainz and, if configured, Last.fm) and lists them on{" "}
<b>Discover</b>. Nothing is downloaded automatically you Follow/Want from there.
<br />
<b>Interval</b> hours between sweeps. <b>Seeds per chunk</b> how many followed
artists are processed per worker cycle (keeps job-claiming responsive on big rosters).
<b> Similar per seed</b> candidates pulled per artist. <b>Albums per artist</b>
releases surfaced per suggested artist. <b>Min score</b> drops weak suggestions.{" "}
<b>ListenBrainz base URL</b> leave blank for the default endpoint.
</p>
<div className="field-grid">
{DISCOVERY_FIELDS.map(([key, label]) => (
<label key={key} className="field">