Group Settings panel into accordion sections
Multiple-open accordion with per-section state persisted to localStorage. Sections: General, Backends, Container, Git/SSH, Tools, Updates. General is open by default; the rest are collapsed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -9,6 +9,7 @@ import EnvVarsModal from "../projects/EnvVarsModal";
|
|||||||
import { detectHostTimezone } from "../../lib/tauri-commands";
|
import { detectHostTimezone } from "../../lib/tauri-commands";
|
||||||
import type { EnvVar } from "../../lib/types";
|
import type { EnvVar } from "../../lib/types";
|
||||||
import Tooltip from "../ui/Tooltip";
|
import Tooltip from "../ui/Tooltip";
|
||||||
|
import AccordionSection from "../ui/AccordionSection";
|
||||||
import WebTerminalSettings from "./WebTerminalSettings";
|
import WebTerminalSettings from "./WebTerminalSettings";
|
||||||
import SttSettings from "./SttSettings";
|
import SttSettings from "./SttSettings";
|
||||||
|
|
||||||
@@ -61,153 +62,160 @@ export default function SettingsPanel() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-4 space-y-6">
|
<div className="p-4 space-y-3">
|
||||||
<h2 className="text-xs font-semibold uppercase text-[var(--text-secondary)]">
|
<h2 className="text-xs font-semibold uppercase text-[var(--text-secondary)]">
|
||||||
Settings
|
Settings
|
||||||
</h2>
|
</h2>
|
||||||
<DockerSettings />
|
|
||||||
<AwsSettings />
|
|
||||||
|
|
||||||
{/* Default SSH Key Directory */}
|
<AccordionSection id="general" title="General">
|
||||||
<div>
|
{/* Container Timezone */}
|
||||||
<label className="block text-sm font-medium mb-1">Default SSH Key Directory<Tooltip text="Global default SSH key directory. Mounted into containers that don't have a per-project SSH path set." /></label>
|
<div>
|
||||||
<p className="text-xs text-[var(--text-secondary)] mb-1.5">
|
<label className="block text-sm font-medium mb-1">Container Timezone<Tooltip text="Sets the timezone inside containers. Affects scheduled task timing and log timestamps." /></label>
|
||||||
Mounted into all containers unless overridden by a per-project setting.
|
<p className="text-xs text-[var(--text-secondary)] mb-1.5">
|
||||||
</p>
|
Timezone for containers — affects scheduled task timing (IANA format, e.g. America/New_York)
|
||||||
<input
|
</p>
|
||||||
type="text"
|
<input
|
||||||
value={sshKeyPath}
|
type="text"
|
||||||
onChange={(e) => setSshKeyPath(e.target.value)}
|
value={timezone}
|
||||||
onBlur={async () => {
|
onChange={(e) => setTimezone(e.target.value)}
|
||||||
if (appSettings) {
|
onBlur={async () => {
|
||||||
await saveSettings({ ...appSettings, default_ssh_key_path: sshKeyPath || null });
|
if (appSettings) {
|
||||||
}
|
await saveSettings({ ...appSettings, timezone: timezone || null });
|
||||||
}}
|
}
|
||||||
placeholder="~/.ssh"
|
}}
|
||||||
className="w-full px-2 py-1 text-sm bg-[var(--bg-primary)] border border-[var(--border-color)] rounded focus:outline-none focus:border-[var(--accent)]"
|
placeholder="UTC"
|
||||||
/>
|
className="w-full px-2 py-1 text-sm bg-[var(--bg-primary)] border border-[var(--border-color)] rounded focus:outline-none focus:border-[var(--accent)]"
|
||||||
</div>
|
/>
|
||||||
|
|
||||||
{/* Default Git Name */}
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium mb-1">Default Git Name<Tooltip text="Sets git user.name inside containers. Per-project Git Name takes precedence." /></label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={gitName}
|
|
||||||
onChange={(e) => setGitName(e.target.value)}
|
|
||||||
onBlur={async () => {
|
|
||||||
if (appSettings) {
|
|
||||||
await saveSettings({ ...appSettings, default_git_user_name: gitName || null });
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
placeholder="Your Name"
|
|
||||||
className="w-full px-2 py-1 text-sm bg-[var(--bg-primary)] border border-[var(--border-color)] rounded focus:outline-none focus:border-[var(--accent)]"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Default Git Email */}
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium mb-1">Default Git Email<Tooltip text="Sets git user.email inside containers. Per-project Git Email takes precedence." /></label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={gitEmail}
|
|
||||||
onChange={(e) => setGitEmail(e.target.value)}
|
|
||||||
onBlur={async () => {
|
|
||||||
if (appSettings) {
|
|
||||||
await saveSettings({ ...appSettings, default_git_user_email: gitEmail || null });
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
placeholder="you@example.com"
|
|
||||||
className="w-full px-2 py-1 text-sm bg-[var(--bg-primary)] border border-[var(--border-color)] rounded focus:outline-none focus:border-[var(--accent)]"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Container Timezone */}
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium mb-1">Container Timezone<Tooltip text="Sets the timezone inside containers. Affects scheduled task timing and log timestamps." /></label>
|
|
||||||
<p className="text-xs text-[var(--text-secondary)] mb-1.5">
|
|
||||||
Timezone for containers — affects scheduled task timing (IANA format, e.g. America/New_York)
|
|
||||||
</p>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={timezone}
|
|
||||||
onChange={(e) => setTimezone(e.target.value)}
|
|
||||||
onBlur={async () => {
|
|
||||||
if (appSettings) {
|
|
||||||
await saveSettings({ ...appSettings, timezone: timezone || null });
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
placeholder="UTC"
|
|
||||||
className="w-full px-2 py-1 text-sm bg-[var(--bg-primary)] border border-[var(--border-color)] rounded focus:outline-none focus:border-[var(--accent)]"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Global Claude Instructions */}
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium mb-1">Claude Instructions<Tooltip text="Global instructions applied to all projects. Written to ~/.claude/CLAUDE.md in every container." /></label>
|
|
||||||
<p className="text-xs text-[var(--text-secondary)] mb-1.5">
|
|
||||||
Global instructions applied to all projects (written to ~/.claude/CLAUDE.md in containers)
|
|
||||||
</p>
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<span className="text-xs text-[var(--text-secondary)]">
|
|
||||||
{globalInstructions ? "Configured" : "Not set"}
|
|
||||||
</span>
|
|
||||||
<button
|
|
||||||
onClick={() => setShowInstructionsModal(true)}
|
|
||||||
className="text-xs px-2 py-0.5 text-[var(--accent)] hover:text-[var(--accent-hover)] hover:bg-[var(--bg-primary)] rounded transition-colors"
|
|
||||||
>
|
|
||||||
Edit
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Global Environment Variables */}
|
{/* Global Claude Instructions */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium mb-1">Global Environment Variables<Tooltip text="Env vars injected into all containers. Per-project vars with the same key take precedence." /></label>
|
<label className="block text-sm font-medium mb-1">Claude Instructions<Tooltip text="Global instructions applied to all projects. Written to ~/.claude/CLAUDE.md in every container." /></label>
|
||||||
<p className="text-xs text-[var(--text-secondary)] mb-1.5">
|
<p className="text-xs text-[var(--text-secondary)] mb-1.5">
|
||||||
Applied to all project containers. Per-project variables override global ones with the same key.
|
Global instructions applied to all projects (written to ~/.claude/CLAUDE.md in containers)
|
||||||
</p>
|
</p>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span className="text-xs text-[var(--text-secondary)]">
|
<span className="text-xs text-[var(--text-secondary)]">
|
||||||
{globalEnvVars.length > 0 ? `${globalEnvVars.length} variable${globalEnvVars.length === 1 ? "" : "s"}` : "None"}
|
{globalInstructions ? "Configured" : "Not set"}
|
||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowEnvVarsModal(true)}
|
onClick={() => setShowInstructionsModal(true)}
|
||||||
className="text-xs px-2 py-0.5 text-[var(--accent)] hover:text-[var(--accent-hover)] hover:bg-[var(--bg-primary)] rounded transition-colors"
|
className="text-xs px-2 py-0.5 text-[var(--accent)] hover:text-[var(--accent-hover)] hover:bg-[var(--bg-primary)] rounded transition-colors"
|
||||||
>
|
>
|
||||||
Edit
|
Edit
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Global Claude Code Settings */}
|
{/* Global Environment Variables */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium mb-1">Claude Code Settings<Tooltip text="Global defaults for Claude Code CLI behavior (TUI mode, effort, focus mode, caching, etc.). Per-project settings override these." /></label>
|
<label className="block text-sm font-medium mb-1">Global Environment Variables<Tooltip text="Env vars injected into all containers. Per-project vars with the same key take precedence." /></label>
|
||||||
<p className="text-xs text-[var(--text-secondary)] mb-1.5">
|
<p className="text-xs text-[var(--text-secondary)] mb-1.5">
|
||||||
Default Claude Code CLI settings applied to all projects. Per-project settings take precedence.
|
Applied to all project containers. Per-project variables override global ones with the same key.
|
||||||
</p>
|
</p>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span className="text-xs text-[var(--text-secondary)]">
|
<span className="text-xs text-[var(--text-secondary)]">
|
||||||
{appSettings?.global_claude_code_settings ? "Configured" : "Using defaults"}
|
{globalEnvVars.length > 0 ? `${globalEnvVars.length} variable${globalEnvVars.length === 1 ? "" : "s"}` : "None"}
|
||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowClaudeCodeSettingsModal(true)}
|
onClick={() => setShowEnvVarsModal(true)}
|
||||||
className="text-xs px-2 py-0.5 text-[var(--accent)] hover:text-[var(--accent-hover)] hover:bg-[var(--bg-primary)] rounded transition-colors"
|
className="text-xs px-2 py-0.5 text-[var(--accent)] hover:text-[var(--accent-hover)] hover:bg-[var(--bg-primary)] rounded transition-colors"
|
||||||
>
|
>
|
||||||
Edit
|
Edit
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Web Terminal */}
|
{/* Global Claude Code Settings */}
|
||||||
<WebTerminalSettings />
|
<div>
|
||||||
|
<label className="block text-sm font-medium mb-1">Claude Code Settings<Tooltip text="Global defaults for Claude Code CLI behavior (TUI mode, effort, focus mode, caching, etc.). Per-project settings override these." /></label>
|
||||||
|
<p className="text-xs text-[var(--text-secondary)] mb-1.5">
|
||||||
|
Default Claude Code CLI settings applied to all projects. Per-project settings take precedence.
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-xs text-[var(--text-secondary)]">
|
||||||
|
{appSettings?.global_claude_code_settings ? "Configured" : "Using defaults"}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowClaudeCodeSettingsModal(true)}
|
||||||
|
className="text-xs px-2 py-0.5 text-[var(--accent)] hover:text-[var(--accent-hover)] hover:bg-[var(--bg-primary)] rounded transition-colors"
|
||||||
|
>
|
||||||
|
Edit
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</AccordionSection>
|
||||||
|
|
||||||
{/* Speech to Text */}
|
<AccordionSection id="backends" title="Backends" defaultOpen={false}>
|
||||||
<SttSettings />
|
<AwsSettings />
|
||||||
|
</AccordionSection>
|
||||||
|
|
||||||
{/* Updates section */}
|
<AccordionSection id="container" title="Container" defaultOpen={false}>
|
||||||
<div>
|
<DockerSettings />
|
||||||
<label className="block text-sm font-medium mb-2">Updates<Tooltip text="Check for new versions of the Triple-C app and container image." /></label>
|
</AccordionSection>
|
||||||
|
|
||||||
|
<AccordionSection id="git-ssh" title="Git / SSH" defaultOpen={false}>
|
||||||
|
{/* Default SSH Key Directory */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium mb-1">Default SSH Key Directory<Tooltip text="Global default SSH key directory. Mounted into containers that don't have a per-project SSH path set." /></label>
|
||||||
|
<p className="text-xs text-[var(--text-secondary)] mb-1.5">
|
||||||
|
Mounted into all containers unless overridden by a per-project setting.
|
||||||
|
</p>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={sshKeyPath}
|
||||||
|
onChange={(e) => setSshKeyPath(e.target.value)}
|
||||||
|
onBlur={async () => {
|
||||||
|
if (appSettings) {
|
||||||
|
await saveSettings({ ...appSettings, default_ssh_key_path: sshKeyPath || null });
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
placeholder="~/.ssh"
|
||||||
|
className="w-full px-2 py-1 text-sm bg-[var(--bg-primary)] border border-[var(--border-color)] rounded focus:outline-none focus:border-[var(--accent)]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Default Git Name */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium mb-1">Default Git Name<Tooltip text="Sets git user.name inside containers. Per-project Git Name takes precedence." /></label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={gitName}
|
||||||
|
onChange={(e) => setGitName(e.target.value)}
|
||||||
|
onBlur={async () => {
|
||||||
|
if (appSettings) {
|
||||||
|
await saveSettings({ ...appSettings, default_git_user_name: gitName || null });
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
placeholder="Your Name"
|
||||||
|
className="w-full px-2 py-1 text-sm bg-[var(--bg-primary)] border border-[var(--border-color)] rounded focus:outline-none focus:border-[var(--accent)]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Default Git Email */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium mb-1">Default Git Email<Tooltip text="Sets git user.email inside containers. Per-project Git Email takes precedence." /></label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={gitEmail}
|
||||||
|
onChange={(e) => setGitEmail(e.target.value)}
|
||||||
|
onBlur={async () => {
|
||||||
|
if (appSettings) {
|
||||||
|
await saveSettings({ ...appSettings, default_git_user_email: gitEmail || null });
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
placeholder="you@example.com"
|
||||||
|
className="w-full px-2 py-1 text-sm bg-[var(--bg-primary)] border border-[var(--border-color)] rounded focus:outline-none focus:border-[var(--accent)]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</AccordionSection>
|
||||||
|
|
||||||
|
<AccordionSection id="tools" title="Tools" defaultOpen={false}>
|
||||||
|
<WebTerminalSettings />
|
||||||
|
<SttSettings />
|
||||||
|
</AccordionSection>
|
||||||
|
|
||||||
|
<AccordionSection id="updates" title="Updates" defaultOpen={false}>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{appVersion && (
|
{appVersion && (
|
||||||
<p className="text-xs text-[var(--text-secondary)]">
|
<p className="text-xs text-[var(--text-secondary)]">
|
||||||
@@ -237,11 +245,11 @@ export default function SettingsPanel() {
|
|||||||
{imageUpdateInfo && (
|
{imageUpdateInfo && (
|
||||||
<div className="flex items-center gap-2 px-3 py-2 text-xs bg-[var(--bg-primary)] border border-[var(--warning,#f59e0b)] rounded">
|
<div className="flex items-center gap-2 px-3 py-2 text-xs bg-[var(--bg-primary)] border border-[var(--warning,#f59e0b)] rounded">
|
||||||
<span className="inline-block w-2 h-2 rounded-full bg-[var(--warning,#f59e0b)]" />
|
<span className="inline-block w-2 h-2 rounded-full bg-[var(--warning,#f59e0b)]" />
|
||||||
<span>A newer container image is available. Re-pull the image in Docker settings above to update.</span>
|
<span>A newer container image is available. Re-pull the image in Container settings above to update.</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</AccordionSection>
|
||||||
|
|
||||||
{showInstructionsModal && (
|
{showInstructionsModal && (
|
||||||
<ClaudeInstructionsModal
|
<ClaudeInstructionsModal
|
||||||
|
|||||||
65
app/src/components/ui/AccordionSection.tsx
Normal file
65
app/src/components/ui/AccordionSection.tsx
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import { useEffect, useState, type ReactNode } from "react";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
defaultOpen?: boolean;
|
||||||
|
storageKey?: string;
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadOpenState(storageKey: string, fallback: boolean): boolean {
|
||||||
|
try {
|
||||||
|
const stored = localStorage.getItem(storageKey);
|
||||||
|
if (stored === null) return fallback;
|
||||||
|
return stored === "1";
|
||||||
|
} catch {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function persistOpenState(storageKey: string, open: boolean) {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(storageKey, open ? "1" : "0");
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AccordionSection({ id, title, defaultOpen = true, storageKey, children }: Props) {
|
||||||
|
const key = storageKey ?? `triple-c.accordion.${id}`;
|
||||||
|
const [open, setOpen] = useState<boolean>(() => loadOpenState(key, defaultOpen));
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
persistOpenState(key, open);
|
||||||
|
}, [key, open]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="border border-[var(--border-color)] rounded">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setOpen(o => !o)}
|
||||||
|
aria-expanded={open}
|
||||||
|
className="w-full flex items-center justify-between px-3 py-2 text-left text-sm font-medium text-[var(--text-primary)] hover:bg-[var(--bg-primary)] transition-colors"
|
||||||
|
>
|
||||||
|
<span>{title}</span>
|
||||||
|
<svg
|
||||||
|
className={`w-4 h-4 text-[var(--text-secondary)] transition-transform ${open ? "rotate-90" : ""}`}
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
>
|
||||||
|
<polyline points="9 18 15 12 9 6" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
{open && (
|
||||||
|
<div className="px-3 py-3 border-t border-[var(--border-color)] space-y-4">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user