refactor: extract SettingsPage GeneralSettingsTab

- Move GeneralSettingsTab to components/features/settings/
- Re-export from page for backward compatibility
- Slim SettingsPage from 284 to ~150 lines

Quality gates: tsc --noEmit passes, npm run build passes
This commit is contained in:
Developer
2026-06-05 19:20:36 +00:00
parent e7f219f7c3
commit e49d049455
2 changed files with 151 additions and 149 deletions
@@ -0,0 +1,149 @@
import { useOutletContext } from "react-router-dom";
import { Icon } from "../../icon";
import type { UserConfig, UserConfigUpdate } from "../../../api/settings";
type SettingsOutletContext = {
config: UserConfig;
handleChange: (
key: keyof UserConfigUpdate,
value: string | string[] | null,
) => void;
handleSave: () => Promise<void>;
saveStatus: "idle" | "saving" | "saved" | "error";
};
const THEME_OPTIONS = [
{ value: "system", label: "System" },
{ value: "light", label: "Light" },
{ value: "dark", label: "Dark" },
];
const TOAST_LEVEL_OPTIONS = [
{ value: "all", label: "All" },
{ value: "errors", label: "Errors only" },
{ value: "none", label: "None" },
];
const MUTE_CATEGORIES = ["instance", "system", "health", "security"];
export const GeneralSettingsTab = () => {
const { config, handleChange, handleSave, saveStatus } =
useOutletContext<SettingsOutletContext>();
return (
<div className="stack">
<h2>General</h2>
<label className="form-field">
Theme
<select
value={config.theme}
onChange={(e) => handleChange("theme", e.target.value)}
>
{THEME_OPTIONS.map((opt) => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</select>
</label>
<label className="form-field">
Git user name
<input
type="text"
value={config.git_user_name ?? ""}
onChange={(e) =>
handleChange("git_user_name", e.target.value || null)
}
placeholder="Your git commit name"
/>
</label>
<label className="form-field">
Git user email
<input
type="email"
value={config.git_user_email ?? ""}
onChange={(e) =>
handleChange("git_user_email", e.target.value || null)
}
placeholder="your.email@example.com"
/>
</label>
<label className="form-field">
Default editor
<input
type="text"
value={config.default_editor ?? ""}
onChange={(e) =>
handleChange("default_editor", e.target.value || null)
}
placeholder="e.g., vscode, vim, cursor"
/>
</label>
<h3>Notifications</h3>
<label className="form-field">
Toast level
<select
value={config.notification_toast_level ?? "all"}
onChange={(e) =>
handleChange("notification_toast_level", e.target.value)
}
>
{TOAST_LEVEL_OPTIONS.map((opt) => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</select>
</label>
<fieldset className="form-field">
<legend>Mute categories</legend>
<div className="stack-sm">
{MUTE_CATEGORIES.map((cat) => (
<label
key={cat}
style={{ display: "flex", alignItems: "center", gap: "0.5rem" }}
>
<input
type="checkbox"
checked={(config.notification_mute_categories ?? []).includes(
cat,
)}
onChange={(e) => {
const current = config.notification_mute_categories ?? [];
const next = e.target.checked
? [...current, cat]
: current.filter((c) => c !== cat);
handleChange("notification_mute_categories", next);
}}
/>
{cat}
</label>
))}
</div>
</fieldset>
<div className="settings-actions">
<button
className="primary-button"
onClick={() => void handleSave()}
type="button"
>
{saveStatus === "saving" ? (
<>
<Icon name="loading" size="sm" /> Saving...
</>
) : (
<>
<Icon name="save" size="sm" /> Save Settings
</>
)}
</button>
{saveStatus === "saved" && (
<span className="success-text">Settings saved!</span>
)}
{saveStatus === "error" && (
<span className="error-text">Failed to save</span>
)}
</div>
</div>
);
};