Redesign popup UI, rename thresholds panel to Settings, surface errors on main screen
UX changes: - Settings load/save errors now surface as a dismissible banner on the main popup screen, not only inside the collapsed panel. Settings are fetched eagerly (like lock state/connection status already were) instead of lazily on first expand, so a load failure is visible immediately. - The "Adjust loss thresholds" toggle/panel is renamed to "Settings" (with "Loss thresholds" as a subsection heading, since that's the only setting today), and gains a "Done" affordance to collapse it back rather than only being expandable. - Lock status is now the visual hero (a bordered status card with an icon), rather than one item in a flat list alongside connection status and account actions. Visual changes: - style.css rewritten around CSS custom-property design tokens (spacing/radius/font-size scale, semantic colors) with explicit light and dark palettes, instead of hardcoded hex colors reused as-is across both color schemes. - Popup widened 280px -> 320px for breathing room; card-based grouping (status, settings) replaces the previous flat stack of sections. No wire-protocol or messaging-contract changes. Co-Authored-By: Claude Sonnet 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Tn7uDYjTuZEbLPwpEiSCUw
This commit is contained in:
@@ -20,6 +20,9 @@ const CONNECTION_LABELS: Record<ConnectionStatus, string> = {
|
||||
export function App() {
|
||||
const [lockState, setLockState] = useState<LockState>(DEFAULT_LOCK_STATE);
|
||||
const [connectionStatus, setConnectionStatus] = useState<ConnectionStatus>(DEFAULT_CONNECTION_STATUS);
|
||||
const [settings, setSettings] = useState<ThresholdSettings | undefined>(undefined);
|
||||
const [settingsStatus, setSettingsStatus] = useState<'loading' | 'idle' | 'saving'>('loading');
|
||||
const [settingsError, setSettingsError] = useState<string | undefined>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
browser.runtime
|
||||
@@ -30,6 +33,17 @@ export function App() {
|
||||
.sendMessage({ type: 'osias:get-connection-status' } satisfies OsiasMessage)
|
||||
.then((status: ConnectionStatus | undefined) => setConnectionStatus(status ?? DEFAULT_CONNECTION_STATUS));
|
||||
|
||||
browser.runtime
|
||||
.sendMessage({ type: 'osias:get-settings' } satisfies OsiasMessage)
|
||||
.then((result: SettingsResult | undefined) => {
|
||||
setSettingsStatus('idle');
|
||||
if (result && 'settings' in result) {
|
||||
setSettings(result.settings);
|
||||
} else {
|
||||
setSettingsError(result?.error ?? 'Failed to load settings.');
|
||||
}
|
||||
});
|
||||
|
||||
const listener = (message: OsiasMessage) => {
|
||||
if (message?.type === 'osias:lock-state-changed') {
|
||||
setLockState(message.state);
|
||||
@@ -49,44 +63,80 @@ export function App() {
|
||||
void browser.runtime.sendMessage({ type: 'osias:switch-account' } satisfies OsiasMessage);
|
||||
}
|
||||
|
||||
function saveSettings(form: ThresholdForm): void {
|
||||
setSettingsStatus('saving');
|
||||
setSettingsError(undefined);
|
||||
browser.runtime
|
||||
.sendMessage({ type: 'osias:update-settings', settings: fromForm(form) } satisfies OsiasMessage)
|
||||
.then((result: SettingsResult | undefined) => {
|
||||
setSettingsStatus('idle');
|
||||
if (result && 'settings' in result) {
|
||||
setSettings(result.settings);
|
||||
} else {
|
||||
setSettingsError(result?.error ?? 'Failed to save settings.');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="popup">
|
||||
<header>
|
||||
<h1>Osias</h1>
|
||||
<p className="subtitle">Trading companion</p>
|
||||
<div className="brand">
|
||||
<h1>Osias</h1>
|
||||
<p className="subtitle">Trading companion</p>
|
||||
</div>
|
||||
<div className="connection" title={CONNECTION_LABELS[connectionStatus]}>
|
||||
<span className={`connection-dot connection-${connectionStatus}`} />
|
||||
{CONNECTION_LABELS[connectionStatus]}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section className="connection">
|
||||
<span className={`connection-dot connection-${connectionStatus}`} />
|
||||
{' '}
|
||||
{CONNECTION_LABELS[connectionStatus]}
|
||||
</section>
|
||||
{settingsError && (
|
||||
<div className="error-banner" role="alert">
|
||||
<span className="error-icon" aria-hidden="true">
|
||||
⚠
|
||||
</span>
|
||||
<span className="error-text">{settingsError}</span>
|
||||
<button type="button" className="error-dismiss" aria-label="Dismiss" onClick={() => setSettingsError(undefined)}>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<section className="status">
|
||||
<section className={`status-card ${lockState.locked ? 'status-card-locked' : 'status-card-unlocked'}`}>
|
||||
{lockState.locked ? (
|
||||
<>
|
||||
<div className="badge badge-locked">🔒 Locked</div>
|
||||
<p className="reason">{lockState.reason ?? 'No reason provided.'}</p>
|
||||
<p className="scope">Scope: {lockState.scope}</p>
|
||||
<div className="status-icon">🔒</div>
|
||||
<div className="status-body">
|
||||
<div className="badge badge-locked">Locked</div>
|
||||
<p className="reason">{lockState.reason ?? 'No reason provided.'}</p>
|
||||
<p className="scope">Scope: {lockState.scope}</p>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="badge badge-unlocked">Unlocked</div>
|
||||
<>
|
||||
<div className="status-icon">✓</div>
|
||||
<div className="status-body">
|
||||
<div className="badge badge-unlocked">Unlocked</div>
|
||||
<p className="reason reason-muted">No active restrictions.</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{connectionStatus === 'connected' && (
|
||||
<div className="account-actions">
|
||||
<button type="button" className="sign-out-button" onClick={switchAccount}>
|
||||
<button type="button" className="ghost-button" onClick={switchAccount}>
|
||||
Switch account
|
||||
</button>
|
||||
<button type="button" className="sign-out-button" onClick={signOut}>
|
||||
<button type="button" className="ghost-button" onClick={signOut}>
|
||||
Sign out
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<ThresholdsPanel />
|
||||
</>
|
||||
<SettingsPanel settings={settings} status={settingsStatus} onSave={saveSettings} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -128,60 +178,44 @@ function fromForm(form: ThresholdForm): Partial<ThresholdSettings> {
|
||||
};
|
||||
}
|
||||
|
||||
function ThresholdsPanel() {
|
||||
function SettingsPanel({
|
||||
settings,
|
||||
status,
|
||||
onSave,
|
||||
}: {
|
||||
settings: ThresholdSettings | undefined;
|
||||
status: 'loading' | 'idle' | 'saving';
|
||||
onSave: (form: ThresholdForm) => void;
|
||||
}) {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const [form, setForm] = useState<ThresholdForm>(() => toForm(DEFAULT_THRESHOLD_SETTINGS));
|
||||
const [status, setStatus] = useState<'idle' | 'loading' | 'saving'>('idle');
|
||||
const [error, setError] = useState<string | undefined>(undefined);
|
||||
const [form, setForm] = useState<ThresholdForm>(() => toForm(settings ?? DEFAULT_THRESHOLD_SETTINGS));
|
||||
|
||||
function expand(): void {
|
||||
setExpanded(true);
|
||||
if (status !== 'idle') return;
|
||||
setStatus('loading');
|
||||
setError(undefined);
|
||||
browser.runtime
|
||||
.sendMessage({ type: 'osias:get-settings' } satisfies OsiasMessage)
|
||||
.then((result: SettingsResult | undefined) => {
|
||||
if (result && 'settings' in result) {
|
||||
setForm(toForm(result.settings));
|
||||
setStatus('idle');
|
||||
} else {
|
||||
setError(result?.error ?? 'Failed to load settings.');
|
||||
setStatus('idle');
|
||||
}
|
||||
});
|
||||
}
|
||||
useEffect(() => {
|
||||
if (settings) setForm(toForm(settings));
|
||||
}, [settings]);
|
||||
|
||||
function field(key: keyof ThresholdForm, value: string): void {
|
||||
setForm((prev) => ({ ...prev, [key]: value }));
|
||||
}
|
||||
|
||||
function save(): void {
|
||||
setStatus('saving');
|
||||
setError(undefined);
|
||||
browser.runtime
|
||||
.sendMessage({ type: 'osias:update-settings', settings: fromForm(form) } satisfies OsiasMessage)
|
||||
.then((result: SettingsResult | undefined) => {
|
||||
setStatus('idle');
|
||||
if (result && 'settings' in result) {
|
||||
setForm(toForm(result.settings));
|
||||
} else {
|
||||
setError(result?.error ?? 'Failed to save settings.');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!expanded) {
|
||||
return (
|
||||
<button type="button" className="thresholds-toggle" onClick={expand}>
|
||||
Adjust loss thresholds →
|
||||
<button type="button" className="settings-toggle" onClick={() => setExpanded(true)}>
|
||||
<span>Settings</span>
|
||||
<span aria-hidden="true">→</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="thresholds-panel">
|
||||
<h2>Loss thresholds</h2>
|
||||
<section className="settings-panel">
|
||||
<div className="settings-panel-header">
|
||||
<h2>Settings</h2>
|
||||
<button type="button" className="settings-collapse" onClick={() => setExpanded(false)}>
|
||||
Done
|
||||
</button>
|
||||
</div>
|
||||
<h3>Loss thresholds</h3>
|
||||
|
||||
<label>
|
||||
Exit counts as "full" at
|
||||
@@ -267,9 +301,12 @@ function ThresholdsPanel() {
|
||||
</div>
|
||||
</label>
|
||||
|
||||
{error && <p className="thresholds-error">{error}</p>}
|
||||
|
||||
<button type="button" className="sign-out-button" onClick={save} disabled={status === 'saving'}>
|
||||
<button
|
||||
type="button"
|
||||
className="primary-button"
|
||||
onClick={() => onSave(form)}
|
||||
disabled={status === 'saving' || !settings}
|
||||
>
|
||||
{status === 'saving' ? 'Saving…' : 'Save'}
|
||||
</button>
|
||||
</section>
|
||||
|
||||
Reference in New Issue
Block a user