feat(web): marketing theme toggle, site search palette, and phone dev access

- Theme toggle in the nav: follows the device until chosen, remembers the choice, sets data-theme before first paint (no flash).
- Site search: Cmd/Ctrl+K command palette on every marketing page, opened by the shortcut, a nav Search button, or the Docs search field. Searches pages, docs topics and articles that have a page.
- Docs topics move to content.ts so the page and the search index share one source.
- next.config: allowedDevOrigins for private network ranges so a phone on the same Wi-Fi hydrates the dev site. Dev only.
- Sun and moon icons added by hand (not Figma exports); noted in the icons README.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
SinachPat
2026-10-13 22:27:34 +01:00
co-authored by Claude Sonnet 5.5
parent 0cff3902b2
commit f69ce33dd0
21 changed files with 818 additions and 17 deletions
+51 -1
View File
@@ -1,9 +1,24 @@
import { render, screen, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { describe, expect, it } from 'vitest';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { Nav } from '../../src/marketing/Nav.tsx';
function deviceTheme(dark: boolean) {
window.matchMedia = vi.fn().mockImplementation((query: string) => ({
matches: dark && query === '(prefers-color-scheme: dark)',
media: query,
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
})) as unknown as typeof window.matchMedia;
}
describe('Nav', () => {
beforeEach(() => {
localStorage.clear();
document.documentElement.removeAttribute('data-theme');
deviceTheme(false);
});
it('links the brand to the home page', () => {
render(<Nav />);
expect(screen.getByRole('link', { name: 'Wursor' })).toHaveAttribute('href', '/');
@@ -57,4 +72,39 @@ describe('Nav', () => {
await userEvent.click(within(menu).getByRole('link', { name: 'Pricing' }));
expect(screen.queryByRole('navigation', { name: 'Mobile' })).not.toBeInTheDocument();
});
describe('theme toggle', () => {
it('is off on a light device', () => {
render(<Nav />);
expect(screen.getByRole('button', { name: 'Dark theme' })).toHaveAttribute('aria-pressed', 'false');
});
it('is on when the device is dark and nothing is saved', () => {
deviceTheme(true);
render(<Nav />);
expect(screen.getByRole('button', { name: 'Dark theme' })).toHaveAttribute('aria-pressed', 'true');
});
it('is on when dark was chosen earlier, even on a light device', () => {
localStorage.setItem('wursor-theme', 'dark');
render(<Nav />);
expect(screen.getByRole('button', { name: 'Dark theme' })).toHaveAttribute('aria-pressed', 'true');
});
it('switches the page to dark and remembers it', async () => {
render(<Nav />);
await userEvent.click(screen.getByRole('button', { name: 'Dark theme' }));
expect(document.documentElement).toHaveAttribute('data-theme', 'dark');
expect(localStorage.getItem('wursor-theme')).toBe('dark');
expect(screen.getByRole('button', { name: 'Dark theme' })).toHaveAttribute('aria-pressed', 'true');
});
it('switches a dark device to light', async () => {
deviceTheme(true);
render(<Nav />);
await userEvent.click(screen.getByRole('button', { name: 'Dark theme' }));
expect(document.documentElement).toHaveAttribute('data-theme', 'light');
expect(screen.getByRole('button', { name: 'Dark theme' })).toHaveAttribute('aria-pressed', 'false');
});
});
});