import { useStore } from '@nanostores/react' import { useEffect, useState } from 'react' import { $settings, $settingsData, $hasChanges, $saveSettings, $changedFields } from '../stores/settings' import { addToast } from '../stores/app' import { SaveBar, DesignPageSkeleton } from '../components/shared' import './DesignPage.preview.css' const fontConfigs = { system: { family: 'system-ui, -apple-system, sans-serif', url: '' }, inter: { family: "'Inter', sans-serif", url: 'https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap' }, georgia: { family: 'Georgia, serif', url: '' }, merriweather: { family: "'Merriweather', serif", url: 'https://fonts.googleapis.com/css2?family=Merriweather:wght@400;700&display=swap' }, 'source-serif': { family: "'Source Serif 4', serif", url: 'https://fonts.googleapis.com/css2?family=Source+Serif+4:wght@400;600&display=swap' }, 'jetbrains-mono': { family: "'JetBrains Mono', monospace", url: 'https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500&display=swap' }, } const themePreviewColors: Record = { github: { label: 'GitHub Light', bg: '#f6f8fa', text: '#24292e', keyword: '#d73a49', string: '#032f62', comment: '#6a737d' }, 'github-dark': { label: 'GitHub Dark', bg: '#0d1117', text: '#c9d1d9', keyword: '#ff7b72', string: '#a5d6ff', comment: '#8b949e' }, vs: { label: 'VS Light', bg: '#ffffff', text: '#000000', keyword: '#0000ff', string: '#a31515', comment: '#008000' }, xcode: { label: 'Xcode Light', bg: '#ffffff', text: '#000000', keyword: '#aa0d91', string: '#c41a16', comment: '#007400' }, 'xcode-dark': { label: 'Xcode Dark', bg: '#1f1f24', text: '#ffffff', keyword: '#fc5fa3', string: '#fc6a5d', comment: '#6c7986' }, 'solarized-light': { label: 'Solarized Light', bg: '#fdf6e3', text: '#657b83', keyword: '#859900', string: '#2aa198', comment: '#93a1a1' }, 'solarized-dark': { label: 'Solarized Dark', bg: '#002b36', text: '#839496', keyword: '#859900', string: '#2aa198', comment: '#586e75' }, 'gruvbox-light': { label: 'Gruvbox Light', bg: '#fbf1c7', text: '#3c3836', keyword: '#9d0006', string: '#79740e', comment: '#928374' }, gruvbox: { label: 'Gruvbox Dark', bg: '#282828', text: '#ebdbb2', keyword: '#fb4934', string: '#b8bb26', comment: '#928374' }, nord: { label: 'Nord', bg: '#2e3440', text: '#d8dee9', keyword: '#81a1c1', string: '#a3be8c', comment: '#616e88' }, onedark: { label: 'One Dark', bg: '#282c34', text: '#abb2bf', keyword: '#c678dd', string: '#98c379', comment: '#5c6370' }, dracula: { label: 'Dracula', bg: '#282a36', text: '#f8f8f2', keyword: '#ff79c6', string: '#f1fa8c', comment: '#6272a4' }, monokai: { label: 'Monokai', bg: '#272822', text: '#f8f8f2', keyword: '#f92672', string: '#e6db74', comment: '#75715e' }, } const defaultDarkColors = { bg: '#1e1e1e', text: '#d4d4d4', keyword: '#569cd6', string: '#ce9178', comment: '#6a9955' } const defaultLightColors = { bg: '#ffffff', text: '#000000', keyword: '#0000ff', string: '#a31515', comment: '#008000' } function getThemeColors(theme: string) { if (themePreviewColors[theme]) return themePreviewColors[theme] const isDark = theme.includes('dark') || ['monokai', 'dracula', 'nord', 'gruvbox', 'onedark', 'vim', 'emacs'].some(d => theme.includes(d)) return { label: theme, ...(isDark ? defaultDarkColors : defaultLightColors) } } function formatThemeLabel(theme: string): string { if (themePreviewColors[theme]) return themePreviewColors[theme].label return theme.split(/[-_]/).map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ') } const fonts = [ { value: 'system', label: 'System Default' }, { value: 'inter', label: 'Inter' }, { value: 'georgia', label: 'Georgia' }, { value: 'merriweather', label: 'Merriweather' }, { value: 'source-serif', label: 'Source Serif' }, { value: 'jetbrains-mono', label: 'JetBrains Mono' }, ] const layouts = [ { value: 'default', label: 'Classic' }, { value: 'minimal', label: 'Minimal' }, { value: 'magazine', label: 'Magazine' }, ] function useFontLoader(fontKey: string) { useEffect(() => { const config = fontConfigs[fontKey as keyof typeof fontConfigs] if (!config?.url) return const existing = document.querySelector(`link[href="${config.url}"]`) if (existing) return const link = document.createElement('link') link.rel = 'stylesheet' link.href = config.url document.head.appendChild(link) }, [fontKey]) } function CodePreview({ theme }: { theme: string }) { const colors = getThemeColors(theme) return (
function greet(name) {'{'}
return `Hello, ${'{'}name{'}'}`
{'}'}
// Welcome to your blog
) } function LayoutPreview({ layout, selected }: { layout: string; selected: boolean }) { const accent = selected ? 'var(--color-accent)' : 'var(--color-border)' const mutedBar = selected ? 'var(--color-accent)' : 'var(--color-muted)' if (layout === 'minimal') { return ( ) } if (layout === 'magazine') { return ( ) } // Default layout return ( ) } function PreviewCodeBlock({ theme }: { theme: string }) { const colors = getThemeColors(theme) return (
const api = await fetch('/posts')
) } function PreviewPostCard() { return (
Jan 15, 2024

Building APIs

A deep dive into REST patterns and best practices.

) } function LivePreview({ settings }: { settings: Record }) { const fontKey = settings.font || 'system' const fontConfig = fontConfigs[fontKey as keyof typeof fontConfigs] || fontConfigs.system const codeTheme = settings.code_theme || 'github' const accent = settings.accent_color || '#10b981' const layout = settings.layout || 'default' const compactness = settings.compactness || 'cozy' useFontLoader(fontKey) return (
{/* Browser chrome */}
yourblog.writekit.dev
{/* Header */}
Your Blog
{/* Content - varies by layout */} {layout === 'magazine' ? (
) : (
Jan 15, 2024

Building Better APIs

A deep dive into REST design patterns and best practices for modern web development.

typescript react
)} {/* Footer */}
© 2024 Your Blog
) } export default function DesignPage() { const settings = useStore($settings) const { data } = useStore($settingsData) const hasChanges = useStore($hasChanges) const changedFields = useStore($changedFields) const saveSettings = useStore($saveSettings) const [availableThemes, setAvailableThemes] = useState(Object.keys(themePreviewColors)) useEffect(() => { fetch('/api/studio/code-themes') .then(r => r.json()) .then((themes: string[]) => setAvailableThemes(themes)) .catch(() => {}) }, []) // Load all fonts for previews Object.keys(fontConfigs).forEach(useFontLoader) const handleSave = async () => { try { await saveSettings.mutate(settings) addToast('Settings saved', 'success') } catch { addToast('Failed to save settings', 'error') } } if (!data) return return (

Design

Customize how your blog looks

{/* Panel container - full-bleed borders */}
{/* Live Preview */}
Live Preview
} />
{/* Presets */}
Quick Presets
{([ { name: 'Developer', desc: 'Monospace, dark code, minimal', font: 'jetbrains-mono', code_theme: 'onedark', layout: 'minimal', compactness: 'compact', accent_color: '#10b981' }, { name: 'Writer', desc: 'Serif, light code, spacious', font: 'merriweather', code_theme: 'github', layout: 'default', compactness: 'spacious', accent_color: '#6366f1' }, { name: 'Magazine', desc: 'Sans-serif, grid layout', font: 'inter', code_theme: 'nord', layout: 'magazine', compactness: 'cozy', accent_color: '#f59e0b' }, ] as const).map(preset => ( ))}
{/* Accent Color */}
Accent Color
$settings.setKey('accent_color', e.target.value)} className="w-12 h-12 border border-border cursor-pointer bg-transparent" /> $settings.setKey('accent_color', e.target.value)} className="input w-32 font-mono text-sm" placeholder="#10b981" />
{['#10b981', '#6366f1', '#f59e0b', '#ef4444', '#8b5cf6', '#06b6d4'].map(color => (
{/* Typography */}
Typography
{fonts.map(font => { const config = fontConfigs[font.value as keyof typeof fontConfigs] return ( ) })}
{/* Code Theme */}
Code Theme
{availableThemes.map(theme => ( ))}
{/* Layout */}
Layout
{layouts.map(layout => ( ))}
{/* Density */}
Content Density
{([ { value: 'compact', label: 'Compact', lines: 4 }, { value: 'cozy', label: 'Cozy', lines: 3 }, { value: 'spacious', label: 'Spacious', lines: 2 }, ] as const).map(option => ( ))}
{hasChanges && }
) }