From 07fea387641e8ea6eb695c8184ea113f01566aee Mon Sep 17 00:00:00 2001 From: Jordan Harrison Date: Sun, 20 Sep 2026 17:52:13 +0100 Subject: [PATCH] feat(brand): apply the brand guidelines to meith.dev and the meith theme Site: token-driven buttons, square header controls, mono uppercase nav with a current-item rule, a search bar, a three-way scheme toggle, an accent Get started, one site-wide arrow convention, demo and community links, and British copy without ampersands. Theme: fills every slot in the registry, semantic marks on the greyscale ramp, a neutral accent wash, 60% link underlines, 160-180ms motion, arrows that stay legible on filled actions, a 60rem post measure and a full-width subforum list. Docs: a brand guidelines page under contributing, registered and indexed. Co-Authored-By: Claude Fable 5.1 --- apps/web/app/docs/page.tsx | 2 +- apps/web/app/page.tsx | 16 +- apps/web/content/docs.manifest.json | 10 + apps/web/e2e/marketing.spec.ts | 26 ++- apps/web/src/components/docs-search.tsx | 2 +- apps/web/src/components/forum-link.tsx | 12 +- apps/web/src/components/nav-link.tsx | 23 ++ apps/web/src/components/site-bands.tsx | 4 +- apps/web/src/components/site-footer.tsx | 7 + apps/web/src/components/site-header.tsx | 18 +- apps/web/src/components/theme-toggle.tsx | 92 ++++---- apps/web/src/content/segments.ts | 10 +- apps/web/src/content/site.ts | 1 + apps/web/src/og/card.tsx | 2 +- apps/web/src/styles/chrome.css | 138 ++++++++---- apps/web/src/styles/globals.css | 32 ++- apps/web/src/styles/home.css | 30 +-- apps/web/src/styles/marketing.css | 2 +- docs/README.md | 1 + docs/contributing/brand.md | 175 ++++++++++++++++ docs/extensions/theme-design.md | 15 +- themes/meith/src/copy.ts | 75 ++++++- themes/meith/src/messages/en.json | 37 ++++ themes/meith/src/shared.tsx | 29 ++- themes/meith/src/slots/auth-page.tsx | 46 ++++ themes/meith/src/slots/discovery-view.tsx | 116 +++++++++++ themes/meith/src/slots/editor-toolbar.tsx | 89 ++++++++ themes/meith/src/slots/error-notice.tsx | 45 ++++ themes/meith/src/slots/footer.tsx | 2 +- themes/meith/src/slots/forum-jump.tsx | 41 ++++ themes/meith/src/slots/header.tsx | 10 +- themes/meith/src/slots/notice.tsx | 8 +- themes/meith/src/slots/panel-nav.tsx | 107 ++++++++++ themes/meith/src/slots/panel-page.tsx | 85 ++++++++ themes/meith/src/slots/panel-shell.tsx | 34 +++ themes/meith/src/slots/post-form.tsx | 41 ++++ themes/meith/src/slots/quick-reply.tsx | 9 + themes/meith/src/slots/redirect-notice.tsx | 29 +++ themes/meith/src/slots/search-form.tsx | 145 +++++++++++++ themes/meith/src/slots/search-results.tsx | 231 +++++++++++++++++++++ themes/meith/src/slots/subforum-list.tsx | 6 +- themes/meith/src/slots/user-panel.tsx | 6 +- themes/meith/src/theme.test.ts | 40 ++-- themes/meith/src/theme.ts | 47 +++++ themes/meith/src/tokens.ts | 30 ++- 45 files changed, 1737 insertions(+), 189 deletions(-) create mode 100644 apps/web/src/components/nav-link.tsx create mode 100644 docs/contributing/brand.md create mode 100644 themes/meith/src/slots/auth-page.tsx create mode 100644 themes/meith/src/slots/discovery-view.tsx create mode 100644 themes/meith/src/slots/editor-toolbar.tsx create mode 100644 themes/meith/src/slots/error-notice.tsx create mode 100644 themes/meith/src/slots/forum-jump.tsx create mode 100644 themes/meith/src/slots/panel-nav.tsx create mode 100644 themes/meith/src/slots/panel-page.tsx create mode 100644 themes/meith/src/slots/panel-shell.tsx create mode 100644 themes/meith/src/slots/post-form.tsx create mode 100644 themes/meith/src/slots/quick-reply.tsx create mode 100644 themes/meith/src/slots/redirect-notice.tsx create mode 100644 themes/meith/src/slots/search-form.tsx create mode 100644 themes/meith/src/slots/search-results.tsx diff --git a/apps/web/app/docs/page.tsx b/apps/web/app/docs/page.tsx index 52823fe2b..545793b5d 100644 --- a/apps/web/app/docs/page.tsx +++ b/apps/web/app/docs/page.tsx @@ -30,7 +30,7 @@ export default function DocsIndexPage() { Let’s get you started.

From your first board to your next big idea.

- + Start the quickstart ↗ diff --git a/apps/web/app/page.tsx b/apps/web/app/page.tsx index f6cf93bd1..4d0c7cef2 100644 --- a/apps/web/app/page.tsx +++ b/apps/web/app/page.tsx @@ -19,7 +19,7 @@ const communities = [ body: 'Keep the conversations that bring your people together.', }, { - name: 'Clubs & associations', + name: 'Clubs and associations', slug: 'clubs-and-associations', body: 'A shared space for members, plans and club life.', }, @@ -95,14 +95,19 @@ export default async function LandingPage() { On your domain. On your terms.

- + Start your forum + + Try the demo + - See Meith in use + Join the community
-

Free & open source. Self-hosted. Yours to keep.

+

+ Free and open source. Self-hosted. Yours to keep. The demo runs on sample data. +

@@ -265,6 +270,9 @@ export default async function LandingPage() { Create your community + + Try the demo + Read the docs diff --git a/apps/web/content/docs.manifest.json b/apps/web/content/docs.manifest.json index 2908301fe..3401f1795 100644 --- a/apps/web/content/docs.manifest.json +++ b/apps/web/content/docs.manifest.json @@ -851,6 +851,16 @@ "primary": false, "group": "Development workflow" }, + { + "slug": "brand", + "file": "contributing/brand.md", + "section": "contributing", + "title": "Brand guidelines", + "blurb": "How Meith looks, sounds and spends its one colour.", + "generated": false, + "primary": false, + "group": "Development workflow" + }, { "slug": "repository-checks", "file": "contributing/repository-checks.md", diff --git a/apps/web/e2e/marketing.spec.ts b/apps/web/e2e/marketing.spec.ts index 101aa5062..079709fe5 100644 --- a/apps/web/e2e/marketing.spec.ts +++ b/apps/web/e2e/marketing.spec.ts @@ -40,19 +40,22 @@ test('colour scheme persists across navigation and resets to the system preferen page, }) => { await page.goto('/') - await page.getByRole('button', { name: 'Colour scheme: system. Switch to light' }).click() + const scheme = page.getByRole('group', { name: 'Colour scheme' }) + await scheme.getByRole('button', { name: 'Light' }).click() await expect(page.locator('html')).toHaveAttribute('data-theme', 'light') - await page.getByRole('button', { name: 'Colour scheme: light. Switch to dark' }).click() + await scheme.getByRole('button', { name: 'Dark' }).click() await expect(page.locator('html')).toHaveAttribute('data-theme', 'dark') await page.goto('/about') await expect(page.locator('html')).toHaveAttribute('data-theme', 'dark') - await page.getByRole('button', { name: 'Colour scheme: dark. Switch to system' }).click() + await expect(scheme.getByRole('button', { name: 'Dark' })).toHaveAttribute('aria-pressed', 'true') + await scheme.getByRole('button', { name: 'System' }).click() await expect(page.locator('html')).not.toHaveAttribute('data-theme') await page.reload() - await expect( - page.getByRole('button', { name: 'Colour scheme: system. Switch to light' }), - ).toBeVisible() + await expect(scheme.getByRole('button', { name: 'System' })).toHaveAttribute( + 'aria-pressed', + 'true', + ) }) test('mobile navigation closes on Escape, outside interaction, and link selection', async ({ @@ -70,7 +73,10 @@ test('mobile navigation closes on Escape, outside interaction, and link selectio await expect(menu).toBeFocused() await menu.click() - await page.getByRole('button', { name: /^Colour scheme:/ }).click() + await page + .getByRole('group', { name: 'Colour scheme' }) + .getByRole('button', { name: 'System' }) + .click() await expect(navigation).toBeHidden() await menu.click() @@ -92,6 +98,10 @@ test('site navigation points to real destinations and exposes customisation guid ] as const) { await expect(nav.getByRole('link', { name, exact: true })).toHaveAttribute('href', href) } + await expect(nav.getByRole('link', { name: /^Demo/ })).toHaveAttribute( + 'href', + /^https:\/\/demo\./, + ) await expect(nav.getByRole('link', { name: /^Community/ })).toHaveAttribute( 'href', /^https:\/\/forum\./, @@ -110,7 +120,7 @@ test('site navigation points to real destinations and exposes customisation guid { name: /^Developers/, slug: 'developers' }, { name: /^Open-source projects/, slug: 'open-source' }, { name: /^Communities/, slug: 'communities' }, - { name: /^Clubs & associations/, slug: 'clubs-and-associations' }, + { name: /^Clubs and associations/, slug: 'clubs-and-associations' }, ] for (const audience of audiences) { diff --git a/apps/web/src/components/docs-search.tsx b/apps/web/src/components/docs-search.tsx index 98c50cee8..40cb592f1 100644 --- a/apps/web/src/components/docs-search.tsx +++ b/apps/web/src/components/docs-search.tsx @@ -105,7 +105,7 @@ export function DocsSearch() { className="inline-flex items-center gap-2 rounded-[var(--radius-control)] border border-border px-2.5 py-1.5 text-micro text-fg-subtle transition-colors hover:border-border-strong hover:text-fg" > ⌕ - Search + Search the docs ⌘K diff --git a/apps/web/src/components/forum-link.tsx b/apps/web/src/components/forum-link.tsx index 52893ebe8..2aa63e29a 100644 --- a/apps/web/src/components/forum-link.tsx +++ b/apps/web/src/components/forum-link.tsx @@ -2,9 +2,17 @@ import type { ReactNode } from 'react' import { site } from '../content/site' -export function ForumLink({ children, className }: { children: ReactNode; className?: string }) { +export function ForumLink({ + children, + className, + href = site.forum, +}: { + children: ReactNode + className?: string + href?: string +}) { return ( - + {children} (opens in a new tab) diff --git a/apps/web/src/components/nav-link.tsx b/apps/web/src/components/nav-link.tsx new file mode 100644 index 000000000..92e1c2d34 --- /dev/null +++ b/apps/web/src/components/nav-link.tsx @@ -0,0 +1,23 @@ +'use client' + +import Link from 'next/link' +import { usePathname } from 'next/navigation' +import type { ReactNode } from 'react' + +export function NavLink({ + href, + children, +}: { + readonly href: string + readonly children: ReactNode +}) { + const pathname = usePathname() + const current = + href === '/' ? pathname === '/' : pathname === href || pathname.startsWith(`${href}/`) + + return ( + + {children} + + ) +} diff --git a/apps/web/src/components/site-bands.tsx b/apps/web/src/components/site-bands.tsx index 7911cd982..6ad51c776 100644 --- a/apps/web/src/components/site-bands.tsx +++ b/apps/web/src/components/site-bands.tsx @@ -24,7 +24,7 @@ export function ClosingBand({
- Get started → + Get started ↗ {docsHref === undefined ? ( @@ -32,7 +32,7 @@ export function ClosingBand({ ) : ( - Explore the docs → + Explore the docs ↗ )}
diff --git a/apps/web/src/components/site-footer.tsx b/apps/web/src/components/site-footer.tsx index 30f3d379a..c5bc1ab8c 100644 --- a/apps/web/src/components/site-footer.tsx +++ b/apps/web/src/components/site-footer.tsx @@ -4,6 +4,7 @@ import { about } from '../content/about' import { audienceIndexHref } from '../content/segments' import { licence, licenceHref, site } from '../content/site' import { version } from '../content/version' +import { ForumLink } from './forum-link' export function SiteFooter() { return ( @@ -18,6 +19,12 @@ export function SiteFooter() { Who it’s for Extensions Docs + + Demo ↗ + + + Community ↗ + GitHub ↗ diff --git a/apps/web/src/components/site-header.tsx b/apps/web/src/components/site-header.tsx index b737a54c3..812ce0887 100644 --- a/apps/web/src/components/site-header.tsx +++ b/apps/web/src/components/site-header.tsx @@ -2,11 +2,13 @@ import Link from 'next/link' import { about } from '../content/about' import { audienceIndexHref } from '../content/segments' +import { site } from '../content/site' import { quickstartHref } from '../docs/registry' import { DocsSearch } from './docs-search' import { ForumLink } from './forum-link' import { Logomark } from './logomark' import { MobileMenu } from './mobile-menu' +import { NavLink } from './nav-link' import { ThemeToggle } from './theme-toggle' export function SiteHeader() { @@ -21,10 +23,13 @@ export function SiteHeader() {