Files
EJClaw/apps/dashboard/src/SettingsPanelChrome.tsx
Eyejoker fd3145e2a7 feat: refresh dashboard settings UX and Codex feature toggles (#153)
* feat: refresh dashboard settings UX and Codex feature toggles

Improve settings/tasks mobile layout, model effort validation by agent type, and preset models for GPT 5.5 and Opus 4.7. Store Codex fast mode and goals in config.toml with Claude fastMode session sync and updated docs for Codex 0.133.

Co-authored-by: Cursor <cursoragent@cursor.com>

* style: apply pre-commit formatting after dashboard settings commit

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-23 12:18:42 +09:00

268 lines
6.5 KiB
TypeScript

import { useState, type ReactNode } from 'react';
import { LOCALES, languageNames, type Locale, type Messages } from './i18n';
export const SETTINGS_NAV_ITEMS = [
{ targetId: 'settings-general', navKey: 'general' },
{ targetId: 'settings-models', navKey: 'models' },
{ targetId: 'settings-runtime', navKey: 'runtime' },
{ targetId: 'settings-moa', navKey: 'moa' },
{ targetId: 'settings-codex', navKey: 'codex' },
{ targetId: 'settings-accounts', navKey: 'accounts' },
] as const;
export type SettingsSectionId = (typeof SETTINGS_NAV_ITEMS)[number]['targetId'];
type SettingsNavKey = (typeof SETTINGS_NAV_ITEMS)[number]['navKey'];
function navItem(t: Messages, navKey: SettingsNavKey) {
return t.settings.nav[navKey];
}
export function SettingsNav({
activeSection,
onSelect,
t,
}: {
activeSection: SettingsSectionId;
onSelect: (section: SettingsSectionId) => void;
t: Messages;
}) {
return (
<div className="settings-nav-scroll">
<div
className="settings-nav"
aria-label={t.settings.navAria}
role="tablist"
>
{SETTINGS_NAV_ITEMS.map((item) => {
const copy = navItem(t, item.navKey);
return (
<button
aria-controls={item.targetId}
aria-selected={activeSection === item.targetId}
data-settings-target={item.targetId}
id={`${item.targetId}-tab`}
key={item.targetId}
onClick={() => onSelect(item.targetId)}
role="tab"
type="button"
>
<strong>{copy.title}</strong>
<small>{copy.detail}</small>
</button>
);
})}
</div>
</div>
);
}
export function GeneralSettings({
locale,
nickname,
onLocaleChange,
onNicknameChange,
t,
}: {
locale: Locale;
nickname: string;
onLocaleChange: (locale: Locale) => void;
onNicknameChange: (next: string) => void;
t: Messages;
}) {
const section = t.settings.sections.general;
return (
<section
aria-labelledby="settings-general-tab"
className="settings-section"
id="settings-general"
role="tabpanel"
>
<SettingsSectionHeading
description={section.description}
detail={section.kicker}
title={section.title}
/>
<div className="settings-card">
<div className="settings-form-grid">
<label className="settings-row">
<span className="settings-label">{t.settings.nicknameLabel}</span>
<input
maxLength={32}
onChange={(event) => onNicknameChange(event.target.value)}
placeholder={t.settings.nicknamePlaceholder}
type="text"
value={nickname}
/>
<small className="settings-hint">{t.settings.nicknameHelp}</small>
</label>
<label className="settings-row">
<span className="settings-label">{t.settings.languageLabel}</span>
<select
aria-label={t.settings.languageLabel}
onChange={(event) => onLocaleChange(event.target.value as Locale)}
value={locale}
>
{LOCALES.map((item) => (
<option key={item} value={item}>
{languageNames[item]}
</option>
))}
</select>
</label>
</div>
</div>
</section>
);
}
export function SettingsSectionHeading({
detail,
title,
description,
}: {
detail: string;
title: string;
description: string;
}) {
return (
<header className="settings-section-head">
<span>{detail}</span>
<h3>{title}</h3>
<p>{description}</p>
</header>
);
}
export function SettingsCard({
title,
description,
actions,
children,
}: {
title?: string;
description?: string;
actions?: ReactNode;
children: ReactNode;
}) {
return (
<section className="settings-card">
{title || description || actions ? (
<header className="settings-card-head">
<div>
{title ? <h4>{title}</h4> : null}
{description ? (
<p className="settings-hint">{description}</p>
) : null}
</div>
{actions ? (
<div className="settings-card-actions">{actions}</div>
) : null}
</header>
) : null}
{children}
</section>
);
}
export function SettingsCollapsible({
title,
description,
defaultOpen = false,
children,
}: {
title: string;
description?: string;
defaultOpen?: boolean;
children: ReactNode;
}) {
const [open, setOpen] = useState(defaultOpen);
return (
<section className={`settings-collapsible${open ? ' is-open' : ''}`}>
<button
aria-expanded={open}
className="settings-collapsible-trigger"
onClick={() => setOpen((value) => !value)}
type="button"
>
<span className="settings-collapsible-text">
<strong>{title}</strong>
{description ? <small>{description}</small> : null}
</span>
<span aria-hidden="true" className="settings-collapsible-chevron">
{open ? '▾' : '▸'}
</span>
</button>
{open ? (
<div className="settings-collapsible-body">{children}</div>
) : null}
</section>
);
}
export function SettingsSaveBar({
busy,
dirty,
label,
savingLabel,
onSave,
savedHint,
showSavedHint,
saveDisabled = false,
}: {
busy: boolean;
dirty: boolean;
label: string;
savingLabel: string;
onSave: () => void;
savedHint?: string;
showSavedHint?: boolean;
saveDisabled?: boolean;
}) {
return (
<div className="settings-actions">
<button
className="settings-save"
disabled={!dirty || busy || saveDisabled}
onClick={onSave}
type="button"
>
{busy ? savingLabel : label}
</button>
{showSavedHint && savedHint ? (
<small className="settings-hint">{savedHint}</small>
) : null}
</div>
);
}
export function SettingsApplyCard({
onRestartStack,
t,
}: {
onRestartStack: () => void;
t: Messages;
}) {
const apply = t.settings.apply;
return (
<section aria-label={apply.aria} className="settings-apply-card">
<div>
<span className="settings-kicker">{apply.kicker}</span>
<strong>{apply.title}</strong>
<small>{apply.hint}</small>
</div>
<button
className="settings-restart"
onClick={onRestartStack}
type="button"
>
{apply.restart}
</button>
</section>
);
}