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:
2026-09-08 13:20:05 +02:00
co-authored by claude
parent 5cfb0f0b53
commit 9a774a38d4
3 changed files with 359 additions and 139 deletions
+97 -60
View File
@@ -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>