feat(site-builder): re-author templates group C onto design tokens

event-conference, resume-cv, blog and wedding now reference var(--wsb-*)
tokens instead of repeating their design palette/fonts in component props.

Refs #44

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
shadowdaoandClaude Sonnet 5.5 committed 2026-10-07 11:36:41 -07:00
1 parent ce3db23506
commit 9869db8514
2 files changed
+222 -88

No files matched your search

@@ -0,0 +1,126 @@
import { describe, test, expect, afterEach } from 'vitest';
import { allTemplates } from './definitions';
import type { TemplateComponent, TemplateDefinition } from './definitions';
import { templateComponentToTreeNode } from './apply-template';
import { buildNodeTree } from '../utils/craft-tree';
import { exportBodyHtml, exportToHtml } from '../utils/html-export';
import { DEFAULT_SITE_DESIGN } from '../state/SiteDesignContext';
import { renderEditorHarness, EditorHarness } from '../test-utils/editorHarness';
/**
* #44 group C: event-conference, resume-cv, blog, wedding must reference
* design tokens (`var(--wsb-*)`) instead of re-stating their own `design`
* palette / fonts in component props, so changing Site Design restyles them.
*/
const IDS = ['event-conference', 'resume-cv', 'blog', 'wedding'];
const templates: TemplateDefinition[] = IDS.map((id) => allTemplates.find((t) => t.id === id)!);
/** The template's own palette + fonts that must now be token refs. */
function designValues(tpl: TemplateDefinition): string[] {
const d = tpl.design as Record<string, unknown>;
const keys = [
'primaryColor', 'secondaryColor', 'accentColor', 'backgroundColor', 'textColor',
'mutedTextColor', 'borderColor', 'linkColor', 'headingFont', 'bodyFont',
];
return keys.map((k) => d[k]).filter((v): v is string => typeof v === 'string').map((v) => v.toLowerCase());
}
/** Every string leaf of props (recursive), excluding visible copy (`text`, `heading`, ...). */
const COPY_KEYS = new Set(['text', 'heading', 'subtitle', 'description', 'title', 'label', 'placeholder', 'tagline']);
function styleStrings(v: unknown, out: string[] = [], key = ''): string[] {
if (typeof v === 'string') {
if (!COPY_KEYS.has(key)) out.push(v);
} else if (Array.isArray(v)) {
v.forEach((x) => styleStrings(x, out, key));
} else if (v && typeof v === 'object') {
for (const [k, x] of Object.entries(v)) styleStrings(x, out, k);
}
return out;
}
function walk(comps: TemplateComponent[], fn: (c: TemplateComponent) => void) {
for (const c of comps) {
fn(c);
if (c.children) walk(c.children, fn);
}
}
function allTrees(tpl: TemplateDefinition): { where: string; comps: TemplateComponent[] }[] {
return [
{ where: 'header', comps: tpl.header.components },
{ where: 'footer', comps: tpl.footer.components },
...tpl.pages.map((p) => ({ where: `page:${p.slug}`, comps: p.content.components })),
];
}
function offenders(tpl: TemplateDefinition): string[] {
const vals = designValues(tpl);
const bad: string[] = [];
for (const { where, comps } of allTrees(tpl)) {
walk(comps, (c) => {
for (const s of styleStrings(c.props)) {
const low = s.toLowerCase();
// substring check so gradients / shorthand values cannot smuggle a hex back in
if (vals.some((v) => low.includes(v))) bad.push(`${tpl.id} ${where} ${c.type}: ${s}`);
}
});
}
return bad;
}
let harness: EditorHarness | null = null;
afterEach(() => {
harness?.unmount();
harness = null;
});
describe.each(templates)('tokens-C: $id', (tpl) => {
test('template exists and declares the design roles it uses', () => {
expect(tpl).toBeDefined();
expect(tpl.design.primaryColor).toMatch(/^#/);
expect(tpl.design.mutedTextColor).toMatch(/^#/);
});
test('no prop/style value re-states a design colour or font (all token refs)', () => {
expect(offenders(tpl)).toEqual([]);
});
test('token refs are actually used (not just stripped)', () => {
const all = allTrees(tpl).flatMap(({ comps }) => {
const s: string[] = [];
walk(comps, (c) => s.push(...styleStrings(c.props)));
return s;
});
expect(all.some((s) => s === 'var(--wsb-primary)')).toBe(true);
expect(all.some((s) => s.startsWith('var(--wsb-') && s.endsWith('font)'))).toBe(
tpl.id !== 'resume-cv',
);
});
test('applies through buildNodeTree in a real harness without throwing and exports tokens', () => {
harness = renderEditorHarness();
const page = tpl.pages[0];
expect(() => {
harness!.act(() => {
for (const comp of page.content.components) {
const tree = buildNodeTree(harness!.query, templateComponentToTreeNode(comp));
harness!.actions.addNodeTree(tree, 'ROOT');
}
});
}).not.toThrow();
const serialized = harness.getSerialized();
const { html: body } = exportBodyHtml(serialized);
expect(body).toContain('var(--wsb-primary)');
expect(body.toLowerCase()).not.toContain(tpl.design.primaryColor!.toLowerCase());
// Visual parity: :root carries the template primary, body no longer has the hex.
const design = { ...DEFAULT_SITE_DESIGN, ...tpl.design };
const { html: doc } = exportToHtml(serialized, { design });
const root = doc.match(/:root\{[^}]*\}/)?.[0] ?? '';
expect(root.toLowerCase()).toContain(`--wsb-primary:${tpl.design.primaryColor!.toLowerCase()}`);
const bodyOnly = doc.slice(doc.indexOf('<body'));
expect(bodyOnly.toLowerCase()).not.toContain(tpl.design.primaryColor!.toLowerCase());
});
});
+96 -88
View File
@@ -1,4 +1,5 @@
import { SiteDesign } from '../state/SiteDesignContext';
import { tokenRef } from '../utils/tokens';
// ---------------------------------------------------------------------------
// Types
@@ -1722,6 +1723,7 @@ const eventConference: TemplateDefinition = {
bodyFont: 'Inter, sans-serif',
backgroundColor: '#fffbeb',
textColor: '#0f172a',
mutedTextColor: '#92400e',
navStyle: 'dark',
},
header: makeHeader(
@@ -1731,11 +1733,11 @@ const eventConference: TemplateDefinition = {
{ text: 'Schedule', href: '#schedule' },
{ text: 'Register', href: '#register', isCta: true },
],
'#0f172a',
'#fbbf24',
'#f59e0b',
tokenRef('secondary'),
tokenRef('accent'),
tokenRef('primary'),
),
footer: makeFooterContent('SUMMIT Conference', '#0f172a', '#f59e0b', {
footer: makeFooterContent('SUMMIT Conference', tokenRef('secondary'), tokenRef('primary'), {
tagline: 'October 15-17, 2026 \u00b7 San Francisco',
contactText: 'info@summit2026.com',
socialText: 'Twitter \u00b7 LinkedIn \u00b7 YouTube',
@@ -1753,16 +1755,16 @@ const eventConference: TemplateDefinition = {
subtitle: 'The premier technology conference. October 15-17, San Francisco. 3 days, 60+ speakers, 2,000 attendees.',
buttonText: 'Register Now',
buttonHref: '#register',
gradient: 'linear-gradient(135deg, #0f172a 0%, #1e3a5f 50%, #0f172a 100%)',
gradient: 'linear-gradient(135deg, var(--wsb-secondary) 0%, #1e3a5f 50%, var(--wsb-secondary) 100%)',
style: { minHeight: '550px' },
},
},
{
type: 'Section',
props: { style: { padding: '60px 20px', backgroundColor: '#fffbeb', textAlign: 'center' } },
props: { style: { padding: '60px 20px', backgroundColor: tokenRef('bg'), textAlign: 'center' } },
children: [
{ type: 'Heading', props: { text: 'October 15-17, 2026', level: 'h2', style: { color: '#f59e0b', fontFamily: 'Montserrat, sans-serif', fontSize: '32px', marginBottom: '8px' } } },
{ type: 'TextBlock', props: { text: 'Moscone Center, San Francisco', style: { color: '#92400e', fontSize: '18px' } } },
{ type: 'Heading', props: { text: 'October 15-17, 2026', level: 'h2', style: { color: tokenRef('primary'), fontFamily: tokenRef('heading-font'), fontSize: '32px', marginBottom: '8px' } } },
{ type: 'TextBlock', props: { text: 'Moscone Center, San Francisco', style: { color: tokenRef('muted'), fontSize: '18px' } } },
],
},
{
@@ -1778,9 +1780,9 @@ const eventConference: TemplateDefinition = {
},
{
type: 'Section',
props: { style: { padding: '80px 20px', backgroundColor: '#fffbeb' } },
props: { style: { padding: '80px 20px', backgroundColor: tokenRef('bg') } },
children: [
{ type: 'Heading', props: { text: 'Schedule Highlights', level: 'h2', style: { textAlign: 'center', color: '#0f172a', fontFamily: 'Montserrat, sans-serif', marginBottom: '24px' } } },
{ type: 'Heading', props: { text: 'Schedule Highlights', level: 'h2', style: { textAlign: 'center', color: tokenRef('text'), fontFamily: tokenRef('heading-font'), marginBottom: '24px' } } },
{ type: 'TextBlock', props: { text: 'Day 1 \u2014 Opening keynote, AI & ML track, Networking reception\nDay 2 \u2014 Infrastructure track, Design track, Startup showcase\nDay 3 \u2014 Workshops, Lightning talks, Closing party', style: { textAlign: 'center', color: '#78350f', lineHeight: '2', maxWidth: '600px', margin: '0 auto' } } },
],
},
@@ -1791,7 +1793,7 @@ const eventConference: TemplateDefinition = {
description: 'Early bird tickets start at $299. Student discounts available.',
buttonText: 'Register Now',
buttonHref: '#register',
gradient: 'linear-gradient(135deg, #f59e0b 0%, #d97706 100%)',
gradient: 'linear-gradient(135deg, var(--wsb-primary) 0%, #d97706 100%)',
},
},
],
@@ -1821,6 +1823,7 @@ const resumeCv: TemplateDefinition = {
bodyFont: 'Inter, sans-serif',
backgroundColor: '#ffffff',
textColor: '#1e293b',
mutedTextColor: '#64748b',
navStyle: 'light',
},
header: makeHeader(
@@ -1830,11 +1833,12 @@ const resumeCv: TemplateDefinition = {
{ text: 'Skills', href: '#skills' },
{ text: 'Download CV', href: '#', isCta: true },
],
'#ffffff',
'#1e293b',
'#3b82f6',
tokenRef('bg'),
tokenRef('text'),
tokenRef('primary'),
tokenRef('bg'),
),
footer: makeFooterContent('Alex Morgan', '#1e293b', '#94a3b8', {
footer: makeFooterContent('Alex Morgan', tokenRef('secondary'), '#94a3b8', {
contactText: 'alex@alexmorgan.dev \u00b7 San Francisco, CA',
socialText: 'LinkedIn \u00b7 GitHub \u00b7 Twitter',
}),
@@ -1846,24 +1850,24 @@ const resumeCv: TemplateDefinition = {
components: [
{
type: 'Section',
props: { style: { padding: '80px 20px', backgroundColor: '#ffffff' } },
props: { style: { padding: '80px 20px', backgroundColor: tokenRef('bg') } },
children: [
{ type: 'Heading', props: { text: 'Alex Morgan', level: 'h1', style: { fontSize: '44px', fontWeight: '700', color: '#1e293b', marginBottom: '8px' } } },
{ type: 'TextBlock', props: { text: 'Senior Software Engineer', style: { fontSize: '20px', color: '#3b82f6', fontWeight: '500', marginBottom: '16px' } } },
{ type: 'TextBlock', props: { text: 'Full-stack engineer with 8+ years of experience building scalable web applications. Passionate about clean architecture, developer experience, and shipping products that make a difference.', style: { color: '#64748b', lineHeight: '1.8', maxWidth: '700px', fontSize: '16px' } } },
{ type: 'Heading', props: { text: 'Alex Morgan', level: 'h1', style: { fontSize: '44px', fontWeight: '700', color: tokenRef('text'), marginBottom: '8px' } } },
{ type: 'TextBlock', props: { text: 'Senior Software Engineer', style: { fontSize: '20px', color: tokenRef('primary'), fontWeight: '500', marginBottom: '16px' } } },
{ type: 'TextBlock', props: { text: 'Full-stack engineer with 8+ years of experience building scalable web applications. Passionate about clean architecture, developer experience, and shipping products that make a difference.', style: { color: tokenRef('muted'), lineHeight: '1.8', maxWidth: '700px', fontSize: '16px' } } },
],
},
{
type: 'Section',
props: { style: { padding: '60px 20px', backgroundColor: '#f8fafc' } },
children: [
{ type: 'Heading', props: { text: 'Experience', level: 'h2', style: { color: '#1e293b', marginBottom: '24px' } } },
{ type: 'Heading', props: { text: 'Senior Engineer \u2014 Stripe (2022\u2013Present)', level: 'h4', style: { color: '#1e293b', marginBottom: '8px' } } },
{ type: 'TextBlock', props: { text: 'Led a team of 6 engineers building payment processing infrastructure. Reduced API latency by 35% and shipped 3 major product features.', style: { color: '#64748b', lineHeight: '1.7', marginBottom: '24px' } } },
{ type: 'Heading', props: { text: 'Software Engineer \u2014 Vercel (2019\u20132022)', level: 'h4', style: { color: '#1e293b', marginBottom: '8px' } } },
{ type: 'TextBlock', props: { text: 'Core contributor to the Next.js framework. Built serverless deployment tooling used by 100K+ developers.', style: { color: '#64748b', lineHeight: '1.7', marginBottom: '24px' } } },
{ type: 'Heading', props: { text: 'Junior Developer \u2014 Acme Corp (2017\u20132019)', level: 'h4', style: { color: '#1e293b', marginBottom: '8px' } } },
{ type: 'TextBlock', props: { text: 'Full-stack development on the core product team. Migrated legacy PHP codebase to React + Node.js.', style: { color: '#64748b', lineHeight: '1.7' } } },
{ type: 'Heading', props: { text: 'Experience', level: 'h2', style: { color: tokenRef('text'), marginBottom: '24px' } } },
{ type: 'Heading', props: { text: 'Senior Engineer \u2014 Stripe (2022\u2013Present)', level: 'h4', style: { color: tokenRef('text'), marginBottom: '8px' } } },
{ type: 'TextBlock', props: { text: 'Led a team of 6 engineers building payment processing infrastructure. Reduced API latency by 35% and shipped 3 major product features.', style: { color: tokenRef('muted'), lineHeight: '1.7', marginBottom: '24px' } } },
{ type: 'Heading', props: { text: 'Software Engineer \u2014 Vercel (2019\u20132022)', level: 'h4', style: { color: tokenRef('text'), marginBottom: '8px' } } },
{ type: 'TextBlock', props: { text: 'Core contributor to the Next.js framework. Built serverless deployment tooling used by 100K+ developers.', style: { color: tokenRef('muted'), lineHeight: '1.7', marginBottom: '24px' } } },
{ type: 'Heading', props: { text: 'Junior Developer \u2014 Acme Corp (2017\u20132019)', level: 'h4', style: { color: tokenRef('text'), marginBottom: '8px' } } },
{ type: 'TextBlock', props: { text: 'Full-stack development on the core product team. Migrated legacy PHP codebase to React + Node.js.', style: { color: tokenRef('muted'), lineHeight: '1.7' } } },
],
},
{
@@ -1874,24 +1878,24 @@ const resumeCv: TemplateDefinition = {
{ title: 'Backend', description: 'Node.js, Python, Go, PostgreSQL, Redis', icon: '\u2699\ufe0f' },
{ title: 'DevOps', description: 'AWS, Docker, Kubernetes, CI/CD, Terraform', icon: '\u2601\ufe0f' },
],
style: { backgroundColor: '#ffffff' },
style: { backgroundColor: tokenRef('bg') },
},
},
{
type: 'Section',
props: { style: { padding: '60px 20px', backgroundColor: '#f8fafc' } },
children: [
{ type: 'Heading', props: { text: 'Education', level: 'h2', style: { color: '#1e293b', marginBottom: '16px' } } },
{ type: 'Heading', props: { text: 'B.S. Computer Science \u2014 Stanford University (2017)', level: 'h4', style: { color: '#1e293b', marginBottom: '8px' } } },
{ type: 'TextBlock', props: { text: 'Graduated with honors. Focus on distributed systems and machine learning. Teaching assistant for CS 161 (Operating Systems).', style: { color: '#64748b', lineHeight: '1.7' } } },
{ type: 'Heading', props: { text: 'Education', level: 'h2', style: { color: tokenRef('text'), marginBottom: '16px' } } },
{ type: 'Heading', props: { text: 'B.S. Computer Science \u2014 Stanford University (2017)', level: 'h4', style: { color: tokenRef('text'), marginBottom: '8px' } } },
{ type: 'TextBlock', props: { text: 'Graduated with honors. Focus on distributed systems and machine learning. Teaching assistant for CS 161 (Operating Systems).', style: { color: tokenRef('muted'), lineHeight: '1.7' } } },
],
},
{
type: 'Section',
props: { style: { padding: '60px 20px', backgroundColor: '#ffffff', textAlign: 'center' } },
props: { style: { padding: '60px 20px', backgroundColor: tokenRef('bg'), textAlign: 'center' } },
children: [
{ type: 'Heading', props: { text: 'Get in Touch', level: 'h2', style: { color: '#1e293b', marginBottom: '12px' } } },
{ type: 'TextBlock', props: { text: 'alex@alexmorgan.dev\nSan Francisco, CA\nLinkedIn / GitHub / Twitter', style: { color: '#64748b', lineHeight: '1.8' } } },
{ type: 'Heading', props: { text: 'Get in Touch', level: 'h2', style: { color: tokenRef('text'), marginBottom: '12px' } } },
{ type: 'TextBlock', props: { text: 'alex@alexmorgan.dev\nSan Francisco, CA\nLinkedIn / GitHub / Twitter', style: { color: tokenRef('muted'), lineHeight: '1.8' } } },
],
},
],
@@ -1917,6 +1921,8 @@ const blog: TemplateDefinition = {
bodyFont: 'Inter, sans-serif',
backgroundColor: '#ffffff',
textColor: '#1e293b',
mutedTextColor: '#64748b',
borderColor: '#e2e8f0',
navStyle: 'light',
},
header: makeHeader(
@@ -1927,11 +1933,12 @@ const blog: TemplateDefinition = {
{ text: 'About', href: 'about' },
{ text: 'Subscribe', href: 'contact', isCta: true },
],
'#ffffff',
'#1e293b',
'#059669',
tokenRef('bg'),
tokenRef('text'),
tokenRef('primary'),
tokenRef('bg'),
),
footer: makeFooterContent('The Daily Byte', '#1e293b', '#94a3b8', {
footer: makeFooterContent('The Daily Byte', tokenRef('secondary'), '#94a3b8', {
tagline: 'Words that matter.',
socialText: 'RSS \u00b7 Twitter \u00b7 Mastodon',
}),
@@ -1943,32 +1950,32 @@ const blog: TemplateDefinition = {
components: [
{
type: 'Section',
props: { style: { padding: '80px 20px', backgroundColor: '#ffffff' } },
props: { style: { padding: '80px 20px', backgroundColor: tokenRef('bg') } },
children: [
{ type: 'Heading', props: { text: 'The Daily Byte', level: 'h1', style: { fontSize: '48px', fontFamily: 'Merriweather, serif', color: '#1e293b', marginBottom: '12px' } } },
{ type: 'TextBlock', props: { text: 'Thoughts on technology, design, and the craft of building things for the web.', style: { fontSize: '18px', color: '#64748b', marginBottom: '40px' } } },
{ type: 'Heading', props: { text: 'The Daily Byte', level: 'h1', style: { fontSize: '48px', fontFamily: tokenRef('heading-font'), color: tokenRef('text'), marginBottom: '12px' } } },
{ type: 'TextBlock', props: { text: 'Thoughts on technology, design, and the craft of building things for the web.', style: { fontSize: '18px', color: tokenRef('muted'), marginBottom: '40px' } } },
],
},
{
type: 'Section',
props: { style: { padding: '40px 20px', backgroundColor: '#f8fafc' } },
children: [
{ type: 'Heading', props: { text: 'The Case for Boring Technology', level: 'h2', style: { fontFamily: 'Merriweather, serif', color: '#1e293b', marginBottom: '8px', fontSize: '24px' } } },
{ type: 'TextBlock', props: { text: 'March 28, 2026', style: { color: '#059669', fontSize: '13px', marginBottom: '12px' } } },
{ type: 'Heading', props: { text: 'The Case for Boring Technology', level: 'h2', style: { fontFamily: tokenRef('heading-font'), color: tokenRef('text'), marginBottom: '8px', fontSize: '24px' } } },
{ type: 'TextBlock', props: { text: 'March 28, 2026', style: { color: tokenRef('primary'), fontSize: '13px', marginBottom: '12px' } } },
{ type: 'TextBlock', props: { text: 'Every new project brings the temptation to reach for the latest framework. But there is immense value in choosing proven, well-understood tools...', style: { color: '#475569', lineHeight: '1.8' } } },
{ type: 'Divider', props: { color: '#e2e8f0', style: { margin: '32px 0' } } },
{ type: 'Heading', props: { text: 'Why I Switched to TypeScript (and Never Looked Back)', level: 'h2', style: { fontFamily: 'Merriweather, serif', color: '#1e293b', marginBottom: '8px', fontSize: '24px' } } },
{ type: 'TextBlock', props: { text: 'March 15, 2026', style: { color: '#059669', fontSize: '13px', marginBottom: '12px' } } },
{ type: 'Divider', props: { color: tokenRef('border'), style: { margin: '32px 0' } } },
{ type: 'Heading', props: { text: 'Why I Switched to TypeScript (and Never Looked Back)', level: 'h2', style: { fontFamily: tokenRef('heading-font'), color: tokenRef('text'), marginBottom: '8px', fontSize: '24px' } } },
{ type: 'TextBlock', props: { text: 'March 15, 2026', style: { color: tokenRef('primary'), fontSize: '13px', marginBottom: '12px' } } },
{ type: 'TextBlock', props: { text: 'After years of writing JavaScript, the move to TypeScript felt like putting on glasses for the first time. Everything became clearer...', style: { color: '#475569', lineHeight: '1.8' } } },
],
},
{
type: 'Section',
props: { style: { padding: '60px 20px', backgroundColor: '#ffffff', textAlign: 'center' } },
props: { style: { padding: '60px 20px', backgroundColor: tokenRef('bg'), textAlign: 'center' } },
children: [
{ type: 'Heading', props: { text: 'Never Miss a Post', level: 'h3', style: { fontFamily: 'Merriweather, serif', color: '#1e293b', marginBottom: '12px' } } },
{ type: 'TextBlock', props: { text: 'Subscribe to get new articles delivered straight to your inbox.', style: { color: '#64748b', marginBottom: '20px' } } },
{ type: 'ButtonLink', props: { text: 'Subscribe', href: 'contact', style: { display: 'inline-block', padding: '12px 32px', backgroundColor: '#059669', color: '#ffffff', borderRadius: '8px', fontWeight: '600' } } },
{ type: 'Heading', props: { text: 'Never Miss a Post', level: 'h3', style: { fontFamily: tokenRef('heading-font'), color: tokenRef('text'), marginBottom: '12px' } } },
{ type: 'TextBlock', props: { text: 'Subscribe to get new articles delivered straight to your inbox.', style: { color: tokenRef('muted'), marginBottom: '20px' } } },
{ type: 'ButtonLink', props: { text: 'Subscribe', href: 'contact', style: { display: 'inline-block', padding: '12px 32px', backgroundColor: tokenRef('primary'), color: tokenRef('bg'), borderRadius: '8px', fontWeight: '600' } } },
],
},
],
@@ -1981,17 +1988,17 @@ const blog: TemplateDefinition = {
components: [
{
type: 'Section',
props: { style: { padding: '60px 20px', backgroundColor: '#ffffff' } },
props: { style: { padding: '60px 20px', backgroundColor: tokenRef('bg') } },
children: [
{ type: 'Heading', props: { text: 'All Posts', level: 'h1', style: { fontFamily: 'Merriweather, serif', color: '#1e293b', marginBottom: '32px' } } },
{ type: 'Heading', props: { text: 'The Case for Boring Technology', level: 'h3', style: { fontFamily: 'Merriweather, serif', color: '#1e293b', marginBottom: '4px' } } },
{ type: 'TextBlock', props: { text: 'March 28, 2026 \u2014 Every new project brings the temptation to reach for the latest framework...', style: { color: '#64748b', marginBottom: '24px' } } },
{ type: 'Heading', props: { text: 'Why I Switched to TypeScript', level: 'h3', style: { fontFamily: 'Merriweather, serif', color: '#1e293b', marginBottom: '4px' } } },
{ type: 'TextBlock', props: { text: 'March 15, 2026 \u2014 After years of writing JavaScript, the move to TypeScript felt like putting on glasses...', style: { color: '#64748b', marginBottom: '24px' } } },
{ type: 'Heading', props: { text: 'Designing for Accessibility First', level: 'h3', style: { fontFamily: 'Merriweather, serif', color: '#1e293b', marginBottom: '4px' } } },
{ type: 'TextBlock', props: { text: 'March 2, 2026 \u2014 Accessibility is not an afterthought. It is a design principle that benefits everyone...', style: { color: '#64748b', marginBottom: '24px' } } },
{ type: 'Heading', props: { text: 'The Art of Code Review', level: 'h3', style: { fontFamily: 'Merriweather, serif', color: '#1e293b', marginBottom: '4px' } } },
{ type: 'TextBlock', props: { text: 'February 20, 2026 \u2014 A good code review is a conversation, not a critique. Here is how to make them productive...', style: { color: '#64748b' } } },
{ type: 'Heading', props: { text: 'All Posts', level: 'h1', style: { fontFamily: tokenRef('heading-font'), color: tokenRef('text'), marginBottom: '32px' } } },
{ type: 'Heading', props: { text: 'The Case for Boring Technology', level: 'h3', style: { fontFamily: tokenRef('heading-font'), color: tokenRef('text'), marginBottom: '4px' } } },
{ type: 'TextBlock', props: { text: 'March 28, 2026 \u2014 Every new project brings the temptation to reach for the latest framework...', style: { color: tokenRef('muted'), marginBottom: '24px' } } },
{ type: 'Heading', props: { text: 'Why I Switched to TypeScript', level: 'h3', style: { fontFamily: tokenRef('heading-font'), color: tokenRef('text'), marginBottom: '4px' } } },
{ type: 'TextBlock', props: { text: 'March 15, 2026 \u2014 After years of writing JavaScript, the move to TypeScript felt like putting on glasses...', style: { color: tokenRef('muted'), marginBottom: '24px' } } },
{ type: 'Heading', props: { text: 'Designing for Accessibility First', level: 'h3', style: { fontFamily: tokenRef('heading-font'), color: tokenRef('text'), marginBottom: '4px' } } },
{ type: 'TextBlock', props: { text: 'March 2, 2026 \u2014 Accessibility is not an afterthought. It is a design principle that benefits everyone...', style: { color: tokenRef('muted'), marginBottom: '24px' } } },
{ type: 'Heading', props: { text: 'The Art of Code Review', level: 'h3', style: { fontFamily: tokenRef('heading-font'), color: tokenRef('text'), marginBottom: '4px' } } },
{ type: 'TextBlock', props: { text: 'February 20, 2026 \u2014 A good code review is a conversation, not a critique. Here is how to make them productive...', style: { color: tokenRef('muted') } } },
],
},
{
@@ -2011,9 +2018,9 @@ const blog: TemplateDefinition = {
components: [
{
type: 'Section',
props: { style: { padding: '60px 20px', backgroundColor: '#ffffff' } },
props: { style: { padding: '60px 20px', backgroundColor: tokenRef('bg') } },
children: [
{ type: 'Heading', props: { text: 'About This Blog', level: 'h1', style: { fontFamily: 'Merriweather, serif', color: '#1e293b', marginBottom: '16px' } } },
{ type: 'Heading', props: { text: 'About This Blog', level: 'h1', style: { fontFamily: tokenRef('heading-font'), color: tokenRef('text'), marginBottom: '16px' } } },
{ type: 'TextBlock', props: { text: 'The Daily Byte is a personal blog where I share lessons learned from 10+ years of building software. I write about web development, design systems, developer tools, and the occasional philosophical tangent about technology.\n\nI believe in writing that is honest, practical, and accessible to everyone \u2014 from juniors to staff engineers.', style: { color: '#475569', lineHeight: '1.8', maxWidth: '700px', fontSize: '16px' } } },
],
},
@@ -2021,16 +2028,16 @@ const blog: TemplateDefinition = {
type: 'Section',
props: { style: { padding: '60px 20px', backgroundColor: '#f8fafc' } },
children: [
{ type: 'Heading', props: { text: 'About the Author', level: 'h2', style: { fontFamily: 'Merriweather, serif', color: '#1e293b', marginBottom: '16px' } } },
{ type: 'Heading', props: { text: 'About the Author', level: 'h2', style: { fontFamily: tokenRef('heading-font'), color: tokenRef('text'), marginBottom: '16px' } } },
{ type: 'TextBlock', props: { text: 'I am a software engineer based in Austin, TX. By day I work on developer tools at a mid-stage startup. By night I write about the things I learn, the mistakes I make, and the patterns that keep showing up across projects.\n\nPreviously I worked at GitHub and Shopify. I hold a CS degree from the University of Texas at Austin.', style: { color: '#475569', lineHeight: '1.8', maxWidth: '700px' } } },
],
},
{
type: 'Section',
props: { style: { padding: '40px 20px', backgroundColor: '#ffffff', textAlign: 'center' } },
props: { style: { padding: '40px 20px', backgroundColor: tokenRef('bg'), textAlign: 'center' } },
children: [
{ type: 'Heading', props: { text: 'Colophon', level: 'h3', style: { fontFamily: 'Merriweather, serif', color: '#1e293b', marginBottom: '12px' } } },
{ type: 'TextBlock', props: { text: 'This blog is built with a static site generator, hosted on Vercel, and written in Markdown. The fonts are Merriweather (headings) and Inter (body).', style: { color: '#64748b', lineHeight: '1.8', maxWidth: '500px', margin: '0 auto' } } },
{ type: 'Heading', props: { text: 'Colophon', level: 'h3', style: { fontFamily: tokenRef('heading-font'), color: tokenRef('text'), marginBottom: '12px' } } },
{ type: 'TextBlock', props: { text: 'This blog is built with a static site generator, hosted on Vercel, and written in Markdown. The fonts are Merriweather (headings) and Inter (body).', style: { color: tokenRef('muted'), lineHeight: '1.8', maxWidth: '500px', margin: '0 auto' } } },
],
},
],
@@ -2043,16 +2050,16 @@ const blog: TemplateDefinition = {
components: [
{
type: 'Section',
props: { style: { padding: '60px 20px', backgroundColor: '#ffffff' } },
props: { style: { padding: '60px 20px', backgroundColor: tokenRef('bg') } },
children: [
{ type: 'Heading', props: { text: 'Subscribe & Contact', level: 'h1', style: { fontFamily: 'Merriweather, serif', color: '#1e293b', marginBottom: '16px' } } },
{ type: 'TextBlock', props: { text: 'Get new posts delivered to your inbox. No spam, unsubscribe anytime.', style: { color: '#64748b', marginBottom: '32px' } } },
{ type: 'Heading', props: { text: 'Subscribe & Contact', level: 'h1', style: { fontFamily: tokenRef('heading-font'), color: tokenRef('text'), marginBottom: '16px' } } },
{ type: 'TextBlock', props: { text: 'Get new posts delivered to your inbox. No spam, unsubscribe anytime.', style: { color: tokenRef('muted'), marginBottom: '32px' } } },
{
type: 'FormContainer',
props: { style: { maxWidth: '500px' } },
children: [
{ type: 'InputField', props: { label: 'Email Address', placeholder: 'you@example.com', inputType: 'email' } },
{ type: 'FormButton', props: { text: 'Subscribe', style: { backgroundColor: '#059669', color: '#ffffff' } } },
{ type: 'FormButton', props: { text: 'Subscribe', style: { backgroundColor: tokenRef('primary'), color: tokenRef('bg') } } },
],
},
],
@@ -2061,8 +2068,8 @@ const blog: TemplateDefinition = {
type: 'Section',
props: { style: { padding: '40px 20px', backgroundColor: '#f8fafc' } },
children: [
{ type: 'Heading', props: { text: 'Other Ways to Reach Me', level: 'h3', style: { fontFamily: 'Merriweather, serif', color: '#1e293b', marginBottom: '12px' } } },
{ type: 'TextBlock', props: { text: 'Twitter: @dailybyte\nMastodon: @dailybyte@hachyderm.io\nEmail: hello@thedailybyte.dev\n\nI read every message and try to respond within a few days.', style: { color: '#64748b', lineHeight: '1.8' } } },
{ type: 'Heading', props: { text: 'Other Ways to Reach Me', level: 'h3', style: { fontFamily: tokenRef('heading-font'), color: tokenRef('text'), marginBottom: '12px' } } },
{ type: 'TextBlock', props: { text: 'Twitter: @dailybyte\nMastodon: @dailybyte@hachyderm.io\nEmail: hello@thedailybyte.dev\n\nI read every message and try to respond within a few days.', style: { color: tokenRef('muted'), lineHeight: '1.8' } } },
],
},
],
@@ -2088,6 +2095,7 @@ const wedding: TemplateDefinition = {
bodyFont: 'Inter, sans-serif',
backgroundColor: '#fffbeb',
textColor: '#78350f',
mutedTextColor: '#92400e',
navStyle: 'light',
},
header: makeHeader(
@@ -2097,11 +2105,11 @@ const wedding: TemplateDefinition = {
{ text: 'Schedule', href: '#schedule' },
{ text: 'RSVP', href: '#rsvp', isCta: true },
],
'#fffbeb',
'#78350f',
'#b45309',
tokenRef('bg'),
tokenRef('text'),
tokenRef('primary'),
),
footer: makeFooterContent('Emma & James', '#78350f', '#d4a574', {
footer: makeFooterContent('Emma & James', tokenRef('secondary'), '#d4a574', {
tagline: 'September 12, 2026 \u00b7 Napa Valley, CA',
}),
pages: [
@@ -2112,28 +2120,28 @@ const wedding: TemplateDefinition = {
components: [
{
type: 'Section',
props: { style: { padding: '120px 20px 80px', backgroundColor: '#fffbeb', textAlign: 'center' } },
props: { style: { padding: '120px 20px 80px', backgroundColor: tokenRef('bg'), textAlign: 'center' } },
children: [
{ type: 'TextBlock', props: { text: 'Together with their families', style: { color: '#92400e', fontSize: '16px', fontStyle: 'italic', marginBottom: '16px' } } },
{ type: 'Heading', props: { text: 'Emma & James', level: 'h1', style: { fontSize: '56px', fontFamily: 'Playfair Display, serif', color: '#78350f', marginBottom: '16px' } } },
{ type: 'TextBlock', props: { text: 'invite you to celebrate their wedding', style: { color: '#92400e', fontSize: '18px', marginBottom: '8px' } } },
{ type: 'Heading', props: { text: 'September 12, 2026', level: 'h2', style: { fontFamily: 'Playfair Display, serif', color: '#b45309', fontSize: '28px' } } },
{ type: 'TextBlock', props: { text: 'Together with their families', style: { color: tokenRef('muted'), fontSize: '16px', fontStyle: 'italic', marginBottom: '16px' } } },
{ type: 'Heading', props: { text: 'Emma & James', level: 'h1', style: { fontSize: '56px', fontFamily: tokenRef('heading-font'), color: tokenRef('text'), marginBottom: '16px' } } },
{ type: 'TextBlock', props: { text: 'invite you to celebrate their wedding', style: { color: tokenRef('muted'), fontSize: '18px', marginBottom: '8px' } } },
{ type: 'Heading', props: { text: 'September 12, 2026', level: 'h2', style: { fontFamily: tokenRef('heading-font'), color: tokenRef('primary'), fontSize: '28px' } } },
],
},
{
type: 'Section',
props: { style: { padding: '60px 20px', backgroundColor: '#ffffff', textAlign: 'center' } },
children: [
{ type: 'Heading', props: { text: 'The Details', level: 'h2', style: { fontFamily: 'Playfair Display, serif', color: '#78350f', marginBottom: '24px' } } },
{ type: 'TextBlock', props: { text: 'Ceremony: 4:00 PM\nSunset Gardens, Napa Valley\n\nReception: 6:00 PM\nThe Grand Pavilion\n\nDinner, dancing, and celebration to follow', style: { color: '#92400e', lineHeight: '2', fontSize: '16px' } } },
{ type: 'Heading', props: { text: 'The Details', level: 'h2', style: { fontFamily: tokenRef('heading-font'), color: tokenRef('text'), marginBottom: '24px' } } },
{ type: 'TextBlock', props: { text: 'Ceremony: 4:00 PM\nSunset Gardens, Napa Valley\n\nReception: 6:00 PM\nThe Grand Pavilion\n\nDinner, dancing, and celebration to follow', style: { color: tokenRef('muted'), lineHeight: '2', fontSize: '16px' } } },
],
},
{
type: 'Section',
props: { style: { padding: '60px 20px', backgroundColor: '#fef3c7', textAlign: 'center' } },
children: [
{ type: 'Heading', props: { text: 'Our Story', level: 'h2', style: { fontFamily: 'Playfair Display, serif', color: '#78350f', marginBottom: '16px' } } },
{ type: 'TextBlock', props: { text: 'We met at a coffee shop in Brooklyn on a rainy Tuesday in 2020. What started with a shared table and a spilled latte turned into the greatest adventure of our lives. Five years, three apartments, and one very spoiled dog later, we can not wait to start this next chapter together.', style: { color: '#92400e', lineHeight: '1.8', maxWidth: '650px', margin: '0 auto' } } },
{ type: 'Heading', props: { text: 'Our Story', level: 'h2', style: { fontFamily: tokenRef('heading-font'), color: tokenRef('text'), marginBottom: '16px' } } },
{ type: 'TextBlock', props: { text: 'We met at a coffee shop in Brooklyn on a rainy Tuesday in 2020. What started with a shared table and a spilled latte turned into the greatest adventure of our lives. Five years, three apartments, and one very spoiled dog later, we can not wait to start this next chapter together.', style: { color: tokenRef('muted'), lineHeight: '1.8', maxWidth: '650px', margin: '0 auto' } } },
],
},
{
@@ -2149,10 +2157,10 @@ const wedding: TemplateDefinition = {
},
{
type: 'Section',
props: { style: { padding: '60px 20px', backgroundColor: '#fffbeb', textAlign: 'center' } },
props: { style: { padding: '60px 20px', backgroundColor: tokenRef('bg'), textAlign: 'center' } },
children: [
{ type: 'Heading', props: { text: 'RSVP', level: 'h2', style: { fontFamily: 'Playfair Display, serif', color: '#78350f', marginBottom: '16px' } } },
{ type: 'TextBlock', props: { text: 'Kindly respond by August 1, 2026', style: { color: '#92400e', marginBottom: '24px' } } },
{ type: 'Heading', props: { text: 'RSVP', level: 'h2', style: { fontFamily: tokenRef('heading-font'), color: tokenRef('text'), marginBottom: '16px' } } },
{ type: 'TextBlock', props: { text: 'Kindly respond by August 1, 2026', style: { color: tokenRef('muted'), marginBottom: '24px' } } },
{
type: 'FormContainer',
props: { style: { maxWidth: '500px', margin: '0 auto' } },
@@ -2161,7 +2169,7 @@ const wedding: TemplateDefinition = {
{ type: 'InputField', props: { label: 'Email', placeholder: 'your@email.com', inputType: 'email' } },
{ type: 'InputField', props: { label: 'Number of Guests', placeholder: '1' } },
{ type: 'TextareaField', props: { label: 'Dietary Restrictions', placeholder: 'Any allergies or preferences...' } },
{ type: 'FormButton', props: { text: 'Send RSVP', style: { backgroundColor: '#b45309', color: '#ffffff' } } },
{ type: 'FormButton', props: { text: 'Send RSVP', style: { backgroundColor: tokenRef('primary'), color: '#ffffff' } } },
],
},
],