Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/remove-builtin-theme-seeds.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@pmndrs/docs': patch
---

Remove the built-in `ThemeSeeds` component and its static fragment renderer. The component now belongs to the design-system documentation and must be registered through consumer configuration when needed.

Consumers using `<ThemeSeeds />` must provide their own component in `docs.config.ts`, including a static renderer for standalone HTML fragments. See the [custom components guide](https://docs.pmnd.rs/authoring/custom-components) and the [design-system implementation](https://github.com/pmndrs/design-system/pull/73). Shared theme controls and preset persistence remain available. The former authoring page redirects to the design-system Colors page.
2 changes: 0 additions & 2 deletions docs/authoring/introduction.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,5 @@ Plus:

See the following pages for detailed documentation on each custom component.

For named source-seed previews and their seed table, see [`ThemeSeeds`](theme-seeds).

To register your own React components and shared data for MDX pages, see
[Custom components and data](custom-components).
101 changes: 0 additions & 101 deletions docs/authoring/theme-seeds.mdx

This file was deleted.

5 changes: 5 additions & 0 deletions next.config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,11 @@ const nextConfig = {
rewrites: output === 'export' ? undefined : rewrites,
async redirects() {
return [
{
source: '/authoring/theme-seeds',
destination: 'https://pmndrs.github.io/design-system/tokens/colors#seeds',
permanent: true,
},
// Where the markdown of a page was first published. Redirects only apply to the request
// as it comes in, never to the destination of a rewrite, so this does not loop with the
// rewrite above, which still serves the markdown from `/md`.
Expand Down
128 changes: 29 additions & 99 deletions src/app/theme-presets/page.test.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,11 @@
import { expect, test, type Page } from '@playwright/test'
import { builder } from 'material-theme-builder'

declare global {
interface Window {
presetLabels: string[]
}
}

test.setTimeout(60000)

const presetPage = '/authoring/theme-seeds'
const surfaceInk = () =>
getComputedStyle(document.documentElement).getPropertyValue('--md-sys-color-on-surface').trim()

function recordPresetFrames() {
const view = window
view.presetLabels = []
function sample() {
const button = document.querySelector('button[aria-label="Seed preset"]')
if (button) view.presetLabels.push(button.textContent?.trim() ?? '')
requestAnimationFrame(sample)
}
requestAnimationFrame(sample)
}

function expectedSurfaceInk(source: string, contrast: number, neutral?: string) {
const css = builder(source, {
colorMatch: true,
Expand All @@ -38,105 +20,53 @@ function expectedSurfaceInk(source: string, contrast: number, neutral?: string)
}

async function showThemeControls(page: Page) {
if (!(await page.getByRole('button', { name: /^Contrast:/ }).isVisible())) {
const contrast = page.getByRole('button', { name: /^Contrast:/ })
if (page.viewportSize()!.width < 1024 && !(await contrast.isVisible())) {
await page.getByRole('button', { name: 'Toggle Sidebar', exact: true }).click()
}
await expect(contrast).toBeVisible()
}

for (const preset of [
{ label: 'Purple', source: '#D855F9', neutral: undefined },
{ label: 'Lime', source: '#CAF543', neutral: '#c1b793' },
]) {
test(`${preset.label} keeps its native neutral palette across navigation, contrast and reload`, async ({
page,
}) => {
await page.addInitScript(() => localStorage.setItem('theme', 'light'))
await page.goto(presetPage)
const selector = page.getByRole('button', { name: 'Seed preset', exact: true })
await selector.click()
await page.getByRole('menuitemradio', { name: preset.label, exact: true }).click()
await page.keyboard.press('Escape')
await expect(selector).toHaveText(preset.label)
test(`${preset.label} stored seeds survive navigation, contrast and reload`, async ({ page }) => {
await page.goto('/getting-started/introduction')
// Seed once, as a consumer component would. Later reloads must use persisted choices.
await page.evaluate(({ source, neutral }) => {
localStorage.setItem('theme', 'light')
localStorage.setItem('pmndrs-docs:primary-color', source)
localStorage.setItem('pmndrs-docs:color-match', 'true')
localStorage.setItem(
'pmndrs-docs:source-preset',
JSON.stringify({ source, neutral, neutralVariant: neutral ? '#495720' : undefined }),
)
}, preset)
await page.reload()
const ink = () => page.evaluate(surfaceInk)
await expect.poll(ink).toBe(expectedSurfaceInk(preset.source, 0, preset.neutral))
const introduction = page.getByRole('link', { name: 'getting started', exact: true })
if (!(await introduction.isVisible())) {
await page.getByRole('button', { name: 'Toggle Sidebar', exact: true }).click()
}
await introduction.click()
await expect(page).toHaveURL(/\/getting-started\/introduction/)

await page.getByRole('link', { name: 'reusable workflow', exact: true }).click()
await expect(page).toHaveURL(/\/github-actions\/introduction/)
await expect.poll(ink).toBe(expectedSurfaceInk(preset.source, 0, preset.neutral))
await showThemeControls(page)
await page.getByRole('button', { name: /Contrast: standard, switch to medium/ }).click()
await expect.poll(ink).toBe(expectedSurfaceInk(preset.source, 0.5, preset.neutral))
await page.reload()
await expect.poll(ink).toBe(expectedSurfaceInk(preset.source, 0.5, preset.neutral))
await page.goto(presetPage)
await expect(selector).toHaveText(preset.label)
await expect.poll(ink).toBe(expectedSurfaceInk(preset.source, 0.5, preset.neutral))
await selector.click()
await page.getByRole('menuitemradio', { name: 'Lime', exact: true }).click()
await page.keyboard.press('Escape')
await expect.poll(ink).toBe(expectedSurfaceInk('#CAF543', 0.5, '#c1b793'))

await showThemeControls(page)
await expect(
page.getByRole('button', { name: /Contrast: medium, switch to high/ }),
).toBeVisible()
// A different footer source must stop applying the saved preset's neutral overrides.
await page.locator('input[type="color"]').last().fill('#123456', { force: true })
await expect.poll(ink).toBe(expectedSurfaceInk('#123456', 0.5, '#c1b793'))
await page.reload()
await expect.poll(ink).toBe(expectedSurfaceInk('#123456', 0.5, '#c1b793'))
await showThemeControls(page)
await page.locator('input[type="color"]').last().fill(preset.source, { force: true })
await expect.poll(ink).toBe(expectedSurfaceInk(preset.source, 0.5, preset.neutral))
})
}

test('switches native Teal and Cyan presets without resetting the saved contrast', async ({
page,
}) => {
await page.addInitScript(recordPresetFrames)
await page.addInitScript(() => localStorage.setItem('theme', 'light'))
await page.goto(presetPage)
const selector = page.getByRole('button', { name: 'Seed preset', exact: true })
await selector.click()
await page.getByRole('menuitemradio', { name: 'Teal', exact: true }).click()
await page.keyboard.press('Escape')
await showThemeControls(page)
await page.getByRole('button', { name: /Contrast: standard, switch to medium/ }).click()
await expect.poll(() => page.evaluate(surfaceInk)).toBe(expectedSurfaceInk('#00F7A3', 0.5))
if (await page.getByRole('dialog', { name: 'Sidebar', exact: true }).isVisible()) {
await page.locator('[data-slot="sheet-overlay"]').click({
position: { x: page.viewportSize()!.width - 2, y: 2 },
})
await expect(page.getByRole('dialog', { name: 'Sidebar', exact: true })).not.toBeVisible()
}
await page.evaluate(() => {
window.presetLabels = []
})
await selector.click()
await page.getByRole('menuitemradio', { name: 'Cyan', exact: true }).click()
await page.keyboard.press('Escape')
await expect(selector).toHaveText('Cyan')
await expect.poll(() => page.evaluate(surfaceInk)).toBe(expectedSurfaceInk('#2BDCF6', 0.5))
const transitionLabels = await page.evaluate(() => window.presetLabels)
expect(transitionLabels.every((label) => label === 'Teal' || label === 'Cyan')).toBe(true)
await page.reload()
await expect(selector).toHaveText('Cyan')
await expect.poll(() => page.evaluate(surfaceInk)).toBe(expectedSurfaceInk('#2BDCF6', 0.5))
const reloadLabels = await page.evaluate(() => window.presetLabels)
expect(reloadLabels.length).toBeGreaterThan(0)
expect(reloadLabels.every((label) => label === 'Cyan')).toBe(true)
await showThemeControls(page)
await expect(page.getByRole('button', { name: /Contrast: medium, switch to high/ })).toBeVisible()
})

test('recognizes a saved footer source and preserves custom footer colours', async ({ page }) => {
await page.addInitScript(() => {
localStorage.setItem('theme', 'light')
localStorage.setItem('pmndrs-docs:primary-color', '#d855f9')
localStorage.setItem('pmndrs-docs:color-match', 'true')
})
await page.goto(presetPage)
const selector = page.getByRole('button', { name: 'Seed preset', exact: true })
await expect(selector).toHaveText('Purple')
await expect.poll(() => page.evaluate(surfaceInk)).toBe(expectedSurfaceInk('#D855F9', 0))
await page.locator('input[type="color"]').last().fill('#123456', { force: true })
await expect(selector).toHaveText('Custom')
await expect(page.getByRole('table', { name: 'Current theme seeds' })).toContainText('#123456')
await expect
.poll(() => page.evaluate(surfaceInk))
.toBe(expectedSurfaceInk('#123456', 0, '#c1b793'))
})
41 changes: 0 additions & 41 deletions src/cli/render.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,47 +9,6 @@ import { renderFragment } from './render'
const render = (source: string) =>
renderFragment(source, { absoluteFilePath: join(process.cwd(), 'docs/authoring/fixture.mdx') })

test('exports the first seed preset as a static table without theme providers', async () => {
const $ = load(
await render(
'<ThemeSeeds presets={[{name: "theme-lime", label: "Lime", config: {source: "#CAF543", colorMatch: true, contrast: 0, neutral: "#c1b793"}}, {name: "theme-teal", label: "Teal", config: {source: "#00F7A3"}}]} />',
),
)
const table = $('table[aria-label="Current theme seeds"]')
expect(table).toHaveLength(1)
expect(
table
.find('tbody tr')
.map((_, row) =>
$(row)
.find('code')
.map((_, cell) => $(cell).text())
.get(),
)
.get(),
).toEqual(['source', '#CAF543', 'colorMatch', 'true', 'neutral', '#c1b793'])
expect($('button[aria-label="Seed preset"]')).toHaveLength(0)
expect(table.text()).not.toContain('#00F7A3')
})

test('omits undefined neutral seeds from the fragment table', async () => {
const $ = load(
await render(
'<ThemeSeeds presets={[{name: "theme-purple", label: "Purple", config: {source: "#D855F9", colorMatch: true}}]} />',
),
)
expect(
$('tbody tr')
.map((_, row) =>
$(row)
.find('code')
.map((_, cell) => $(cell).text())
.get(),
)
.get(),
).toEqual(['source', '#D855F9', 'colorMatch', 'true'])
})

/**
* One case per authoring module, asserting the markup it is recognizable by rather than the
* whole of its output — the latter changes every time a class name does.
Expand Down
5 changes: 0 additions & 5 deletions src/cli/render.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import type { DocToC } from '@/app/[...slug]/DocsContext'
import type { DocsConfig } from '@/config'
import type { SeedPreset } from '@/components/mdx/ThemeSeeds'
import { ThemeSeedsTable } from '@/components/mdx/ThemeSeedsTable'
import { compileMdxContent } from '@/utils/compileMdxContent'
import matter from 'gray-matter'
import type { ComponentProps, ReactNode } from 'react'
Expand All @@ -12,9 +10,6 @@ import { renderToReadableStream } from 'react-dom/server'
* its `h1` renders nothing. A fragment has no layout, so it must keep the title it is given.
*/
export const fragmentComponents = {
/** A standalone fragment has no theme provider or controls: show the first preset's seeds. */
ThemeSeeds: ({ presets }: { presets: SeedPreset[] }) =>
presets[0] ? <ThemeSeedsTable config={presets[0].config} /> : null,
h1: (props: ComponentProps<'h1'>) => (
<h1 className="mb-2 text-5xl font-bold tracking-tighter" {...props} />
),
Expand Down
Loading
Loading