site-builder: feedback batch (social, features, header menu, spacer)
Five of six items from user feedback (Contact Form email delivery split into a focused, live-tested follow-up): - Social Links: add Spotify + Twitch (FA 4.7.0 already ships both glyphs). - Features Grid: per-feature icon/image toggle (upload + URL) and an optional button (text + url); render, settings, and HTML export updated, backward compatible with existing icon-only features. - Header: seed the default header with a Navbar (logo + Home/About/Services/ Contact) so new sites open with an editable menu-with-links instead of an empty header zone. Adds a vitest guard that the seed deserializes and exports a real <nav>. - Canvas: slim the empty header/footer placeholder from a padded band to a thin hint line so an empty zone no longer reads as a stray spacer. Design spec: docs/superpowers/specs/2026-07-06-site-builder-feedback-batch-design.md Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -60,6 +60,60 @@ const EMPTY_HEADER =
|
||||
const EMPTY_FOOTER =
|
||||
'{"ROOT":{"type":{"resolvedName":"Container"},"isCanvas":true,"props":{"style":{"minHeight":"60px","backgroundColor":"#0f172a","color":"#94a3b8","padding":"40px 24px","textAlign":"center"},"tag":"footer"},"displayName":"Container","custom":{},"hidden":false,"nodes":[],"linkedNodes":{}}}';
|
||||
|
||||
// Default header seed: a ROOT header Container holding a Navbar with default
|
||||
// links. New sites previously opened with an EMPTY header (just a bare
|
||||
// Container), so there was no menu to edit and the empty zone rendered as a
|
||||
// stray band above the page. Seeding a real Navbar gives every new site an
|
||||
// editable menu-with-links out of the box (and removes the empty-header gap).
|
||||
// Node shape matches serializeTreeForCraft() / Craft's actions.deserialize().
|
||||
export const DEFAULT_HEADER_STATE = JSON.stringify({
|
||||
ROOT: {
|
||||
type: { resolvedName: 'Container' },
|
||||
isCanvas: true,
|
||||
props: { style: { width: '100%' }, tag: 'header' },
|
||||
displayName: 'Container',
|
||||
custom: {},
|
||||
hidden: false,
|
||||
nodes: ['header-navbar'],
|
||||
linkedNodes: {},
|
||||
},
|
||||
'header-navbar': {
|
||||
type: { resolvedName: 'Navbar' },
|
||||
isCanvas: false,
|
||||
props: {
|
||||
logoType: 'text',
|
||||
logoText: 'MySite',
|
||||
logoImage: '',
|
||||
logoWidth: '120px',
|
||||
logoUrl: '/',
|
||||
logoFontFamily: 'Inter, sans-serif',
|
||||
logoFontSize: '20px',
|
||||
links: [
|
||||
{ text: 'Home', href: '/' },
|
||||
{ text: 'About', href: '#about' },
|
||||
{ text: 'Services', href: '#services' },
|
||||
{ text: 'Contact', href: '#contact', isCta: true },
|
||||
],
|
||||
backgroundColor: '#ffffff',
|
||||
textColor: '#3f3f46',
|
||||
hoverColor: '#3b82f6',
|
||||
ctaColor: '#3b82f6',
|
||||
ctaTextColor: '#ffffff',
|
||||
padding: '16px 24px',
|
||||
navAlignment: 'space-between',
|
||||
isSticky: false,
|
||||
showMobileMenu: false,
|
||||
style: { borderBottom: '1px solid #e4e4e7' },
|
||||
},
|
||||
displayName: 'Navbar',
|
||||
parent: 'ROOT',
|
||||
custom: {},
|
||||
hidden: false,
|
||||
nodes: [],
|
||||
linkedNodes: {},
|
||||
},
|
||||
});
|
||||
|
||||
const PageContext = createContext<PageContextValue>({
|
||||
pages: [],
|
||||
headerPage: { id: HEADER_ID, name: 'Header', slug: '__header__', craftState: null, headCode: '' },
|
||||
@@ -106,7 +160,7 @@ const DEFAULT_HEADER: PageData = {
|
||||
id: HEADER_ID,
|
||||
name: 'Header',
|
||||
slug: '__header__',
|
||||
craftState: null,
|
||||
craftState: DEFAULT_HEADER_STATE,
|
||||
headCode: '',
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { describe, test, expect } from 'vitest';
|
||||
import { DEFAULT_HEADER_STATE } from './PageContext';
|
||||
import { exportBodyHtml } from '../utils/html-export';
|
||||
|
||||
/**
|
||||
* The default header is seeded as a hand-authored serialized Craft state. A
|
||||
* malformed node map would not fail typecheck but would break the header on
|
||||
* every new site at runtime, so we validate it deserializes + exports through
|
||||
* the same path the editor preview and the WHP publish step use.
|
||||
*/
|
||||
describe('DEFAULT_HEADER_STATE seed', () => {
|
||||
test('is valid JSON with a ROOT header + a Navbar child', () => {
|
||||
const parsed = JSON.parse(DEFAULT_HEADER_STATE);
|
||||
expect(parsed.ROOT).toBeDefined();
|
||||
expect(parsed.ROOT.type.resolvedName).toBe('Container');
|
||||
expect(parsed.ROOT.props.tag).toBe('header');
|
||||
expect(parsed.ROOT.nodes).toContain('header-navbar');
|
||||
expect(parsed['header-navbar'].type.resolvedName).toBe('Navbar');
|
||||
expect(parsed['header-navbar'].parent).toBe('ROOT');
|
||||
});
|
||||
|
||||
test('exports to header HTML containing the nav and its default links', () => {
|
||||
const { html } = exportBodyHtml(DEFAULT_HEADER_STATE);
|
||||
expect(html).toContain('<nav');
|
||||
expect(html).toContain('MySite');
|
||||
for (const link of ['Home', 'About', 'Services', 'Contact']) {
|
||||
expect(html).toContain(link);
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user