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:
2026-07-06 17:53:22 -07:00
co-authored by Claude Opus 4.8
parent d0925d9e2d
commit 53c40f856f
6 changed files with 268 additions and 6 deletions
+30
View File
@@ -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);
}
});
});