diff --git a/apps/web/src/components/admin/settings/appearance/__tests__/page-titles-section.test.tsx b/apps/web/src/components/admin/settings/appearance/__tests__/page-titles-section.test.tsx new file mode 100644 index 0000000000..e5bb64b69a --- /dev/null +++ b/apps/web/src/components/admin/settings/appearance/__tests__/page-titles-section.test.tsx @@ -0,0 +1,58 @@ +// @vitest-environment happy-dom +/** + * Appearance words each page's title and puts the titles on the band. A page + * whose words are cleared goes back to its usual title. + */ +import { afterEach, describe, expect, it } from 'vitest' +import { useState } from 'react' +import { cleanup, fireEvent, render, screen } from '@testing-library/react' +import { defaultDesign, type PortalDesign } from '@/lib/shared/design/schema' +import { PageTitlesSection } from '../page-titles-section' + +const GATES = { + feedback: true, + roadmap: true, + changelog: true, + help: false, + support: true, + status: true, +} + +let latest: PortalDesign + +function Harness() { + const [design, setDesign] = useState(defaultDesign()) + latest = design + return ( + + setDesign((current) => { + const draft = structuredClone(current) + change(draft) + return draft + }) + } + /> + ) +} + +afterEach(cleanup) + +describe('Appearance’s page titles', () => { + it('words the first shown page, and gives it back its usual title when cleared', () => { + render() + const title = screen.getByLabelText('Title') + fireEvent.change(title, { target: { value: 'What we are building' } }) + expect(latest.pages.titles?.roadmap?.title).toEqual({ en: 'What we are building' }) + fireEvent.change(title, { target: { value: '' } }) + expect(latest.pages.titles).toBeUndefined() + }) + + it('puts the titles on the band', () => { + render() + fireEvent.click(screen.getByRole('radio', { name: /On the band/ })) + expect(latest.layout.pageTitles).toBe('band') + }) +}) diff --git a/apps/web/src/components/admin/settings/appearance/brand-panel.tsx b/apps/web/src/components/admin/settings/appearance/brand-panel.tsx index 4af1ef24ff..b8d4db1804 100644 --- a/apps/web/src/components/admin/settings/appearance/brand-panel.tsx +++ b/apps/web/src/components/admin/settings/appearance/brand-panel.tsx @@ -394,6 +394,19 @@ export function BrandPanel({ drawing: , }))} /> + + update((draft) => void (draft.shape.fill = on ? 'ink' : 'accent')) + } + /> + } + /> diff --git a/apps/web/src/components/admin/settings/appearance/drawings.tsx b/apps/web/src/components/admin/settings/appearance/drawings.tsx index bfd5c7a85f..e8b2fabc65 100644 --- a/apps/web/src/components/admin/settings/appearance/drawings.tsx +++ b/apps/web/src/components/admin/settings/appearance/drawings.tsx @@ -356,3 +356,19 @@ export function FocusDrawing({ focus }: { focus: NonNullable ) } + +export function PageTitlesDrawing({ place }: { place: NonNullable }) { + return ( + + + + + + + + + + ) +} diff --git a/apps/web/src/components/admin/settings/appearance/fonts-section.tsx b/apps/web/src/components/admin/settings/appearance/fonts-section.tsx index bb75d0d3ff..2ceaa83c75 100644 --- a/apps/web/src/components/admin/settings/appearance/fonts-section.tsx +++ b/apps/web/src/components/admin/settings/appearance/fonts-section.tsx @@ -80,20 +80,26 @@ export function guessFamily(name: string): string { return base.replace(/([a-z])([A-Z])/g, '$1 $2').trim() } +/** The choice that leaves an optional font to another. */ +const SAME = '__same__' + function FontSelect({ id, label, value, uploaded, onChange, + same, }: { id: string label: string - value: DesignFont + value: DesignFont | undefined uploaded: UploadedFont[] - onChange: (font: DesignFont) => void + onChange: (font: DesignFont | undefined) => void + /** Offers leaving it to another font, named so. */ + same?: string }) { - const current = value.kind === 'bundled' ? value.id : `${UPLOADED}${value.family}` + const current = !value ? SAME : value.kind === 'bundled' ? value.id : `${UPLOADED}${value.family}` return (
+ {(['title', 'subtitle'] as const).map((field) => ( +
+ + + update((draft) => { + const titles = (draft.pages.titles ??= {}) + const words = titles[page] ?? { title: {}, subtitle: {} } + const text = event.target.value + if (text) words[field][locale] = text + else delete words[field][locale] + if (Object.keys(words.title).length + Object.keys(words.subtitle).length) { + titles[page] = words + } else delete titles[page] + if (!Object.keys(titles).length) delete draft.pages.titles + }) + } + /> +
+ ))} + + ) +} diff --git a/apps/web/src/components/admin/settings/appearance/sections-panel.tsx b/apps/web/src/components/admin/settings/appearance/sections-panel.tsx index f002b0e7a4..8fe21fdbc2 100644 --- a/apps/web/src/components/admin/settings/appearance/sections-panel.tsx +++ b/apps/web/src/components/admin/settings/appearance/sections-panel.tsx @@ -20,6 +20,7 @@ import type { PortalPageGates } from '@/lib/shared/design/paths' import { DesignTiles } from './design-tiles' import { LanguagePicker } from './language-picker' import { HomeSection } from './home-section' +import { PageTitlesSection } from './page-titles-section' import { MenuSection } from './menu-section' import { CssSection, type CssWarning } from './css-section' import { ImageSlot } from './image-slot' @@ -354,6 +355,8 @@ export function SectionsPanel({ )} + +
+

diff --git a/apps/web/src/components/help-center/help-center-hero.tsx b/apps/web/src/components/help-center/help-center-hero.tsx index 927c376155..eece60eafd 100644 --- a/apps/web/src/components/help-center/help-center-hero.tsx +++ b/apps/web/src/components/help-center/help-center-hero.tsx @@ -1,5 +1,6 @@ import type { ReactNode } from 'react' import { cn } from '@/lib/shared/utils' +import { PortalPageTitle, useDesignTitles } from '@/components/public/portal-page-title' interface HelpCenterHeroProps { /** 'home' shows the heading + subtitle above the search; 'compact' is search-only. */ @@ -29,6 +30,21 @@ const GRID_MASK = 'radial-gradient(ellipse 80% 62% at 50% 0%, #000 30%, transpar * overflow below it; only the decorative background layer clips its own glow. */ export function HelpCenterHero({ variant, title, description, children }: HelpCenterHeroProps) { + // On the design engine the help centre's title is the design's, as every + // page's is, and its pages carry no decoration of their own. + const designTitles = useDesignTitles() + if (designTitles && variant === 'home' && title) + return ( + +

{children}
+ + ) + if (designTitles) + return ( +
+
{children}
+
+ ) return (