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:
@@ -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
@@ -259,7 +259,7 @@ nav.contents .sep { flex: 1; }
|
|||||||
.field.wide { grid-column: 1 / -1; }
|
.field.wide { grid-column: 1 / -1; }
|
||||||
.subhead {
|
.subhead {
|
||||||
font-family: var(--mono); font-size: 0.66rem; letter-spacing: 0.16em; text-transform: uppercase;
|
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); }
|
.field .set { color: var(--accent); }
|
||||||
.info-panel {
|
.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 { 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 b { color: var(--ink); font-weight: 600; }
|
||||||
.help code { font-family: var(--mono); font-size: 0.85em; }
|
.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; }
|
.info-panel + .info-panel { margin-top: 14px; }
|
||||||
|
|
||||||
/* ── Modal ────────────────────────────────────────────── */
|
/* ── Modal ────────────────────────────────────────────── */
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { PageHead } from "../_ui/page-head";
|
import { PageHead } from "../_ui/page-head";
|
||||||
|
import { HelpButton } from "../_ui/help-button";
|
||||||
|
|
||||||
type Loaded = {
|
type Loaded = {
|
||||||
qobuzEmail: string;
|
qobuzEmail: string;
|
||||||
@@ -192,12 +193,14 @@ export function SettingsForm() {
|
|||||||
|
|
||||||
{tab === "Qobuz" ? (
|
{tab === "Qobuz" ? (
|
||||||
<form className="settings-section" onSubmit={saveCredentials}>
|
<form className="settings-section" onSubmit={saveCredentials}>
|
||||||
<p className="help">
|
<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
|
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
|
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).
|
them in your logged-in Qobuz web session (bundle/app secret + user auth token).
|
||||||
</p>
|
</HelpButton>
|
||||||
<div className="subhead">Auth token</div>
|
</div>
|
||||||
<div className="field-grid">
|
<div className="field-grid">
|
||||||
<label className="field">
|
<label className="field">
|
||||||
<span>Qobuz user ID</span>
|
<span>Qobuz user ID</span>
|
||||||
@@ -339,20 +342,22 @@ export function SettingsForm() {
|
|||||||
/>
|
/>
|
||||||
Automatic monitoring (grab & upgrade)
|
Automatic monitoring (grab & upgrade)
|
||||||
</label>
|
</label>
|
||||||
</div>
|
<HelpButton title="Monitoring">
|
||||||
<p className="help">
|
|
||||||
When on, the worker periodically checks your followed artists: it <b>grabs new
|
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
|
releases</b> as they appear and <b>upgrades</b> owned albums that fall below the
|
||||||
quality cutoff.
|
quality cutoff.
|
||||||
<br />
|
<br />
|
||||||
|
<br />
|
||||||
<b>Quality cutoff</b> — the minimum acceptable quality class: <code>1</code> = lossy,{" "}
|
<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
|
<code>2</code> = CD-lossless (FLAC 16/44.1), <code>3</code> = hi-res. An owned album is
|
||||||
kept as-is when it’s ≥ the cutoff; otherwise a better copy is chased. Range 1–3.
|
kept as-is when it’s ≥ the cutoff; otherwise a better copy is chased. Range 1–3.
|
||||||
<br />
|
<br />
|
||||||
|
<br />
|
||||||
<b>Poll interval</b> — hours between new-release checks. <b>Retry interval</b> — hours
|
<b>Poll interval</b> — hours between new-release checks. <b>Retry interval</b> — hours
|
||||||
before re-attempting a release that couldn’t be found yet. <b>Upgrade window</b> — how
|
before re-attempting a release that couldn’t be found yet. <b>Upgrade window</b> — how
|
||||||
many days after first acquiring an album to keep looking for a better copy.
|
many days after first acquiring an album to keep looking for a better copy.
|
||||||
</p>
|
</HelpButton>
|
||||||
|
</div>
|
||||||
<div className="field-grid">
|
<div className="field-grid">
|
||||||
{MONITOR_FIELDS.map(([key, label]) => (
|
{MONITOR_FIELDS.map(([key, label]) => (
|
||||||
<label key={key} className="field">
|
<label key={key} className="field">
|
||||||
@@ -379,18 +384,19 @@ export function SettingsForm() {
|
|||||||
/>
|
/>
|
||||||
Scheduled discovery sweep
|
Scheduled discovery sweep
|
||||||
</label>
|
</label>
|
||||||
</div>
|
<HelpButton title="Discovery">
|
||||||
<p className="help">
|
|
||||||
When on, the worker periodically sweeps your followed artists for similar artists +
|
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{" "}
|
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.
|
<b>Discover</b>. Nothing is downloaded automatically — you Follow/Want from there.
|
||||||
<br />
|
<br />
|
||||||
|
<br />
|
||||||
<b>Interval</b> — hours between sweeps. <b>Seeds per chunk</b> — how many followed
|
<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).
|
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> —
|
<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.{" "}
|
releases surfaced per suggested artist. <b>Min score</b> — drops weak suggestions.{" "}
|
||||||
<b>ListenBrainz base URL</b> — leave blank for the default endpoint.
|
<b>ListenBrainz base URL</b> — leave blank for the default endpoint.
|
||||||
</p>
|
</HelpButton>
|
||||||
|
</div>
|
||||||
<div className="field-grid">
|
<div className="field-grid">
|
||||||
{DISCOVERY_FIELDS.map(([key, label]) => (
|
{DISCOVERY_FIELDS.map(([key, label]) => (
|
||||||
<label key={key} className="field">
|
<label key={key} className="field">
|
||||||
|
|||||||
Reference in New Issue
Block a user