GUI: sub-tab the config panel so it fits the window

The MCP section made the Config tab overflow the fixed window. Split it into
Account / Sync / AI access sub-tabs with a scrollable body and a pinned Save
bar, so each section stays short and Save is always visible. Also fix the
select sitting flush against its help text, and reword the hints without dash
separators.
This commit is contained in:
Anthony
2026-06-09 16:08:31 +02:00
parent a0601d1230
commit 5beb420034
2 changed files with 225 additions and 109 deletions
+78
View File
@@ -513,6 +513,84 @@
margin-top: 12px; margin-top: 12px;
} }
/* ── Config panel: sub-tabbed, scrollable body, pinned actions ── */
.config-panel {
display: flex;
flex-direction: column;
height: 100%;
min-height: 0;
}
.config-subtabs {
display: flex;
gap: 2px;
margin-bottom: 16px;
flex-shrink: 0;
}
.config-subtabs button {
padding: 5px 12px;
font-size: 13px;
font-weight: 400;
border: 1px solid transparent;
border-radius: 8px;
background: none;
color: var(--on-surface-variant);
cursor: pointer;
transition: all 0.15s;
}
.config-subtabs button:hover {
background: var(--surface-container-high);
}
.config-subtabs button.active {
background: var(--primary-container);
color: var(--on-primary-container);
font-weight: 500;
}
.config-body {
flex: 1;
min-height: 0;
overflow-y: auto;
padding-right: 4px;
}
.config-panel .form-actions {
flex-shrink: 0;
align-items: center;
border-top: 1px solid var(--outline-variant);
padding-top: 12px;
}
.config-restart-hint {
margin: 0 auto 0 0;
}
.form-group select {
width: 100%;
padding: 9px 12px;
font-size: 14px;
border: 1px solid var(--outline-variant);
border-radius: 8px;
background: var(--surface);
color: var(--on-surface);
box-sizing: border-box;
font-family: var(--sans);
}
/* A control sitting right under explanatory text needs breathing room. */
.field-hint + select,
.field-hint + input,
.field-hint + .checkbox-field {
margin-top: 10px;
}
.config-body button {
margin-top: 8px;
}
/* ── Buttons ── */ /* ── Buttons ── */
button { button {
padding: 9px 18px; padding: 9px 18px;
+59 -21
View File
@@ -10,7 +10,10 @@ interface Props {
onRestart: () => Promise<void>; onRestart: () => Promise<void>;
} }
type Section = "account" | "sync" | "ai";
export function ConfigPanel({ config, status, loading, onSave, onRestart }: Props) { export function ConfigPanel({ config, status, loading, onSave, onRestart }: Props) {
const [section, setSection] = useState<Section>("account");
const [email, setEmail] = useState(""); const [email, setEmail] = useState("");
const [imapPort, setImapPort] = useState(1143); const [imapPort, setImapPort] = useState(1143);
const [smtpPort, setSmtpPort] = useState(1025); const [smtpPort, setSmtpPort] = useState(1025);
@@ -58,13 +61,36 @@ export function ConfigPanel({ config, status, loading, onSave, onRestart }: Prop
setMcpCopied(true); setMcpCopied(true);
setTimeout(() => setMcpCopied(false), 2000); setTimeout(() => setMcpCopied(false), 2000);
} catch { } catch {
/* clipboard denied ignore */ /* clipboard denied, ignore */
} }
}; };
return ( return (
<div className="panel"> <div className="panel config-panel">
<h2>Configuration</h2> <nav className="config-subtabs">
<button
className={section === "account" ? "active" : ""}
onClick={() => setSection("account")}
>
Account
</button>
<button
className={section === "sync" ? "active" : ""}
onClick={() => setSection("sync")}
>
Sync
</button>
<button
className={section === "ai" ? "active" : ""}
onClick={() => setSection("ai")}
>
AI access
</button>
</nav>
<div className="config-body">
{section === "account" && (
<>
<div className="form-group"> <div className="form-group">
<label>Email</label> <label>Email</label>
<input <input
@@ -100,12 +126,17 @@ export function ConfigPanel({ config, status, loading, onSave, onRestart }: Prop
onChange={(e) => setApiUrl(e.target.value)} onChange={(e) => setApiUrl(e.target.value)}
/> />
</div> </div>
</>
)}
{section === "sync" && (
<div className="form-group"> <div className="form-group">
<label>Offline message bodies</label> <label>Offline message bodies</label>
<small className="field-hint"> <small className="field-hint">
Your whole mailbox is always listed and searchable by subject, sender Your whole mailbox is always listed and searchable by subject,
and date. This only sets how many recent message <em>bodies</em> are sender and date. This only sets how many recent message{" "}
kept ready offline older ones load on demand when you open them. <em>bodies</em> are kept ready offline. Older ones load on demand
when you open them.
</small> </small>
<label className="checkbox-field"> <label className="checkbox-field">
<input <input
@@ -117,7 +148,8 @@ export function ConfigPanel({ config, status, loading, onSave, onRestart }: Prop
</label> </label>
{fetchAll ? ( {fetchAll ? (
<small className="field-hint"> <small className="field-hint">
Downloads every body slow + uses the most disk on large accounts. Downloads every body. Slowest option, and uses the most disk on
large accounts.
</small> </small>
) : ( ) : (
<input <input
@@ -129,13 +161,15 @@ export function ConfigPanel({ config, status, loading, onSave, onRestart }: Prop
/> />
)} )}
</div> </div>
)}
{section === "ai" && (
<div className="form-group"> <div className="form-group">
<label>AI access (MCP server)</label> <label>AI access (MCP server)</label>
<small className="field-hint"> <small className="field-hint">
Lets an LLM client (Claude Desktop / Code) <strong>read</strong> this Lets an LLM client (Claude Desktop / Code) <strong>read</strong>{" "}
mailbox over a local MCP server. Strictly read-only it can never this mailbox over a local MCP server. It is strictly read-only and
send, move or delete mail. can never send, move or delete mail.
</small> </small>
<select <select
value={mcpPermission} value={mcpPermission}
@@ -143,15 +177,15 @@ export function ConfigPanel({ config, status, loading, onSave, onRestart }: Prop
> >
<option value="disabled">Disabled (off)</option> <option value="disabled">Disabled (off)</option>
<option value="metadata"> <option value="metadata">
Metadata only folders, search, headers (no body) Metadata only (folders, search, headers)
</option> </option>
<option value="full">Full read also message bodies</option> <option value="full">Full read (includes message bodies)</option>
</select> </select>
{mcpPermission === "full" && ( {mcpPermission === "full" && (
<small className="field-hint"> <small className="field-hint">
The connected LLM can read full message content. Body text is The connected LLM can read full message content. Body text is
untrusted a malicious email could try to mislead the model. Only untrusted, so a malicious email could try to mislead the model.
enable with a client you trust. Only enable with a client you trust.
</small> </small>
)} )}
{mcpPermission !== "disabled" && ( {mcpPermission !== "disabled" && (
@@ -162,22 +196,26 @@ export function ConfigPanel({ config, status, loading, onSave, onRestart }: Prop
onChange={(e) => setMcpPort(Number(e.target.value))} onChange={(e) => setMcpPort(Number(e.target.value))}
placeholder="MCP port (127.0.0.1)" placeholder="MCP port (127.0.0.1)"
/> />
<button type="button" onClick={handleCopyMcpConfig}> <button type="button" className="secondary" onClick={handleCopyMcpConfig}>
{mcpCopied ? "Copied!" : "Copy client config"} {mcpCopied ? "Copied!" : "Copy client config"}
</button> </button>
<small className="field-hint"> <small className="field-hint">
Save first, then paste the copied snippet into your MCP client. Save first, then paste the copied snippet into your MCP
The server listens on 127.0.0.1 and requires the bridge password client. The server listens on 127.0.0.1 and requires the
as a bearer token. bridge password as a bearer token.
</small> </small>
</> </>
)} )}
</div> </div>
{isRunning && (
<small className="field-hint">Changes apply after a restart.</small>
)} )}
</div>
<div className="form-actions"> <div className="form-actions">
{isRunning && (
<small className="field-hint config-restart-hint">
Changes apply after a restart.
</small>
)}
<button className="primary" onClick={handleSave} disabled={loading || !email}> <button className="primary" onClick={handleSave} disabled={loading || !email}>
{saved ? "Saved!" : "Save"} {saved ? "Saved!" : "Save"}
</button> </button>