From c7d4bf82d7830c6b82b182d1bc271c1de546f572 Mon Sep 17 00:00:00 2001 From: Eyejoker Date: Mon, 4 May 2026 01:50:15 +0900 Subject: [PATCH] Refine settings IA with tabbed sections Split Settings into tabbed sections while preserving draft state, improve mobile Settings nav/touch targets, and update dashboard UX coverage for the Codex tab. --- apps/dashboard/src/MoaSettingsPanel.tsx | 7 +- apps/dashboard/src/SettingsPanel.test.ts | 13 +- apps/dashboard/src/SettingsPanel.tsx | 201 +++++++-------------- apps/dashboard/src/SettingsPanelChrome.tsx | 140 ++++++++++++++ apps/dashboard/src/styles.css | 27 ++- scripts/dashboard-ux.ts | 8 + 6 files changed, 246 insertions(+), 150 deletions(-) create mode 100644 apps/dashboard/src/SettingsPanelChrome.tsx diff --git a/apps/dashboard/src/MoaSettingsPanel.tsx b/apps/dashboard/src/MoaSettingsPanel.tsx index 8ff2f32..e277612 100644 --- a/apps/dashboard/src/MoaSettingsPanel.tsx +++ b/apps/dashboard/src/MoaSettingsPanel.tsx @@ -113,7 +113,12 @@ export function MoaSettingsPanel() { Object.values(apiKeys).some((value) => value.trim())); return ( -
+
Arbiter references

MoA 참조 모델

diff --git a/apps/dashboard/src/SettingsPanel.test.ts b/apps/dashboard/src/SettingsPanel.test.ts index 064ef1b..b6d934e 100644 --- a/apps/dashboard/src/SettingsPanel.test.ts +++ b/apps/dashboard/src/SettingsPanel.test.ts @@ -34,21 +34,20 @@ describe('SettingsPanel', () => { it('renders model, MoA, fast mode, and account controls', () => { const html = renderToStaticMarkup(createElement(SettingsPanel, baseProps)); - expect(html).toContain('모델'); - expect(html).toContain('MoA 참조 모델'); - expect(html).toContain('패스트 모드'); - expect(html).toContain('Codex 실험 기능'); + expect(html).toContain('role="tablist"'); + expect(html).toContain('role="tabpanel"'); + expect(html).toContain('aria-selected="true"'); + expect(html).toContain('data-settings-target="settings-models"'); + expect(html).toContain('data-settings-target="settings-moa"'); expect(html).toContain('data-settings-target="settings-codex"'); + expect(html).toContain('data-settings-target="settings-accounts"'); expect(html).toContain('aria-controls="settings-codex"'); expect(html).not.toContain('href="#settings-codex"'); - expect(html).toContain('/goal'); expect(html).toContain('settings-apply-card'); expect(html).not.toContain('settings-apply-bar'); expect(html).toContain('저장 후 재시작'); - expect(html).toContain('불러오는 중'); expect(html).toContain('Claude'); expect(html).toContain('계정'); - expect(html).toContain('전체 갱신'); expect(html).toContain('스택 재시작'); expect(html.match(/class="settings-restart"/g)).toHaveLength(1); }); diff --git a/apps/dashboard/src/SettingsPanel.tsx b/apps/dashboard/src/SettingsPanel.tsx index b436760..5727c3b 100644 --- a/apps/dashboard/src/SettingsPanel.tsx +++ b/apps/dashboard/src/SettingsPanel.tsx @@ -20,8 +20,15 @@ import { updateFastMode, updateModels, } from './api'; -import { LOCALES, languageNames, type Locale, type Messages } from './i18n'; +import { type Locale, type Messages } from './i18n'; import { MoaSettingsPanel } from './MoaSettingsPanel'; +import { + GeneralSettings, + SettingsApplyCard, + SettingsNav, + SettingsSectionHeading, + type SettingsSectionId, +} from './SettingsPanelChrome'; type AccountProvider = 'claude' | 'codex'; @@ -31,18 +38,6 @@ interface AccountData { codexCurrentIndex?: number; } -const SETTINGS_NAV_ITEMS = [ - { targetId: 'settings-general', title: '일반', detail: '표시 · 언어' }, - { - targetId: 'settings-models', - title: '모델', - detail: 'owner · reviewer · arbiter', - }, - { targetId: 'settings-moa', title: 'MoA', detail: '참조 모델 · 연결 테스트' }, - { targetId: 'settings-codex', title: 'Codex', detail: 'fast mode · /goal' }, - { targetId: 'settings-accounts', title: '계정', detail: 'Claude · Codex' }, -] as const; - export interface SettingsPanelProps { locale: Locale; nickname: string; @@ -60,142 +55,51 @@ export function SettingsPanel({ onRestartStack, t, }: SettingsPanelProps) { + const [activeSection, setActiveSection] = + useState('settings-general'); + return (
-
-
+
- + - + -
- -
- - -
-
+ - +
); } -function SettingsNav() { - const scrollToSection = (targetId: string) => { - document - .getElementById(targetId) - ?.scrollIntoView({ behavior: 'smooth', block: 'start' }); - }; - - return ( - - ); -} - -function SettingsSectionHeading({ - detail, - title, - description, -}: { - detail: string; - title: string; - description: string; -}) { - return ( -
- {detail} -

{title}

-

{description}

-
- ); -} - -function SettingsApplyCard({ onRestartStack }: { onRestartStack: () => void }) { - return ( -
-
- Apply - 저장 후 재시작 - 모델, MoA, Codex, 계정 변경은 스택 재시작 후 반영됩니다. -
- -
- ); -} - function ModelSettings() { const [config, setConfig] = useState(null); const [draft, setDraft] = useState(null); @@ -261,7 +165,12 @@ function ModelSettings() { JSON.stringify(draft) !== JSON.stringify(config); return ( -
+
+ +
+ + +
+
+ ); +} + function FastModeSettings() { const [state, setState] = useState(null); const [busy, setBusy] = useState(false); @@ -611,7 +541,12 @@ function AccountSettings() { } return ( -
+
void; +}) { + return ( +
+ {SETTINGS_NAV_ITEMS.map((item) => ( + + ))} +
+ ); +} + +export function GeneralSettings({ + locale, + nickname, + onLocaleChange, + onNicknameChange, + t, +}: { + locale: Locale; + nickname: string; + onLocaleChange: (locale: Locale) => void; + onNicknameChange: (next: string) => void; + t: Messages; +}) { + return ( +
+ +
+ + +
+
+ ); +} + +export function SettingsSectionHeading({ + detail, + title, + description, +}: { + detail: string; + title: string; + description: string; +}) { + return ( +
+ {detail} +

{title}

+

{description}

+
+ ); +} + +export function SettingsApplyCard({ + onRestartStack, +}: { + onRestartStack: () => void; +}) { + return ( +
+
+ Apply + 저장 후 재시작 + 모델, MoA, Codex, 계정 변경은 스택 재시작 후 반영됩니다. +
+ +
+ ); +} diff --git a/apps/dashboard/src/styles.css b/apps/dashboard/src/styles.css index 00b2069..dc5a1cc 100644 --- a/apps/dashboard/src/styles.css +++ b/apps/dashboard/src/styles.css @@ -669,6 +669,7 @@ button:disabled { .settings-nav button { display: grid; gap: 2px; + min-height: 52px; padding: 10px 12px; border: 1px solid transparent; border-radius: 12px; @@ -685,7 +686,8 @@ button:disabled { } .settings-nav button:hover, -.settings-nav button:focus-visible { +.settings-nav button:focus-visible, +.settings-nav button[aria-selected='true'] { transform: translateX(2px); border-color: rgba(214, 130, 88, 0.24); background: rgba(214, 130, 88, 0.08); @@ -728,7 +730,7 @@ button:disabled { .settings-row input, .settings-row select { - min-height: 38px; + min-height: 44px; padding: 0 12px; border: 1px solid var(--panel-border); border-radius: 10px; @@ -929,7 +931,7 @@ button:disabled { .settings-save, .settings-restart { - min-height: 32px; + min-height: 44px; padding: 0 14px; border: 0; border-radius: 999px; @@ -1006,7 +1008,7 @@ button:disabled { } .settings-secondary { - min-height: 28px; + min-height: 36px; padding: 0 10px; border: 1px solid var(--panel-border-strong); border-radius: 999px; @@ -1050,7 +1052,7 @@ button:disabled { .settings-moa-grid input, .settings-moa-grid select { - min-height: 32px; + min-height: 40px; min-width: 0; padding: 0 10px; border: 1px solid var(--panel-border); @@ -1141,7 +1143,7 @@ button:disabled { } .settings-delete { - min-height: 28px; + min-height: 36px; padding: 0 10px; border: 1px solid rgba(224, 100, 75, 0.32); border-radius: 999px; @@ -1178,7 +1180,7 @@ button:disabled { .settings-add-token button { align-self: end; - min-height: 36px; + min-height: 44px; padding: 0 14px; border: 0; border-radius: 999px; @@ -1204,8 +1206,7 @@ button:disabled { } .settings-nav { - grid-template-columns: repeat(5, minmax(120px, 1fr)); - overflow-x: auto; + grid-template-columns: repeat(auto-fit, minmax(132px, 1fr)); } .settings-apply-card { @@ -1215,6 +1216,14 @@ button:disabled { } @media (max-width: 720px) { + .settings-nav { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .settings-nav button { + min-height: 58px; + } + .settings-apply-card { align-items: stretch; grid-template-columns: 1fr; diff --git a/scripts/dashboard-ux.ts b/scripts/dashboard-ux.ts index 6064971..cb81f7f 100644 --- a/scripts/dashboard-ux.ts +++ b/scripts/dashboard-ux.ts @@ -67,6 +67,7 @@ async function main() { baseUrl, async (page, state) => { await openSettings(page, baseUrl); + await openSettingsSection(page, 'settings-codex'); const goalToggle = page.getByRole('checkbox', { name: /\/goal/ }); await assertVisible(goalToggle); @@ -236,6 +237,13 @@ async function openSettings(page: Page, baseUrl: string) { ); } +async function openSettingsSection(page: Page, targetId: string) { + await page + .locator(`.settings-nav button[data-settings-target="${targetId}"]`) + .click(); + await assertVisible(page.locator(`#${targetId}`)); +} + async function assertVisible(locator: ReturnType) { await locator.waitFor({ state: 'visible', timeout: 5_000 }); }