diff --git a/apps/web/src/components/admin/settings/appearance/__tests__/appearance-view.test.tsx b/apps/web/src/components/admin/settings/appearance/__tests__/appearance-view.test.tsx index a3fef2d6b7..5a32f366fd 100644 --- a/apps/web/src/components/admin/settings/appearance/__tests__/appearance-view.test.tsx +++ b/apps/web/src/components/admin/settings/appearance/__tests__/appearance-view.test.tsx @@ -4,7 +4,7 @@ * draft bar at once, and Review and publish lists it before it goes live. */ import { afterEach, describe, expect, it, vi } from 'vitest' -import { cleanup, fireEvent, render, screen, waitFor, within } from '@testing-library/react' +import { act, cleanup, fireEvent, render, screen, waitFor, within } from '@testing-library/react' import type { ReactNode } from 'react' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { defaultDesign } from '@/lib/shared/design/schema' @@ -14,6 +14,7 @@ const server = vi.hoisted(() => ({ published: [] as unknown[] })) vi.mock('@tanstack/react-router', () => ({ useRouter: () => ({ invalidate: async () => {} }), ClientOnly: ({ children }: { children: ReactNode }) => <>{children}>, + Link: ({ children }: { children: ReactNode }) => {children}, })) vi.mock('@/components/admin/settings/branding/portal-preview', () => ({ PortalPreview: ({ design }: { design: { css: string } }) => ( @@ -70,6 +71,20 @@ describe('AppearanceView', () => { expect(screen.getByRole('status').textContent).toContain('Draft · 1 change, not live') }) + it('opens Sections when a section is picked in the preview', async () => { + mount() + expect(screen.queryByText('Menu style')).toBeNull() + act(() => { + window.dispatchEvent( + new MessageEvent('message', { + data: { type: 'quackback:preview-section', section: 'band' }, + origin: window.location.origin, + }) + ) + }) + expect(await screen.findByText('Menu style')).toBeTruthy() + }) + it('applies a preset’s whole look to the draft', async () => { mount() fireEvent.click(screen.getByRole('button', { name: 'Start from a preset' })) diff --git a/apps/web/src/components/admin/settings/appearance/__tests__/change-list.test.ts b/apps/web/src/components/admin/settings/appearance/__tests__/change-list.test.ts new file mode 100644 index 0000000000..1f9b923fdd --- /dev/null +++ b/apps/web/src/components/admin/settings/appearance/__tests__/change-list.test.ts @@ -0,0 +1,22 @@ +/** + * Review and publish names each change the way the Appearance controls do. + */ +import { describe, expect, it } from 'vitest' +import { changeLabel } from '../change-list' + +describe('changeLabel', () => { + it.each([ + ['brand.light.accent', 'Accent'], + ['brand.dark.ink', 'Dark text'], + ['brand.fonts.heading.id', 'Heading font'], + ['layout.nav', 'Menu style'], + ['pages.feedback.defaultSort', 'Default sort'], + ['pages.feedback.headline.title.en', 'Headline'], + ['pages.feedback.headline', 'Headline'], + ['pages.feedback.headline.subtitle.de', 'Subtitle'], + ['pages.feedback.welcome.show', 'Welcome message'], + ['pages.feedback.welcome.body.content', 'Welcome message text'], + ])('%s is %s', (path, label) => { + expect(changeLabel(path)).toBe(label) + }) +}) diff --git a/apps/web/src/components/admin/settings/appearance/appearance-view.tsx b/apps/web/src/components/admin/settings/appearance/appearance-view.tsx index ec36cbda32..b0e020ff5a 100644 --- a/apps/web/src/components/admin/settings/appearance/appearance-view.tsx +++ b/apps/web/src/components/admin/settings/appearance/appearance-view.tsx @@ -1,4 +1,4 @@ -import { useMemo, useState } from 'react' +import { useEffect, useMemo, useState } from 'react' import { isEmptyTiptapDoc } from '@/lib/shared/utils/is-empty-tiptap-doc' import type { TiptapContent } from '@/lib/shared/db-types' import { ClientOnly } from '@tanstack/react-router' @@ -22,9 +22,11 @@ import { useDesignDraft } from './use-design-draft' export function AppearanceView({ workspaceName, faviconUrl, + logoUrl = null, }: { workspaceName: string faviconUrl: string | null + logoUrl?: string | null }) { const draft = useDesignDraft() const [theme, setTheme] = useState<'light' | 'dark'>( @@ -33,6 +35,23 @@ export function AppearanceView({ const [viewport, setViewport] = useState<'desktop' | 'mobile'>('desktop') const [reviewing, setReviewing] = useState(false) const [history, setHistory] = useState(false) + const [tab, setTab] = useState('brand') + // A section clicked in the preview opens its controls. + useEffect(() => { + function onMessage(event: MessageEvent) { + if (event.origin !== window.location.origin) return + const message = event.data as { type?: string; section?: unknown } | null + if (message?.type !== 'quackback:preview-section' || typeof message.section !== 'string') + return + setTab('sections') + const id = `design-section-${message.section}` + requestAnimationFrame(() => + document.getElementById(id)?.scrollIntoView({ behavior: 'smooth', block: 'start' }) + ) + } + window.addEventListener('message', onMessage) + return () => window.removeEventListener('message', onMessage) + }, []) // The welcome message reaches the preview on the structural channel, which // the feedback page already reads; an empty body hides it. const { show, body } = draft.design.pages.feedback.welcome @@ -50,7 +69,7 @@ export function AppearanceView({ return (