🔧 centralize config

This commit is contained in:
2026-03-06 21:52:06 +10:00
parent 30982c4cb6
commit 19e584dc47
13 changed files with 429 additions and 57 deletions

View File

@@ -0,0 +1,179 @@
.settings-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.6);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.settings-dialog {
background: var(--message-bg);
border: 1px solid var(--border);
border-radius: 8px;
width: 480px;
max-width: calc(100vw - 48px);
display: flex;
flex-direction: column;
font-family: var(--font-sans);
}
.settings-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px 14px;
border-bottom: 1px solid var(--border);
}
.settings-title {
font-size: 14px;
font-weight: 600;
color: var(--text-primary);
letter-spacing: 0.01em;
}
.settings-close {
background: none;
border: none;
color: var(--text-muted);
cursor: pointer;
font-size: 13px;
padding: 2px 4px;
border-radius: 3px;
line-height: 1;
}
.settings-close:hover {
color: var(--text-primary);
background: var(--hover-bg);
}
.settings-body {
padding: 20px;
display: flex;
flex-direction: column;
gap: 20px;
}
.settings-field {
display: flex;
flex-direction: column;
gap: 6px;
}
.settings-label {
font-size: 12px;
font-weight: 600;
color: var(--text-secondary);
text-transform: uppercase;
letter-spacing: 0.06em;
}
.settings-input {
background: var(--input-bg);
border: 1px solid var(--border);
border-radius: 4px;
color: var(--text-primary);
font-family: var(--font-sans);
font-size: 13px;
padding: 7px 10px;
outline: none;
width: 100%;
}
.settings-input:focus {
border-color: var(--accent);
}
.settings-input--path {
flex: 1;
border-radius: 4px 0 0 4px;
}
.settings-path-row {
display: flex;
}
.settings-browse-btn {
background: var(--input-bg);
border: 1px solid var(--border);
border-left: none;
border-radius: 0 4px 4px 0;
color: var(--text-secondary);
cursor: pointer;
font-family: var(--font-sans);
font-size: 12px;
padding: 7px 12px;
white-space: nowrap;
}
.settings-browse-btn:hover {
background: var(--hover-bg);
color: var(--text-primary);
}
.settings-hint {
font-size: 11px;
color: var(--text-muted);
margin: 0;
}
.settings-hint--warn {
color: var(--accent);
}
.settings-footer {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 20px;
border-top: 1px solid var(--border);
gap: 12px;
}
.settings-config-path {
font-size: 11px;
color: var(--text-muted);
font-family: monospace;
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.settings-footer-actions {
display: flex;
gap: 8px;
}
.settings-btn {
border: none;
border-radius: 4px;
cursor: pointer;
font-family: var(--font-sans);
font-size: 13px;
padding: 6px 16px;
transition: background 0.1s;
}
.settings-btn--secondary {
background: var(--input-bg);
color: var(--text-secondary);
border: 1px solid var(--border);
}
.settings-btn--secondary:hover {
background: var(--hover-bg);
color: var(--text-primary);
}
.settings-btn--primary {
background: var(--accent);
color: #fff;
}
.settings-btn--primary:hover {
background: var(--accent-hover);
}

View File

@@ -0,0 +1,104 @@
import { useEffect, useRef, useState } from 'react'
import './Settings.css'
interface Props {
onClose: () => void
}
export function SettingsDialog({ onClose }: Props): JSX.Element {
const [apiKey, setApiKey] = useState('')
const [projectPath, setProjectPath] = useState('')
const [originalPath, setOriginalPath] = useState('')
const [saved, setSaved] = useState(false)
const overlayRef = useRef<HTMLDivElement>(null)
useEffect(() => {
window.api.readConfig().then((cfg) => {
setApiKey(cfg.apiKey ?? '')
setProjectPath(cfg.projectPath ?? '')
setOriginalPath(cfg.projectPath ?? '')
})
}, [])
useEffect(() => {
const handler = (e: KeyboardEvent): void => {
if (e.key === 'Escape') onClose()
}
window.addEventListener('keydown', handler)
return () => window.removeEventListener('keydown', handler)
}, [onClose])
const handleOverlayClick = (e: React.MouseEvent): void => {
if (e.target === overlayRef.current) onClose()
}
const handleBrowse = async (): Promise<void> => {
const picked = await window.api.pickProjectFolder()
if (picked) setProjectPath(picked)
}
const handleSave = async (): Promise<void> => {
await window.api.writeConfig({ apiKey: apiKey || undefined, projectPath: projectPath || undefined })
setSaved(true)
setTimeout(() => setSaved(false), 2000)
}
const pathChanged = projectPath !== originalPath
return (
<div className="settings-overlay" ref={overlayRef} onClick={handleOverlayClick}>
<div className="settings-dialog" role="dialog" aria-modal="true" aria-label="Preferences">
<div className="settings-header">
<span className="settings-title">Preferences</span>
<button className="settings-close" onClick={onClose} aria-label="Close"></button>
</div>
<div className="settings-body">
<div className="settings-field">
<label className="settings-label" htmlFor="settings-api-key">Anthropic API Key</label>
<input
id="settings-api-key"
className="settings-input"
type="password"
value={apiKey}
onChange={(e) => setApiKey(e.target.value)}
placeholder="sk-ant-…"
autoComplete="off"
spellCheck={false}
/>
<p className="settings-hint">Changes take effect immediately no restart needed.</p>
</div>
<div className="settings-field">
<label className="settings-label" htmlFor="settings-project-path">Project Folder</label>
<div className="settings-path-row">
<input
id="settings-project-path"
className="settings-input settings-input--path"
type="text"
value={projectPath}
onChange={(e) => setProjectPath(e.target.value)}
placeholder="/path/to/draft"
spellCheck={false}
/>
<button className="settings-browse-btn" onClick={handleBrowse}>Browse</button>
</div>
{pathChanged && (
<p className="settings-hint settings-hint--warn">Restart the app to load the new project.</p>
)}
</div>
</div>
<div className="settings-footer">
<span className="settings-config-path">Config: ~/.hohoff/config.json</span>
<div className="settings-footer-actions">
<button className="settings-btn settings-btn--secondary" onClick={onClose}>Cancel</button>
<button className="settings-btn settings-btn--primary" onClick={handleSave}>
{saved ? 'Saved ✓' : 'Save'}
</button>
</div>
</div>
</div>
</div>
)
}