diff --git a/frontend/src/v2/__tests__/V2BillingPanel.test.tsx b/frontend/src/v2/__tests__/V2BillingPanel.test.tsx index 9a3692df7..69815939b 100644 --- a/frontend/src/v2/__tests__/V2BillingPanel.test.tsx +++ b/frontend/src/v2/__tests__/V2BillingPanel.test.tsx @@ -31,7 +31,7 @@ describe('V2BillingPanel', () => { test('can defer the section heading to Settings without hiding the current tier', () => { (useAuth as jest.Mock).mockReturnValue({ currentUser: { entitlements: { pro: true } } }); - render(); + render(); expect(screen.queryByRole('heading', { name: 'Plan' })).not.toBeInTheDocument(); expect(screen.getByText('Pro · $12 a month')).toBeInTheDocument(); diff --git a/frontend/src/v2/__tests__/V2SettingsPage.test.tsx b/frontend/src/v2/__tests__/V2SettingsPage.test.tsx index 9373db085..e5887a45c 100644 --- a/frontend/src/v2/__tests__/V2SettingsPage.test.tsx +++ b/frontend/src/v2/__tests__/V2SettingsPage.test.tsx @@ -48,7 +48,7 @@ jest.mock('../components/V2AvatarCropDialog', () => { }); jest.mock('../components/V2BillingPanel', () => { - const MockV2BillingPanel = ({ showHeading }: { showHeading?: boolean }) =>
Plan controls
; + const MockV2BillingPanel = () =>
Plan controls
; MockV2BillingPanel.displayName = 'MockV2BillingPanel'; return MockV2BillingPanel; }); @@ -105,7 +105,7 @@ describe('V2SettingsPage', () => { (axios.get as jest.Mock).mockResolvedValue({ data: { hasToken: false } }); renderSettings(); - expect(languageRadioLabels()).toEqual(['English', '中文']); + expect(languageRadioLabels()).toEqual(['EN', '中文']); }); test('builds the language radios from the registry, and only those', async () => { @@ -113,7 +113,7 @@ describe('V2SettingsPage', () => { (axios.get as jest.Mock).mockResolvedValue({ data: { hasToken: false } }); renderSettings(); - expect(languageRadioLabels()).toEqual(LANGUAGE_REGISTRY.map(({ label }) => label)); + expect(languageRadioLabels()).toEqual(LANGUAGE_REGISTRY.map(({ short }) => short)); }); test('Look carries the language as a segment that switches the app language', async () => { @@ -151,7 +151,6 @@ describe('V2SettingsPage', () => { expect(screen.getByText('lily')).toBeInTheDocument(); expect(screen.getByText('Plan controls')).toBeInTheDocument(); expect(screen.getByText('Device controls')).toBeInTheDocument(); - expect(screen.getByText('Plan controls')).toHaveAttribute('data-show-heading', 'false'); expect(screen.getByText('Device controls')).toHaveAttribute('data-show-heading', 'false'); // Developer apps stay hidden unless REACT_APP_DEVELOPER_APPS=true (TASK-674). expect(screen.queryByText('Connected app controls')).not.toBeInTheDocument(); @@ -222,12 +221,32 @@ describe('V2SettingsPage', () => { expect(document.getElementById('v2-settings-profile-edit')).toHaveClass('v2-settings__edit--closed'); }); + test('Change photo opens the profile editor', () => { + (axios.get as jest.Mock).mockResolvedValue({ data: { hasToken: false } }); + renderSettings(); + + const photo = screen.getByRole('button', { name: 'Change photo' }); + expect(photo).toHaveAttribute('aria-expanded', 'false'); + fireEvent.click(photo); + expect(photo).toHaveAttribute('aria-expanded', 'true'); + expect(document.getElementById('v2-settings-profile-edit')).not.toHaveClass('v2-settings__edit--closed'); + }); + + test('the avatar and token actions render as the primitive keys the 44px phone rule sizes', () => { + (axios.get as jest.Mock).mockResolvedValue({ data: { hasToken: false } }); + renderSettings(); + + for (const name of ['Regenerate', 'Upload photo']) { + expect(screen.getByRole('button', { name })).toHaveClass('v2p-b'); + } + }); + test('generates and reveals a new API token without leaving Settings', async () => { (axios.get as jest.Mock).mockResolvedValue({ data: { hasToken: false } }); (axios.post as jest.Mock).mockResolvedValue({ data: { apiToken: 'cm_user_secret', createdAt: TOKEN_CREATED_AT } }); renderSettings(); - fireEvent.click(screen.getByRole('button', { name: 'Generate API token' })); + fireEvent.click(screen.getByRole('button', { name: 'Generate' })); await waitFor(() => expect(axios.post).toHaveBeenCalledWith('/api/auth/api-token/generate', {})); expect(await screen.findByText('cm_user_secret')).toBeInTheDocument(); diff --git a/frontend/src/v2/__tests__/v2-layout-invariants.test.ts b/frontend/src/v2/__tests__/v2-layout-invariants.test.ts index 19c13c8e3..8d32f1893 100644 --- a/frontend/src/v2/__tests__/v2-layout-invariants.test.ts +++ b/frontend/src/v2/__tests__/v2-layout-invariants.test.ts @@ -24,6 +24,9 @@ import { blockAt, blockContaining } from '../lib/cssBlocks'; const read = (rel: string): string => fs.readFileSync(path.join(__dirname, rel), 'utf8'); +// Settings keeps its own sheet, lifted from the d215 frame (TASK-688). +const ss = read('../components/V2SettingsPage.css'); + // The team's phone block: the `@media (max-width: 760px)` block that carries // `.v2-team__grid`, wherever it sits in the sheet — not the last one. const teamPhoneBlock = (css: string): string => @@ -1231,11 +1234,11 @@ describe('v2 layout invariants (CSS rule presence)', () => { expect(settingsPage).toContain(''); expect(settingsPage).toContain('to="/v2/connectors"'); expect(settingsPage).toContain('Developer apps'); - expect(settingsPage).toContain(''); + expect(settingsPage).toContain(''); expect(settingsPage).toContain(''); - expect(ruleBody(v2, '.v2-settings__avatar')).toContain('width: 44px'); - expect(ruleBody(v2, '.v2-settings__avatar')).toContain('border-radius: var(--v2-radius-sm)'); - expect(ruleBody(v2, '.v2-settings__avatar img')).toContain('border-radius: var(--v2-radius-sm)'); + expect(ruleBody(ss, '.v2-settings__avatar')).toContain('width: 44px'); + expect(ruleBody(ss, '.v2-settings__avatar')).toContain('border-radius: var(--v2-radius-sm)'); + expect(ruleBody(ss, '.v2-settings__avatar img')).toContain('border-radius: var(--v2-radius-sm)'); expect(avatar).toContain("borderRadius: 'inherit'"); expect(v2App).toMatch(/path="settings\/devices"\s+element=\{\}/); expect(v2App).toMatch(/path="profile"\s+element=\{\}/); @@ -1244,12 +1247,21 @@ describe('v2 layout invariants (CSS rule presence)', () => { expect(app).toContain('} />'); }); + test('Settings errors never sit inside the value column the phone hides', () => { + const devicesPanel = read('../components/V2DevicesPanel.tsx'); + // billing error is its own line under the row, not a child of .vl + expect(billingPanel).toMatch(/<\/span>\s*\{error && [^]*v2-settings__error[^]*<\/span>\s*/); + expect(ruleBody(ss, '.v2-settings__error--row')).toContain('grid-column: 1 / -1'); + // devices: a load failure keeps its summary visible at 390 + expect(devicesPanel).toContain("'vl v2-settings__vl--error'"); + expect(ss).toMatch(/\.v2-settings \.v2p-disc > summary \.v2-settings__vl--error \{ display: flex/); + }); + test('Settings is one screen: grouped cards, no jump nav', () => { expect(settingsPage).not.toContain('v2-settings__nav'); - expect(ruleBody(v2, '.v2-settings__content')).toContain('width: min(100%, 760px)'); - expect(ruleBody(v2, '.v2-settings__edit--closed')).toContain('display: none'); - expect(ruleBody(v2, '.v2-settings__group,\n.v2-settings__fold')).toContain('overflow: hidden'); - expect(ruleBody(v2, '.v2-settings__row > .v2-settings__row-value')).toContain('min-width: 0'); + expect(ruleBody(ss, '.v2-settings__content')).toContain('width: min(100%, 760px)'); + expect(ruleBody(ss, '.v2-settings__edit--closed')).toContain('display: none'); }); test('Settings saves a display name while allowing handle changes and exposing the email change affordance', () => { @@ -1265,42 +1277,33 @@ describe('v2 layout invariants (CSS rule presence)', () => { expect(settingsPage).toContain('type="button">Change'); expect(settingsPage).not.toContain('Email changes go through re-verification; ask us for now.'); expect(settingsPage).not.toContain('v2-settings__nav-note'); - expect(ruleBody(v2, '.v2-settings__account-fields')).toContain('grid-template-columns: repeat(2, minmax(0, 1fr))'); - expect(ruleBody(v2, '.v2-settings__account-fields > label')).toContain('align-content: start'); - expect(ruleBody(v2, '.v2-settings__account-fields > .v2-settings__account-email')).toContain('grid-column: 1 / -1'); - expect(ruleBody(v2, '.v2-settings__account-fields input')).toContain('border-radius: var(--v2-radius-sm)'); - expect(ruleBody(v2, '.v2-settings__email-control')).toContain('display: flex'); - const accountForm = ruleBody(v2, '.v2-settings form.v2-settings__account'); + expect(ruleBody(ss, '.v2-settings__account-fields')).toContain('grid-template-columns: repeat(2, minmax(0, 1fr))'); + expect(ruleBody(ss, '.v2-settings__account-fields > label')).toContain('align-content: start'); + expect(ruleBody(ss, '.v2-settings__account-fields > .v2-settings__account-email')).toContain('grid-column: 1 / -1'); + expect(ruleBody(ss, '.v2-settings__account-fields input')).toContain('border-radius: var(--v2-radius-sm)'); + expect(ruleBody(ss, '.v2-settings__email-control')).toContain('display: flex'); + const accountForm = ruleBody(ss, '.v2-settings form.v2-settings__account'); expect(accountForm).toContain('padding: 0'); expect(accountForm).toContain('background: transparent'); expect(accountForm).toContain('box-shadow: none'); }); test('Settings avatar controls keep the account face square and the crop step usable on phones', () => { - const avatar = ruleBody(v2, '.v2-settings__avatar'); + const avatar = ruleBody(ss, '.v2-settings__avatar'); expect(avatar).toContain('width: 44px'); expect(avatar).toContain('height: 44px'); expect(avatar).toContain('border-radius: var(--v2-radius-sm)'); - expect(ruleBody(v2, '.v2-settings__avatar-overlay')).toContain('background: rgba(16, 24, 40, 0.55)'); + expect(ruleBody(ss, '.v2-settings__avatar-overlay')).toContain('background: rgba(16, 24, 40, 0.55)'); expect(avatarCropDialog).toContain('className="v2-root v2-modal__overlay v2-settings__avatar-overlay"'); expect(avatarCropDialog).toContain('className="v2-modal v2-settings__avatar-dialog"'); - expect(ruleBody(v2, '.v2-settings__avatar-crop-stage')).toContain('aspect-ratio: 392 / 260'); - expect(ruleBody(v2, '.v2-settings__avatar-crop-frame')).toContain('border: 1px solid var(--v2-surface)'); - expect(ruleBody(v2, '.v2-settings__avatar-crop-dim span')).toContain('background: rgba(16, 24, 40, 0.55)'); - expect(ruleBody(v2, '.v2-root .v2-settings__avatar-zoom input[type="range"]::-webkit-slider-thumb')).toContain('width: 16px'); - expect(ruleBody(v2, '.v2-root .v2-settings__avatar-dialog-actions button')).toContain('min-width: 112px'); - const ariaDisabledStyle = selectorRuleBody( - v2, - '.v2-root button.v2-settings__primary[aria-disabled="true"]', - ); - expect(ariaDisabledStyle).toContain('cursor: default'); - expect(ariaDisabledStyle).toContain('opacity: 0.55'); - expect(v2).toContain('.v2-root button.v2-settings__primary:hover:not(:disabled):not([aria-disabled="true"])'); - expect(v2).toContain('.v2-root button.v2-settings__secondary:hover:not(:disabled):not([aria-disabled="true"])'); - - const phone = blockContaining(v2, '@media (max-width: 680px)', '.v2-settings__avatar-actions'); + expect(ruleBody(ss, '.v2-settings__avatar-crop-stage')).toContain('aspect-ratio: 392 / 260'); + expect(ruleBody(ss, '.v2-settings__avatar-crop-frame')).toContain('border: 1px solid var(--v2-surface)'); + expect(ruleBody(ss, '.v2-settings__avatar-crop-dim span')).toContain('background: rgba(16, 24, 40, 0.55)'); + expect(ruleBody(ss, '.v2-root .v2-settings__avatar-zoom input[type="range"]::-webkit-slider-thumb')).toContain('width: 16px'); + expect(ruleBody(ss, '.v2-root .v2-settings__avatar-dialog-actions button')).toContain('min-width: 112px'); + const phone = blockContaining(ss, '@media (max-width: 680px)', '.v2-settings__avatar-actions'); expect(ruleBody(phone, '.v2-settings__avatar-actions', { within: phone })).toContain('grid-template-columns: repeat(2, minmax(0, 1fr))'); - expect(ruleBody(phone, '.v2-root .v2-settings__avatar-actions button.v2-settings__secondary', { within: phone })).toContain('min-height: 44px'); + expect(ruleBody(phone, '.v2-settings__avatar-actions .v2p-b', { within: phone })).toContain('min-height: 44px'); expect(ruleBody(phone, '.v2-settings__avatar-footer', { within: phone })).toContain('flex-direction: column'); expect(ruleBody(phone, '.v2-root .v2-settings__avatar-dialog-actions button', { within: phone })).toContain('min-height: 44px'); @@ -1325,13 +1328,9 @@ describe('v2 layout invariants (CSS rule presence)', () => { expect(billingPanel).toContain("t(isPro ? 'billing.manage' : 'billing.upgrade')"); expect(billingPanel).not.toContain('free in beta'); expect(billingPanel).not.toContain('included during beta'); - const badge = ruleBody(v2, '.v2-settings__section .v2-billing__badge'); - expect(badge).toContain('background: transparent'); - expect(badge).toContain('border: 0'); - expect(badge).toContain('padding: 0'); - const proBadge = ruleBody(v2, '.v2-settings__section .v2-billing__badge--pro'); - expect(proBadge).toContain('background: transparent'); - expect(proBadge).toContain('color: var(--v2-text-primary)'); + // The frame reads the plan as plain text in the row value, not a chip. + expect(billingPanel).toContain(''); + expect(billingPanel).not.toContain('v2-billing__badge'); }); test('Settings flattens connected apps and folds Developer Tips under the form', () => { @@ -1341,8 +1340,8 @@ describe('v2 layout invariants (CSS rule presence)', () => { expect(appsManagement).toContain('className="apps-management__developer-tips"'); expect(appsManagement).toContain('{!isSettingsVariant && ('); expect(appsManagement).toContain('? {children}'); - expect(ruleBody(v2, '.v2-settings .apps-management--settings .apps-management__section + .apps-management__section')).toContain('border-top: 1px solid var(--v2-border-soft)'); - expect(ruleBody(v2, '.v2-settings .apps-management--settings .apps-management__integration')).toContain('border: 1px solid var(--v2-border)'); + expect(ruleBody(ss, '.v2-settings .apps-management--settings .apps-management__section + .apps-management__section')).toContain('border-top: 1px solid var(--v2-border-soft)'); + expect(ruleBody(ss, '.v2-settings .apps-management--settings .apps-management__integration')).toContain('border: 1px solid var(--v2-border)'); }); test('mention treatment is a tight chip on the handle, never a row wash or rail', () => { diff --git a/frontend/src/v2/__tests__/v2-phone-shell.test.ts b/frontend/src/v2/__tests__/v2-phone-shell.test.ts index 390f6fd50..cf430832e 100644 --- a/frontend/src/v2/__tests__/v2-phone-shell.test.ts +++ b/frontend/src/v2/__tests__/v2-phone-shell.test.ts @@ -6,6 +6,7 @@ import path from 'path'; // that carry it, the same way v2-layout-invariants.test.ts does. const css = fs.readFileSync(path.join(__dirname, '../v2.css'), 'utf8'); +const settingsCss = fs.readFileSync(path.join(__dirname, '../components/V2SettingsPage.css'), 'utf8'); // The block is the tail of the sheet, from its marker comment to the end. const phone = css.slice(css.indexOf('D223 (TASK-730 shell)')); @@ -34,7 +35,7 @@ describe('D223 phone shell', () => { test('the block comes last in the sheet, so it outranks the earlier phone rules', () => { const at = css.indexOf('D223 (TASK-730 shell)'); - expect(at).toBeGreaterThan(css.indexOf('.v2-settings__layout {')); + expect(at).toBeGreaterThan(css.indexOf('.v2-feature__body {')); expect(css.indexOf('.v2-feature__body {')).toBeLessThan(at); }); @@ -54,11 +55,11 @@ describe('D223 phone shell', () => { }); test('the settings head spans the pane and only the body scrolls', () => { - expect(phone).toMatch(/\.v2-feature--settings > \.v2-feature__body\s*\{[^}]*padding:\s*0/); - expect(phone).toMatch(/\.v2-feature--settings \.v2-feature__legacy\s*\{[^}]*max-width:\s*none/); + expect(settingsCss).toMatch(/\.v2-feature--settings > \.v2-feature__body\s*\{[^}]*padding:\s*0/); + expect(settingsCss).toMatch(/\.v2-feature--settings \.v2-feature__legacy\s*\{[^}]*max-width:\s*none/); expect(fs.readFileSync(path.join(__dirname, '../V2App.tsx'), 'utf8')).toContain("'v2-feature--settings'"); expect(fs.readFileSync(path.join(__dirname, '../components/V2SettingsPage.tsx'), 'utf8')).toContain('
'); - expect(css).toMatch(/\.v2-settings__content\s*\{[^}]*width:\s*min\(100%, 760px\)/); - expect(phone).toMatch(/\.v2-feature--settings \.v2-settings__layout\s*\{[^}]*overflow-y:\s*auto/); + expect(settingsCss).toMatch(/\.v2-settings__content\s*\{[^}]*width:\s*min\(100%, 760px\)/); + expect(settingsCss).toMatch(/\.v2-feature--settings \.v2-settings__layout\s*\{[^}]*overflow-y:\s*auto/); }); }); diff --git a/frontend/src/v2/__tests__/v2-type-floors.test.ts b/frontend/src/v2/__tests__/v2-type-floors.test.ts index 36f1af4ce..c63c3108c 100644 --- a/frontend/src/v2/__tests__/v2-type-floors.test.ts +++ b/frontend/src/v2/__tests__/v2-type-floors.test.ts @@ -130,7 +130,6 @@ const ALLOWLIST: string[] = [ '.v2-root button.v2-chat__new-pod-copy', // 11.0px — no-mono-family // PR3 selectors meet the floor; no remaining allowances. // PR4 - '.v2-billing__badge', // 11.0px — no-mono-family // TASK-166 took `.v2-byo__mode-kicker` (10.0px, no family) and // `.v2-byo__preview-label` (11.0px, no family) to mono 500 11px, so both // entries left this burn-down list rather than being updated in place. @@ -191,7 +190,7 @@ describe('v2 type floors (ruling h)', () => { // `--v2-fs-label: 11px` is consumed by rules that declare no literal size. // If substitution regressed, those rules would silently drop out of scope. expect(CSS).toMatch(/--v2-fs-label:\s*11px/); - expect(CSS).toContain('font-size: var(--v2-fs-label)'); + expect(CSS + fs.readFileSync(path.join(__dirname, '../components/V2SettingsPage.css'), 'utf8')).toContain('font-size: var(--v2-fs-label)'); }); test('a rule inside an @media block is keyed to its own selector', () => { diff --git a/frontend/src/v2/components/V2AvatarCropDialog.tsx b/frontend/src/v2/components/V2AvatarCropDialog.tsx index e131d1324..4eff2e732 100644 --- a/frontend/src/v2/components/V2AvatarCropDialog.tsx +++ b/frontend/src/v2/components/V2AvatarCropDialog.tsx @@ -321,10 +321,10 @@ const V2AvatarCropDialog: React.FC = ({ ))}
- -
diff --git a/frontend/src/v2/components/V2BillingPanel.tsx b/frontend/src/v2/components/V2BillingPanel.tsx index 6b246b6e4..38d852c45 100644 --- a/frontend/src/v2/components/V2BillingPanel.tsx +++ b/frontend/src/v2/components/V2BillingPanel.tsx @@ -6,11 +6,12 @@ import { useAuth } from '../../context/AuthContext'; /** * Plan panel on /v2/settings. */ -interface V2BillingPanelProps { - showHeading?: boolean; -} +const Card = () => ( + +); -const V2BillingPanel: React.FC = ({ showHeading = true }) => { +const V2BillingPanel: React.FC = () => { const { t } = useTranslation(); const { currentUser } = useAuth(); const [busy, setBusy] = useState(false); @@ -33,25 +34,23 @@ const V2BillingPanel: React.FC = ({ showHeading = true }) = }; return ( -
-
- {showHeading &&

{t('billing.title')}

} - - {t(isPro ? 'billing.tier.pro' : 'billing.tier.free')} - -
- {error &&

{error}

} -
+
+ Plan + + {t(isPro ? 'billing.tier.pro' : 'billing.tier.free')} + + -
-
+ + {error && {error}} + ); }; diff --git a/frontend/src/v2/components/V2DeleteAccount.tsx b/frontend/src/v2/components/V2DeleteAccount.tsx index e02ab39be..82c956e93 100644 --- a/frontend/src/v2/components/V2DeleteAccount.tsx +++ b/frontend/src/v2/components/V2DeleteAccount.tsx @@ -66,7 +66,7 @@ const V2DeleteAccount: React.FC = () => { - diff --git a/frontend/src/v2/components/V2DevicesPanel.tsx b/frontend/src/v2/components/V2DevicesPanel.tsx index 916b71350..d8c9087a8 100644 --- a/frontend/src/v2/components/V2DevicesPanel.tsx +++ b/frontend/src/v2/components/V2DevicesPanel.tsx @@ -54,11 +54,13 @@ const V2DevicesPanel: React.FC = ({ showHeading = true, asF const summary = loading ? 'Loading…' : error ? 'Couldn’t load' : live.length === 0 ? 'None' : `${live.length} CLI device${live.length === 1 ? '' : 's'}${lastUsed ? ` · used ${(() => { const when = new Date(lastUsed as string); return when.toDateString() === new Date().toDateString() ? when.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' }) : when.toLocaleDateString([], { month: 'short', day: 'numeric' }); })()}` : ''}`; return ( -
- - - Devices - {summary} +
+ + + + Devices + + {summary}
{renderPanel()}
diff --git a/frontend/src/v2/components/V2SettingsPage.css b/frontend/src/v2/components/V2SettingsPage.css new file mode 100644 index 000000000..603c3d9f8 --- /dev/null +++ b/frontend/src/v2/components/V2SettingsPage.css @@ -0,0 +1,626 @@ +/* Settings (/v2/settings), lifted from the d215 artboard (TASK-688): the frame's grouped + rows (v2p-gtitle, v2p-list, v2p-lrow), disclosure rows and 760px column. The editors, + the avatar crop and the device list below are controls the frame does not draw. */ +.v2-settings { width: 100%; color: var(--v2-text-primary); } +.v2-settings__layout { min-width: 0; } +.v2-settings__content { width: min(100%, 760px); min-width: 0; margin: 0 auto; display: grid; gap: 0; } +.v2-settings__section { min-width: 0; } +.v2-settings__section + .v2-settings__section > .v2p-gtitle { margin-top: 18px; } +.v2-settings__icon { flex: 0 0 auto; color: var(--v2-text-tertiary); } +.v2-settings .v2p-lrow .mu { color: var(--v2-text-muted); } +.v2-settings__mail { font: 500 12.5px/1 var(--v2-font-mono); color: var(--v2-text-secondary); overflow-wrap: anywhere; } +.v2-settings__photo-btn { padding: 0; height: auto; } +.v2-settings__account-copy { display: grid; gap: 2px; min-width: 0; } +.v2-settings__account-name { font-size: 14px; } +.v2-settings__meta { font: 500 11.5px/1.3 var(--v2-font-mono); color: var(--v2-text-muted); } +.v2-settings__link { color: inherit; text-decoration: none; } +.v2-settings__link:hover { background: var(--v2-surface-hover); } +.v2-settings__link:focus-visible { outline: 2px solid var(--v2-accent); outline-offset: -2px; } +.v2-settings__edit { padding: 12px 16px 14px; border-top: 1px solid var(--v2-border-soft); } +.v2-settings__edit--closed { display: none; } +.v2-settings__token-detail { padding: 0 16px 12px; } +.v2-settings__actions-list { margin-top: 14px; } +.v2-settings__developer-disclosure { margin: 0; padding: 12px 16px; border-top: 1px solid var(--v2-border-soft); } +.v2-settings__developer-disclosure > summary { color: var(--v2-text-primary); cursor: pointer; font: 600 13px/18px var(--v2-font); } +.v2-settings__developer-disclosure[open] > summary { margin-bottom: 16px; } + +/* disclosure rows: the frame's details.disc */ +.v2p-disc { border-top: 1px solid var(--v2-border-soft); } +.v2p-disc:first-child, .v2-settings__actions-list > .v2p-disc { border-top: 0; } +.v2p-disc > summary { + list-style: none; cursor: pointer; display: grid; grid-template-columns: 180px minmax(0, 1fr) auto; + align-items: center; gap: 14px; min-height: 52px; padding: 8px 14px 8px 16px; +} +.v2p-disc > summary::-webkit-details-marker { display: none; } +.v2p-disc > summary .lb { font-size: 13px; font-weight: 600; color: var(--v2-text-primary); display: flex; align-items: center; gap: 8px; } +.v2p-disc > summary .vl { font-size: 13.5px; color: var(--v2-ink-2); min-width: 0; display: flex; align-items: center; gap: 10px; flex-wrap: wrap; } +.v2p-disc > summary > svg:last-child { color: var(--v2-text-placeholder); transition: transform 160ms; } +.v2p-disc[open] > summary > svg:last-child { transform: rotate(180deg); } +.v2p-disc:focus-within > summary:focus-visible { outline: 2px solid var(--v2-accent); outline-offset: -2px; } +.v2-settings__fold-body { padding: 12px 16px 14px; border-top: 1px solid var(--v2-border-soft); display: flex; flex-direction: column; gap: 12px; } + +/* the frame's 390 rows: one label column, the action at the end, values dropped */ +@media (max-width: 480px) { + .v2-settings .v2p-lrow { grid-template-columns: minmax(0, 1fr) auto; min-height: 48px; padding: 8px 12px; } + .v2-settings .v2p-lrow .lb, .v2-settings .v2p-disc > summary .lb { font-weight: 500; } + .v2-settings .v2p-lrow .vl { display: none; } + .v2-settings .v2p-disc > summary { grid-template-columns: minmax(0, 1fr) auto auto; min-height: 48px; padding: 8px 12px; } + .v2-settings .v2p-disc > summary .vl { display: none; } + .v2-settings .v2p-disc > summary .v2-settings__vl--error { display: flex; grid-column: 1 / -1; order: 3; } + .v2-settings__edit, .v2-settings__token-detail, .v2-settings__fold-body { padding-left: 12px; padding-right: 12px; } +} + +.v2-settings__account-row { min-height: 64px; } + +.v2-settings__avatar { + width: 44px; + height: 44px; + flex: 0 0 44px; + border: 0; + border-radius: var(--v2-radius-sm); + font-size: 12px; +} + +.v2-settings__avatar img { + width: 64px; + height: 64px; + border-radius: var(--v2-radius-sm); +} + +.v2-settings__account-copy { + min-width: 0; +} + +.v2-settings__account { + min-width: 0; +} + +.v2-settings__avatar-file-input { + display: none; +} + +.v2-settings__avatar-actions { + display: flex; + gap: 8px; + margin-top: 12px; +} + +.v2-settings__avatar-description { + margin: 12px 0 0; + color: var(--v2-text-secondary); + font-size: var(--v2-fs-body); + line-height: var(--v2-lh-body); +} + +.v2-settings__avatar-overlay { + z-index: 1000; + background: rgba(16, 24, 40, 0.55); +} + +.v2-settings__avatar-dialog { + width: min(560px, 100%); +} + +.v2-settings__avatar-crop-stage { + position: relative; + width: 100%; + aspect-ratio: 392 / 260; + overflow: hidden; + border-radius: var(--v2-radius-sm); + background: var(--v2-surface-tint); +} + +.v2-settings__avatar-crop { + display: block; + position: relative; + z-index: 0; + width: 100%; + height: 100%; + border: 0; + background: transparent; + cursor: grab; + touch-action: none; +} + +.v2-settings__avatar-crop:active { + cursor: grabbing; +} + +.v2-settings__avatar-crop-dim { + position: absolute; + z-index: 1; + inset: 0; + pointer-events: none; +} + +.v2-settings__avatar-crop-dim span { + position: absolute; + display: block; + background: rgba(16, 24, 40, 0.55); +} + +.v2-settings__avatar-crop-dim-top { + inset: 0 0 auto; + height: 21%; +} + +.v2-settings__avatar-crop-dim-right { + inset: 21% 0 21% auto; + width: 30.765%; +} + +.v2-settings__avatar-crop-dim-bottom { + inset: auto 0 0; + height: 21%; +} + +.v2-settings__avatar-crop-dim-left { + inset: 21% auto 21% 0; + width: 30.765%; +} + +.v2-settings__avatar-crop-frame { + position: absolute; + z-index: 2; + top: 50%; + left: 50%; + width: 38.47%; + aspect-ratio: 1; + transform: translate(-50%, -50%); + border: 1px solid var(--v2-surface); + pointer-events: none; +} + +.v2-settings__avatar-crop-loading { + margin: -8px 0 0; + color: var(--v2-text-tertiary); + font-size: var(--v2-fs-meta); +} + +.v2-settings__avatar-zoom { + display: flex; + align-items: center; + gap: 12px; + width: 100%; + color: var(--v2-text-secondary); + font-family: var(--v2-font-mono); + font-size: var(--v2-fs-label); + line-height: var(--v2-lh-label); + text-transform: uppercase; +} + +.v2-root .v2-settings__avatar-zoom input[type="range"] { + flex: 1; + min-width: 0; + height: 16px; + margin: 0; + padding: 0; + appearance: none; + -webkit-appearance: none; + border: 0; + background: transparent; + cursor: pointer; +} + +.v2-root .v2-settings__avatar-zoom input[type="range"]::-webkit-slider-runnable-track { + height: 2px; + background: linear-gradient( + 90deg, + var(--v2-ink) 0 var(--avatar-zoom-progress), + var(--v2-border-soft) var(--avatar-zoom-progress) 100% + ); +} + +.v2-root .v2-settings__avatar-zoom input[type="range"]::-moz-range-track { + height: 2px; + border: 0; + background: var(--v2-border-soft); +} + +.v2-root .v2-settings__avatar-zoom input[type="range"]::-moz-range-progress { + height: 2px; + background: var(--v2-ink); +} + +.v2-root .v2-settings__avatar-zoom input[type="range"]::-webkit-slider-thumb { + width: 16px; + height: 16px; + margin-top: -7px; + appearance: none; + -webkit-appearance: none; + border: 0; + border-radius: var(--v2-radius-sm); + background: var(--v2-ink); +} + +.v2-root .v2-settings__avatar-zoom input[type="range"]::-moz-range-thumb { + width: 16px; + height: 16px; + border: 0; + border-radius: var(--v2-radius-sm); + background: var(--v2-ink); +} + +.v2-root .v2-settings__avatar-zoom input[type="range"]:focus-visible { + outline: 2px solid var(--v2-accent); + outline-offset: 4px; +} + +.v2-settings__avatar-footer { + display: flex; + align-items: flex-end; + justify-content: space-between; + gap: 12px; + width: 100%; +} + +.v2-settings__avatar-previews { + display: flex; + align-items: flex-end; + justify-content: flex-start; + gap: 12px; +} + +.v2-settings__avatar-preview { + display: flex; + flex-direction: column; + align-items: center; + gap: 6px; + color: var(--v2-text-tertiary); + font-size: 12px; + line-height: var(--v2-lh-label); +} + +.v2-settings__avatar-preview-image { + display: block; + max-width: 64px; + max-height: 64px; + object-fit: cover; + border-radius: var(--v2-radius-sm); +} + +.v2-settings__avatar-preview-image--64 { width: 64px; height: 64px; } +.v2-settings__avatar-preview-image--32 { width: 32px; height: 32px; } +.v2-settings__avatar-preview-image--22 { width: 22px; height: 22px; } + +.v2-settings__avatar-dialog-actions { + display: flex; + justify-content: flex-end; + gap: 12px; +} + +.v2-root .v2-settings__avatar-dialog-actions button { + min-width: 112px; +} + +/* Neutralize App.css's bare auth-form shell: Settings owns this form's + * surface and spacing, so it must stay flat inside its section. */ +.v2-settings form.v2-settings__account { + max-width: none; + margin: 0; + padding: 0; + border-radius: 0; + background: transparent; + box-shadow: none; +} + +.v2-settings__account-name { + overflow: hidden; + color: var(--v2-text-primary); + font-family: var(--v2-font-display); + font-size: 14px; + font-weight: 700; + letter-spacing: -0.01em; + text-overflow: ellipsis; + white-space: nowrap; +} + +.v2-settings__account-type, +.v2-settings__meta { + color: var(--v2-text-tertiary); + font-family: var(--v2-font-mono); + font-size: var(--v2-fs-meta); + line-height: var(--v2-lh-meta); +} + +/* Administration (admin-only): two bordered link rows, ink label + muted detail. */ +.v2-settings__admin-links { display: flex; flex-direction: column; gap: 8px; } +.v2-settings__admin-link { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 12px; + padding: 10px 12px; + border: 1px solid var(--v2-border); + border-radius: 4px; + color: var(--v2-text-primary); + text-decoration: none; +} +.v2-settings__admin-link:hover { background: var(--v2-surface-hover); } +.v2-settings__admin-link-label { font: 600 13px/18px var(--v2-font); } +.v2-settings__admin-link-detail { font-size: 12px; color: var(--v2-text-muted); text-align: right; } + +.v2-settings__account-type { + margin-left: auto; + text-transform: lowercase; +} + +.v2-settings__account-fields { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 12px; + margin-top: 16px; +} + +.v2-settings__account-fields label, +.v2-settings__account-email { + display: grid; + gap: 6px; + min-width: 0; + color: var(--v2-text-tertiary); + font-family: var(--v2-font-mono); + font-size: var(--v2-fs-meta); + line-height: var(--v2-lh-meta); +} + +.v2-settings__account-fields > label { + align-content: start; +} + +.v2-settings__account-fields > .v2-settings__account-email { + grid-column: 1 / -1; +} + +.v2-settings__account-fields input { + box-sizing: border-box; + width: 100%; + min-height: 36px; + padding: 7px 10px; + border: 1px solid var(--v2-border); + border-radius: var(--v2-radius-sm); + background: var(--v2-surface); + color: var(--v2-text-primary); + font: 400 14px/20px var(--v2-font); +} + +.v2-settings__account-fields input:focus { + outline: none; + border-color: var(--v2-accent); + box-shadow: var(--v2-focus-ring); +} + +.v2-settings__account-fields input[readonly] { + color: var(--v2-text-secondary); + cursor: default; +} + +.v2-settings__email-control { + display: flex; + align-items: center; + gap: 8px; +} + +.v2-settings__email-control input { + min-width: 0; +} + +.v2-settings__email-control .v2p-b { + align-self: stretch; + flex: 0 0 auto; + white-space: nowrap; +} + +.v2-settings__username-control { + display: flex; + align-items: center; + gap: 8px; +} + +.v2-settings__username-control input { + min-width: 0; +} + +.v2-settings__username-control .v2p-b { + align-self: stretch; + flex: 0 0 auto; + white-space: nowrap; +} + +.v2-settings__field-help { + color: var(--v2-text-muted); + font: 400 11px/16px var(--v2-font-mono); +} + +.v2-settings__section .v2-devices { + margin: 0; + padding: 0; + border: 0; + border-radius: 0; + background: transparent; +} + +/* The frame reads the plan as plain text in the row's value. */ +.v2-settings__plan { font-weight: 600; color: var(--v2-text-primary); } +.v2-settings__error { color: var(--v2-text-primary); font-size: 12.5px; } +/* errors are a state the frame doesn't draw: they sit on their own line and stay visible at 390 */ +.v2-settings__error--row { grid-column: 1 / -1; } + +.v2-settings__section .v2-devices__item { + border-radius: var(--v2-radius-sm); +} + +.v2-settings__section .v2-devices__empty { + padding: 0; + background: transparent; +} + +.v2-settings__token-value { + display: grid; + grid-template-columns: minmax(0, 1fr) auto auto; + gap: 8px; + align-items: center; +} + +.v2-settings__token-value code { + overflow: hidden; + min-height: 40px; + padding: 10px 12px; + border: 1px solid var(--v2-border); + border-radius: var(--v2-radius-sm); + color: var(--v2-text-primary); + font-family: var(--v2-font-mono); + font-size: 12px; + line-height: 18px; + text-overflow: ellipsis; + white-space: nowrap; +} + +.v2-settings__token-meta { + margin: 8px 0 0; + color: var(--v2-text-tertiary); + font-family: var(--v2-font); + font-size: var(--v2-fs-body); + line-height: var(--v2-lh-body); +} + +.v2-settings__actions { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-top: 12px; +} + +.v2-settings__message { + margin: 0 0 12px; + color: var(--v2-text-secondary); + font-size: 13px; + line-height: 20px; +} + +.v2-settings__message--error { + color: var(--v2-text-primary); +} + +/* Settings owns the connected-apps controls without inheriting the legacy + * page chrome: its section label is the only page-level heading. */ +.v2-settings .apps-management--settings { + color: var(--v2-text-primary); + font-family: var(--v2-font); +} + +.v2-settings .apps-management--settings .MuiCardContent-root { + padding: 0; +} + +.v2-settings .apps-management--settings .MuiCardActions-root { + padding: 8px 0 0; +} + +.v2-settings .apps-management--settings .apps-management__section + .apps-management__section { + margin-top: 24px; + padding-top: 24px; + border-top: 1px solid var(--v2-border-soft); +} + +.v2-settings .apps-management--settings .apps-management__integration { + height: 100%; + padding: 12px; + border: 1px solid var(--v2-border); +} + +.v2-settings .apps-management--settings .apps-management__developer-tips { + margin: 14px 0 0; + color: var(--v2-text-tertiary); + font-family: var(--v2-font-mono); + font-size: var(--v2-fs-meta); + line-height: var(--v2-lh-meta); +} + +.v2-settings .apps-management--settings .MuiTypography-root, +.v2-settings .apps-management--settings .MuiInputBase-input, +.v2-settings .apps-management--settings .MuiInputLabel-root, +.v2-settings .apps-management--settings .MuiButton-root { + font-family: inherit; +} + +.v2-settings .apps-management--settings .MuiSvgIcon-colorPrimary, +.v2-settings .apps-management--settings .MuiButton-textPrimary { + color: var(--v2-ink); +} + +/* Cobalt remains a mark; buttons that commit a change are ink. */ +.v2-settings .apps-management--settings .MuiButton-containedPrimary { + border: 1px solid var(--v2-ink); + border-radius: var(--v2-radius-sm); + background: var(--v2-ink); + color: var(--v2-on-ink); +} + +.v2-settings .apps-management--settings .MuiButton-containedPrimary:hover { + border-color: var(--v2-ink-hover); + background: var(--v2-ink-hover); +} + +@media (max-width: 560px) { + .v2-settings .v2-devices__heading { + display: flex; + align-items: flex-start; + } + + .v2-settings .v2-devices__heading > button { + margin-left: auto; + } +} + +@media (max-width: 680px) { + .v2-settings__layout { + display: block; + padding-top: 24px; + } + + .v2-settings__token-value { + grid-template-columns: 1fr 1fr; + } + + .v2-settings__account-fields { + grid-template-columns: 1fr; + } + + .v2-settings__avatar-actions { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .v2-settings__avatar-actions .v2p-b { + min-height: 44px; + } + + .v2-settings__avatar-footer { + flex-direction: column; + align-items: stretch; + } + + .v2-settings__avatar-dialog-actions { + width: 100%; + gap: 8px; + } + + .v2-root .v2-settings__avatar-dialog-actions button { + flex: 1; + min-width: 0; + min-height: 44px; + } + + .v2-settings__token-value code { + grid-column: 1 / -1; + } +} + + +/* D223 (TASK-688): the artboard's 64px head spans the pane edge to edge and only the + body below it scrolls, so the settings route drops the feature body padding and the + legacy wrapper's max-width, the same way connectors does. */ +.v2-feature--settings > .v2-feature__body { padding: 0; display: flex; flex-direction: column; overflow: hidden; } +.v2-feature--settings .v2-feature__legacy { flex: 1 1 auto; width: 100%; max-width: none; min-height: 0; margin: 0; display: flex; flex-direction: column; } +.v2-feature--settings .v2-settings { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; width: 100%; } +.v2-feature--settings .v2-settings__layout { flex: 1 1 auto; min-height: 0; overflow-y: auto; padding-top: 24px; } diff --git a/frontend/src/v2/components/V2SettingsPage.tsx b/frontend/src/v2/components/V2SettingsPage.tsx index 422ba6fb7..109fde067 100644 --- a/frontend/src/v2/components/V2SettingsPage.tsx +++ b/frontend/src/v2/components/V2SettingsPage.tsx @@ -6,6 +6,7 @@ import { getAvatarSrc } from '../../utils/avatarUtils'; import { useAuth } from '../../context/AuthContext'; import AppsManagement from '../../components/AppsManagement'; import '../primitives/primitives.css'; +import './V2SettingsPage.css'; import V2BillingPanel from './V2BillingPanel'; import V2DevicesPanel from './V2DevicesPanel'; import V2DeleteAccount from './V2DeleteAccount'; @@ -61,8 +62,8 @@ const SettingsSection: React.FC = ({ id, title, children } id={settingsSectionId(id)} aria-labelledby={`${settingsSectionId(id)}-label`} > -

{title}

-
{children}
+

{title}

+
{children}
); @@ -78,6 +79,7 @@ const Chat = () => ; const Card = () => ; const Globe = () => ; const Moon = () => ; +const Next = () => ; const Warn = () => ; const V2AccountSection: React.FC = () => { @@ -233,46 +235,64 @@ const V2AccountSection: React.FC = () => { return (
-
- -
-
{name || accountName}
-
- @{savedUsername} · {currentUser?.role === 'admin' ? 'administrator' : 'member'} -
-
- +
+ + + + {name || accountName} + + @{savedUsername} · {currentUser?.role === 'admin' ? 'administrator' : 'member'} + + + + + + +
-
- - Email - {accountEmail || '—'} - +
+ Email + {accountEmail || '—'} + + +
{ />
+
{error &&

{error}

} {notice &&

{notice}

}
- +
{avatarFile && ( @@ -389,16 +409,16 @@ const V2AccountActions: React.FC = () => { }; return ( -
- - - Account actions +
+ + Account actions +
{error &&

{error}

}
- @@ -493,25 +513,26 @@ const V2ApiTokenSection: React.FC = () => { const statusLabel = hasToken ? `Active${last4 ? ` · ends in ${last4}` : ''}` : 'None'; return ( -
-
- - API token - {statusLabel} - {hasToken ? ( - - - + + + ) : ( + - - ) : ( - - )} + )} +
{(error || notice || token) && (
@@ -521,17 +542,17 @@ const V2ApiTokenSection: React.FC = () => { <>
{token && showToken ? token : maskedToken} - - +
{createdLabel &&

{createdLabel}

} )}
)} -
+ ); }; @@ -542,28 +563,30 @@ const V2LanguageSection: React.FC = () => { const chooseTheme = (pref: V2ThemePref) => setV2Theme(pref); return ( -
-
- {t('common.language.label')} - +
+ {t('common.language.label')} + + language.code === active) || LANGUAGE_REGISTRY[0]).code} onChange={(code) => { void i18n.changeLanguage(code); }} - options={LANGUAGE_REGISTRY.map((language) => ({ value: language.code, label: language.label }))} - /> + options={LANGUAGE_REGISTRY.map((language) => ({ value: language.code, label: language.short, ariaLabel: language.label }))} + />
-
- {t('common.theme.label')} - + {t('common.theme.label')} + + ({ value: pref, label: t(`common.theme.${pref}`) }))} - /> + />
-
+ ); }; @@ -589,20 +612,15 @@ const V2SettingsPage: React.FC = () => {
-
- - Plan - -
+
- - - Connectors - Manage connected services - + + Connectors + Manage connected services + {developerAppsEnabled() && (
@@ -612,7 +630,7 @@ const V2SettingsPage: React.FC = () => { )} - +
{isAdmin && ( )} diff --git a/frontend/src/v2/v2.css b/frontend/src/v2/v2.css index ca97177bf..0da91ff46 100644 --- a/frontend/src/v2/v2.css +++ b/frontend/src/v2/v2.css @@ -7682,839 +7682,6 @@ body.modern-ui.v2-canvas { color: var(--v2-text-tertiary); } -/* ---------- Billing panel (/v2/settings) ---------- */ -/* `.v2-root button.` prefix throughout keeps each billing action's visual - * treatment explicit. The global button reset is a low-specificity default, - * so ordinary component rules can now override it without a specificity hack. */ -.v2-billing { - border: 1px solid var(--v2-border); - border-radius: var(--v2-radius-md); - padding: 16px; - margin-bottom: 16px; - background: var(--v2-bg); -} -.v2-billing__head { - display: flex; - align-items: center; - gap: 10px; -} -.v2-billing__title { - margin: 0; - font-size: 15px; - font-weight: 700; - color: var(--v2-text-primary); -} -.v2-billing__badge { - font-size: 11px; - font-weight: 600; - letter-spacing: 0.04em; - text-transform: uppercase; - padding: 2px 8px; - border-radius: 999px; - background: var(--v2-surface); - border: 1px solid var(--v2-border); - color: var(--v2-text-secondary); -} -.v2-billing__badge--pro { - background: var(--v2-accent-soft); - border-color: var(--v2-accent); - color: var(--v2-accent-text); -} -.v2-billing__note { - margin: 8px 0 0; - font-size: 13px; - line-height: 1.5; - color: var(--v2-text-secondary); -} -.v2-billing__warn { - margin: 8px 0 0; - font-size: 12px; - color: var(--v2-text-secondary); -} -.v2-billing__error { - margin: 8px 0 0; - font-size: 12px; - color: var(--v2-danger, #c0392b); -} -.v2-billing__actions { - margin-top: 14px; - display: flex; - gap: 8px; -} -.v2-root button.v2-billing__btn { - height: 34px; - padding: 0 14px; - border-radius: var(--v2-radius-sm); - border: 1px solid var(--v2-border); - background: var(--v2-bg); - color: var(--v2-text-primary); - font-size: 13px; - font-weight: 600; - cursor: pointer; - transition: background 80ms ease, border-color 80ms ease; -} -.v2-root button.v2-billing__btn:hover:not(:disabled) { - border-color: var(--v2-border-strong); -} -.v2-root button.v2-billing__btn--primary { - background: var(--v2-ink); - border-color: var(--v2-ink); - color: var(--v2-on-ink); -} -.v2-root button.v2-billing__btn--primary:hover:not(:disabled) { - background: var(--v2-ink-hover); - border-color: var(--v2-ink-hover); -} -.v2-root button.v2-billing__btn:disabled { - opacity: 0.7; - cursor: default; -} - -/* ---------- Settings (/v2/settings) ---------- */ -.v2-settings { - width: min(100%, 1040px); - margin: 0 auto; - color: var(--v2-text-primary); -} - -.v2-settings__layout { - display: flex; - flex-direction: column; - gap: 24px; - padding-top: 40px; -} - - -.v2-settings__section { min-width: 0; } - -.v2-settings__label { - margin: 0 0 8px; - color: var(--v2-text-tertiary); - font-family: var(--v2-font-mono); - font-size: 12px; - font-weight: 500; - line-height: 16px; -} - -/* One bordered card per group; rows are separated by hairlines. */ -.v2-settings__group, -.v2-settings__fold { - min-width: 0; - overflow: hidden; - border: 1px solid var(--v2-border); - border-radius: var(--v2-radius-lg); - background: var(--v2-surface); -} -.v2-settings__group > * + *, -.v2-settings__group .v2-settings__fold { border-top: 1px solid var(--v2-border-soft); } -.v2-settings__group .v2-settings__fold { border-width: 0; border-radius: 0; border-top: 1px solid var(--v2-border-soft); } -.v2-settings__group > .v2-settings__fold:first-child { border-top: 0; } -.v2-settings__group > .v2-settings__account { border-top: 0; } -.v2-settings__account > .v2-settings__row + .v2-settings__row, -.v2-settings__account > .v2-settings__edit { border-top: 1px solid var(--v2-border-soft); } -.v2-settings__row { - display: flex; - min-height: 48px; - align-items: center; - gap: 12px; - padding: 8px 14px; - color: var(--v2-text-primary); - text-decoration: none; -} -.v2-settings__row > .v2-settings__row-label { flex: 0 0 auto; min-width: 84px; font: 600 13px/18px var(--v2-font); } -.v2-settings__row > .v2-settings__row-value { flex: 1 1 auto; min-width: 0; color: var(--v2-text-muted); font-size: 12px; } -.v2-settings__row-value--mono { font-family: var(--v2-font-mono); } -.v2-settings__row-actions { display: inline-flex; gap: 8px; margin-left: auto; } -.v2-settings__row > .v2-settings__secondary, -.v2-settings__row > .v2-settings__icon-btn, -.v2-settings__row > .v2-settings__row-actions { margin-left: auto; } -.v2-settings__icon { flex: 0 0 auto; color: var(--v2-text-tertiary); } -.v2-settings__summary { cursor: pointer; list-style: none; } -.v2-settings__summary::-webkit-details-marker { display: none; } -.v2-settings__summary > .v2-settings__icon:last-child { margin-left: auto; transition: transform .15s; } -details[open] > .v2-settings__summary > .v2-settings__icon:last-child { transform: rotate(180deg); } -.v2-settings__fold-body { padding: 12px 14px 14px; border-top: 1px solid var(--v2-border-soft); } -.v2-settings__fold--actions .v2-settings__fold-body { display: flex; flex-direction: column; gap: 12px; } -.v2-settings__row--link:hover { background: var(--v2-surface-hover); } -.v2-settings__row--link:focus-visible { outline: 2px solid var(--v2-accent); outline-offset: -2px; } -.v2-settings__row-arrow { margin-left: auto; color: var(--v2-text-tertiary); font-size: 18px; line-height: 18px; } -.v2-settings__row--plan > .v2-billing { display: flex; flex: 1 1 auto; align-items: center; justify-content: space-between; gap: 12px; } -.v2-settings__row--plan .v2-billing__head { margin: 0; } -.v2-settings__row--plan .v2-billing__badge, -.v2-settings__row--plan .v2-billing__btn { white-space: nowrap; } -.v2-settings__row--plan .v2-billing__actions { margin: 0; } -.v2-root .v2-settings button.v2-settings__icon-btn { - display: inline-flex; align-items: center; justify-content: center; - width: 32px; height: 32px; padding: 0; border: 0; border-radius: var(--v2-radius-sm); - background: transparent; color: var(--v2-text-tertiary); cursor: pointer; -} -.v2-root .v2-settings button.v2-settings__icon-btn:hover { background: var(--v2-surface-hover); color: var(--v2-text-primary); } -.v2-settings__edit { padding: 12px 14px 14px; } -.v2-settings__edit--closed { display: none; } -.v2-settings__group .v2-settings__developer-disclosure { margin: 0; padding: 12px 14px; } -.v2-settings__token-detail { padding: 0 14px 12px; } -@media (max-width: 560px) { - .v2-settings__layout { padding-top: 20px; gap: 16px; } - .v2-settings__row > .v2-settings__row-label { min-width: 0; } - .v2-settings__token > .v2-settings__row { flex-wrap: wrap; } - .v2-settings__row-actions { flex: 1 0 100%; justify-content: flex-end; } - .v2-settings__row--plan > .v2-billing { flex-wrap: wrap; justify-content: flex-start; } - .v2-settings__row--plan .v2-billing__actions { margin-left: auto; } -} - -.v2-settings__developer-disclosure { - margin-top: 20px; - border-top: 1px solid var(--v2-border-soft); - padding-top: 16px; -} -.v2-settings__developer-disclosure > summary { - color: var(--v2-text-primary); - cursor: pointer; - font: 600 13px/18px var(--v2-font); -} -.v2-settings__developer-disclosure[open] > summary { margin-bottom: 16px; } - -.v2-settings__content { - display: flex; - flex-direction: column; - gap: 20px; - width: min(100%, 760px); - min-width: 0; - margin: 0 auto; -} - -.v2-settings__account-row { min-height: 64px; } - -.v2-settings__avatar { - width: 44px; - height: 44px; - flex: 0 0 44px; - border: 0; - border-radius: var(--v2-radius-sm); - font-size: 12px; -} - -.v2-settings__avatar img { - width: 64px; - height: 64px; - border-radius: var(--v2-radius-sm); -} - -.v2-settings__account-copy { - min-width: 0; -} - -.v2-settings__account { - min-width: 0; -} - -.v2-settings__avatar-file-input { - display: none; -} - -.v2-settings__avatar-actions { - display: flex; - gap: 8px; - margin-top: 12px; -} - -.v2-settings__avatar-description { - margin: 12px 0 0; - color: var(--v2-text-secondary); - font-size: var(--v2-fs-body); - line-height: var(--v2-lh-body); -} - -.v2-settings__avatar-overlay { - z-index: 1000; - background: rgba(16, 24, 40, 0.55); -} - -.v2-settings__avatar-dialog { - width: min(560px, 100%); -} - -.v2-settings__avatar-crop-stage { - position: relative; - width: 100%; - aspect-ratio: 392 / 260; - overflow: hidden; - border-radius: var(--v2-radius-sm); - background: var(--v2-surface-tint); -} - -.v2-settings__avatar-crop { - display: block; - position: relative; - z-index: 0; - width: 100%; - height: 100%; - border: 0; - background: transparent; - cursor: grab; - touch-action: none; -} - -.v2-settings__avatar-crop:active { - cursor: grabbing; -} - -.v2-settings__avatar-crop-dim { - position: absolute; - z-index: 1; - inset: 0; - pointer-events: none; -} - -.v2-settings__avatar-crop-dim span { - position: absolute; - display: block; - background: rgba(16, 24, 40, 0.55); -} - -.v2-settings__avatar-crop-dim-top { - inset: 0 0 auto; - height: 21%; -} - -.v2-settings__avatar-crop-dim-right { - inset: 21% 0 21% auto; - width: 30.765%; -} - -.v2-settings__avatar-crop-dim-bottom { - inset: auto 0 0; - height: 21%; -} - -.v2-settings__avatar-crop-dim-left { - inset: 21% auto 21% 0; - width: 30.765%; -} - -.v2-settings__avatar-crop-frame { - position: absolute; - z-index: 2; - top: 50%; - left: 50%; - width: 38.47%; - aspect-ratio: 1; - transform: translate(-50%, -50%); - border: 1px solid var(--v2-surface); - pointer-events: none; -} - -.v2-settings__avatar-crop-loading { - margin: -8px 0 0; - color: var(--v2-text-tertiary); - font-size: var(--v2-fs-meta); -} - -.v2-settings__avatar-zoom { - display: flex; - align-items: center; - gap: 12px; - width: 100%; - color: var(--v2-text-secondary); - font-family: var(--v2-font-mono); - font-size: var(--v2-fs-label); - line-height: var(--v2-lh-label); - text-transform: uppercase; -} - -.v2-root .v2-settings__avatar-zoom input[type="range"] { - flex: 1; - min-width: 0; - height: 16px; - margin: 0; - padding: 0; - appearance: none; - -webkit-appearance: none; - border: 0; - background: transparent; - cursor: pointer; -} - -.v2-root .v2-settings__avatar-zoom input[type="range"]::-webkit-slider-runnable-track { - height: 2px; - background: linear-gradient( - 90deg, - var(--v2-ink) 0 var(--avatar-zoom-progress), - var(--v2-border-soft) var(--avatar-zoom-progress) 100% - ); -} - -.v2-root .v2-settings__avatar-zoom input[type="range"]::-moz-range-track { - height: 2px; - border: 0; - background: var(--v2-border-soft); -} - -.v2-root .v2-settings__avatar-zoom input[type="range"]::-moz-range-progress { - height: 2px; - background: var(--v2-ink); -} - -.v2-root .v2-settings__avatar-zoom input[type="range"]::-webkit-slider-thumb { - width: 16px; - height: 16px; - margin-top: -7px; - appearance: none; - -webkit-appearance: none; - border: 0; - border-radius: var(--v2-radius-sm); - background: var(--v2-ink); -} - -.v2-root .v2-settings__avatar-zoom input[type="range"]::-moz-range-thumb { - width: 16px; - height: 16px; - border: 0; - border-radius: var(--v2-radius-sm); - background: var(--v2-ink); -} - -.v2-root .v2-settings__avatar-zoom input[type="range"]:focus-visible { - outline: 2px solid var(--v2-accent); - outline-offset: 4px; -} - -.v2-settings__avatar-footer { - display: flex; - align-items: flex-end; - justify-content: space-between; - gap: 12px; - width: 100%; -} - -.v2-settings__avatar-previews { - display: flex; - align-items: flex-end; - justify-content: flex-start; - gap: 12px; -} - -.v2-settings__avatar-preview { - display: flex; - flex-direction: column; - align-items: center; - gap: 6px; - color: var(--v2-text-tertiary); - font-size: 12px; - line-height: var(--v2-lh-label); -} - -.v2-settings__avatar-preview-image { - display: block; - max-width: 64px; - max-height: 64px; - object-fit: cover; - border-radius: var(--v2-radius-sm); -} - -.v2-settings__avatar-preview-image--64 { width: 64px; height: 64px; } -.v2-settings__avatar-preview-image--32 { width: 32px; height: 32px; } -.v2-settings__avatar-preview-image--22 { width: 22px; height: 22px; } - -.v2-settings__avatar-dialog-actions { - display: flex; - justify-content: flex-end; - gap: 12px; -} - -.v2-root .v2-settings__avatar-dialog-actions button { - min-width: 112px; -} - -/* Neutralize App.css's bare auth-form shell: Settings owns this form's - * surface and spacing, so it must stay flat inside its section. */ -.v2-settings form.v2-settings__account { - max-width: none; - margin: 0; - padding: 0; - border-radius: 0; - background: transparent; - box-shadow: none; -} - -.v2-settings__account-name { - overflow: hidden; - color: var(--v2-text-primary); - font-family: var(--v2-font-display); - font-size: 14px; - font-weight: 700; - letter-spacing: -0.01em; - text-overflow: ellipsis; - white-space: nowrap; -} - -.v2-settings__account-type, -.v2-settings__meta { - color: var(--v2-text-tertiary); - font-family: var(--v2-font-mono); - font-size: var(--v2-fs-meta); - line-height: var(--v2-lh-meta); -} - -/* Administration (admin-only): two bordered link rows, ink label + muted detail. */ -.v2-settings__admin-links { display: flex; flex-direction: column; gap: 8px; } -.v2-settings__admin-link { - display: flex; - align-items: baseline; - justify-content: space-between; - gap: 12px; - padding: 10px 12px; - border: 1px solid var(--v2-border); - border-radius: 4px; - color: var(--v2-text-primary); - text-decoration: none; -} -.v2-settings__admin-link:hover { background: var(--v2-surface-hover); } -.v2-settings__admin-link-label { font: 600 13px/18px var(--v2-font); } -.v2-settings__admin-link-detail { font-size: 12px; color: var(--v2-text-muted); text-align: right; } - -.v2-settings__account-type { - margin-left: auto; - text-transform: lowercase; -} - -.v2-settings__account-fields { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 12px; - margin-top: 16px; -} - -.v2-settings__account-fields label, -.v2-settings__account-email { - display: grid; - gap: 6px; - min-width: 0; - color: var(--v2-text-tertiary); - font-family: var(--v2-font-mono); - font-size: var(--v2-fs-meta); - line-height: var(--v2-lh-meta); -} - -.v2-settings__account-fields > label { - align-content: start; -} - -.v2-settings__account-fields > .v2-settings__account-email { - grid-column: 1 / -1; -} - -.v2-settings__account-fields input { - box-sizing: border-box; - width: 100%; - min-height: 36px; - padding: 7px 10px; - border: 1px solid var(--v2-border); - border-radius: var(--v2-radius-sm); - background: var(--v2-surface); - color: var(--v2-text-primary); - font: 400 14px/20px var(--v2-font); -} - -.v2-settings__account-fields input:focus { - outline: none; - border-color: var(--v2-accent); - box-shadow: var(--v2-focus-ring); -} - -.v2-settings__account-fields input[readonly] { - color: var(--v2-text-secondary); - cursor: default; -} - -.v2-settings__email-control { - display: flex; - align-items: center; - gap: 8px; -} - -.v2-settings__email-control input { - min-width: 0; -} - -.v2-settings__email-control .v2-settings__secondary { - align-self: stretch; - flex: 0 0 auto; - white-space: nowrap; -} - -.v2-settings__username-control { - display: flex; - align-items: center; - gap: 8px; -} - -.v2-settings__username-control input { - min-width: 0; -} - -.v2-settings__username-control .v2-settings__secondary { - align-self: stretch; - flex: 0 0 auto; - white-space: nowrap; -} - -.v2-settings__field-help { - color: var(--v2-text-muted); - font: 400 11px/16px var(--v2-font-mono); -} - -.v2-settings__section .v2-billing, -.v2-settings__section .v2-devices { - margin: 0; - padding: 0; - border: 0; - border-radius: 0; - background: transparent; -} - -.v2-settings__section .v2-billing__badge { - /* The artboard reads the plan as plain text in the row's value, not a chip. */ - padding: 0; - border: 0; - border-radius: 0; - background: transparent; - color: var(--v2-text-primary); - font-family: var(--v2-font); - font-size: 13.5px; - font-weight: 600; - letter-spacing: 0; - line-height: 18px; - text-transform: none; -} - -.v2-settings__section .v2-billing__badge--pro { - background: transparent; - border-color: transparent; - color: var(--v2-text-primary); -} - -.v2-settings__section .v2-devices__item { - border-radius: var(--v2-radius-sm); -} - -.v2-settings__section .v2-devices__empty { - padding: 0; - background: transparent; -} - -.v2-settings__token-value { - display: grid; - grid-template-columns: minmax(0, 1fr) auto auto; - gap: 8px; - align-items: center; -} - -.v2-settings__token-value code { - overflow: hidden; - min-height: 40px; - padding: 10px 12px; - border: 1px solid var(--v2-border); - border-radius: var(--v2-radius-sm); - color: var(--v2-text-primary); - font-family: var(--v2-font-mono); - font-size: 12px; - line-height: 18px; - text-overflow: ellipsis; - white-space: nowrap; -} - -.v2-settings__token-meta { - margin: 8px 0 0; - color: var(--v2-text-tertiary); - font-family: var(--v2-font); - font-size: var(--v2-fs-body); - line-height: var(--v2-lh-body); -} - -.v2-settings__actions { - display: flex; - flex-wrap: wrap; - gap: 8px; - margin-top: 12px; -} - -.v2-root button.v2-settings__primary, -.v2-root button.v2-settings__secondary { - min-height: 36px; - padding: 0 12px; - border: 1px solid var(--v2-border); - border-radius: var(--v2-radius-sm); - background: var(--v2-bg); - color: var(--v2-text-primary); - font: 600 13px/20px var(--v2-font); - cursor: pointer; -} - -.v2-root button.v2-settings__primary { - border-color: var(--v2-ink); - background: var(--v2-ink); - color: var(--v2-on-ink); -} - -.v2-root button.v2-settings__primary:hover:not(:disabled):not([aria-disabled="true"]) { - border-color: var(--v2-ink-hover); - background: var(--v2-ink-hover); -} - -.v2-root button.v2-settings__secondary:hover:not(:disabled):not([aria-disabled="true"]) { - border-color: var(--v2-border-strong); -} - -.v2-root button.v2-settings__primary:focus-visible, -.v2-root button.v2-settings__secondary:focus-visible { - outline: none; - border-color: var(--v2-accent); - box-shadow: var(--v2-focus-ring); -} - -.v2-root button.v2-settings__primary:disabled, -.v2-root button.v2-settings__secondary:disabled, -.v2-root button.v2-settings__primary[aria-disabled="true"], -.v2-root button.v2-settings__secondary[aria-disabled="true"] { - cursor: default; - opacity: 0.55; -} - -.v2-settings__message { - margin: 0 0 12px; - color: var(--v2-text-secondary); - font-size: 13px; - line-height: 20px; -} - -.v2-settings__message--error { - color: var(--v2-text-primary); -} - -/* D215 Settings › Look: a label and a segment per row. Language moved here - from the rail (TASK-691). */ -.v2-settings__look { display: flex; flex-direction: column; } -.v2-settings__look .v2-settings__row { justify-content: flex-start; } -.v2-settings__look .v2-settings__row > :last-child { margin-left: auto; } -.v2-settings__look .v2-settings__row + .v2-settings__row { border-top: 1px solid var(--v2-border-soft); } - - -/* Settings owns the connected-apps controls without inheriting the legacy - * page chrome: its section label is the only page-level heading. */ -.v2-settings .apps-management--settings { - color: var(--v2-text-primary); - font-family: var(--v2-font); -} - -.v2-settings .apps-management--settings .MuiCardContent-root { - padding: 0; -} - -.v2-settings .apps-management--settings .MuiCardActions-root { - padding: 8px 0 0; -} - -.v2-settings .apps-management--settings .apps-management__section + .apps-management__section { - margin-top: 24px; - padding-top: 24px; - border-top: 1px solid var(--v2-border-soft); -} - -.v2-settings .apps-management--settings .apps-management__integration { - height: 100%; - padding: 12px; - border: 1px solid var(--v2-border); -} - -.v2-settings .apps-management--settings .apps-management__developer-tips { - margin: 14px 0 0; - color: var(--v2-text-tertiary); - font-family: var(--v2-font-mono); - font-size: var(--v2-fs-meta); - line-height: var(--v2-lh-meta); -} - -.v2-settings .apps-management--settings .MuiTypography-root, -.v2-settings .apps-management--settings .MuiInputBase-input, -.v2-settings .apps-management--settings .MuiInputLabel-root, -.v2-settings .apps-management--settings .MuiButton-root { - font-family: inherit; -} - -.v2-settings .apps-management--settings .MuiSvgIcon-colorPrimary, -.v2-settings .apps-management--settings .MuiButton-textPrimary { - color: var(--v2-ink); -} - -/* Cobalt remains a mark; buttons that commit a change are ink. */ -.v2-settings .apps-management--settings .MuiButton-containedPrimary { - border: 1px solid var(--v2-ink); - border-radius: var(--v2-radius-sm); - background: var(--v2-ink); - color: var(--v2-on-ink); -} - -.v2-settings .apps-management--settings .MuiButton-containedPrimary:hover { - border-color: var(--v2-ink-hover); - background: var(--v2-ink-hover); -} - -@media (max-width: 560px) { - .v2-settings .v2-devices__heading { - display: flex; - align-items: flex-start; - } - - .v2-settings .v2-devices__heading > button { - margin-left: auto; - } -} - -@media (max-width: 680px) { - .v2-settings__layout { - display: block; - padding-top: 24px; - } - - .v2-settings__token-value { - grid-template-columns: 1fr 1fr; - } - - .v2-settings__account-fields { - grid-template-columns: 1fr; - } - - .v2-settings__avatar-actions { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - } - - .v2-root .v2-settings__avatar-actions button.v2-settings__secondary { - min-height: 44px; - } - - .v2-settings__avatar-footer { - flex-direction: column; - align-items: stretch; - } - - .v2-settings__avatar-dialog-actions { - width: 100%; - gap: 8px; - } - - .v2-root .v2-settings__avatar-dialog-actions button { - flex: 1; - min-width: 0; - min-height: 44px; - } - - .v2-settings__token-value code { - grid-column: 1 / -1; - } -} - /* ── V2 Pod Board — /v2/pods/:podId/board ──────────────────────────────── */ .v2-board { @@ -10866,10 +10033,6 @@ details[open] > .v2-settings__summary > .v2-settings__icon:last-child { transfor .v2-root:lang(zh) .v2-feature__header .v2-feature__title, .v2-root:lang(zh) .v2-agent-settings > .v2-page-head h1, .v2-root:lang(zh) .v2-agent-settings > .v2-agent-memory__head .v2-agent-memory__ident h1, -.v2-root:lang(zh) .v2-settings__label, -.v2-root:lang(zh) .v2-settings__account-name, -.v2-root:lang(zh) .v2-settings__section .v2-billing__title, -.v2-root:lang(zh) .v2-settings__section .v2-devices h2, .v2-root:lang(zh) .v2-connector-row__name, .v2-root:lang(zh) .v2-connector-aside__card h2, .v2-root:lang(zh) .v2-feature__legacy .MuiTypography-h1, @@ -10940,7 +10103,6 @@ details[open] > .v2-settings__summary > .v2-settings__icon:last-child { transfor .v2-root:lang(zh) .v2-modal__hint, .v2-root:lang(zh) .v2-invite-card__description, .v2-root:lang(zh) .v2-login__field-hint, -.v2-root:lang(zh) .v2-billing__note, .v2-root:lang(zh) .v2-approval__summary, .v2-root:lang(zh) .v2-approval__action { line-height: 1.6; @@ -14056,13 +13218,4 @@ details[open] > .v2-settings__summary > .v2-settings__icon:last-child { transfor } .v2-feature__header .v2-page-head__meta { min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .v2-feature__body { padding: 12px 12px 16px; } - .v2-settings__layout { padding-top: 0; } } - -/* D223 (TASK-688): the artboard's 64px head spans the pane edge to edge and only the - body below it scrolls, so the settings route drops the feature body padding and the - legacy wrapper's max-width, the same way connectors does. */ -.v2-feature--settings > .v2-feature__body { padding: 0; display: flex; flex-direction: column; overflow: hidden; } -.v2-feature--settings .v2-feature__legacy { flex: 1 1 auto; width: 100%; max-width: none; min-height: 0; margin: 0; display: flex; flex-direction: column; } -.v2-feature--settings .v2-settings { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; width: 100%; } -.v2-feature--settings .v2-settings__layout { flex: 1 1 auto; min-height: 0; overflow-y: auto; padding-top: 24px; }