mirror of
https://github.com/spartanz51/tutabridge.git
synced 2026-06-24 10:54:32 +02:00
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:
@@ -513,6 +513,84 @@
|
||||
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 ── */
|
||||
button {
|
||||
padding: 9px 18px;
|
||||
|
||||
+147
-109
@@ -10,7 +10,10 @@ interface Props {
|
||||
onRestart: () => Promise<void>;
|
||||
}
|
||||
|
||||
type Section = "account" | "sync" | "ai";
|
||||
|
||||
export function ConfigPanel({ config, status, loading, onSave, onRestart }: Props) {
|
||||
const [section, setSection] = useState<Section>("account");
|
||||
const [email, setEmail] = useState("");
|
||||
const [imapPort, setImapPort] = useState(1143);
|
||||
const [smtpPort, setSmtpPort] = useState(1025);
|
||||
@@ -58,126 +61,161 @@ export function ConfigPanel({ config, status, loading, onSave, onRestart }: Prop
|
||||
setMcpCopied(true);
|
||||
setTimeout(() => setMcpCopied(false), 2000);
|
||||
} catch {
|
||||
/* clipboard denied — ignore */
|
||||
/* clipboard denied, ignore */
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="panel">
|
||||
<h2>Configuration</h2>
|
||||
<div className="form-group">
|
||||
<label>Email</label>
|
||||
<input
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder="your@tuta.com"
|
||||
/>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<div className="form-group">
|
||||
<label>IMAP Port</label>
|
||||
<input
|
||||
type="number"
|
||||
value={imapPort}
|
||||
onChange={(e) => setImapPort(Number(e.target.value))}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>SMTP Port</label>
|
||||
<input
|
||||
type="number"
|
||||
value={smtpPort}
|
||||
onChange={(e) => setSmtpPort(Number(e.target.value))}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>API URL</label>
|
||||
<input
|
||||
type="url"
|
||||
value={apiUrl}
|
||||
onChange={(e) => setApiUrl(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>Offline message bodies</label>
|
||||
<small className="field-hint">
|
||||
Your whole mailbox is always listed and searchable by subject, sender
|
||||
and date. This only sets how many recent message <em>bodies</em> are
|
||||
kept ready offline — older ones load on demand when you open them.
|
||||
</small>
|
||||
<label className="checkbox-field">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={fetchAll}
|
||||
onChange={(e) => setFetchAll(e.target.checked)}
|
||||
/>
|
||||
<span>Keep every message body offline (full local copy)</span>
|
||||
</label>
|
||||
{fetchAll ? (
|
||||
<small className="field-hint">
|
||||
Downloads every body — slow + uses the most disk on large accounts.
|
||||
</small>
|
||||
) : (
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
value={syncLimit}
|
||||
onChange={(e) => setSyncLimit(Math.max(1, Number(e.target.value)))}
|
||||
placeholder="Bodies to keep offline (most recent)"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label>AI access (MCP server)</label>
|
||||
<small className="field-hint">
|
||||
Lets an LLM client (Claude Desktop / Code) <strong>read</strong> this
|
||||
mailbox over a local MCP server. Strictly read-only — it can never
|
||||
send, move or delete mail.
|
||||
</small>
|
||||
<select
|
||||
value={mcpPermission}
|
||||
onChange={(e) => setMcpPermission(e.target.value as McpPermission)}
|
||||
<div className="panel config-panel">
|
||||
<nav className="config-subtabs">
|
||||
<button
|
||||
className={section === "account" ? "active" : ""}
|
||||
onClick={() => setSection("account")}
|
||||
>
|
||||
<option value="disabled">Disabled (off)</option>
|
||||
<option value="metadata">
|
||||
Metadata only — folders, search, headers (no body)
|
||||
</option>
|
||||
<option value="full">Full read — also message bodies</option>
|
||||
</select>
|
||||
{mcpPermission === "full" && (
|
||||
<small className="field-hint">
|
||||
⚠️ The connected LLM can read full message content. Body text is
|
||||
untrusted — a malicious email could try to mislead the model. Only
|
||||
enable with a client you trust.
|
||||
</small>
|
||||
)}
|
||||
{mcpPermission !== "disabled" && (
|
||||
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" && (
|
||||
<>
|
||||
<input
|
||||
type="number"
|
||||
value={mcpPort}
|
||||
onChange={(e) => setMcpPort(Number(e.target.value))}
|
||||
placeholder="MCP port (127.0.0.1)"
|
||||
/>
|
||||
<button type="button" onClick={handleCopyMcpConfig}>
|
||||
{mcpCopied ? "Copied!" : "Copy client config"}
|
||||
</button>
|
||||
<small className="field-hint">
|
||||
Save first, then paste the copied snippet into your MCP client.
|
||||
The server listens on 127.0.0.1 and requires the bridge password
|
||||
as a bearer token.
|
||||
</small>
|
||||
<div className="form-group">
|
||||
<label>Email</label>
|
||||
<input
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder="your@tuta.com"
|
||||
/>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<div className="form-group">
|
||||
<label>IMAP Port</label>
|
||||
<input
|
||||
type="number"
|
||||
value={imapPort}
|
||||
onChange={(e) => setImapPort(Number(e.target.value))}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>SMTP Port</label>
|
||||
<input
|
||||
type="number"
|
||||
value={smtpPort}
|
||||
onChange={(e) => setSmtpPort(Number(e.target.value))}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>API URL</label>
|
||||
<input
|
||||
type="url"
|
||||
value={apiUrl}
|
||||
onChange={(e) => setApiUrl(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{section === "sync" && (
|
||||
<div className="form-group">
|
||||
<label>Offline message bodies</label>
|
||||
<small className="field-hint">
|
||||
Your whole mailbox is always listed and searchable by subject,
|
||||
sender and date. This only sets how many recent message{" "}
|
||||
<em>bodies</em> are kept ready offline. Older ones load on demand
|
||||
when you open them.
|
||||
</small>
|
||||
<label className="checkbox-field">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={fetchAll}
|
||||
onChange={(e) => setFetchAll(e.target.checked)}
|
||||
/>
|
||||
<span>Keep every message body offline (full local copy)</span>
|
||||
</label>
|
||||
{fetchAll ? (
|
||||
<small className="field-hint">
|
||||
Downloads every body. Slowest option, and uses the most disk on
|
||||
large accounts.
|
||||
</small>
|
||||
) : (
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
value={syncLimit}
|
||||
onChange={(e) => setSyncLimit(Math.max(1, Number(e.target.value)))}
|
||||
placeholder="Bodies to keep offline (most recent)"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{section === "ai" && (
|
||||
<div className="form-group">
|
||||
<label>AI access (MCP server)</label>
|
||||
<small className="field-hint">
|
||||
Lets an LLM client (Claude Desktop / Code) <strong>read</strong>{" "}
|
||||
this mailbox over a local MCP server. It is strictly read-only and
|
||||
can never send, move or delete mail.
|
||||
</small>
|
||||
<select
|
||||
value={mcpPermission}
|
||||
onChange={(e) => setMcpPermission(e.target.value as McpPermission)}
|
||||
>
|
||||
<option value="disabled">Disabled (off)</option>
|
||||
<option value="metadata">
|
||||
Metadata only (folders, search, headers)
|
||||
</option>
|
||||
<option value="full">Full read (includes message bodies)</option>
|
||||
</select>
|
||||
{mcpPermission === "full" && (
|
||||
<small className="field-hint">
|
||||
⚠️ The connected LLM can read full message content. Body text is
|
||||
untrusted, so a malicious email could try to mislead the model.
|
||||
Only enable with a client you trust.
|
||||
</small>
|
||||
)}
|
||||
{mcpPermission !== "disabled" && (
|
||||
<>
|
||||
<input
|
||||
type="number"
|
||||
value={mcpPort}
|
||||
onChange={(e) => setMcpPort(Number(e.target.value))}
|
||||
placeholder="MCP port (127.0.0.1)"
|
||||
/>
|
||||
<button type="button" className="secondary" onClick={handleCopyMcpConfig}>
|
||||
{mcpCopied ? "Copied!" : "Copy client config"}
|
||||
</button>
|
||||
<small className="field-hint">
|
||||
Save first, then paste the copied snippet into your MCP
|
||||
client. The server listens on 127.0.0.1 and requires the
|
||||
bridge password as a bearer token.
|
||||
</small>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isRunning && (
|
||||
<small className="field-hint">Changes apply after a restart.</small>
|
||||
)}
|
||||
<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}>
|
||||
{saved ? "Saved!" : "Save"}
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user