feat(topbar): search button opens the command palette; Preview head parity

- Icon button with tooltip 'Search (Ctrl/Cmd+K)' calls openCommandPalette()
- Preview passes fontScanHtml: composedBody and trims metaTitle so the
  Preview head matches the published head

Refs #49

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
shadowdaoandClaude Opus 5.5 committed 2026-10-07 12:41:36 -07:00
1 parent db54341006
commit ebf75ebfe0
2 files changed
+56 -3

No files matched your search

+44 -2
View File
@@ -34,12 +34,12 @@ import { act } from 'react-dom/test-utils';
// #54: what the pre-publish checks see. Default (no pages) means no checks, so
// every pre-existing test publishes directly.
const checkFixture: { pages: any[]; liveState: string; activePageId: string } = { pages: [], liveState: '{}', activePageId: 'home' };
const checkFixture: { pages: any[]; liveState: string; activePageId: string; footerState: string | null } = { pages: [], liveState: '{}', activePageId: 'home', footerState: null };
vi.mock('../../state/PageContext', () => ({
usePages: () => ({
pages: checkFixture.pages,
headerPage: { id: '__header__', name: 'Header', slug: '__header__', craftState: null },
footerPage: { id: '__footer__', name: 'Footer', slug: '__footer__', craftState: null },
footerPage: { id: '__footer__', name: 'Footer', slug: '__footer__', craftState: checkFixture.footerState },
activePageId: checkFixture.activePageId,
switchPage: vi.fn(),
}),
@@ -71,6 +71,8 @@ vi.mock('../../hooks/useWhpApi', () => ({
vi.mock('./TemplateModal', () => ({ TemplateModal: () => null }));
vi.mock('./HeadCodeModal', () => ({ HeadCodeModal: () => null }));
vi.mock('../sitesmith/SitesmithButton', () => ({ SitesmithButton: () => null }));
const openPaletteMock = vi.fn();
vi.mock('../command-palette/CommandPalette', () => ({ openCommandPalette: () => openPaletteMock() }));
import { TopBar } from './TopBar';
import { EditorConfigProvider } from '../../state/EditorConfigContext';
@@ -130,6 +132,8 @@ afterEach(() => {
checkFixture.pages = [];
checkFixture.liveState = '{}';
checkFixture.activePageId = 'home';
checkFixture.footerState = null;
openPaletteMock.mockReset();
});
describe('TopBar publish-warnings wiring', () => {
@@ -315,3 +319,41 @@ describe('TopBar pre-publish checklist', () => {
expect(container.querySelector('[data-testid="publish-warnings"]')!.textContent).toContain('Generated sitemap.xml and robots.txt.');
});
});
describe('TopBar search button (#49 integration)', () => {
test('opens the command palette and carries the shortcut tooltip', () => {
render();
const btn = container.querySelector<HTMLButtonElement>('button[aria-label="Search"]');
expect(btn).not.toBeNull();
expect(btn!.getAttribute('data-tooltip')).toBe('Search (Ctrl/Cmd+K)');
act(() => { btn!.dispatchEvent(new MouseEvent('click', { bubbles: true })); });
expect(openPaletteMock).toHaveBeenCalledTimes(1);
});
});
describe('TopBar Preview head parity (item 14)', () => {
const rootState = (style: Record<string, string> = {}) => JSON.stringify({
ROOT: { type: { resolvedName: 'Container' }, isCanvas: true, props: { tag: 'div', style }, displayName: 'Container', custom: {}, hidden: false, nodes: [], linkedNodes: {} },
});
test('scans the composed header+body+footer for preset fonts and trims metaTitle', async () => {
checkFixture.liveState = rootState();
checkFixture.footerState = rootState({ fontFamily: 'Merriweather, serif' });
checkFixture.pages = [{ id: 'home', name: 'Home', slug: 'index', craftState: null, seo: { metaTitle: ' ' } }];
let blob: Blob | null = null;
const createUrl = vi.spyOn(URL, 'createObjectURL').mockImplementation((b) => { blob = b as Blob; return 'blob:x'; });
const openSpy = vi.spyOn(window, 'open').mockImplementation(() => null);
try {
render();
const btn = container.querySelector<HTMLButtonElement>('button[aria-label="Preview"]')!;
await act(async () => { btn.dispatchEvent(new MouseEvent('click', { bubbles: true })); });
await vi.waitFor(() => expect(blob).not.toBeNull());
const html = await blob!.text();
expect(html).toContain('family=Inter:wght@300;400;500;600;700&family=Merriweather:wght@300;400;700&display=swap');
expect(html).toContain('<title>Home</title>');
} finally {
createUrl.mockRestore();
openSpy.mockRestore();
}
});
});
+12 -1
View File
@@ -24,6 +24,7 @@ import { SitesmithButton } from '../sitesmith/SitesmithButton';
import { useSitesmithModal } from '../../state/SitesmithContext';
import { useRegisterCommands } from '../../editor/commands';
import { TopBarCommandMenu } from './TopBarCommandMenu';
import { openCommandPalette } from '../command-palette/CommandPalette';
interface TopBarProps {
/** Item 10: canvas dashed guide outlines, default ON. */
@@ -306,7 +307,7 @@ export const TopBar: React.FC<TopBarProps> = ({ showGuides, onToggleGuides }) =>
// html-export's buildSeoMeta/og:title fallback both key off it.
const activePage = pages.find((p) => p.id === activePageId);
const seo = activePage?.seo;
const title = seo?.metaTitle || activePage?.name || whpConfig?.siteName || 'Preview';
const title = seo?.metaTitle?.trim() || activePage?.name || whpConfig?.siteName || 'Preview';
const result = exportToHtml(serialized, {
title,
@@ -323,6 +324,8 @@ export const TopBar: React.FC<TopBarProps> = ({ showGuides, onToggleGuides }) =>
? `https://${whpConfig.siteDomain}/${activePage.slug === 'index' ? '' : activePage.slug}`
: undefined,
design,
// Fonts: scan header+body+footer like the published head does (head parity).
fontScanHtml: composedBody,
});
// Replace the body in the full document with our composed version.
@@ -528,6 +531,14 @@ export const TopBar: React.FC<TopBarProps> = ({ showGuides, onToggleGuides }) =>
>
<i className="fa fa-code" />
</button>
<button
className="topbar-btn icon-only"
aria-label="Search"
data-tooltip="Search (Ctrl/Cmd+K)"
onClick={() => openCommandPalette()}
>
<i className="fa fa-search" aria-hidden />
</button>
<button className="topbar-btn icon-only" aria-label="Preview" data-tooltip="Preview" onClick={handlePreview}>
<i className="fa fa-eye" />
</button>