2998 lines
79 KiB
Markdown
2998 lines
79 KiB
Markdown
|
|
# GlyphDiff.com - Font Comparison Project Plan
|
|||
|
|
|
|||
|
|
> **Status:** Learning-Focused MVP | **Tech Stack:** SvelteKit + TypeScript + Tailwind CSS | **Transition Path:** React → Svelte
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Table of Contents
|
|||
|
|
|
|||
|
|
1. [Project Overview](#project-overview)
|
|||
|
|
2. [Executive Summary](#executive-summary)
|
|||
|
|
3. [Architecture Decisions](#architecture-decisions)
|
|||
|
|
4. [Font Provider Integration Strategy](#font-provider-integration-strategy)
|
|||
|
|
5. [Performance Optimization](#performance-optimization)
|
|||
|
|
6. [Frontend Architecture](#frontend-architecture)
|
|||
|
|
7. [Data Models](#data-models)
|
|||
|
|
8. [Implementation Roadmap](#implementation-roadmap)
|
|||
|
|
9. [TanStack Query Research](#tanstack-query-research)
|
|||
|
|
10. [Learning Guide: React → Svelte](#learning-guide-react--svelte)
|
|||
|
|
11. [Scalability & Commercialization](#scalability--commercialization)
|
|||
|
|
12. [Deployment](#deployment)
|
|||
|
|
13. [Risk Assessment](#risk-assessment)
|
|||
|
|
14. [Success Metrics](#success-metrics)
|
|||
|
|
15. [Appendix](#appendix)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Project Overview
|
|||
|
|
|
|||
|
|
### Vision
|
|||
|
|
|
|||
|
|
Create a modern, fast, and intuitive web application for comparing fonts side-by-side. The tool will help designers, developers, and typographers make informed decisions about font choices by providing real-time visual comparisons, filtering, and customization options.
|
|||
|
|
|
|||
|
|
### Target Audience
|
|||
|
|
|
|||
|
|
- **UI/UX Designers:** Selecting typefaces for web and mobile applications
|
|||
|
|
- **Web Developers:** Choosing fonts for implementation in projects
|
|||
|
|
- **Graphic Designers:** Finding complementary fonts for branding
|
|||
|
|
- **Typography Enthusiasts:** Exploring and comparing font characteristics
|
|||
|
|
|
|||
|
|
### Core Features (MVP)
|
|||
|
|
|
|||
|
|
1. **Font Catalog:** Browse available fonts from multiple providers
|
|||
|
|
2. **Side-by-Side Comparison:** Compare up to 4 fonts simultaneously
|
|||
|
|
3. **Custom Preview Text:** Enter custom text for realistic comparison
|
|||
|
|
4. **Filter & Search:** Filter by category, popularity, weight, width
|
|||
|
|
5. **Responsive Design:** Seamless experience across all devices
|
|||
|
|
6. **Dark Mode:** Built-in theme toggle for comfortable viewing
|
|||
|
|
7. **URL Sharing:** Share comparison sessions via shareable URLs
|
|||
|
|
|
|||
|
|
### Tech Stack
|
|||
|
|
|
|||
|
|
| Layer | Technology | Purpose |
|
|||
|
|
|-------|-----------|---------|
|
|||
|
|
| Framework | SvelteKit 2.x | Full-stack framework with SSR/SSG support |
|
|||
|
|
| UI Language | Svelte 5 | Reactive component framework |
|
|||
|
|
| Styling | Tailwind CSS 4.x | Utility-first CSS framework |
|
|||
|
|
| Component Library | Bits UI | Accessible Svelte components (Radix UI port) |
|
|||
|
|
| Type Safety | TypeScript 5.x | Static type checking |
|
|||
|
|
| Hosting | Vercel | Edge deployment & preview environments |
|
|||
|
|
|
|||
|
|
### Project Goals
|
|||
|
|
|
|||
|
|
#### Learning Goals (Primary)
|
|||
|
|
- Gain proficiency in Svelte 5 and its reactive primitives (`$state`, `$derived`, `$effect`)
|
|||
|
|
- Understand SvelteKit's routing and data loading architecture
|
|||
|
|
- Learn client-side state management patterns in Svelte ecosystem
|
|||
|
|
- Explore performance optimization techniques for static web apps
|
|||
|
|
|
|||
|
|
#### Project Goals (Secondary)
|
|||
|
|
- Build a functional, visually appealing font comparison tool
|
|||
|
|
- Create a performant, accessible user interface
|
|||
|
|
- Establish a foundation for future commercialization
|
|||
|
|
- Demonstrate mastery of modern frontend development practices
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Executive Summary
|
|||
|
|
|
|||
|
|
### Recommended Approach: Pure Client-Side Architecture
|
|||
|
|
|
|||
|
|
**Recommendation:** Build the MVP as a pure client-side application using SvelteKit's static site generation (SSG) capabilities, with no backend infrastructure.
|
|||
|
|
|
|||
|
|
### Key Rationale
|
|||
|
|
|
|||
|
|
1. **Learning Focus:** Minimize backend complexity to concentrate on Svelte/SvelteKit mastery
|
|||
|
|
2. **Data Availability:** Font metadata is publicly available via Google Fonts API and Fontshare CDN
|
|||
|
|
3. **Performance:** Static generation provides instant page loads and optimal Core Web Vitals
|
|||
|
|
4. **Cost Efficiency:** No server costs during MVP phase
|
|||
|
|
5. **Future Flexibility:** Can easily add backend later when user data collection becomes necessary
|
|||
|
|
|
|||
|
|
### Architecture at a Glance
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
┌─────────────────────────────────────────────────────────────┐
|
|||
|
|
│ User Browser │
|
|||
|
|
│ ┌─────────────────────────────────────────────────────────┐ │
|
|||
|
|
│ │ SvelteKit Client Application │ │
|
|||
|
|
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
|
|||
|
|
│ │ │ Font Store │ │ Filter Store│ │ Comp Store │ │ │
|
|||
|
|
│ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │
|
|||
|
|
│ │ ┌─────────────────────────────────────────────────┐ │ │
|
|||
|
|
│ │ │ Font Provider Services │ │ │
|
|||
|
|
│ │ │ ┌──────────────┐ ┌──────────────┐ │ │ │
|
|||
|
|
│ │ │ │ Google Fonts │ │ Fontshare │ │ │ │
|
|||
|
|
│ │ │ │ API │ │ CDN │ │ │ │
|
|||
|
|
│ │ │ └──────────────┘ └──────────────┘ │ │ │
|
|||
|
|
│ │ └─────────────────────────────────────────────────┘ │ │
|
|||
|
|
│ └─────────────────────────────────────────────────────────┘ │
|
|||
|
|
└─────────────────────────────────────────────────────────────┘
|
|||
|
|
│ │
|
|||
|
|
│ HTTP Requests │ CSS Font Files
|
|||
|
|
▼ ▼
|
|||
|
|
┌──────────────────┐ ┌──────────────────┐
|
|||
|
|
│ Google Fonts API │ │ Fontshare CDN │
|
|||
|
|
└──────────────────┘ └──────────────────┘
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Architecture Decisions
|
|||
|
|
|
|||
|
|
### Backend Necessity Analysis
|
|||
|
|
|
|||
|
|
#### Option A: Pure Client-Side (Recommended for MVP)
|
|||
|
|
|
|||
|
|
**Pros:**
|
|||
|
|
- **Simplicity:** No server code, database, or API endpoints to maintain
|
|||
|
|
- **Performance:** Static sites load faster and score better on Core Web Vitals
|
|||
|
|
- **Cost:** Zero hosting costs on Vercel free tier
|
|||
|
|
- **Learning Focus:** Maximum time spent on Svelte/SvelteKit fundamentals
|
|||
|
|
- **Deployment:** Instant deployments with zero configuration
|
|||
|
|
- **Scalability:** Vercel Edge handles CDN distribution automatically
|
|||
|
|
|
|||
|
|
**Cons:**
|
|||
|
|
- **No User Data:** Cannot save user preferences or comparison history
|
|||
|
|
- **No Analytics:** Limited to client-side analytics (e.g., Google Analytics)
|
|||
|
|
- **Rate Limiting:** Google Fonts API has usage limits (1,000 requests/day)
|
|||
|
|
- **Caching:** Cannot implement server-side caching strategies
|
|||
|
|
- **Data Freshness:** Font metadata fetched on each session (mitigated by browser caching)
|
|||
|
|
|
|||
|
|
**Use Cases This Supports:**
|
|||
|
|
- ✅ Browse and compare fonts
|
|||
|
|
- ✅ Share comparisons via URL
|
|||
|
|
- ✅ Filter and search fonts
|
|||
|
|
- ✅ Save preferences locally (localStorage)
|
|||
|
|
- ✅ Responsive, dark mode UI
|
|||
|
|
|
|||
|
|
**Use Cases This Does NOT Support:**
|
|||
|
|
- ❌ User accounts and saved comparisons
|
|||
|
|
- ❌ Custom font uploads
|
|||
|
|
- ❌ Collaborative comparisons
|
|||
|
|
- ❌ Advanced analytics
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
#### Option B: Hybrid with Serverless Backend
|
|||
|
|
|
|||
|
|
**Pros:**
|
|||
|
|
- **Caching Layer:** Server-side caching reduces API calls and improves load times
|
|||
|
|
- **Rate Limit Management:** Proxy requests to avoid Google Fonts API limits
|
|||
|
|
- **User Data:** Save user preferences and comparison history
|
|||
|
|
- **Custom Endpoints:** Create specialized API endpoints for font data
|
|||
|
|
- **Preprocessing:** Transform and normalize font data before sending to client
|
|||
|
|
|
|||
|
|
**Cons:**
|
|||
|
|
- **Complexity:** Additional infrastructure to build and maintain
|
|||
|
|
- **Cost:** Vercel Pro plan may be required for serverless functions
|
|||
|
|
- **Latency:** Server round-trip adds latency to initial page load
|
|||
|
|
- **Development Overhead:** Testing backend code requires additional considerations
|
|||
|
|
- **Learning Curve:** Takes time away from Svelte learning goals
|
|||
|
|
|
|||
|
|
**Recommended Backend Stack (if needed later):**
|
|||
|
|
- **API Routes:** SvelteKit server endpoints (`+page.server.ts`, `+server.ts`)
|
|||
|
|
- **Database:** Vercel Postgres (Neon) or SQLite (better-sqlite3)
|
|||
|
|
- **Caching:** Vercel KV (Redis) or in-memory caching
|
|||
|
|
- **Auth:** Lucia Auth or Clerk for user authentication
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### Recommendation
|
|||
|
|
|
|||
|
|
**Build Option A (Pure Client-Side) for MVP with these considerations:**
|
|||
|
|
|
|||
|
|
1. **Design for future backend:** Structure data fetching and state management to be easily adapted to server-side later
|
|||
|
|
2. **Implement smart caching:** Use browser localStorage for font metadata cache with TTL
|
|||
|
|
3. **Optimize API usage:** Batch requests and use Google Fonts API efficiently
|
|||
|
|
4. **Monitor usage:** Track API usage and user engagement to inform backend decision
|
|||
|
|
5. **Migration path:** Keep server components clear of logic that could be moved to `+page.server.ts` later
|
|||
|
|
|
|||
|
|
### When to Add Backend
|
|||
|
|
|
|||
|
|
Consider adding backend when:
|
|||
|
|
- ✅ User growth exceeds 1,000 daily active users
|
|||
|
|
- ✅ Analytics show users repeatedly saving comparisons manually
|
|||
|
|
- ✅ Feedback indicates desire for user accounts
|
|||
|
|
- ✅ Performance degrades due to repeated API calls
|
|||
|
|
- ✅ Planning to add premium features requiring payment processing
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Font Provider Integration Strategy
|
|||
|
|
|
|||
|
|
### Provider Overview
|
|||
|
|
|
|||
|
|
| Provider | API Type | License | Font Count | Integration Complexity |
|
|||
|
|
|----------|----------|---------|------------|------------------------|
|
|||
|
|
| Google Fonts | REST API | Open Source (SIL/OFL) | 1,600+ | Low - Official API available |
|
|||
|
|
| Fontshare | GitHub/CDN | Free License | 100+ | Very Low - Static JSON available |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### Google Fonts Integration
|
|||
|
|
|
|||
|
|
#### API Endpoint
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
https://www.googleapis.com/webfonts/v1/webfonts?key=YOUR_API_KEY
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### TypeScript Implementation
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
// src/lib/services/google-fonts.ts
|
|||
|
|
import type { FontMetadata, FontVariant, FontCategory } from '$lib/types';
|
|||
|
|
|
|||
|
|
export interface GoogleFontsResponse {
|
|||
|
|
kind: string;
|
|||
|
|
items: GoogleFontItem[];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export interface GoogleFontItem {
|
|||
|
|
family: string;
|
|||
|
|
variants: string[];
|
|||
|
|
subsets: string[];
|
|||
|
|
version: string;
|
|||
|
|
lastModified: string;
|
|||
|
|
files: Record<string, string>;
|
|||
|
|
category: 'sans-serif' | 'serif' | 'display' | 'handwriting' | 'monospace';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export async function fetchGoogleFonts(apiKey: string): Promise<FontMetadata[]> {
|
|||
|
|
const response = await fetch(
|
|||
|
|
`https://www.googleapis.com/webfonts/v1/webfonts?key=${apiKey}`
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
if (!response.ok) {
|
|||
|
|
throw new Error(`Google Fonts API error: ${response.statusText}`);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const data: GoogleFontsResponse = await response.json();
|
|||
|
|
|
|||
|
|
return data.items.map(mapGoogleFontToMetadata);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function mapGoogleFontToMetadata(item: GoogleFontItem): FontMetadata {
|
|||
|
|
const category = mapGoogleCategory(item.category);
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
id: `google-${item.family.toLowerCase().replace(/\s+/g, '-')}`,
|
|||
|
|
provider: 'google-fonts',
|
|||
|
|
family: item.family,
|
|||
|
|
displayName: item.family,
|
|||
|
|
category,
|
|||
|
|
variants: mapGoogleVariants(item.variants),
|
|||
|
|
license: {
|
|||
|
|
type: 'open-source',
|
|||
|
|
url: 'https://fonts.google.com/attribution',
|
|||
|
|
attribution: 'Google Fonts',
|
|||
|
|
commercial: true,
|
|||
|
|
embedding: 'webfont'
|
|||
|
|
},
|
|||
|
|
languages: item.subsets,
|
|||
|
|
previewUrl: getGoogleFontPreviewUrl(item.family),
|
|||
|
|
cssUrl: getGoogleFontCssUrl(item.family, '400'),
|
|||
|
|
metadata: {
|
|||
|
|
version: item.version,
|
|||
|
|
lastModified: item.lastModified,
|
|||
|
|
files: item.files
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function mapGoogleCategory(
|
|||
|
|
category: GoogleFontItem['category']
|
|||
|
|
): FontCategory {
|
|||
|
|
const mapping = {
|
|||
|
|
'sans-serif': 'sans-serif',
|
|||
|
|
'serif': 'serif',
|
|||
|
|
'display': 'display',
|
|||
|
|
'handwriting': 'handwriting',
|
|||
|
|
'monospace': 'monospace'
|
|||
|
|
} as const;
|
|||
|
|
return mapping[category] || 'display';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function mapGoogleVariants(variants: string[]): FontVariant[] {
|
|||
|
|
return variants.map(v => {
|
|||
|
|
const weight = parseInt(v) || 400;
|
|||
|
|
const style = v.includes('italic') ? 'italic' : 'normal';
|
|||
|
|
return { weight, style, name: v };
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function getGoogleFontPreviewUrl(family: string): string {
|
|||
|
|
return `https://fonts.googleapis.com/css2?family=${encodeURIComponent(family)}:wght@400&display=swap`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function getGoogleFontCssUrl(family: string, weight: string): string {
|
|||
|
|
return `https://fonts.googleapis.com/css2?family=${encodeURIComponent(family)}:wght@${weight}&display=swap`;
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### Dynamic Font Loading
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
// src/lib/utils/font-loader.ts
|
|||
|
|
let loadedFonts = new Set<string>();
|
|||
|
|
|
|||
|
|
export async function loadFont(family: string, weight: number = 400): Promise<void> {
|
|||
|
|
const fontKey = `${family}-${weight}`;
|
|||
|
|
|
|||
|
|
if (loadedFonts.has(fontKey)) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const cssUrl = `https://fonts.googleapis.com/css2?family=${encodeURIComponent(family)}:wght@${weight}&display=swap`;
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
// Create and inject link element
|
|||
|
|
const link = document.createElement('link');
|
|||
|
|
link.rel = 'stylesheet';
|
|||
|
|
link.href = cssUrl;
|
|||
|
|
link.crossOrigin = 'anonymous';
|
|||
|
|
|
|||
|
|
await new Promise<void>((resolve, reject) => {
|
|||
|
|
link.onload = () => {
|
|||
|
|
loadedFonts.add(fontKey);
|
|||
|
|
resolve();
|
|||
|
|
};
|
|||
|
|
link.onerror = () => reject(new Error(`Failed to load font: ${family}`));
|
|||
|
|
document.head.appendChild(link);
|
|||
|
|
});
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error(`Failed to load font ${family}:`, error);
|
|||
|
|
throw error;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export function unloadFont(family: string, weight: number = 400): void {
|
|||
|
|
const cssUrl = `https://fonts.googleapis.com/css2?family=${encodeURIComponent(family)}:wght@${weight}&display=swap`;
|
|||
|
|
const links = document.querySelectorAll(`link[href="${cssUrl}"]`);
|
|||
|
|
links.forEach(link => link.remove());
|
|||
|
|
loadedFonts.delete(`${family}-${weight}`);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export function preloadFonts(fonts: { family: string; weight: number }[]): void {
|
|||
|
|
fonts.forEach(({ family, weight }) => {
|
|||
|
|
const link = document.createElement('link');
|
|||
|
|
link.rel = 'preload';
|
|||
|
|
link.as = 'font';
|
|||
|
|
link.type = 'font/woff2';
|
|||
|
|
link.href = `https://fonts.gstatic.com/s/${family.toLowerCase()}/v${weight}`;
|
|||
|
|
link.crossOrigin = 'anonymous';
|
|||
|
|
document.head.appendChild(link);
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### Fontshare Integration
|
|||
|
|
|
|||
|
|
#### Data Source
|
|||
|
|
|
|||
|
|
Fontshare provides a static JSON file with all font metadata:
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
https://api.fontshare.com/v2/css?f[]=<font-family-name>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Alternative: Use the GitHub repository data or scrape from the website.
|
|||
|
|
|
|||
|
|
#### TypeScript Implementation
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
// src/lib/services/fontshare.ts
|
|||
|
|
import type { FontMetadata } from '$lib/types';
|
|||
|
|
|
|||
|
|
export interface FontshareMetadata {
|
|||
|
|
family: string;
|
|||
|
|
slug: string;
|
|||
|
|
styles: FontshareStyle[];
|
|||
|
|
subsets: string[];
|
|||
|
|
category: string;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export interface FontshareStyle {
|
|||
|
|
name: string;
|
|||
|
|
weight: number;
|
|||
|
|
file: string;
|
|||
|
|
unicodeRange?: string;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Fontshare static font list (maintained manually or fetched from their docs)
|
|||
|
|
const FONTHARE_FONTS: FontshareMetadata[] = [
|
|||
|
|
{
|
|||
|
|
family: 'Albert Sans',
|
|||
|
|
slug: 'albert-sans',
|
|||
|
|
styles: [
|
|||
|
|
{ name: 'Regular', weight: 400, file: 'https://api.fontshare.com/v2/css?f[]=albert-sans@400' },
|
|||
|
|
{ name: 'Medium', weight: 500, file: 'https://api.fontshare.com/v2/css?f[]=albert-sans@500' },
|
|||
|
|
{ name: 'Bold', weight: 700, file: 'https://api.fontshare.com/v2/css?f[]=albert-sans@700' }
|
|||
|
|
],
|
|||
|
|
subsets: ['latin', 'latin-ext'],
|
|||
|
|
category: 'sans-serif'
|
|||
|
|
},
|
|||
|
|
// Add more Fontshare fonts as needed...
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
export async function fetchFontshareFonts(): Promise<FontMetadata[]> {
|
|||
|
|
// In a real implementation, you might fetch this from a maintained JSON file
|
|||
|
|
// For MVP, we use the static list or fetch from Fontshare's API
|
|||
|
|
|
|||
|
|
// Option 1: Use static list
|
|||
|
|
return FONTHARE_FONTS.map(mapFontshareToMetadata);
|
|||
|
|
|
|||
|
|
// Option 2: Fetch from Fontshare API (if available)
|
|||
|
|
// const response = await fetch('https://api.fontshare.com/v2/fonts');
|
|||
|
|
// const data = await response.json();
|
|||
|
|
// return data.map(mapFontshareToMetadata);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function mapFontshareToMetadata(font: FontshareMetadata): FontMetadata {
|
|||
|
|
return {
|
|||
|
|
id: `fontshare-${font.slug}`,
|
|||
|
|
provider: 'fontshare',
|
|||
|
|
family: font.family,
|
|||
|
|
displayName: font.family,
|
|||
|
|
category: mapFontshareCategory(font.category),
|
|||
|
|
variants: font.styles.map(style => ({
|
|||
|
|
weight: style.weight,
|
|||
|
|
style: 'normal',
|
|||
|
|
name: style.name,
|
|||
|
|
url: style.file
|
|||
|
|
})),
|
|||
|
|
license: {
|
|||
|
|
type: 'open-source',
|
|||
|
|
url: 'https://www.fontshare.com/license',
|
|||
|
|
attribution: 'Fontshare',
|
|||
|
|
commercial: true,
|
|||
|
|
embedding: 'webfont'
|
|||
|
|
},
|
|||
|
|
languages: font.subsets,
|
|||
|
|
previewUrl: font.styles[0]?.file || '',
|
|||
|
|
cssUrl: font.styles[0]?.file || '',
|
|||
|
|
metadata: {
|
|||
|
|
slug: font.slug,
|
|||
|
|
styles: font.styles
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function mapFontshareCategory(category: string): FontCategory {
|
|||
|
|
const validCategories: FontCategory[] = ['sans-serif', 'serif', 'display', 'handwriting', 'monospace'];
|
|||
|
|
const normalized = category.toLowerCase().replace(' ', '-');
|
|||
|
|
return validCategories.includes(normalized as FontCategory)
|
|||
|
|
? (normalized as FontCategory)
|
|||
|
|
: 'display';
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### Data Normalization
|
|||
|
|
|
|||
|
|
Both providers return different data structures. Normalize to a unified interface:
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
// src/lib/types/fonts.ts
|
|||
|
|
export type FontCategory = 'sans-serif' | 'serif' | 'display' | 'handwriting' | 'monospace';
|
|||
|
|
export type FontProvider = 'google-fonts' | 'fontshare';
|
|||
|
|
|
|||
|
|
export interface LicenseInfo {
|
|||
|
|
type: 'open-source' | 'commercial' | 'free-for-personal';
|
|||
|
|
url: string;
|
|||
|
|
attribution: string;
|
|||
|
|
commercial: boolean;
|
|||
|
|
embedding: 'webfont' | 'self-host' | 'both';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export interface FontVariant {
|
|||
|
|
weight: number;
|
|||
|
|
style: 'normal' | 'italic';
|
|||
|
|
name: string;
|
|||
|
|
url?: string;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export interface FontMetadata {
|
|||
|
|
id: string;
|
|||
|
|
provider: FontProvider;
|
|||
|
|
family: string;
|
|||
|
|
displayName: string;
|
|||
|
|
category: FontCategory;
|
|||
|
|
variants: FontVariant[];
|
|||
|
|
license: LicenseInfo;
|
|||
|
|
languages: string[];
|
|||
|
|
previewUrl: string;
|
|||
|
|
cssUrl: string;
|
|||
|
|
popularity?: number; // For sorting
|
|||
|
|
trending?: boolean;
|
|||
|
|
metadata: Record<string, unknown>;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export interface ComparisonFont {
|
|||
|
|
font: FontMetadata;
|
|||
|
|
variant: FontVariant;
|
|||
|
|
size: number;
|
|||
|
|
lineHeight: number;
|
|||
|
|
letterSpacing: number;
|
|||
|
|
color: string;
|
|||
|
|
text?: string; // Override default preview text
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export interface ComparisonSettings {
|
|||
|
|
text: string;
|
|||
|
|
backgroundColor: string;
|
|||
|
|
showGrid: boolean;
|
|||
|
|
watermark: boolean;
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Performance Optimization
|
|||
|
|
|
|||
|
|
### Lazy Loading Strategy
|
|||
|
|
|
|||
|
|
#### Font Lazy Loading
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
// src/lib/hooks/useLazyFontLoader.ts
|
|||
|
|
import { tick } from 'svelte';
|
|||
|
|
import { loadFont } from '$lib/utils/font-loader';
|
|||
|
|
|
|||
|
|
export function useLazyFontLoader() {
|
|||
|
|
let observer: IntersectionObserver | null = null;
|
|||
|
|
let elementsToLoad = new Map<HTMLElement, { family: string; weight: number }>();
|
|||
|
|
|
|||
|
|
function observe(
|
|||
|
|
element: HTMLElement,
|
|||
|
|
family: string,
|
|||
|
|
weight: number = 400
|
|||
|
|
) {
|
|||
|
|
if (!observer) {
|
|||
|
|
observer = new IntersectionObserver(
|
|||
|
|
(entries) => {
|
|||
|
|
entries.forEach(async (entry) => {
|
|||
|
|
if (entry.isIntersecting) {
|
|||
|
|
const data = elementsToLoad.get(entry.target as HTMLElement);
|
|||
|
|
if (data) {
|
|||
|
|
await loadFont(data.family, data.weight);
|
|||
|
|
observer?.unobserve(entry.target);
|
|||
|
|
elementsToLoad.delete(entry.target as HTMLElement);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
rootMargin: '200px', // Load before element enters viewport
|
|||
|
|
threshold: 0.01
|
|||
|
|
}
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
elementsToLoad.set(element, { family, weight });
|
|||
|
|
observer.observe(element);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function destroy() {
|
|||
|
|
observer?.disconnect();
|
|||
|
|
observer = null;
|
|||
|
|
elementsToLoad.clear();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return { observe, destroy };
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### Component-Level Usage
|
|||
|
|
|
|||
|
|
```svelte
|
|||
|
|
<!-- src/lib/components/FontPreview.svelte -->
|
|||
|
|
<script lang="ts">
|
|||
|
|
import { onMount, onDestroy } from 'svelte';
|
|||
|
|
import { useLazyFontLoader } from '$lib/hooks/useLazyFontLoader';
|
|||
|
|
|
|||
|
|
export let family: string;
|
|||
|
|
export let weight: number = 400;
|
|||
|
|
export let text: string = 'The quick brown fox jumps over the lazy dog';
|
|||
|
|
|
|||
|
|
let element: HTMLElement;
|
|||
|
|
const { observe, destroy } = useLazyFontLoader();
|
|||
|
|
|
|||
|
|
onMount(() => {
|
|||
|
|
observe(element, family, weight);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
onDestroy(() => {
|
|||
|
|
destroy();
|
|||
|
|
});
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<div
|
|||
|
|
bind:this={element}
|
|||
|
|
class="font-preview"
|
|||
|
|
style:font-family="{family}"
|
|||
|
|
style:font-weight="{weight}"
|
|||
|
|
>
|
|||
|
|
{text}
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<style>
|
|||
|
|
.font-preview {
|
|||
|
|
/* Ensure element has height for intersection observer */
|
|||
|
|
min-height: 48px;
|
|||
|
|
}
|
|||
|
|
</style>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Caching Strategy
|
|||
|
|
|
|||
|
|
#### localStorage Caching with TTL
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
// src/lib/utils/cache.ts
|
|||
|
|
const CACHE_KEY = 'glyphdiff:font-cache';
|
|||
|
|
const CACHE_VERSION = 1;
|
|||
|
|
const CACHE_TTL = 24 * 60 * 60 * 1000; // 24 hours
|
|||
|
|
|
|||
|
|
interface CacheEntry {
|
|||
|
|
version: number;
|
|||
|
|
timestamp: number;
|
|||
|
|
data: unknown;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export function setCache<T>(key: string, data: T, ttl: number = CACHE_TTL): void {
|
|||
|
|
const entry: CacheEntry = {
|
|||
|
|
version: CACHE_VERSION,
|
|||
|
|
timestamp: Date.now(),
|
|||
|
|
data
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const cache = getCacheData();
|
|||
|
|
cache[key] = entry;
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
localStorage.setItem(CACHE_KEY, JSON.stringify(cache));
|
|||
|
|
} catch (error) {
|
|||
|
|
console.warn('Failed to set cache:', error);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export function getCache<T>(key: string): T | null {
|
|||
|
|
const cache = getCacheData();
|
|||
|
|
const entry = cache[key];
|
|||
|
|
|
|||
|
|
if (!entry) {
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Check version
|
|||
|
|
if (entry.version !== CACHE_VERSION) {
|
|||
|
|
delete cache[key];
|
|||
|
|
saveCacheData(cache);
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Check TTL
|
|||
|
|
if (Date.now() - entry.timestamp > CACHE_TTL) {
|
|||
|
|
delete cache[key];
|
|||
|
|
saveCacheData(cache);
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return entry.data as T;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export function clearCache(key?: string): void {
|
|||
|
|
if (key) {
|
|||
|
|
const cache = getCacheData();
|
|||
|
|
delete cache[key];
|
|||
|
|
saveCacheData(cache);
|
|||
|
|
} else {
|
|||
|
|
localStorage.removeItem(CACHE_KEY);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function getCacheData(): Record<string, CacheEntry> {
|
|||
|
|
try {
|
|||
|
|
const cached = localStorage.getItem(CACHE_KEY);
|
|||
|
|
return cached ? JSON.parse(cached) : {};
|
|||
|
|
} catch {
|
|||
|
|
return {};
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function saveCacheData(data: Record<string, CacheEntry>): void {
|
|||
|
|
try {
|
|||
|
|
localStorage.setItem(CACHE_KEY, JSON.stringify(data));
|
|||
|
|
} catch (error) {
|
|||
|
|
console.warn('Failed to save cache:', error);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### Service Worker Caching (Optional for Enhanced PWA)
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
// src/lib/service-worker.ts
|
|||
|
|
const CACHE_NAME = 'glyphdiff-v1';
|
|||
|
|
const FONT_CACHE_PREFIX = 'font-';
|
|||
|
|
|
|||
|
|
self.addEventListener('install', (event) => {
|
|||
|
|
event.waitUntil(
|
|||
|
|
caches.open(CACHE_NAME).then((cache) => {
|
|||
|
|
return cache.addAll([
|
|||
|
|
'/',
|
|||
|
|
'/fonts',
|
|||
|
|
'/compare',
|
|||
|
|
// Add other static assets
|
|||
|
|
]);
|
|||
|
|
})
|
|||
|
|
);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
self.addEventListener('fetch', (event) => {
|
|||
|
|
const url = new URL(event.request.url);
|
|||
|
|
|
|||
|
|
// Cache Google Fonts
|
|||
|
|
if (url.hostname === 'fonts.googleapis.com' || url.hostname === 'fonts.gstatic.com') {
|
|||
|
|
event.respondWith(
|
|||
|
|
caches.open(`${FONT_CACHE_PREFIX}${CACHE_NAME}`).then((cache) => {
|
|||
|
|
return cache.match(event.request).then((response) => {
|
|||
|
|
if (response) {
|
|||
|
|
return response;
|
|||
|
|
}
|
|||
|
|
return fetch(event.request).then((networkResponse) => {
|
|||
|
|
cache.put(event.request, networkResponse.clone());
|
|||
|
|
return networkResponse;
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
})
|
|||
|
|
);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Network-first for API calls
|
|||
|
|
if (url.pathname.includes('/api/')) {
|
|||
|
|
event.respondWith(
|
|||
|
|
fetch(event.request)
|
|||
|
|
.then((response) => {
|
|||
|
|
const responseClone = response.clone();
|
|||
|
|
caches.open(CACHE_NAME).then((cache) => {
|
|||
|
|
cache.put(event.request, responseClone);
|
|||
|
|
});
|
|||
|
|
return response;
|
|||
|
|
})
|
|||
|
|
.catch(() => caches.match(event.request))
|
|||
|
|
);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Cache-first for static assets
|
|||
|
|
event.respondWith(
|
|||
|
|
caches.match(event.request).then((response) => {
|
|||
|
|
return response || fetch(event.request);
|
|||
|
|
})
|
|||
|
|
);
|
|||
|
|
});
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Performance Metrics
|
|||
|
|
|
|||
|
|
#### Target Core Web Vitals
|
|||
|
|
|
|||
|
|
| Metric | Target | Why It Matters |
|
|||
|
|
|--------|--------|----------------|
|
|||
|
|
| LCP (Largest Contentful Paint) | < 2.5s | Main content loads quickly |
|
|||
|
|
| FID (First Input Delay) | < 100ms | Responsive to user input |
|
|||
|
|
| CLS (Cumulative Layout Shift) | < 0.1 | Visual stability |
|
|||
|
|
| TTFB (Time to First Byte) | < 600ms | Fast initial response |
|
|||
|
|
|
|||
|
|
#### Performance Monitoring
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
// src/lib/utils/performance.ts
|
|||
|
|
export function reportWebVitals(): void {
|
|||
|
|
if (typeof window === 'undefined' || !('PerformanceObserver' in window)) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// LCP
|
|||
|
|
const lcpObserver = new PerformanceObserver((list) => {
|
|||
|
|
const entries = list.getEntries();
|
|||
|
|
const lastEntry = entries[entries.length - 1] as any;
|
|||
|
|
console.log('LCP:', lastEntry.startTime);
|
|||
|
|
// Send to analytics
|
|||
|
|
});
|
|||
|
|
lcpObserver.observe({ entryTypes: ['largest-contentful-paint'] });
|
|||
|
|
|
|||
|
|
// FID
|
|||
|
|
const fidObserver = new PerformanceObserver((list) => {
|
|||
|
|
const entries = list.getEntries();
|
|||
|
|
const fid = entries[0] as any;
|
|||
|
|
console.log('FID:', fid.processingStart - fid.startTime);
|
|||
|
|
// Send to analytics
|
|||
|
|
});
|
|||
|
|
fidObserver.observe({ entryTypes: ['first-input'] });
|
|||
|
|
|
|||
|
|
// CLS
|
|||
|
|
const clsObserver = new PerformanceObserver((list) => {
|
|||
|
|
let clsValue = 0;
|
|||
|
|
for (const entry of list.getEntries() as any[]) {
|
|||
|
|
if (!entry.hadRecentInput) {
|
|||
|
|
clsValue += entry.value;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
console.log('CLS:', clsValue);
|
|||
|
|
// Send to analytics
|
|||
|
|
});
|
|||
|
|
clsObserver.observe({ entryTypes: ['layout-shift'] });
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Frontend Architecture
|
|||
|
|
|
|||
|
|
### Project Structure
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
glyphdiff/
|
|||
|
|
├── src/
|
|||
|
|
│ ├── lib/
|
|||
|
|
│ │ ├── components/
|
|||
|
|
│ │ │ ├── FontCard.svelte
|
|||
|
|
│ │ │ ├── FontPreview.svelte
|
|||
|
|
│ │ │ ├── ComparisonGrid.svelte
|
|||
|
|
│ │ │ ├── FilterBar.svelte
|
|||
|
|
│ │ │ └── DarkModeToggle.svelte
|
|||
|
|
│ │ ├── stores/
|
|||
|
|
│ │ │ ├── fontStore.ts
|
|||
|
|
│ │ │ ├── comparisonStore.ts
|
|||
|
|
│ │ │ ├── filterStore.ts
|
|||
|
|
│ │ │ └── uiStore.ts
|
|||
|
|
│ │ ├── services/
|
|||
|
|
│ │ │ ├── google-fonts.ts
|
|||
|
|
│ │ │ ├── fontshare.ts
|
|||
|
|
│ │ │ └── font-loader.ts
|
|||
|
|
│ │ ├── hooks/
|
|||
|
|
│ │ │ ├── useLazyFontLoader.ts
|
|||
|
|
│ │ │ ├── useKeyboardShortcuts.ts
|
|||
|
|
│ │ │ └── useLocalStorage.ts
|
|||
|
|
│ │ ├── utils/
|
|||
|
|
│ │ │ ├── cache.ts
|
|||
|
|
│ │ │ ├── format.ts
|
|||
|
|
│ │ │ └── url.ts
|
|||
|
|
│ │ ├── types/
|
|||
|
|
│ │ │ ├── fonts.ts
|
|||
|
|
│ │ │ ├── comparison.ts
|
|||
|
|
│ │ │ └── index.ts
|
|||
|
|
│ │ └── constants.ts
|
|||
|
|
│ ├── routes/
|
|||
|
|
│ │ ├── +layout.svelte
|
|||
|
|
│ │ ├── +layout.ts
|
|||
|
|
│ │ ├── +page.svelte # Home page
|
|||
|
|
│ │ ├── fonts/
|
|||
|
|
│ │ │ ├── +page.svelte # Font catalog
|
|||
|
|
│ │ │ ├── +page.ts # Font data loading
|
|||
|
|
│ │ │ └── [slug]/
|
|||
|
|
│ │ │ └── +page.svelte # Font detail page
|
|||
|
|
│ │ └── compare/
|
|||
|
|
│ │ ├── +page.svelte # Comparison page
|
|||
|
|
│ │ └── +page.ts # URL state parsing
|
|||
|
|
│ ├── app.css
|
|||
|
|
│ └── app.d.ts
|
|||
|
|
├── static/
|
|||
|
|
│ ├── favicon.ico
|
|||
|
|
│ └── og-image.png
|
|||
|
|
├── tests/
|
|||
|
|
│ ├── unit/
|
|||
|
|
│ └── e2e/
|
|||
|
|
├── package.json
|
|||
|
|
├── svelte.config.js
|
|||
|
|
├── tailwind.config.js
|
|||
|
|
├── tsconfig.json
|
|||
|
|
└── vite.config.ts
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Component Architecture
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
┌────────────────────────────────────────────────────────────┐
|
|||
|
|
│ +layout.svelte │
|
|||
|
|
│ ┌──────────────────────────────────────────────────────┐ │
|
|||
|
|
│ │ Header │ │
|
|||
|
|
│ │ Logo | Search | DarkModeToggle │ │
|
|||
|
|
│ └──────────────────────────────────────────────────────┘ │
|
|||
|
|
│ ┌──────────────────────────────────────────────────────┐ │
|
|||
|
|
│ │ Main │ │
|
|||
|
|
│ │ ┌────────────────────────────────────────────────┐ │ │
|
|||
|
|
│ │ │ +page.svelte (Home) │ │ │
|
|||
|
|
│ │ │ Hero section → Featured fonts → CTA │ │ │
|
|||
|
|
│ │ └────────────────────────────────────────────────┘ │ │
|
|||
|
|
│ │ ┌────────────────────────────────────────────────┐ │ │
|
|||
|
|
│ │ │ fonts/+page.svelte │ │ │
|
|||
|
|
│ │ │ FilterBar → FontGrid → FontCard │ │ │
|
|||
|
|
│ │ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │ │
|
|||
|
|
│ │ │ │FontCard │ │FontCard │ │FontCard │ ... │ │ │
|
|||
|
|
│ │ │ └─────────┘ └─────────┘ └─────────┘ │ │ │
|
|||
|
|
│ │ └────────────────────────────────────────────────┘ │ │
|
|||
|
|
│ │ ┌────────────────────────────────────────────────┐ │ │
|
|||
|
|
│ │ │ compare/+page.svelte │ │ │
|
|||
|
|
│ │ │ ComparisonGrid → FontPreview × 4 │ │ │
|
|||
|
|
│ │ │ Toolbar (text, size, color, settings) │ │ │
|
|||
|
|
│ │ └────────────────────────────────────────────────┘ │ │
|
|||
|
|
│ └──────────────────────────────────────────────────────┘ │
|
|||
|
|
│ ┌──────────────────────────────────────────────────────┐ │
|
|||
|
|
│ │ Footer │ │
|
|||
|
|
│ └──────────────────────────────────────────────────────┘ │
|
|||
|
|
└────────────────────────────────────────────────────────────┘
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Routing Strategy
|
|||
|
|
|
|||
|
|
#### SvelteKit File-Based Routing
|
|||
|
|
|
|||
|
|
| Route | File | Purpose | SSR/CSR |
|
|||
|
|
|-------|------|---------|---------|
|
|||
|
|
| `/` | `routes/+page.svelte` | Home/Landing page | SSR |
|
|||
|
|
| `/fonts` | `routes/fonts/+page.svelte` | Font catalog | SSG |
|
|||
|
|
| `/fonts/[slug]` | `routes/fonts/[slug]/+page.svelte` | Font details | SSG |
|
|||
|
|
| `/compare` | `routes/compare/+page.svelte` | Comparison tool | CSR |
|
|||
|
|
| `/api/...` | `routes/api/+server.ts` | Future API routes | Serverless |
|
|||
|
|
|
|||
|
|
#### URL State for Comparisons
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
// src/routes/compare/+page.ts
|
|||
|
|
import type { PageLoad } from './$types';
|
|||
|
|
|
|||
|
|
export const load: PageLoad = ({ url }) => {
|
|||
|
|
const searchParams = url.searchParams;
|
|||
|
|
|
|||
|
|
// Parse comparison state from URL
|
|||
|
|
const fontsParam = searchParams.get('fonts');
|
|||
|
|
const text = searchParams.get('text') || 'The quick brown fox...';
|
|||
|
|
const size = parseInt(searchParams.get('size') || '24');
|
|||
|
|
const darkMode = searchParams.get('dark') === 'true';
|
|||
|
|
|
|||
|
|
let comparisonFonts: Array<{ family: string; weight: number }> = [];
|
|||
|
|
|
|||
|
|
if (fontsParam) {
|
|||
|
|
try {
|
|||
|
|
comparisonFonts = JSON.parse(decodeURIComponent(fontsParam));
|
|||
|
|
} catch (e) {
|
|||
|
|
console.error('Failed to parse fonts param:', e);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
initialFonts: comparisonFonts,
|
|||
|
|
settings: { text, size, darkMode }
|
|||
|
|
};
|
|||
|
|
};
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
```svelte
|
|||
|
|
<!-- src/routes/compare/+page.svelte -->
|
|||
|
|
<script lang="ts">
|
|||
|
|
import { page } from '$app/stores';
|
|||
|
|
import { goto } from '$app/navigation';
|
|||
|
|
import { comparisonStore } from '$lib/stores/comparisonStore';
|
|||
|
|
import type { PageData } from './$types';
|
|||
|
|
|
|||
|
|
export let data: PageData;
|
|||
|
|
|
|||
|
|
// Initialize store from URL
|
|||
|
|
$: comparisonStore.setSettings(data.settings);
|
|||
|
|
$: comparisonStore.setFonts(data.initialFonts);
|
|||
|
|
|
|||
|
|
// Sync URL changes
|
|||
|
|
$: {
|
|||
|
|
if (comparisonStore.shareable) {
|
|||
|
|
const url = new URL(window.location.href);
|
|||
|
|
url.searchParams.set(
|
|||
|
|
'fonts',
|
|||
|
|
JSON.stringify(comparisonStore.fonts.map(f => ({ family: f.font.family, weight: f.variant.weight })))
|
|||
|
|
);
|
|||
|
|
url.searchParams.set('text', comparisonStore.settings.text);
|
|||
|
|
url.searchParams.set('size', comparisonStore.settings.size.toString());
|
|||
|
|
|
|||
|
|
// Only update URL without full navigation
|
|||
|
|
window.history.replaceState({}, '', url.toString());
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
</script>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Data Models
|
|||
|
|
|
|||
|
|
### Core Type Definitions
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
// src/lib/types/fonts.ts
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Font provider sources
|
|||
|
|
*/
|
|||
|
|
export type FontProvider = 'google-fonts' | 'fontshare';
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Standard font categories
|
|||
|
|
*/
|
|||
|
|
export type FontCategory =
|
|||
|
|
| 'sans-serif'
|
|||
|
|
| 'serif'
|
|||
|
|
| 'display'
|
|||
|
|
| 'handwriting'
|
|||
|
|
| 'monospace';
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* License information for fonts
|
|||
|
|
*/
|
|||
|
|
export interface LicenseInfo {
|
|||
|
|
/** License type classification */
|
|||
|
|
type: 'open-source' | 'commercial' | 'free-for-personal';
|
|||
|
|
|
|||
|
|
/** URL to full license text */
|
|||
|
|
url: string;
|
|||
|
|
|
|||
|
|
/** Attribution required for display */
|
|||
|
|
attribution: string;
|
|||
|
|
|
|||
|
|
/** Commercial use permitted */
|
|||
|
|
commercial: boolean;
|
|||
|
|
|
|||
|
|
/** Allowed embedding methods */
|
|||
|
|
embedding: 'webfont' | 'self-host' | 'both';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Individual font variant (weight/style combination)
|
|||
|
|
*/
|
|||
|
|
export interface FontVariant {
|
|||
|
|
/** Font weight (100-900) */
|
|||
|
|
weight: number;
|
|||
|
|
|
|||
|
|
/** Font style */
|
|||
|
|
style: 'normal' | 'italic';
|
|||
|
|
|
|||
|
|
/** Display name for the variant */
|
|||
|
|
name: string;
|
|||
|
|
|
|||
|
|
/** URL to the font file (CSS or WOFF2) */
|
|||
|
|
url?: string;
|
|||
|
|
|
|||
|
|
/** Subset information (optional) */
|
|||
|
|
subset?: string;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Complete metadata for a font family
|
|||
|
|
*/
|
|||
|
|
export interface FontMetadata {
|
|||
|
|
/** Unique identifier for the font */
|
|||
|
|
id: string;
|
|||
|
|
|
|||
|
|
/** Source provider */
|
|||
|
|
provider: FontProvider;
|
|||
|
|
|
|||
|
|
/** Font family name (CSS property value) */
|
|||
|
|
family: string;
|
|||
|
|
|
|||
|
|
/** Display name for UI */
|
|||
|
|
displayName: string;
|
|||
|
|
|
|||
|
|
/** Font category */
|
|||
|
|
category: FontCategory;
|
|||
|
|
|
|||
|
|
/** Available variants */
|
|||
|
|
variants: FontVariant[];
|
|||
|
|
|
|||
|
|
/** License information */
|
|||
|
|
license: LicenseInfo;
|
|||
|
|
|
|||
|
|
/** Supported language subsets */
|
|||
|
|
languages: string[];
|
|||
|
|
|
|||
|
|
/** URL for font preview (CSS) */
|
|||
|
|
previewUrl: string;
|
|||
|
|
|
|||
|
|
/** URL for CSS import */
|
|||
|
|
cssUrl: string;
|
|||
|
|
|
|||
|
|
/** Popularity score (for sorting) */
|
|||
|
|
popularity?: number;
|
|||
|
|
|
|||
|
|
/** Trending status */
|
|||
|
|
trending?: boolean;
|
|||
|
|
|
|||
|
|
/** Additional provider-specific metadata */
|
|||
|
|
metadata: Record<string, unknown>;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Font with selected variant for display
|
|||
|
|
*/
|
|||
|
|
export interface ActiveFont {
|
|||
|
|
/** Font metadata */
|
|||
|
|
font: FontMetadata;
|
|||
|
|
|
|||
|
|
/** Selected variant */
|
|||
|
|
variant: FontVariant;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Filter options for font catalog
|
|||
|
|
*/
|
|||
|
|
export interface FontFilters {
|
|||
|
|
/** Category filter */
|
|||
|
|
categories: FontCategory[];
|
|||
|
|
|
|||
|
|
/** Weight range filter */
|
|||
|
|
weightRange: [number, number];
|
|||
|
|
|
|||
|
|
/** Provider filter */
|
|||
|
|
providers: FontProvider[];
|
|||
|
|
|
|||
|
|
/** Language subset filter */
|
|||
|
|
languages: string[];
|
|||
|
|
|
|||
|
|
/** Search query */
|
|||
|
|
search: string;
|
|||
|
|
|
|||
|
|
/** Sort option */
|
|||
|
|
sortBy: 'popularity' | 'name' | 'recent' | 'trending';
|
|||
|
|
|
|||
|
|
/** Sort direction */
|
|||
|
|
sortDirection: 'asc' | 'desc';
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
// src/lib/types/comparison.ts
|
|||
|
|
|
|||
|
|
import type { FontMetadata, FontVariant } from './fonts';
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Display settings for comparison preview
|
|||
|
|
*/
|
|||
|
|
export interface FontDisplaySettings {
|
|||
|
|
/** Font size in pixels */
|
|||
|
|
size: number;
|
|||
|
|
|
|||
|
|
/** Line height (unitless multiplier) */
|
|||
|
|
lineHeight: number;
|
|||
|
|
|
|||
|
|
/** Letter spacing in pixels */
|
|||
|
|
letterSpacing: number;
|
|||
|
|
|
|||
|
|
/** Text color */
|
|||
|
|
color: string;
|
|||
|
|
|
|||
|
|
/** Custom preview text override */
|
|||
|
|
text?: string;
|
|||
|
|
|
|||
|
|
/** Text alignment */
|
|||
|
|
textAlign: 'left' | 'center' | 'right';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Global comparison settings
|
|||
|
|
*/
|
|||
|
|
export interface ComparisonSettings {
|
|||
|
|
/** Default preview text */
|
|||
|
|
text: string;
|
|||
|
|
|
|||
|
|
/** Background color */
|
|||
|
|
backgroundColor: string;
|
|||
|
|
|
|||
|
|
/** Show comparison grid lines */
|
|||
|
|
showGrid: boolean;
|
|||
|
|
|
|||
|
|
/** Show watermarks/attribution */
|
|||
|
|
watermark: boolean;
|
|||
|
|
|
|||
|
|
/** Default display settings */
|
|||
|
|
display: FontDisplaySettings;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Font in a comparison session
|
|||
|
|
*/
|
|||
|
|
export interface ComparisonFont {
|
|||
|
|
/** Font metadata */
|
|||
|
|
font: FontMetadata;
|
|||
|
|
|
|||
|
|
/** Selected variant */
|
|||
|
|
variant: FontVariant;
|
|||
|
|
|
|||
|
|
/** Display settings (overrides global) */
|
|||
|
|
settings: Partial<FontDisplaySettings>;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Complete comparison session
|
|||
|
|
*/
|
|||
|
|
export interface ComparisonSession {
|
|||
|
|
/** Unique session identifier */
|
|||
|
|
id: string;
|
|||
|
|
|
|||
|
|
/** Fonts being compared (max 4) */
|
|||
|
|
fonts: ComparisonFont[];
|
|||
|
|
|
|||
|
|
/** Global comparison settings */
|
|||
|
|
settings: ComparisonSettings;
|
|||
|
|
|
|||
|
|
/** Session metadata */
|
|||
|
|
metadata: {
|
|||
|
|
/** Creation timestamp */
|
|||
|
|
createdAt: number;
|
|||
|
|
|
|||
|
|
/** Last modified timestamp */
|
|||
|
|
updatedAt: number;
|
|||
|
|
|
|||
|
|
/** Session name */
|
|||
|
|
name?: string;
|
|||
|
|
|
|||
|
|
/** Whether session is saved */
|
|||
|
|
saved: boolean;
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Shareable comparison data (for URL encoding)
|
|||
|
|
*/
|
|||
|
|
export interface ShareableComparison {
|
|||
|
|
/** Font identifiers and weights */
|
|||
|
|
fonts: Array<{ family: string; weight: number }>;
|
|||
|
|
|
|||
|
|
/** Preview text */
|
|||
|
|
text?: string;
|
|||
|
|
|
|||
|
|
/** Font size */
|
|||
|
|
size?: number;
|
|||
|
|
|
|||
|
|
/** Dark mode flag */
|
|||
|
|
dark?: boolean;
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
// src/lib/types/ui.ts
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* UI theme options
|
|||
|
|
*/
|
|||
|
|
export type Theme = 'light' | 'dark' | 'system';
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* UI settings persisted in localStorage
|
|||
|
|
*/
|
|||
|
|
export interface UISettings {
|
|||
|
|
/** Selected theme */
|
|||
|
|
theme: Theme;
|
|||
|
|
|
|||
|
|
/** Grid view density */
|
|||
|
|
gridDensity: 'compact' | 'comfortable' | 'spacious';
|
|||
|
|
|
|||
|
|
/** Show font variants in catalog */
|
|||
|
|
showVariants: boolean;
|
|||
|
|
|
|||
|
|
/** Enable animations */
|
|||
|
|
animationsEnabled: boolean;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Toast notification types
|
|||
|
|
*/
|
|||
|
|
export type ToastType = 'success' | 'error' | 'info' | 'warning';
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Toast notification
|
|||
|
|
*/
|
|||
|
|
export interface Toast {
|
|||
|
|
id: string;
|
|||
|
|
type: ToastType;
|
|||
|
|
message: string;
|
|||
|
|
duration?: number;
|
|||
|
|
action?: {
|
|||
|
|
label: string;
|
|||
|
|
handler: () => void;
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Keyboard shortcut definition
|
|||
|
|
*/
|
|||
|
|
export interface KeyboardShortcut {
|
|||
|
|
key: string;
|
|||
|
|
ctrl?: boolean;
|
|||
|
|
shift?: boolean;
|
|||
|
|
alt?: boolean;
|
|||
|
|
description: string;
|
|||
|
|
action: () => void;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Modal/Dialog state
|
|||
|
|
*/
|
|||
|
|
export interface ModalState {
|
|||
|
|
id: string | null;
|
|||
|
|
open: boolean;
|
|||
|
|
props?: Record<string, unknown>;
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
// src/lib/types/index.ts
|
|||
|
|
|
|||
|
|
// Re-export all types for convenient importing
|
|||
|
|
export * from './fonts';
|
|||
|
|
export * from './comparison';
|
|||
|
|
export * from './ui';
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Implementation Roadmap
|
|||
|
|
|
|||
|
|
### Week 1: Svelte Fundamentals
|
|||
|
|
|
|||
|
|
**Goal:** Build foundational understanding of Svelte 5 reactive primitives and set up development environment.
|
|||
|
|
|
|||
|
|
#### Tasks
|
|||
|
|
|
|||
|
|
- [ ] **Project Setup**
|
|||
|
|
- [ ] Create SvelteKit project with TypeScript template
|
|||
|
|
- [ ] Install and configure Tailwind CSS 4.x
|
|||
|
|
- [ ] Install and set up Bits UI component library
|
|||
|
|
- [ ] Configure Prettier and ESLint with Svelte plugin
|
|||
|
|
- [ ] Set up Git repository and .gitignore
|
|||
|
|
|
|||
|
|
- [ ] **Environment Configuration**
|
|||
|
|
- [ ] Create `.env.example` with GOOGLE_FONTS_API_KEY
|
|||
|
|
- [ ] Add `.env` to `.gitignore`
|
|||
|
|
- [ ] Configure environment variable loading in `vite.config.ts`
|
|||
|
|
|
|||
|
|
- [ ] **Core Types Definition**
|
|||
|
|
- [ ] Create `src/lib/types/fonts.ts`
|
|||
|
|
- [ ] Create `src/lib/types/comparison.ts`
|
|||
|
|
- [ ] Create `src/lib/types/ui.ts`
|
|||
|
|
- [ ] Create `src/lib/types/index.ts` for re-exports
|
|||
|
|
|
|||
|
|
- [ ] **Svelte 5 Reactive Primitives Practice**
|
|||
|
|
- [ ] Build Counter component using `$state`
|
|||
|
|
- [ ] Create DerivedCounter component using `$derived`
|
|||
|
|
- [ ] Implement EffectLogger component using `$effect`
|
|||
|
|
- [ ] Practice with reactive arrays and objects
|
|||
|
|
|
|||
|
|
- [ ] **Practice Components**
|
|||
|
|
- [ ] Build simple Button component
|
|||
|
|
- [ ] Create Toggle component
|
|||
|
|
- [ ] Implement Select dropdown component
|
|||
|
|
- [ ] Build simple FilterBar component
|
|||
|
|
|
|||
|
|
#### Learning Outcomes
|
|||
|
|
- Understand Svelte 5's new runes system (`$state`, `$derived`, `$effect`)
|
|||
|
|
- Comfortable with Svelte component structure and syntax
|
|||
|
|
- Familiar with Bits UI components
|
|||
|
|
- Tailwind CSS integration working
|
|||
|
|
|
|||
|
|
#### Week 1 Deliverables
|
|||
|
|
- Working development environment
|
|||
|
|
- 4 practice components demonstrating Svelte reactivity
|
|||
|
|
- Core TypeScript types defined
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### Week 2: SvelteKit & Routing
|
|||
|
|
|
|||
|
|
**Goal:** Build font catalog page with routing, data fetching, and understand SvelteKit's SSR capabilities.
|
|||
|
|
|
|||
|
|
#### Tasks
|
|||
|
|
|
|||
|
|
- [ ] **Google Fonts Service**
|
|||
|
|
- [ ] Create `fetchGoogleFonts()` function
|
|||
|
|
- [ ] Implement `mapGoogleFontToMetadata()` mapper
|
|||
|
|
- [ ] Add TypeScript interfaces for Google Fonts API response
|
|||
|
|
- [ ] Test with Google Fonts API key
|
|||
|
|
|
|||
|
|
- [ ] **Fontshare Service**
|
|||
|
|
- [ ] Create `fetchFontshareFonts()` function
|
|||
|
|
- [ ] Build static font list for Fontshare
|
|||
|
|
- [ ] Implement `mapFontshareToMetadata()` mapper
|
|||
|
|
- [ ] Combine both providers into single font catalog
|
|||
|
|
|
|||
|
|
- [ ] **Font Catalog Page**
|
|||
|
|
- [ ] Create `routes/fonts/+page.svelte`
|
|||
|
|
- [ ] Implement `routes/fonts/+page.ts` for data loading
|
|||
|
|
- [ ] Build FontCard component
|
|||
|
|
- [ ] Create responsive FontGrid layout
|
|||
|
|
|
|||
|
|
- [ ] **Font Detail Page**
|
|||
|
|
- [ ] Create `routes/fonts/[slug]/+page.svelte`
|
|||
|
|
- [ ] Implement dynamic route parameter extraction
|
|||
|
|
- [ ] Build font detail view with all variants
|
|||
|
|
- [ ] Add "Add to Comparison" button
|
|||
|
|
|
|||
|
|
- [ ] **Comparison Page Skeleton**
|
|||
|
|
- [ ] Create `routes/compare/+page.svelte`
|
|||
|
|
- [ ] Implement URL state parsing in `+page.ts`
|
|||
|
|
- [ ] Build basic layout for comparison grid
|
|||
|
|
|
|||
|
|
- [ ] **SSR Understanding**
|
|||
|
|
- [ ] Explore SvelteKit rendering modes (SSR, CSR, SSG)
|
|||
|
|
- [ ] Test page performance with SSR enabled
|
|||
|
|
- [ ] Understand hydration process
|
|||
|
|
- [ ] Configure static generation for font catalog
|
|||
|
|
|
|||
|
|
#### Learning Outcomes
|
|||
|
|
- Understand SvelteKit file-based routing
|
|||
|
|
- Learn data loading patterns (`+page.ts`, `+page.server.ts`)
|
|||
|
|
- Grasp SSR vs CSR concepts and trade-offs
|
|||
|
|
- Comfortable with dynamic routes and parameters
|
|||
|
|
|
|||
|
|
#### Week 2 Deliverables
|
|||
|
|
- Working font catalog page with Google Fonts data
|
|||
|
|
- Font detail pages with all metadata
|
|||
|
|
- Basic comparison page with URL state
|
|||
|
|
- Understanding of SvelteKit rendering modes
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### Week 3: State Management
|
|||
|
|
|
|||
|
|
**Goal:** Implement comprehensive state management with Svelte stores, localStorage persistence, and URL state synchronization.
|
|||
|
|
|
|||
|
|
#### Tasks
|
|||
|
|
|
|||
|
|
- [ ] **Font Store**
|
|||
|
|
- [ ] Create `src/lib/stores/fontStore.ts`
|
|||
|
|
- [ ] Implement `$state` for fonts array
|
|||
|
|
- [ ] Add derived state for filtered fonts
|
|||
|
|
- [ ] Implement localStorage persistence
|
|||
|
|
- [ ] Add loading and error states
|
|||
|
|
|
|||
|
|
- [ ] **Filter Store**
|
|||
|
|
- [ ] Create `src/lib/stores/filterStore.ts`
|
|||
|
|
- [ ] Implement `$state` for all filter options
|
|||
|
|
- [ ] Add derived state for active filter count
|
|||
|
|
- [ ] Implement reset filters function
|
|||
|
|
- [ ] Add localStorage persistence
|
|||
|
|
|
|||
|
|
- [ ] **Comparison Store**
|
|||
|
|
- [ ] Create `src/lib/stores/comparisonStore.ts`
|
|||
|
|
- [ ] Implement `$state` for comparison fonts (max 4)
|
|||
|
|
- [ ] Add functions to add/remove fonts
|
|||
|
|
- [ ] Implement comparison settings state
|
|||
|
|
- [ ] Add shareable URL generation
|
|||
|
|
- [ ] Implement localStorage persistence
|
|||
|
|
|
|||
|
|
- [ ] **UI Store**
|
|||
|
|
- [ ] Create `src/lib/stores/uiStore.ts`
|
|||
|
|
- [ ] Implement theme toggle logic
|
|||
|
|
- [ ] Add settings for grid density, animations
|
|||
|
|
- [ ] Implement system theme detection
|
|||
|
|
- [ ] Add localStorage persistence
|
|||
|
|
|
|||
|
|
- [ ] **Cache Utility**
|
|||
|
|
- [ ] Implement `cache.ts` with TTL support
|
|||
|
|
- [ ] Add caching to Google Fonts fetcher
|
|||
|
|
- [ ] Implement cache invalidation logic
|
|||
|
|
- [ ] Add cache management UI (optional)
|
|||
|
|
|
|||
|
|
- [ ] **URL State Integration**
|
|||
|
|
- [ ] Sync comparison state with URL
|
|||
|
|
- [ ] Sync filter state with URL (optional)
|
|||
|
|
- [ ] Implement deep linking to comparisons
|
|||
|
|
- [ ] Handle URL encoding/decoding
|
|||
|
|
|
|||
|
|
#### Learning Outcomes
|
|||
|
|
- Master Svelte 5 stores with `$state` runes
|
|||
|
|
- Understand localStorage persistence patterns
|
|||
|
|
- Learn URL state synchronization
|
|||
|
|
- Practice derived state and computed values
|
|||
|
|
|
|||
|
|
#### Week 3 Deliverables
|
|||
|
|
- Fully functional stores for fonts, filters, comparisons, UI
|
|||
|
|
- localStorage persistence working
|
|||
|
|
- URL state synchronization working
|
|||
|
|
- Caching layer implemented
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### Week 4: UI & Polish
|
|||
|
|
|
|||
|
|
**Goal:** Build polished UI with Bits UI components, dark mode, responsive design, and smooth animations.
|
|||
|
|
|
|||
|
|
#### Tasks
|
|||
|
|
|
|||
|
|
- [ ] **Layout Components**
|
|||
|
|
- [ ] Build Header component with logo and navigation
|
|||
|
|
- [ ] Create Footer component with links and attribution
|
|||
|
|
- [ ] Implement global layout in `+layout.svelte`
|
|||
|
|
- [ ] Add mobile navigation menu
|
|||
|
|
|
|||
|
|
- [ ] **FilterBar Component**
|
|||
|
|
- [ ] Use Bits UI Select for category filter
|
|||
|
|
- [ ] Use Bits UI Dialog for advanced filters
|
|||
|
|
- [ ] Add search input with debouncing
|
|||
|
|
- [ ] Implement active filter indicators
|
|||
|
|
|
|||
|
|
- [ ] **ComparisonGrid Component**
|
|||
|
|
- [ ] Use Bits UI Grid for responsive layout
|
|||
|
|
- [ ] Implement drag-and-drop reordering (optional)
|
|||
|
|
- [ ] Add font settings panel per column
|
|||
|
|
- [ ] Implement "Remove font" button
|
|||
|
|
|
|||
|
|
- [ ] **FontPreview Component**
|
|||
|
|
- [ ] Build preview text area
|
|||
|
|
- [ ] Add size, weight, line-height controls
|
|||
|
|
- [ ] Implement color picker
|
|||
|
|
- [ ] Add lazy loading for fonts
|
|||
|
|
|
|||
|
|
- [ ] **Dark Mode**
|
|||
|
|
- [ ] Implement dark mode toggle
|
|||
|
|
- [ ] Configure Tailwind dark mode classes
|
|||
|
|
- [ ] Add system preference detection
|
|||
|
|
- [ ] Test color contrast in both themes
|
|||
|
|
|
|||
|
|
- [ ] **Responsive Design**
|
|||
|
|
- [ ] Test on mobile (375px)
|
|||
|
|
- [ ] Test on tablet (768px)
|
|||
|
|
- [ ] Test on desktop (1280px+)
|
|||
|
|
- [ ] Adjust grid columns for breakpoints
|
|||
|
|
|
|||
|
|
- [ ] **Animations & Transitions**
|
|||
|
|
- [ ] Add page transitions using Svelte transitions
|
|||
|
|
- [ ] Implement loading skeletons
|
|||
|
|
- [ ] Add hover effects on FontCard
|
|||
|
|
- [ ] Create toast notification system
|
|||
|
|
|
|||
|
|
- [ ] **Accessibility**
|
|||
|
|
- [ ] Add ARIA labels to interactive elements
|
|||
|
|
- [ ] Ensure keyboard navigation works
|
|||
|
|
- [ ] Test with screen reader
|
|||
|
|
- [ ] Verify color contrast ratios
|
|||
|
|
|
|||
|
|
- [ ] **Performance Optimization**
|
|||
|
|
- [ ] Implement image/font lazy loading
|
|||
|
|
- [ ] Add code splitting where appropriate
|
|||
|
|
- [ ] Test Lighthouse scores
|
|||
|
|
- [ ] Optimize bundle size
|
|||
|
|
|
|||
|
|
#### Learning Outcomes
|
|||
|
|
- Proficient with Bits UI component library
|
|||
|
|
- Comfortable building responsive layouts
|
|||
|
|
- Understand accessibility best practices
|
|||
|
|
- Experience with performance optimization
|
|||
|
|
|
|||
|
|
#### Week 4 Deliverables
|
|||
|
|
- Polished, production-ready UI
|
|||
|
|
- Dark mode fully implemented
|
|||
|
|
- Responsive design across all breakpoints
|
|||
|
|
- Accessibility improvements completed
|
|||
|
|
- Performance targets met
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## TanStack Query Research
|
|||
|
|
|
|||
|
|
### What is TanStack Query?
|
|||
|
|
|
|||
|
|
TanStack Query (formerly React Query) is a powerful data synchronization library for fetching, caching, and managing asynchronous state. It's widely used in the React ecosystem.
|
|||
|
|
|
|||
|
|
### Key Features
|
|||
|
|
|
|||
|
|
- **Automatic caching and refetching**
|
|||
|
|
- **Loading and error state management**
|
|||
|
|
- **Optimistic updates**
|
|||
|
|
- **Pagination support**
|
|||
|
|
- **Request deduplication**
|
|||
|
|
|
|||
|
|
### TanStack Query for Svelte?
|
|||
|
|
|
|||
|
|
TanStack Query has a Svelte implementation (`@tanstack/svelte-query`) that provides similar functionality to the React version.
|
|||
|
|
|
|||
|
|
### Why TanStack Query is NOT Recommended for MVP
|
|||
|
|
|
|||
|
|
| Factor | TanStack Query | SvelteKit Load | Recommendation |
|
|||
|
|
|--------|----------------|----------------|----------------|
|
|||
|
|
| **Data Source** | Client-side API calls | Server-side data loading | SvelteKit Load is sufficient |
|
|||
|
|
| **Complexity** | Additional dependency | Built-in to framework | SvelteKit reduces complexity |
|
|||
|
|
| **SSR Support** | Requires hydration | Native SSR | SvelteKit is cleaner |
|
|||
|
|
| **Learning Curve** | Additional concepts to learn | Learn SvelteKit once | Focus on SvelteKit |
|
|||
|
|
| **Bundle Size** | ~13KB gzipped | 0KB (built-in) | Smaller bundle |
|
|||
|
|
| **Type Safety** | Good | Excellent | SvelteKit with TS is robust |
|
|||
|
|
| **Caching Strategy** | In-memory cache | Static generation + CDN | SSG is more performant |
|
|||
|
|
|
|||
|
|
### Use Cases Where TanStack Query Would Be Useful
|
|||
|
|
|
|||
|
|
Consider adding TanStack Query in the future if:
|
|||
|
|
- Implementing user authentication and profile data
|
|||
|
|
- Building real-time features (WebSocket, polling)
|
|||
|
|
- Complex pagination with infinite scroll
|
|||
|
|
- Multiple API endpoints requiring complex cache invalidation
|
|||
|
|
- Offline-first functionality
|
|||
|
|
|
|||
|
|
### Recommended Data Fetching Approach for MVP
|
|||
|
|
|
|||
|
|
#### Use SvelteKit's Data Loading
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
// ✅ Recommended: SvelteKit load function
|
|||
|
|
// src/routes/fonts/+page.ts
|
|||
|
|
import { fetchGoogleFonts, fetchFontshareFonts } from '$lib/services';
|
|||
|
|
import { setCache, getCache } from '$lib/utils/cache';
|
|||
|
|
|
|||
|
|
export const load = async ({ fetch, depends }) => {
|
|||
|
|
// Check cache first
|
|||
|
|
const cached = getCache('font-catalog');
|
|||
|
|
if (cached) {
|
|||
|
|
return { fonts: cached, cached: true };
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Fetch from providers
|
|||
|
|
const [googleFonts, fontshareFonts] = await Promise.all([
|
|||
|
|
fetchGoogleFonts(import.meta.env.VITE_GOOGLE_FONTS_API_KEY),
|
|||
|
|
fetchFontshareFonts()
|
|||
|
|
]);
|
|||
|
|
|
|||
|
|
const allFonts = [...googleFonts, ...fontshareFonts];
|
|||
|
|
|
|||
|
|
// Cache for 24 hours
|
|||
|
|
setCache('font-catalog', allFonts);
|
|||
|
|
|
|||
|
|
// Invalidate cache when needed
|
|||
|
|
depends('app:fonts');
|
|||
|
|
|
|||
|
|
return { fonts: allFonts, cached: false };
|
|||
|
|
};
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### Use Svelte Stores for Client-Side State
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
// ✅ Recommended: Svelte 5 stores with $state
|
|||
|
|
// src/lib/stores/fontStore.ts
|
|||
|
|
import { setCache, getCache } from '$lib/utils/cache';
|
|||
|
|
|
|||
|
|
class FontStore {
|
|||
|
|
fonts = $state<FontMetadata[]>([]);
|
|||
|
|
loading = $state(false);
|
|||
|
|
error = $state<Error | null>(null);
|
|||
|
|
|
|||
|
|
async loadFonts(apiKey: string) {
|
|||
|
|
this.loading = true;
|
|||
|
|
this.error = null;
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
const cached = getCache('font-catalog');
|
|||
|
|
if (cached) {
|
|||
|
|
this.fonts = cached;
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const [googleFonts, fontshareFonts] = await Promise.all([
|
|||
|
|
fetchGoogleFonts(apiKey),
|
|||
|
|
fetchFontshareFonts()
|
|||
|
|
]);
|
|||
|
|
|
|||
|
|
this.fonts = [...googleFonts, ...fontshareFonts];
|
|||
|
|
setCache('font-catalog', this.fonts);
|
|||
|
|
} catch (e) {
|
|||
|
|
this.error = e as Error;
|
|||
|
|
} finally {
|
|||
|
|
this.loading = false;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export const fontStore = new FontStore();
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Conclusion: Do NOT Use TanStack Query for MVP
|
|||
|
|
|
|||
|
|
**Reasoning:**
|
|||
|
|
1. SvelteKit's data loading capabilities are sufficient for this project's needs
|
|||
|
|
2. Focus on learning Svelte 5 and SvelteKit rather than adding more dependencies
|
|||
|
|
3. Static generation provides better performance than client-side caching
|
|||
|
|
4. Simpler codebase is easier to maintain while learning
|
|||
|
|
|
|||
|
|
**Future Consideration:**
|
|||
|
|
Revisit this decision if the project grows to include:
|
|||
|
|
- User authentication
|
|||
|
|
- Real-time features
|
|||
|
|
- Complex client-side data synchronization
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Learning Guide: React → Svelte
|
|||
|
|
|
|||
|
|
### React vs Svelte 5 Comparison
|
|||
|
|
|
|||
|
|
| React Concept | Svelte 5 Equivalent | Notes |
|
|||
|
|
|--------------|---------------------|-------|
|
|||
|
|
| `useState` | `$state` | No setter function needed, direct reassignment triggers reactivity |
|
|||
|
|
| `useEffect` | `$effect` | Auto-tracks dependencies, no dependency array needed |
|
|||
|
|
| `useMemo` | `$derived` | Computed values that automatically update |
|
|||
|
|
| `useCallback` | Direct function | Functions are auto-memoized in Svelte 5 |
|
|||
|
|
| `children` prop | `<slot />` | Slot composition with named slots |
|
|||
|
|
| `props` | `export let` | Props declared with `export let propName` |
|
|||
|
|
| `context` | `setContext` / `getContext` | Similar but with cleaner API |
|
|||
|
|
| `useRef` | `bind:this` | Direct element binding instead of ref objects |
|
|||
|
|
| `useState(prev => prev + 1)` | `count += 1` | Direct mutation works with $state |
|
|||
|
|
| `useReducer` | Custom store with methods | Svelte stores are more flexible |
|
|||
|
|
| `forwardRef` | N/A | Ref forwarding handled automatically |
|
|||
|
|
| `useImperativeHandle` | N/A | Call methods directly on component instances |
|
|||
|
|
|
|||
|
|
### Code Examples
|
|||
|
|
|
|||
|
|
#### Counter Component
|
|||
|
|
|
|||
|
|
**React:**
|
|||
|
|
|
|||
|
|
```tsx
|
|||
|
|
import { useState, useEffect } from 'react';
|
|||
|
|
|
|||
|
|
function Counter({ initial = 0 }: { initial?: number }) {
|
|||
|
|
const [count, setCount] = useState(initial);
|
|||
|
|
const [doubled, setDoubled] = useState(0);
|
|||
|
|
|
|||
|
|
useEffect(() => {
|
|||
|
|
setDoubled(count * 2);
|
|||
|
|
}, [count]);
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<div>
|
|||
|
|
<h2>Count: {count}</h2>
|
|||
|
|
<h3>Doubled: {doubled}</h3>
|
|||
|
|
<button onClick={() => setCount(c => c + 1)}>Increment</button>
|
|||
|
|
<button onClick={() => setCount(c => c - 1)}>Decrement</button>
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Svelte 5:**
|
|||
|
|
|
|||
|
|
```svelte
|
|||
|
|
<script lang="ts">
|
|||
|
|
export let initial = 0;
|
|||
|
|
|
|||
|
|
let count = $state(initial);
|
|||
|
|
|
|||
|
|
// $derived auto-updates when count changes
|
|||
|
|
let doubled = $derived(count * 2);
|
|||
|
|
|
|||
|
|
function increment() {
|
|||
|
|
count += 1;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function decrement() {
|
|||
|
|
count -= 1;
|
|||
|
|
}
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<div>
|
|||
|
|
<h2>Count: {count}</h2>
|
|||
|
|
<h3>Doubled: {doubled}</h3>
|
|||
|
|
<button onclick={increment}>Increment</button>
|
|||
|
|
<button onclick={decrement}>Decrement</button>
|
|||
|
|
</div>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Key Differences:**
|
|||
|
|
- No setter function needed - direct mutation works with `$state`
|
|||
|
|
- `$derived` eliminates need for `useEffect` with dependencies
|
|||
|
|
- Functions are defined separately (no need for `useCallback`)
|
|||
|
|
- Event handlers use lowercase `onclick` instead of `onClick`
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
#### Data Fetching
|
|||
|
|
|
|||
|
|
**React with React Query:**
|
|||
|
|
|
|||
|
|
```tsx
|
|||
|
|
import { useQuery } from '@tanstack/react-query';
|
|||
|
|
|
|||
|
|
function FontList() {
|
|||
|
|
const {
|
|||
|
|
data: fonts,
|
|||
|
|
isLoading,
|
|||
|
|
error
|
|||
|
|
} = useQuery({
|
|||
|
|
queryKey: ['fonts'],
|
|||
|
|
queryFn: async () => {
|
|||
|
|
const response = await fetch('/api/fonts');
|
|||
|
|
if (!response.ok) throw new Error('Failed to fetch');
|
|||
|
|
return response.json();
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
if (isLoading) return <div>Loading...</div>;
|
|||
|
|
if (error) return <div>Error: {error.message}</div>;
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<ul>
|
|||
|
|
{fonts.map((font: any) => (
|
|||
|
|
<li key={font.id}>{font.displayName}</li>
|
|||
|
|
))}
|
|||
|
|
</ul>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**SvelteKit with load function:**
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
// +page.ts
|
|||
|
|
export const load = async ({ fetch }) => {
|
|||
|
|
const response = await fetch('https://www.googleapis.com/webfonts/v1/webfonts?key=' + import.meta.env.VITE_GOOGLE_FONTS_API_KEY);
|
|||
|
|
const fonts = await response.json();
|
|||
|
|
return { fonts };
|
|||
|
|
};
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
```svelte
|
|||
|
|
<!-- +page.svelte -->
|
|||
|
|
<script lang="ts">
|
|||
|
|
export let data;
|
|||
|
|
|
|||
|
|
// data.fonts is available here, loaded on the server
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
{#if data.fonts}
|
|||
|
|
<ul>
|
|||
|
|
{#each data.fonts as font}
|
|||
|
|
<li>{font.family}</li>
|
|||
|
|
{/each}
|
|||
|
|
</ul>
|
|||
|
|
{/if}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Key Differences:**
|
|||
|
|
- SvelteKit loads data on the server (SSR) by default
|
|||
|
|
- No loading state needed for initial page load
|
|||
|
|
- Data flows from `load` function to page component via props
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
#### State Management
|
|||
|
|
|
|||
|
|
**React with Zustand:**
|
|||
|
|
|
|||
|
|
```ts
|
|||
|
|
// store.ts
|
|||
|
|
import { create } from 'zustand';
|
|||
|
|
|
|||
|
|
interface FontStore {
|
|||
|
|
fonts: FontMetadata[];
|
|||
|
|
selectedFont: string | null;
|
|||
|
|
setFonts: (fonts: FontMetadata[]) => void;
|
|||
|
|
selectFont: (id: string) => void;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export const useFontStore = create<FontStore>((set) => ({
|
|||
|
|
fonts: [],
|
|||
|
|
selectedFont: null,
|
|||
|
|
setFonts: (fonts) => set({ fonts }),
|
|||
|
|
selectFont: (id) => set({ selectedFont: id })
|
|||
|
|
}));
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
```tsx
|
|||
|
|
// Component.tsx
|
|||
|
|
import { useFontStore } from './store';
|
|||
|
|
|
|||
|
|
function FontSelector() {
|
|||
|
|
const { fonts, selectedFont, selectFont } = useFontStore();
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<select
|
|||
|
|
value={selectedFont || ''}
|
|||
|
|
onChange={(e) => selectFont(e.target.value)}
|
|||
|
|
>
|
|||
|
|
<option value="">Select a font</option>
|
|||
|
|
{fonts.map((font) => (
|
|||
|
|
<option key={font.id} value={font.id}>
|
|||
|
|
{font.displayName}
|
|||
|
|
</option>
|
|||
|
|
))}
|
|||
|
|
</select>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Svelte with Store:**
|
|||
|
|
|
|||
|
|
```ts
|
|||
|
|
// fontStore.ts
|
|||
|
|
import type { FontMetadata } from '$lib/types';
|
|||
|
|
|
|||
|
|
class FontStore {
|
|||
|
|
fonts = $state<FontMetadata[]>([]);
|
|||
|
|
selectedFont = $state<string | null>(null);
|
|||
|
|
|
|||
|
|
setFonts(fonts: FontMetadata[]) {
|
|||
|
|
this.fonts = fonts;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
selectFont(id: string) {
|
|||
|
|
this.selectedFont = id;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export const fontStore = new FontStore();
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
```svelte
|
|||
|
|
<!-- FontSelector.svelte -->
|
|||
|
|
<script lang="ts">
|
|||
|
|
import { fontStore } from '$lib/stores/fontStore';
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<select bind:value={fontStore.selectedFont}>
|
|||
|
|
<option value="">Select a font</option>
|
|||
|
|
{#each fontStore.fonts as font}
|
|||
|
|
<option value={font.id}>
|
|||
|
|
{font.displayName}
|
|||
|
|
</option>
|
|||
|
|
{/each}
|
|||
|
|
</select>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Key Differences:**
|
|||
|
|
- Svelte stores use `$state` directly, no need for immer
|
|||
|
|
- Two-way binding with `bind:value` instead of `onChange`
|
|||
|
|
- Store access is simpler - just import and use
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
#### Conditional Rendering
|
|||
|
|
|
|||
|
|
**React:**
|
|||
|
|
|
|||
|
|
```tsx
|
|||
|
|
function ConditionalRender({ fonts, loading }: { fonts: Font[]; loading: boolean }) {
|
|||
|
|
if (loading) {
|
|||
|
|
return <Spinner />;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (fonts.length === 0) {
|
|||
|
|
return <EmptyState message="No fonts found" />;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<div>
|
|||
|
|
{fonts.map(font => (
|
|||
|
|
<FontCard key={font.id} font={font} />
|
|||
|
|
))}
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Svelte:**
|
|||
|
|
|
|||
|
|
```svelte
|
|||
|
|
<script lang="ts">
|
|||
|
|
export let fonts: Font[];
|
|||
|
|
export let loading: boolean;
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
{#if loading}
|
|||
|
|
<Spinner />
|
|||
|
|
{:else if fonts.length === 0}
|
|||
|
|
<EmptyState message="No fonts found" />
|
|||
|
|
{:else}
|
|||
|
|
<div>
|
|||
|
|
{#each fonts as font}
|
|||
|
|
<FontCard {font} />
|
|||
|
|
{/each}
|
|||
|
|
</div>
|
|||
|
|
{/if}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Key Differences:**
|
|||
|
|
- Svelte uses `{#if}`, `{:else if}`, `{:else}`, `{/if}` block syntax
|
|||
|
|
- No ternary operator needed inside JSX
|
|||
|
|
- `{#each}` block with `{/each}` for lists
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
#### Component Props
|
|||
|
|
|
|||
|
|
**React:**
|
|||
|
|
|
|||
|
|
```tsx
|
|||
|
|
interface FontCardProps {
|
|||
|
|
font: FontMetadata;
|
|||
|
|
onClick?: (font: FontMetadata) => void;
|
|||
|
|
size?: number;
|
|||
|
|
showLicense?: boolean;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function FontCard({ font, onClick, size = 16, showLicense = false }: FontCardProps) {
|
|||
|
|
return (
|
|||
|
|
<div className="font-card" style={{ fontSize: size }}>
|
|||
|
|
<h3>{font.displayName}</h3>
|
|||
|
|
{showLicense && <LicenseInfo license={font.license} />}
|
|||
|
|
{onClick && <button onClick={() => onClick(font)}>Select</button>}
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Svelte:**
|
|||
|
|
|
|||
|
|
```svelte
|
|||
|
|
<script lang="ts">
|
|||
|
|
import type { FontMetadata } from '$lib/types';
|
|||
|
|
|
|||
|
|
export let font: FontMetadata;
|
|||
|
|
export let onClick: (font: FontMetadata) => void | undefined;
|
|||
|
|
export let size = 16;
|
|||
|
|
export let showLicense = false;
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<div class="font-card" style:font-size="{size}px">
|
|||
|
|
<h3>{font.displayName}</h3>
|
|||
|
|
{#if showLicense}
|
|||
|
|
<LicenseInfo license={font.license} />
|
|||
|
|
{/if}
|
|||
|
|
{#if onClick}
|
|||
|
|
<button onclick={() => onClick(font)}>Select</button>
|
|||
|
|
{/if}
|
|||
|
|
</div>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Key Differences:**
|
|||
|
|
- Props declared with `export let` instead of function parameters
|
|||
|
|
- Default values assigned with `=` in declaration
|
|||
|
|
- Event handlers use lowercase (`onclick`)
|
|||
|
|
- Props passed as attributes without curly braces for simple values
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
#### Slot/Children Composition
|
|||
|
|
|
|||
|
|
**React:**
|
|||
|
|
|
|||
|
|
```tsx
|
|||
|
|
// Modal.tsx
|
|||
|
|
function Modal({ children, title, footer }: {
|
|||
|
|
children: React.ReactNode;
|
|||
|
|
title: string;
|
|||
|
|
footer?: React.ReactNode;
|
|||
|
|
}) {
|
|||
|
|
return (
|
|||
|
|
<div className="modal">
|
|||
|
|
<header>{title}</header>
|
|||
|
|
<main>{children}</main>
|
|||
|
|
{footer && <footer>{footer}</footer>}
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Usage
|
|||
|
|
<Modal title="Select Font" footer={<Button>Save</Button>}>
|
|||
|
|
<p>Choose a font from the list below.</p>
|
|||
|
|
<FontSelector />
|
|||
|
|
</Modal>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Svelte:**
|
|||
|
|
|
|||
|
|
```svelte
|
|||
|
|
<!-- Modal.svelte -->
|
|||
|
|
<script lang="ts">
|
|||
|
|
export let title: string;
|
|||
|
|
export let footer = false;
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<div class="modal">
|
|||
|
|
<header>{title}</header>
|
|||
|
|
<main>
|
|||
|
|
<slot />
|
|||
|
|
</main>
|
|||
|
|
{#if footer}
|
|||
|
|
<footer>
|
|||
|
|
<slot name="footer" />
|
|||
|
|
</footer>
|
|||
|
|
{/if}
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- Usage -->
|
|||
|
|
<Modal title="Select Font" footer>
|
|||
|
|
<svelte:fragment slot="footer">
|
|||
|
|
<Button>Save</Button>
|
|||
|
|
</svelte:fragment>
|
|||
|
|
<p>Choose a font from the list below.</p>
|
|||
|
|
<FontSelector />
|
|||
|
|
</Modal>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Key Differences:**
|
|||
|
|
- Named slots using `slot="name"` and `<slot name="name" />`
|
|||
|
|
- Multiple slots with different names
|
|||
|
|
- Default slot uses `<slot />` without name
|
|||
|
|
- `<svelte:fragment>` for content without wrapper element
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### Common Patterns
|
|||
|
|
|
|||
|
|
#### Form Handling
|
|||
|
|
|
|||
|
|
**React:**
|
|||
|
|
|
|||
|
|
```tsx
|
|||
|
|
function SearchForm({ onSearch }: { onSearch: (query: string) => void }) {
|
|||
|
|
const [query, setQuery] = useState('');
|
|||
|
|
|
|||
|
|
const handleSubmit = (e: FormEvent) => {
|
|||
|
|
e.preventDefault();
|
|||
|
|
onSearch(query);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<form onSubmit={handleSubmit}>
|
|||
|
|
<input
|
|||
|
|
value={query}
|
|||
|
|
onChange={(e) => setQuery(e.target.value)}
|
|||
|
|
placeholder="Search fonts..."
|
|||
|
|
/>
|
|||
|
|
<button type="submit">Search</button>
|
|||
|
|
</form>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Svelte:**
|
|||
|
|
|
|||
|
|
```svelte
|
|||
|
|
<script lang="ts">
|
|||
|
|
export let onSearch: (query: string) => void;
|
|||
|
|
|
|||
|
|
let query = $state('');
|
|||
|
|
|
|||
|
|
function handleSubmit(e: Event) {
|
|||
|
|
e.preventDefault();
|
|||
|
|
onSearch(query);
|
|||
|
|
}
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<form onsubmit={handleSubmit}>
|
|||
|
|
<input
|
|||
|
|
bind:value={query}
|
|||
|
|
placeholder="Search fonts..."
|
|||
|
|
/>
|
|||
|
|
<button type="submit">Search</button>
|
|||
|
|
</form>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Key Difference:** Two-way binding with `bind:value` eliminates the need for `onChange`.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
#### Lists with Keys
|
|||
|
|
|
|||
|
|
**React:**
|
|||
|
|
|
|||
|
|
```tsx
|
|||
|
|
{fonts.map(font => (
|
|||
|
|
<FontCard
|
|||
|
|
key={font.id}
|
|||
|
|
font={font}
|
|||
|
|
onClick={() => handleSelect(font)}
|
|||
|
|
/>
|
|||
|
|
))}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Svelte:**
|
|||
|
|
|
|||
|
|
```svelte
|
|||
|
|
{#each fonts as font (font.id)}
|
|||
|
|
<FontCard
|
|||
|
|
{font}
|
|||
|
|
onClick={() => handleSelect(font)}
|
|||
|
|
/>
|
|||
|
|
{/each}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Key Difference:** Key specified in parentheses after item name: `as item (key)`.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
#### Lifecycle Methods
|
|||
|
|
|
|||
|
|
**React:**
|
|||
|
|
|
|||
|
|
```tsx
|
|||
|
|
useEffect(() => {
|
|||
|
|
console.log('Mounted');
|
|||
|
|
return () => console.log('Cleanup');
|
|||
|
|
}, []);
|
|||
|
|
|
|||
|
|
useEffect(() => {
|
|||
|
|
console.log('count changed:', count);
|
|||
|
|
}, [count]);
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Svelte:**
|
|||
|
|
|
|||
|
|
```svelte
|
|||
|
|
<script>
|
|||
|
|
import { onMount, onDestroy } from 'svelte';
|
|||
|
|
|
|||
|
|
let count = $state(0);
|
|||
|
|
|
|||
|
|
$effect(() => {
|
|||
|
|
console.log('count changed:', count);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
onMount(() => {
|
|||
|
|
console.log('Mounted');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
onDestroy(() => {
|
|||
|
|
console.log('Cleanup');
|
|||
|
|
});
|
|||
|
|
</script>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Scalability & Commercialization
|
|||
|
|
|
|||
|
|
### When to Add Backend
|
|||
|
|
|
|||
|
|
#### Indicators for Backend Implementation
|
|||
|
|
|
|||
|
|
| Metric | Threshold | Action |
|
|||
|
|
|--------|-----------|--------|
|
|||
|
|
| Daily Active Users | > 1,000 | Consider backend |
|
|||
|
|
| Google Fonts API Usage | > 800/day (80% of limit) | Implement server proxy |
|
|||
|
|
| Feature Requests | > 10/month for user accounts | Plan authentication |
|
|||
|
|
| Comparison Saves | Users frequently export/save | Add database |
|
|||
|
|
| Page Load Time | > 3s on mobile | Optimize with backend caching |
|
|||
|
|
|
|||
|
|
#### Backend Architecture (When Needed)
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
┌─────────────────────────────────────────────────────────────┐
|
|||
|
|
│ Vercel Edge │
|
|||
|
|
│ ┌───────────────────────────────────────────────────────┐ │
|
|||
|
|
│ │ SvelteKit Server Functions │ │
|
|||
|
|
│ │ ┌──────────────┐ ┌──────────────┐ │ │
|
|||
|
|
│ │ │ API Routes │ │ Auth Routes │ │ │
|
|||
|
|
│ │ └──────────────┘ └──────────────┘ │ │
|
|||
|
|
│ └───────────────────────────────────────────────────────┘ │
|
|||
|
|
└─────────────────────────────────────────────────────────────┘
|
|||
|
|
│ │
|
|||
|
|
▼ ▼
|
|||
|
|
┌──────────────────┐ ┌──────────────────┐
|
|||
|
|
│ Vercel Postgres │ │ Vercel KV │
|
|||
|
|
│ (User accounts, │ │ (Caching, │
|
|||
|
|
│ saved comps) │ │ rate limiting) │
|
|||
|
|
└──────────────────┘ └──────────────────┘
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### Recommended Backend Stack
|
|||
|
|
|
|||
|
|
| Component | Technology | Why |
|
|||
|
|
|-----------|------------|-----|
|
|||
|
|
| Server Runtime | Vercel Edge Functions | Fast, global, free tier generous |
|
|||
|
|
| Database | Vercel Postgres (Neon) | Managed, edge-ready, TypeScript native |
|
|||
|
|
| Caching | Vercel KV (Redis) | Fast key-value store, built-in |
|
|||
|
|
| Authentication | Lucia Auth | Lightweight, framework-agnostic |
|
|||
|
|
| ORM | Drizzle ORM | TypeScript-first, no runtime overhead |
|
|||
|
|
| File Storage | Vercel Blob | Simple, scalable storage |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### Monetization Models
|
|||
|
|
|
|||
|
|
#### Tier 1: Freemium (Recommended Launch Model)
|
|||
|
|
|
|||
|
|
| Feature | Free | Pro ($5/mo) |
|
|||
|
|
|---------|------|-------------|
|
|||
|
|
| Font comparisons | 4 fonts max | Unlimited |
|
|||
|
|
| Save comparisons | Browser storage only | Cloud sync |
|
|||
|
|
| Export images | Watermarked | No watermark |
|
|||
|
|
| Custom text | Limited chars | Unlimited |
|
|||
|
|
| Advanced filters | Basic | Advanced |
|
|||
|
|
| API access | ❌ | ✅ |
|
|||
|
|
|
|||
|
|
#### Tier 2: Team/Agency ($29/mo)
|
|||
|
|
|
|||
|
|
- All Pro features
|
|||
|
|
- Team collaboration (shared workspaces)
|
|||
|
|
- Brand management (save brand fonts)
|
|||
|
|
- Custom domains for shared comparisons
|
|||
|
|
- Priority support
|
|||
|
|
- Monthly usage reports
|
|||
|
|
|
|||
|
|
#### Tier 3: Enterprise (Custom)
|
|||
|
|
|
|||
|
|
- Unlimited API access
|
|||
|
|
- Self-hosted deployment option
|
|||
|
|
- Custom integrations
|
|||
|
|
- SSO authentication
|
|||
|
|
- Dedicated support
|
|||
|
|
- SLA guarantees
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### Additional Features for Monetization
|
|||
|
|
|
|||
|
|
1. **Font Pairing Recommendations**
|
|||
|
|
- ML-based font matching
|
|||
|
|
- Designer-curated pairings
|
|||
|
|
- Upload custom fonts for analysis
|
|||
|
|
|
|||
|
|
2. **CSS Generator**
|
|||
|
|
- Export optimized CSS
|
|||
|
|
- Variable font support
|
|||
|
|
- Fallback font stacks
|
|||
|
|
|
|||
|
|
3. **Embeddable Widget**
|
|||
|
|
- Embed comparison on external sites
|
|||
|
|
- White-label options
|
|||
|
|
- Custom branding
|
|||
|
|
|
|||
|
|
4. **Font Inspector**
|
|||
|
|
- Detailed glyph analysis
|
|||
|
|
- Character set viewer
|
|||
|
|
- Font metrics display
|
|||
|
|
|
|||
|
|
5. **A/B Testing**
|
|||
|
|
- Test font combinations
|
|||
|
|
- User feedback collection
|
|||
|
|
- Conversion tracking
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Deployment
|
|||
|
|
|
|||
|
|
### Vercel Configuration
|
|||
|
|
|
|||
|
|
#### vercel.json
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"buildCommand": "npm run build",
|
|||
|
|
"devCommand": "npm run dev",
|
|||
|
|
"installCommand": "npm install",
|
|||
|
|
"framework": "sveltekit",
|
|||
|
|
"regions": ["iad1"],
|
|||
|
|
"headers": [
|
|||
|
|
{
|
|||
|
|
"source": "/(.*)",
|
|||
|
|
"headers": [
|
|||
|
|
{
|
|||
|
|
"key": "X-Content-Type-Options",
|
|||
|
|
"value": "nosniff"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"key": "X-Frame-Options",
|
|||
|
|
"value": "DENY"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"key": "X-XSS-Protection",
|
|||
|
|
"value": "1; mode=block"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"key": "Referrer-Policy",
|
|||
|
|
"value": "strict-origin-when-cross-origin"
|
|||
|
|
}
|
|||
|
|
]
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"source": "/static/(.*)",
|
|||
|
|
"headers": [
|
|||
|
|
{
|
|||
|
|
"key": "Cache-Control",
|
|||
|
|
"value": "public, max-age=31536000, immutable"
|
|||
|
|
}
|
|||
|
|
]
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"source": "/fonts/(.*)",
|
|||
|
|
"headers": [
|
|||
|
|
{
|
|||
|
|
"key": "Cache-Control",
|
|||
|
|
"value": "public, max-age=604800"
|
|||
|
|
}
|
|||
|
|
]
|
|||
|
|
}
|
|||
|
|
],
|
|||
|
|
"rewrites": [
|
|||
|
|
{
|
|||
|
|
"source": "/api/:path*",
|
|||
|
|
"destination": "/api/:path*"
|
|||
|
|
}
|
|||
|
|
]
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### Environment Configuration
|
|||
|
|
|
|||
|
|
#### .env.example
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
# Google Fonts API Key
|
|||
|
|
# Get from: https://developers.google.com/fonts/docs/developer_api
|
|||
|
|
GOOGLE_FONTS_API_KEY=your_api_key_here
|
|||
|
|
|
|||
|
|
# Application Configuration
|
|||
|
|
APP_URL=https://glyphdiff.com
|
|||
|
|
APP_NAME=GlyphDiff
|
|||
|
|
|
|||
|
|
# Analytics (Optional)
|
|||
|
|
VITE_GA_ID=G-XXXXXXXXXX
|
|||
|
|
VITE_PLAUSIBLE_DOMAIN= glyphdiff.com
|
|||
|
|
|
|||
|
|
# Feature Flags
|
|||
|
|
VITE_ENABLE_CACHE=true
|
|||
|
|
VITE_ENABLE_ANALYTICS=false
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### vite.config.ts
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
import { sveltekit } from '@sveltejs/kit/vite';
|
|||
|
|
import { defineConfig } from 'vite';
|
|||
|
|
|
|||
|
|
export default defineConfig({
|
|||
|
|
plugins: [sveltekit()],
|
|||
|
|
define: {
|
|||
|
|
// Expose env variables to client code
|
|||
|
|
'import.meta.env.VITE_GOOGLE_FONTS_API_KEY': JSON.stringify(process.env.GOOGLE_FONTS_API_KEY),
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### DNS Configuration
|
|||
|
|
|
|||
|
|
#### DNS Records for glyphdiff.com
|
|||
|
|
|
|||
|
|
| Type | Name | Value | TTL |
|
|||
|
|
|------|------|-------|-----|
|
|||
|
|
| A | @ | 76.76.21.21 | 3600 |
|
|||
|
|
| CNAME | www | cname.vercel-dns.com | 3600 |
|
|||
|
|
|
|||
|
|
**Note:** 76.76.21.21 is Vercel's IPv4 address for custom domains.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### Build and Deploy Commands
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
# Install dependencies
|
|||
|
|
npm install
|
|||
|
|
|
|||
|
|
# Run development server
|
|||
|
|
npm run dev
|
|||
|
|
|
|||
|
|
# Build for production
|
|||
|
|
npm run build
|
|||
|
|
|
|||
|
|
# Preview production build
|
|||
|
|
npm run preview
|
|||
|
|
|
|||
|
|
# Run tests
|
|||
|
|
npm test
|
|||
|
|
|
|||
|
|
# Run linter
|
|||
|
|
npm run lint
|
|||
|
|
|
|||
|
|
# Format code
|
|||
|
|
npm run format
|
|||
|
|
|
|||
|
|
# Type check
|
|||
|
|
npm run check
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### package.json Scripts
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"name": "glyphdiff",
|
|||
|
|
"version": "0.1.0",
|
|||
|
|
"private": true,
|
|||
|
|
"scripts": {
|
|||
|
|
"dev": "vite dev",
|
|||
|
|
"build": "vite build",
|
|||
|
|
"preview": "vite preview",
|
|||
|
|
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
|||
|
|
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
|||
|
|
"lint": "prettier --check . && eslint .",
|
|||
|
|
"format": "prettier --write .",
|
|||
|
|
"test": "playwright test",
|
|||
|
|
"test:unit": "vitest"
|
|||
|
|
},
|
|||
|
|
"devDependencies": {
|
|||
|
|
"@sveltejs/adapter-vercel": "^5.0.0",
|
|||
|
|
"@sveltejs/kit": "^2.0.0",
|
|||
|
|
"svelte": "^5.0.0",
|
|||
|
|
"typescript": "^5.3.0",
|
|||
|
|
"vite": "^5.0.0"
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### Deployment Steps
|
|||
|
|
|
|||
|
|
1. **Push to GitHub**
|
|||
|
|
```bash
|
|||
|
|
git init
|
|||
|
|
git add .
|
|||
|
|
git commit -m "Initial commit"
|
|||
|
|
git branch -M main
|
|||
|
|
git remote add origin git@github.com:your-username/glyphdiff.git
|
|||
|
|
git push -u origin main
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
2. **Connect to Vercel**
|
|||
|
|
- Go to [vercel.com](https://vercel.com)
|
|||
|
|
- Click "Add New Project"
|
|||
|
|
- Import from GitHub
|
|||
|
|
- Configure environment variables:
|
|||
|
|
- `GOOGLE_FONTS_API_KEY`
|
|||
|
|
|
|||
|
|
3. **Configure Custom Domain**
|
|||
|
|
- Go to Project Settings → Domains
|
|||
|
|
- Add `glyphdiff.com`
|
|||
|
|
- Add `www.glyphdiff.com`
|
|||
|
|
- Update DNS records as shown above
|
|||
|
|
|
|||
|
|
4. **Deploy**
|
|||
|
|
- Push changes to GitHub
|
|||
|
|
- Vercel automatically deploys on push to main
|
|||
|
|
- Preview deployments for pull requests
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Risk Assessment
|
|||
|
|
|
|||
|
|
### Technical Risks
|
|||
|
|
|
|||
|
|
| Risk | Probability | Impact | Mitigation |
|
|||
|
|
|------|-------------|--------|------------|
|
|||
|
|
| Google Fonts API rate limiting | Medium | High | Implement caching, batch requests, consider backend proxy |
|
|||
|
|
| Browser font loading issues | Low | Medium | Use `document.fonts.load()`, implement fallback fonts |
|
|||
|
|
| Poor mobile performance | Medium | High | Lazy loading, code splitting, test on real devices |
|
|||
|
|
| Accessibility violations | Low | Medium | Regular audits with axe DevTools, keyboard testing |
|
|||
|
|
| Vercel free tier limits | Low | Low | Monitor usage, easy upgrade path |
|
|||
|
|
|
|||
|
|
### Learning Curve Risks
|
|||
|
|
|
|||
|
|
| Risk | Probability | Impact | Mitigation |
|
|||
|
|
|------|-------------|--------|------------|
|
|||
|
|
| Difficulty with Svelte 5 runes | Medium | High | Start with simple components, practice with $state basics |
|
|||
|
|
| TypeScript integration issues | Low | Medium | Use strict mode from start, leverage SvelteKit templates |
|
|||
|
|
| CSS/Tailwind complexity | Low | Low | Use utility classes, avoid custom CSS where possible |
|
|||
|
|
| Bits UI learning curve | Low | Medium | Read documentation, check examples, copy patterns |
|
|||
|
|
|
|||
|
|
### External Dependency Risks
|
|||
|
|
|
|||
|
|
| Risk | Probability | Impact | Mitigation |
|
|||
|
|
|------|-------------|--------|------------|
|
|||
|
|
| Google Fonts API changes | Low | High | Use TypeScript interfaces, watch for deprecation notices |
|
|||
|
|
| Fontshare availability changes | Low | Low | Include as optional source, can be removed if needed |
|
|||
|
|
| Vercel downtime | Very Low | Medium | Static files served from CDN, minimal impact |
|
|||
|
|
| Bits UI breaking changes | Medium | Low | Lock to specific version, update carefully |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Success Metrics
|
|||
|
|
|
|||
|
|
### Learning Success Metrics
|
|||
|
|
|
|||
|
|
| Metric | Target | Measurement |
|
|||
|
|
|--------|--------|-------------|
|
|||
|
|
| Svelte 5 proficiency | Comfortable with $state, $derived, $effect | Self-assessment quiz |
|
|||
|
|
| SvelteKit understanding | Build 3+ routes with data loading | Route count |
|
|||
|
|
| Store management | Implement 4 stores with persistence | Store count |
|
|||
|
|
| Component library usage | Use 5+ Bits UI components | Component count |
|
|||
|
|
| TypeScript adoption | 100% typed codebase | Lint check |
|
|||
|
|
|
|||
|
|
### Project Success Metrics
|
|||
|
|
|
|||
|
|
| Metric | Target | Measurement |
|
|||
|
|
|--------|--------|-------------|
|
|||
|
|
| Functional font catalog | 1000+ fonts loaded | Google Fonts API response |
|
|||
|
|
| Comparison tool working | 4 fonts simultaneously | Manual testing |
|
|||
|
|
| Performance score | 90+ Lighthouse score | Lighthouse audit |
|
|||
|
|
| Mobile responsiveness | Pass on 375px - 1280px | Browser DevTools |
|
|||
|
|
| Accessibility | WCAG 2.1 AA compliant | axe DevTools |
|
|||
|
|
|
|||
|
|
### Milestones
|
|||
|
|
|
|||
|
|
- [ ] **Week 1 Complete:** Environment setup, basic components, types defined
|
|||
|
|
- [ ] **Week 2 Complete:** Font catalog with data fetching, routing working
|
|||
|
|
- [ ] **Week 3 Complete:** All stores implemented, URL state working
|
|||
|
|
- [ ] **Week 4 Complete:** Polished UI, dark mode, responsive design
|
|||
|
|
- [ ] **MVP Launch:** Application deployed to glyphdiff.com
|
|||
|
|
- [ ] **Post-Launch:** Collect feedback, iterate on features
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Appendix
|
|||
|
|
|
|||
|
|
### Commands Reference
|
|||
|
|
|
|||
|
|
#### Project Initialization
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
# Create new SvelteKit project
|
|||
|
|
npm create svelte@latest glyphdiff
|
|||
|
|
|
|||
|
|
# Select options:
|
|||
|
|
# - Which Svelte app template? Skeleton project
|
|||
|
|
# - Add type checking? Yes, using TypeScript
|
|||
|
|
# - Select additional options: ESLint, Prettier, Playwright
|
|||
|
|
|
|||
|
|
# Navigate to project
|
|||
|
|
cd glyphdiff
|
|||
|
|
|
|||
|
|
# Install dependencies
|
|||
|
|
npm install
|
|||
|
|
|
|||
|
|
# Add Tailwind CSS
|
|||
|
|
npx svelte-add@latest tailwindcss
|
|||
|
|
|
|||
|
|
# Add Bits UI
|
|||
|
|
npm install bits-ui
|
|||
|
|
|
|||
|
|
# Install additional dependencies
|
|||
|
|
npm install clsx tailwind-merge
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### Development Commands
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
# Start development server
|
|||
|
|
npm run dev
|
|||
|
|
|
|||
|
|
# Open browser to localhost:5173
|
|||
|
|
|
|||
|
|
# Type check
|
|||
|
|
npm run check
|
|||
|
|
|
|||
|
|
# Type check in watch mode
|
|||
|
|
npm run check:watch
|
|||
|
|
|
|||
|
|
# Lint code
|
|||
|
|
npm run lint
|
|||
|
|
|
|||
|
|
# Format code
|
|||
|
|
npm run format
|
|||
|
|
|
|||
|
|
# Run Playwright tests
|
|||
|
|
npm run test
|
|||
|
|
|
|||
|
|
# Run unit tests (Vitest)
|
|||
|
|
npm run test:unit
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### Build Commands
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
# Build for production
|
|||
|
|
npm run build
|
|||
|
|
|
|||
|
|
# Preview production build
|
|||
|
|
npm run preview
|
|||
|
|
|
|||
|
|
# Clean build artifacts
|
|||
|
|
rm -rf .svelte-kit build
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### Git Commands
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
# Initialize git repo
|
|||
|
|
git init
|
|||
|
|
|
|||
|
|
# Create .gitignore
|
|||
|
|
echo ".svelte-kit
|
|||
|
|
node_modules
|
|||
|
|
.env
|
|||
|
|
.DS_Store
|
|||
|
|
dist
|
|||
|
|
build
|
|||
|
|
.vercel" > .gitignore
|
|||
|
|
|
|||
|
|
# Stage all files
|
|||
|
|
git add .
|
|||
|
|
|
|||
|
|
# Commit
|
|||
|
|
git commit -m "Initial commit"
|
|||
|
|
|
|||
|
|
# Create main branch
|
|||
|
|
git branch -M main
|
|||
|
|
|
|||
|
|
# Add remote
|
|||
|
|
git remote add origin git@github.com:username/glyphdiff.git
|
|||
|
|
|
|||
|
|
# Push to remote
|
|||
|
|
git push -u origin main
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### Learning Resources
|
|||
|
|
|
|||
|
|
#### Official Documentation
|
|||
|
|
|
|||
|
|
- [Svelte 5 Documentation](https://svelte-5-preview.vercel.app/)
|
|||
|
|
- [SvelteKit Documentation](https://kit.svelte.dev/docs)
|
|||
|
|
- [Tailwind CSS Documentation](https://tailwindcss.com/docs)
|
|||
|
|
- [Bits UI Documentation](https://www.bits-ui.com/)
|
|||
|
|
|
|||
|
|
#### React to Svelte Migration
|
|||
|
|
|
|||
|
|
- [Svelte for React Developers](https://svelte.dev/docs/svelte-vs-react)
|
|||
|
|
- [Svelte 5 Runes Guide](https://svelte-5-preview.vercel.app/docs/runes-api)
|
|||
|
|
- [SvelteKit Data Loading](https://kit.svelte.dev/docs/load)
|
|||
|
|
|
|||
|
|
#### Google Fonts API
|
|||
|
|
|
|||
|
|
- [Google Fonts API Developer Guide](https://developers.google.com/fonts/docs/developer_api)
|
|||
|
|
- [Google Fonts API Reference](https://developers.google.com/fonts/docs/reference/rest)
|
|||
|
|
|
|||
|
|
#### Video Resources
|
|||
|
|
|
|||
|
|
- [Svelte 5 Tutorial](https://www.youtube.com/results?search_query=svelte+5+tutorial)
|
|||
|
|
- [SvelteKit Crash Course](https://www.youtube.com/results?search_query=sveltekit+crash+course)
|
|||
|
|
- [Tailwind CSS Full Course](https://www.youtube.com/results?search_query=tailwind+css+full+course)
|
|||
|
|
|
|||
|
|
#### Community
|
|||
|
|
|
|||
|
|
- [Svelte Discord](https://svelte.dev/chat)
|
|||
|
|
- [Svelte subreddit](https://reddit.com/r/sveltejs)
|
|||
|
|
- [SvelteKit subreddit](https://reddit.com/r/sveltekit)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### File Templates
|
|||
|
|
|
|||
|
|
#### Component Template
|
|||
|
|
|
|||
|
|
```svelte
|
|||
|
|
<script lang="ts">
|
|||
|
|
import type { Snippet } from 'svelte';
|
|||
|
|
|
|||
|
|
export let prop: string;
|
|||
|
|
|
|||
|
|
// State
|
|||
|
|
let count = $state(0);
|
|||
|
|
|
|||
|
|
// Derived state
|
|||
|
|
let doubled = $derived(count * 2);
|
|||
|
|
|
|||
|
|
// Actions
|
|||
|
|
function increment() {
|
|||
|
|
count += 1;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Lifecycle
|
|||
|
|
import { onMount, onDestroy } from 'svelte';
|
|||
|
|
|
|||
|
|
onMount(() => {
|
|||
|
|
console.log('Component mounted');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
onDestroy(() => {
|
|||
|
|
console.log('Component destroyed');
|
|||
|
|
});
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<div class="component">
|
|||
|
|
<h2>{prop}</h2>
|
|||
|
|
<p>Count: {count}, Doubled: {doubled}</p>
|
|||
|
|
<button onclick={increment}>Increment</button>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<style>
|
|||
|
|
.component {
|
|||
|
|
padding: 1rem;
|
|||
|
|
}
|
|||
|
|
</style>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### Store Template
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
import type { FontMetadata } from '$lib/types';
|
|||
|
|
import { setCache, getCache } from '$lib/utils/cache';
|
|||
|
|
|
|||
|
|
class ExampleStore {
|
|||
|
|
// State
|
|||
|
|
items = $state<FontMetadata[]>([]);
|
|||
|
|
loading = $state(false);
|
|||
|
|
error = $state<Error | null>(null);
|
|||
|
|
|
|||
|
|
// Derived state (computed automatically)
|
|||
|
|
get count() {
|
|||
|
|
return this.items.length;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Methods
|
|||
|
|
async loadItems() {
|
|||
|
|
this.loading = true;
|
|||
|
|
this.error = null;
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
// Load from cache first
|
|||
|
|
const cached = getCache('items');
|
|||
|
|
if (cached) {
|
|||
|
|
this.items = cached;
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Fetch items
|
|||
|
|
// this.items = await fetchItems();
|
|||
|
|
// setCache('items', this.items);
|
|||
|
|
} catch (e) {
|
|||
|
|
this.error = e as Error;
|
|||
|
|
} finally {
|
|||
|
|
this.loading = false;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
addItem(item: FontMetadata) {
|
|||
|
|
this.items = [...this.items, item];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
removeItem(id: string) {
|
|||
|
|
this.items = this.items.filter(item => item.id !== id);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Export singleton instance
|
|||
|
|
export const exampleStore = new ExampleStore();
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### Load Function Template
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
// +page.ts or +page.server.ts
|
|||
|
|
import type { PageLoad } from './$types';
|
|||
|
|
|
|||
|
|
export const load: PageLoad = async ({ fetch, depends, url }) => {
|
|||
|
|
// Declare dependencies for invalidation
|
|||
|
|
depends('app:data');
|
|||
|
|
|
|||
|
|
// Get URL parameters
|
|||
|
|
const search = url.searchParams.get('search') || '';
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
// Fetch data
|
|||
|
|
const response = await fetch('/api/data');
|
|||
|
|
const data = await response.json();
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
data,
|
|||
|
|
search
|
|||
|
|
};
|
|||
|
|
} catch (error) {
|
|||
|
|
return {
|
|||
|
|
data: [],
|
|||
|
|
search,
|
|||
|
|
error: error.message
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### Troubleshooting
|
|||
|
|
|
|||
|
|
#### Common Issues
|
|||
|
|
|
|||
|
|
**Issue: Google Fonts API returns 403**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
# Solution: Check API key and rate limits
|
|||
|
|
# 1. Verify API key is correct
|
|||
|
|
# 2. Check API key restrictions (set to None for testing)
|
|||
|
|
# 3. Verify you haven't exceeded daily quota
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Issue: Tailwind classes not working**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
# Solution: Check configuration
|
|||
|
|
# 1. Ensure app.css imports tailwind directives
|
|||
|
|
# 2. Restart dev server after config changes
|
|||
|
|
# 3. Check vite.config.ts includes postcss config
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Issue: Svelte 5 syntax errors**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
# Solution: Ensure you're using latest Svelte
|
|||
|
|
npm install svelte@next
|
|||
|
|
|
|||
|
|
# If using VS Code, install Svelte extension
|
|||
|
|
# Ensure it's the latest version for Svelte 5 support
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Issue: TypeScript errors with $state**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
# Solution: Update Svelte configuration
|
|||
|
|
# Ensure svelte.config.js includes:
|
|||
|
|
export default {
|
|||
|
|
compilerOptions: {
|
|||
|
|
runes: true
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Issue: Vercel deployment fails**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
# Solution: Check build output
|
|||
|
|
npm run build
|
|||
|
|
|
|||
|
|
# Review error logs in Vercel dashboard
|
|||
|
|
# Common issues:
|
|||
|
|
# - Missing environment variables
|
|||
|
|
# - TypeScript errors
|
|||
|
|
# - Build timeout
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### Performance Checklist
|
|||
|
|
|
|||
|
|
- [ ] Implement lazy loading for fonts
|
|||
|
|
- [ ] Use SvelteKit static generation where possible
|
|||
|
|
- [ ] Optimize images (use webp format)
|
|||
|
|
- [ ] Minimize JavaScript bundle size
|
|||
|
|
- [ ] Use code splitting for routes
|
|||
|
|
- [ ] Implement caching strategy
|
|||
|
|
- [ ] Enable compression (handled by Vercel)
|
|||
|
|
- [ ] Test Lighthouse scores
|
|||
|
|
- [ ] Minimize layout shifts
|
|||
|
|
- [ ] Use appropriate font formats (WOFF2)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Conclusion
|
|||
|
|
|
|||
|
|
This project plan provides a comprehensive roadmap for building glyphdiff.com as a learning-focused MVP. The pure client-side approach prioritizes mastering Svelte 5 and SvelteKit while building a functional font comparison tool.
|
|||
|
|
|
|||
|
|
### Next Steps
|
|||
|
|
|
|||
|
|
1. Set up the development environment (Week 1 tasks)
|
|||
|
|
2. Obtain Google Fonts API key
|
|||
|
|
3. Configure environment variables
|
|||
|
|
4. Begin implementing Svelte fundamentals
|
|||
|
|
|
|||
|
|
Good luck on your React → Svelte journey! 🚀
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
*Document Version: 1.0*
|
|||
|
|
*Last Updated: December 2025*
|
|||
|
|
*Project: glyphdiff.com*
|