Refine settings IA with tabbed sections
This commit is contained in:
@@ -113,7 +113,12 @@ export function MoaSettingsPanel() {
|
|||||||
Object.values(apiKeys).some((value) => value.trim()));
|
Object.values(apiKeys).some((value) => value.trim()));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="settings-section" id="settings-moa">
|
<section
|
||||||
|
aria-labelledby="settings-moa-tab"
|
||||||
|
className="settings-section"
|
||||||
|
id="settings-moa"
|
||||||
|
role="tabpanel"
|
||||||
|
>
|
||||||
<header className="settings-section-head">
|
<header className="settings-section-head">
|
||||||
<span>Arbiter references</span>
|
<span>Arbiter references</span>
|
||||||
<h3>MoA 참조 모델</h3>
|
<h3>MoA 참조 모델</h3>
|
||||||
|
|||||||
@@ -34,21 +34,20 @@ describe('SettingsPanel', () => {
|
|||||||
it('renders model, MoA, fast mode, and account controls', () => {
|
it('renders model, MoA, fast mode, and account controls', () => {
|
||||||
const html = renderToStaticMarkup(createElement(SettingsPanel, baseProps));
|
const html = renderToStaticMarkup(createElement(SettingsPanel, baseProps));
|
||||||
|
|
||||||
expect(html).toContain('모델');
|
expect(html).toContain('role="tablist"');
|
||||||
expect(html).toContain('MoA 참조 모델');
|
expect(html).toContain('role="tabpanel"');
|
||||||
expect(html).toContain('패스트 모드');
|
expect(html).toContain('aria-selected="true"');
|
||||||
expect(html).toContain('Codex 실험 기능');
|
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-codex"');
|
||||||
|
expect(html).toContain('data-settings-target="settings-accounts"');
|
||||||
expect(html).toContain('aria-controls="settings-codex"');
|
expect(html).toContain('aria-controls="settings-codex"');
|
||||||
expect(html).not.toContain('href="#settings-codex"');
|
expect(html).not.toContain('href="#settings-codex"');
|
||||||
expect(html).toContain('/goal');
|
|
||||||
expect(html).toContain('settings-apply-card');
|
expect(html).toContain('settings-apply-card');
|
||||||
expect(html).not.toContain('settings-apply-bar');
|
expect(html).not.toContain('settings-apply-bar');
|
||||||
expect(html).toContain('저장 후 재시작');
|
expect(html).toContain('저장 후 재시작');
|
||||||
expect(html).toContain('불러오는 중');
|
|
||||||
expect(html).toContain('Claude');
|
expect(html).toContain('Claude');
|
||||||
expect(html).toContain('계정');
|
expect(html).toContain('계정');
|
||||||
expect(html).toContain('전체 갱신');
|
|
||||||
expect(html).toContain('스택 재시작');
|
expect(html).toContain('스택 재시작');
|
||||||
expect(html.match(/class="settings-restart"/g)).toHaveLength(1);
|
expect(html.match(/class="settings-restart"/g)).toHaveLength(1);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -20,8 +20,15 @@ import {
|
|||||||
updateFastMode,
|
updateFastMode,
|
||||||
updateModels,
|
updateModels,
|
||||||
} from './api';
|
} from './api';
|
||||||
import { LOCALES, languageNames, type Locale, type Messages } from './i18n';
|
import { type Locale, type Messages } from './i18n';
|
||||||
import { MoaSettingsPanel } from './MoaSettingsPanel';
|
import { MoaSettingsPanel } from './MoaSettingsPanel';
|
||||||
|
import {
|
||||||
|
GeneralSettings,
|
||||||
|
SettingsApplyCard,
|
||||||
|
SettingsNav,
|
||||||
|
SettingsSectionHeading,
|
||||||
|
type SettingsSectionId,
|
||||||
|
} from './SettingsPanelChrome';
|
||||||
|
|
||||||
type AccountProvider = 'claude' | 'codex';
|
type AccountProvider = 'claude' | 'codex';
|
||||||
|
|
||||||
@@ -31,18 +38,6 @@ interface AccountData {
|
|||||||
codexCurrentIndex?: number;
|
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 {
|
export interface SettingsPanelProps {
|
||||||
locale: Locale;
|
locale: Locale;
|
||||||
nickname: string;
|
nickname: string;
|
||||||
@@ -60,142 +55,51 @@ export function SettingsPanel({
|
|||||||
onRestartStack,
|
onRestartStack,
|
||||||
t,
|
t,
|
||||||
}: SettingsPanelProps) {
|
}: SettingsPanelProps) {
|
||||||
|
const [activeSection, setActiveSection] =
|
||||||
|
useState<SettingsSectionId>('settings-general');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="settings-panel">
|
<div className="settings-panel">
|
||||||
<div className="settings-layout">
|
<div className="settings-layout">
|
||||||
<aside className="settings-sidebar" aria-label="설정 탐색과 적용">
|
<aside className="settings-sidebar" aria-label="설정 탐색과 적용">
|
||||||
<SettingsNav />
|
<SettingsNav
|
||||||
|
activeSection={activeSection}
|
||||||
|
onSelect={setActiveSection}
|
||||||
|
/>
|
||||||
<SettingsApplyCard onRestartStack={onRestartStack} />
|
<SettingsApplyCard onRestartStack={onRestartStack} />
|
||||||
</aside>
|
</aside>
|
||||||
<main className="settings-content" aria-label="설정 항목">
|
<main className="settings-content" aria-label="설정 항목">
|
||||||
<section className="settings-section" id="settings-general">
|
<div hidden={activeSection !== 'settings-general'}>
|
||||||
<SettingsSectionHeading
|
<GeneralSettings
|
||||||
detail="Dashboard identity"
|
locale={locale}
|
||||||
title="일반"
|
nickname={nickname}
|
||||||
description="브라우저에서 보이는 이름과 언어만 즉시 바뀝니다."
|
onLocaleChange={onLocaleChange}
|
||||||
|
onNicknameChange={onNicknameChange}
|
||||||
|
t={t}
|
||||||
/>
|
/>
|
||||||
<div className="settings-form-grid">
|
</div>
|
||||||
<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>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<ModelSettings />
|
<div hidden={activeSection !== 'settings-models'}>
|
||||||
|
<ModelSettings />
|
||||||
|
</div>
|
||||||
|
|
||||||
<MoaSettingsPanel />
|
<div hidden={activeSection !== 'settings-moa'}>
|
||||||
|
<MoaSettingsPanel />
|
||||||
|
</div>
|
||||||
|
|
||||||
<section className="settings-section" id="settings-codex">
|
<div hidden={activeSection !== 'settings-codex'}>
|
||||||
<SettingsSectionHeading
|
<CodexRuntimeSettings />
|
||||||
detail="Codex runtime"
|
</div>
|
||||||
title="Codex 옵션"
|
|
||||||
description="빠른 응답과 실험 기능을 관리합니다. /goal은 여기에서 찾을 수 있습니다."
|
|
||||||
/>
|
|
||||||
<div className="settings-section-stack">
|
|
||||||
<FastModeSettings />
|
|
||||||
<CodexFeatureSettings />
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<AccountSettings />
|
<div hidden={activeSection !== 'settings-accounts'}>
|
||||||
|
<AccountSettings />
|
||||||
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function SettingsNav() {
|
|
||||||
const scrollToSection = (targetId: string) => {
|
|
||||||
document
|
|
||||||
.getElementById(targetId)
|
|
||||||
?.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<aside className="settings-nav" aria-label="설정 섹션">
|
|
||||||
{SETTINGS_NAV_ITEMS.map((item) => (
|
|
||||||
<button
|
|
||||||
aria-controls={item.targetId}
|
|
||||||
data-settings-target={item.targetId}
|
|
||||||
key={item.targetId}
|
|
||||||
onClick={() => scrollToSection(item.targetId)}
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
<strong>{item.title}</strong>
|
|
||||||
<small>{item.detail}</small>
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</aside>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function SettingsApplyCard({ onRestartStack }: { onRestartStack: () => void }) {
|
|
||||||
return (
|
|
||||||
<section className="settings-apply-card" aria-label="변경 적용">
|
|
||||||
<div>
|
|
||||||
<span className="settings-kicker">Apply</span>
|
|
||||||
<strong>저장 후 재시작</strong>
|
|
||||||
<small>모델, MoA, Codex, 계정 변경은 스택 재시작 후 반영됩니다.</small>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
className="settings-restart"
|
|
||||||
onClick={onRestartStack}
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
스택 재시작
|
|
||||||
</button>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ModelSettings() {
|
function ModelSettings() {
|
||||||
const [config, setConfig] = useState<ModelConfigSnapshot | null>(null);
|
const [config, setConfig] = useState<ModelConfigSnapshot | null>(null);
|
||||||
const [draft, setDraft] = useState<ModelConfigSnapshot | null>(null);
|
const [draft, setDraft] = useState<ModelConfigSnapshot | null>(null);
|
||||||
@@ -261,7 +165,12 @@ function ModelSettings() {
|
|||||||
JSON.stringify(draft) !== JSON.stringify(config);
|
JSON.stringify(draft) !== JSON.stringify(config);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="settings-section" id="settings-models">
|
<section
|
||||||
|
aria-labelledby="settings-models-tab"
|
||||||
|
className="settings-section"
|
||||||
|
id="settings-models"
|
||||||
|
role="tabpanel"
|
||||||
|
>
|
||||||
<SettingsSectionHeading
|
<SettingsSectionHeading
|
||||||
detail="Agent routing"
|
detail="Agent routing"
|
||||||
title="모델"
|
title="모델"
|
||||||
@@ -315,6 +224,27 @@ function ModelSettings() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function CodexRuntimeSettings() {
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
aria-labelledby="settings-codex-tab"
|
||||||
|
className="settings-section"
|
||||||
|
id="settings-codex"
|
||||||
|
role="tabpanel"
|
||||||
|
>
|
||||||
|
<SettingsSectionHeading
|
||||||
|
detail="Codex runtime"
|
||||||
|
title="Codex 옵션"
|
||||||
|
description="빠른 응답과 실험 기능을 관리합니다. /goal은 여기에서 찾을 수 있습니다."
|
||||||
|
/>
|
||||||
|
<div className="settings-section-stack">
|
||||||
|
<FastModeSettings />
|
||||||
|
<CodexFeatureSettings />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function FastModeSettings() {
|
function FastModeSettings() {
|
||||||
const [state, setState] = useState<FastModeSnapshot | null>(null);
|
const [state, setState] = useState<FastModeSnapshot | null>(null);
|
||||||
const [busy, setBusy] = useState(false);
|
const [busy, setBusy] = useState(false);
|
||||||
@@ -611,7 +541,12 @@ function AccountSettings() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="settings-section" id="settings-accounts">
|
<section
|
||||||
|
aria-labelledby="settings-accounts-tab"
|
||||||
|
className="settings-section"
|
||||||
|
id="settings-accounts"
|
||||||
|
role="tabpanel"
|
||||||
|
>
|
||||||
<SettingsSectionHeading
|
<SettingsSectionHeading
|
||||||
detail="Credentials"
|
detail="Credentials"
|
||||||
title="계정"
|
title="계정"
|
||||||
|
|||||||
140
apps/dashboard/src/SettingsPanelChrome.tsx
Normal file
140
apps/dashboard/src/SettingsPanelChrome.tsx
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
import { LOCALES, languageNames, type Locale, type Messages } from './i18n';
|
||||||
|
|
||||||
|
export 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 type SettingsSectionId = (typeof SETTINGS_NAV_ITEMS)[number]['targetId'];
|
||||||
|
|
||||||
|
export function SettingsNav({
|
||||||
|
activeSection,
|
||||||
|
onSelect,
|
||||||
|
}: {
|
||||||
|
activeSection: SettingsSectionId;
|
||||||
|
onSelect: (section: SettingsSectionId) => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="settings-nav" aria-label="설정 섹션" role="tablist">
|
||||||
|
{SETTINGS_NAV_ITEMS.map((item) => (
|
||||||
|
<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>{item.title}</strong>
|
||||||
|
<small>{item.detail}</small>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GeneralSettings({
|
||||||
|
locale,
|
||||||
|
nickname,
|
||||||
|
onLocaleChange,
|
||||||
|
onNicknameChange,
|
||||||
|
t,
|
||||||
|
}: {
|
||||||
|
locale: Locale;
|
||||||
|
nickname: string;
|
||||||
|
onLocaleChange: (locale: Locale) => void;
|
||||||
|
onNicknameChange: (next: string) => void;
|
||||||
|
t: Messages;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
aria-labelledby="settings-general-tab"
|
||||||
|
className="settings-section"
|
||||||
|
id="settings-general"
|
||||||
|
role="tabpanel"
|
||||||
|
>
|
||||||
|
<SettingsSectionHeading
|
||||||
|
detail="Dashboard identity"
|
||||||
|
title="일반"
|
||||||
|
description="브라우저에서 보이는 이름과 언어만 즉시 바뀝니다."
|
||||||
|
/>
|
||||||
|
<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>
|
||||||
|
</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 SettingsApplyCard({
|
||||||
|
onRestartStack,
|
||||||
|
}: {
|
||||||
|
onRestartStack: () => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<section className="settings-apply-card" aria-label="변경 적용">
|
||||||
|
<div>
|
||||||
|
<span className="settings-kicker">Apply</span>
|
||||||
|
<strong>저장 후 재시작</strong>
|
||||||
|
<small>모델, MoA, Codex, 계정 변경은 스택 재시작 후 반영됩니다.</small>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
className="settings-restart"
|
||||||
|
onClick={onRestartStack}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
스택 재시작
|
||||||
|
</button>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -669,6 +669,7 @@ button:disabled {
|
|||||||
.settings-nav button {
|
.settings-nav button {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 2px;
|
gap: 2px;
|
||||||
|
min-height: 52px;
|
||||||
padding: 10px 12px;
|
padding: 10px 12px;
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
@@ -685,7 +686,8 @@ button:disabled {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.settings-nav button:hover,
|
.settings-nav button:hover,
|
||||||
.settings-nav button:focus-visible {
|
.settings-nav button:focus-visible,
|
||||||
|
.settings-nav button[aria-selected='true'] {
|
||||||
transform: translateX(2px);
|
transform: translateX(2px);
|
||||||
border-color: rgba(214, 130, 88, 0.24);
|
border-color: rgba(214, 130, 88, 0.24);
|
||||||
background: rgba(214, 130, 88, 0.08);
|
background: rgba(214, 130, 88, 0.08);
|
||||||
@@ -728,7 +730,7 @@ button:disabled {
|
|||||||
|
|
||||||
.settings-row input,
|
.settings-row input,
|
||||||
.settings-row select {
|
.settings-row select {
|
||||||
min-height: 38px;
|
min-height: 44px;
|
||||||
padding: 0 12px;
|
padding: 0 12px;
|
||||||
border: 1px solid var(--panel-border);
|
border: 1px solid var(--panel-border);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
@@ -929,7 +931,7 @@ button:disabled {
|
|||||||
|
|
||||||
.settings-save,
|
.settings-save,
|
||||||
.settings-restart {
|
.settings-restart {
|
||||||
min-height: 32px;
|
min-height: 44px;
|
||||||
padding: 0 14px;
|
padding: 0 14px;
|
||||||
border: 0;
|
border: 0;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
@@ -1006,7 +1008,7 @@ button:disabled {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.settings-secondary {
|
.settings-secondary {
|
||||||
min-height: 28px;
|
min-height: 36px;
|
||||||
padding: 0 10px;
|
padding: 0 10px;
|
||||||
border: 1px solid var(--panel-border-strong);
|
border: 1px solid var(--panel-border-strong);
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
@@ -1050,7 +1052,7 @@ button:disabled {
|
|||||||
|
|
||||||
.settings-moa-grid input,
|
.settings-moa-grid input,
|
||||||
.settings-moa-grid select {
|
.settings-moa-grid select {
|
||||||
min-height: 32px;
|
min-height: 40px;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
padding: 0 10px;
|
padding: 0 10px;
|
||||||
border: 1px solid var(--panel-border);
|
border: 1px solid var(--panel-border);
|
||||||
@@ -1141,7 +1143,7 @@ button:disabled {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.settings-delete {
|
.settings-delete {
|
||||||
min-height: 28px;
|
min-height: 36px;
|
||||||
padding: 0 10px;
|
padding: 0 10px;
|
||||||
border: 1px solid rgba(224, 100, 75, 0.32);
|
border: 1px solid rgba(224, 100, 75, 0.32);
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
@@ -1178,7 +1180,7 @@ button:disabled {
|
|||||||
|
|
||||||
.settings-add-token button {
|
.settings-add-token button {
|
||||||
align-self: end;
|
align-self: end;
|
||||||
min-height: 36px;
|
min-height: 44px;
|
||||||
padding: 0 14px;
|
padding: 0 14px;
|
||||||
border: 0;
|
border: 0;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
@@ -1204,8 +1206,7 @@ button:disabled {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.settings-nav {
|
.settings-nav {
|
||||||
grid-template-columns: repeat(5, minmax(120px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(132px, 1fr));
|
||||||
overflow-x: auto;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-apply-card {
|
.settings-apply-card {
|
||||||
@@ -1215,6 +1216,14 @@ button:disabled {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 720px) {
|
||||||
|
.settings-nav {
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-nav button {
|
||||||
|
min-height: 58px;
|
||||||
|
}
|
||||||
|
|
||||||
.settings-apply-card {
|
.settings-apply-card {
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ async function main() {
|
|||||||
baseUrl,
|
baseUrl,
|
||||||
async (page, state) => {
|
async (page, state) => {
|
||||||
await openSettings(page, baseUrl);
|
await openSettings(page, baseUrl);
|
||||||
|
await openSettingsSection(page, 'settings-codex');
|
||||||
|
|
||||||
const goalToggle = page.getByRole('checkbox', { name: /\/goal/ });
|
const goalToggle = page.getByRole('checkbox', { name: /\/goal/ });
|
||||||
await assertVisible(goalToggle);
|
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<Page['locator']>) {
|
async function assertVisible(locator: ReturnType<Page['locator']>) {
|
||||||
await locator.waitFor({ state: 'visible', timeout: 5_000 });
|
await locator.waitFor({ state: 'visible', timeout: 5_000 });
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user