feat: youtube preview
This commit is contained in:
257
specs/002-youtube-design-preview/data-model.md
Normal file
257
specs/002-youtube-design-preview/data-model.md
Normal file
@@ -0,0 +1,257 @@
|
||||
# Data Model: YouTube Design Preview Replica
|
||||
|
||||
**Date**: 2026-01-29
|
||||
|
||||
## Entities
|
||||
|
||||
### PreviewMode (Enum)
|
||||
|
||||
```typescript
|
||||
type PreviewMode = 'desktop' | 'sidebar' | 'mobile';
|
||||
```
|
||||
|
||||
| Value | Description | Thumbnail Size |
|
||||
|-------|-------------|----------------|
|
||||
| `desktop` | YouTube homepage/search grid layout | 360x202px |
|
||||
| `sidebar` | YouTube related videos sidebar | 168x94px |
|
||||
| `mobile` | YouTube mobile app full-width layout | 100% width |
|
||||
|
||||
### ThemeMode (Enum)
|
||||
|
||||
```typescript
|
||||
type ThemeMode = 'light' | 'dark';
|
||||
```
|
||||
|
||||
### VideoMetadata
|
||||
|
||||
User-customizable metadata for preview display.
|
||||
|
||||
```typescript
|
||||
interface VideoMetadata {
|
||||
title: string; // Video title (max 100 chars for display)
|
||||
channelName: string; // Channel name
|
||||
channelAvatarUrl?: string; // Optional channel avatar image URL
|
||||
duration: string; // Format: "MM:SS" or "H:MM:SS"
|
||||
viewCount: number; // Raw number, formatted for display
|
||||
publishedAt: Date; // Used for "X days ago" calculation
|
||||
}
|
||||
```
|
||||
|
||||
**Validation Rules:**
|
||||
- `title`: Required, 1-100 characters
|
||||
- `channelName`: Required, 1-50 characters
|
||||
- `duration`: Required, format `/^\d{1,2}:\d{2}(:\d{2})?$/`
|
||||
- `viewCount`: Required, non-negative integer
|
||||
- `publishedAt`: Required, valid Date, not in future
|
||||
|
||||
### UploadedThumbnail
|
||||
|
||||
Represents a user-uploaded thumbnail image.
|
||||
|
||||
```typescript
|
||||
interface UploadedThumbnail {
|
||||
id: string; // UUID
|
||||
dataUrl: string; // Base64 data URL of the image
|
||||
originalName: string; // Original filename
|
||||
fileSize: number; // Size in bytes (max 5MB)
|
||||
mimeType: string; // 'image/jpeg' | 'image/png' | 'image/webp'
|
||||
width: number; // Original image width
|
||||
height: number; // Original image height
|
||||
uploadedAt: Date; // Timestamp
|
||||
}
|
||||
```
|
||||
|
||||
**Validation Rules:**
|
||||
- `fileSize`: Max 5,242,880 bytes (5MB)
|
||||
- `mimeType`: Only 'image/jpeg', 'image/png', 'image/webp'
|
||||
- `width`, `height`: Positive integers
|
||||
|
||||
### PreviewState
|
||||
|
||||
Complete state for the preview tool, persisted to localStorage.
|
||||
|
||||
```typescript
|
||||
interface PreviewState {
|
||||
// Active preview configuration
|
||||
currentThumbnail: UploadedThumbnail | null;
|
||||
previewMode: PreviewMode;
|
||||
themeMode: ThemeMode;
|
||||
metadata: VideoMetadata;
|
||||
|
||||
// History for quick switching
|
||||
recentThumbnails: UploadedThumbnail[]; // Max 10 items
|
||||
|
||||
// UI state
|
||||
showMetadataEditor: boolean;
|
||||
|
||||
// Persistence metadata
|
||||
lastSaved: Date;
|
||||
version: number; // Schema version for migrations
|
||||
}
|
||||
```
|
||||
|
||||
### VisualTestResult
|
||||
|
||||
Result of Playwright visual comparison test.
|
||||
|
||||
```typescript
|
||||
interface VisualTestResult {
|
||||
testId: string;
|
||||
previewMode: PreviewMode;
|
||||
themeMode: ThemeMode;
|
||||
capturedAt: Date;
|
||||
|
||||
// Comparison metrics
|
||||
matchPercentage: number; // 0-100
|
||||
passed: boolean; // matchPercentage >= threshold (98%)
|
||||
|
||||
// File references
|
||||
actualScreenshotPath: string;
|
||||
expectedScreenshotPath: string;
|
||||
diffImagePath?: string; // Only if failed
|
||||
|
||||
// Error info
|
||||
errorMessage?: string;
|
||||
}
|
||||
```
|
||||
|
||||
### ReferenceScreenshot
|
||||
|
||||
Stored YouTube reference for visual comparison.
|
||||
|
||||
```typescript
|
||||
interface ReferenceScreenshot {
|
||||
id: string;
|
||||
previewMode: PreviewMode;
|
||||
themeMode: ThemeMode;
|
||||
capturedAt: Date;
|
||||
capturedFrom: string; // YouTube URL
|
||||
filePath: string;
|
||||
|
||||
// Metadata
|
||||
viewportWidth: number;
|
||||
viewportHeight: number;
|
||||
browserType: string; // 'chromium' | 'firefox' | 'webkit'
|
||||
|
||||
// Version tracking
|
||||
isActive: boolean; // Current baseline
|
||||
replacedBy?: string; // ID of newer version
|
||||
}
|
||||
```
|
||||
|
||||
## Local Storage Schema
|
||||
|
||||
Key: `thumbpreview_state`
|
||||
|
||||
```typescript
|
||||
interface LocalStorageSchema {
|
||||
version: 1;
|
||||
state: PreviewState;
|
||||
}
|
||||
```
|
||||
|
||||
**Storage Limits:**
|
||||
- Total localStorage budget: ~5MB
|
||||
- Each thumbnail (base64): ~1-2MB typical
|
||||
- Keep max 10 recent thumbnails
|
||||
- Automatic cleanup of oldest when limit reached
|
||||
|
||||
## State Transitions
|
||||
|
||||
### Thumbnail Upload Flow
|
||||
|
||||
```
|
||||
[No Thumbnail]
|
||||
|
|
||||
v (user uploads file)
|
||||
[Validating]
|
||||
|
|
||||
+-- (invalid) --> [Error: show message] --> [No Thumbnail]
|
||||
|
|
||||
v (valid)
|
||||
[Processing]
|
||||
|
|
||||
v (resize/optimize if needed)
|
||||
[Preview Ready]
|
||||
|
|
||||
v (auto-save to localStorage)
|
||||
[Persisted]
|
||||
```
|
||||
|
||||
### Preview Mode Switch
|
||||
|
||||
```
|
||||
[Current Mode]
|
||||
|
|
||||
v (user selects new mode)
|
||||
[Transitioning]
|
||||
|
|
||||
v (re-render with new layout)
|
||||
[New Mode Active]
|
||||
|
|
||||
v (persist preference)
|
||||
[Saved]
|
||||
```
|
||||
|
||||
### Theme Mode Switch
|
||||
|
||||
```
|
||||
[Current Theme]
|
||||
|
|
||||
v (user clicks toggle)
|
||||
[Apply Theme Class]
|
||||
|
|
||||
v (CSS variables update)
|
||||
[New Theme Active]
|
||||
|
|
||||
v (persist preference)
|
||||
[Saved]
|
||||
```
|
||||
|
||||
## Relationships
|
||||
|
||||
```
|
||||
PreviewState
|
||||
|
|
||||
+-- 1:1 --> currentThumbnail (UploadedThumbnail)
|
||||
|
|
||||
+-- 1:N --> recentThumbnails (UploadedThumbnail[])
|
||||
|
|
||||
+-- 1:1 --> metadata (VideoMetadata)
|
||||
|
|
||||
+-- enum --> previewMode (PreviewMode)
|
||||
|
|
||||
+-- enum --> themeMode (ThemeMode)
|
||||
|
||||
VisualTestResult
|
||||
|
|
||||
+-- ref --> ReferenceScreenshot (expectedScreenshotPath)
|
||||
|
|
||||
+-- enum --> previewMode
|
||||
|
|
||||
+-- enum --> themeMode
|
||||
```
|
||||
|
||||
## Default Values
|
||||
|
||||
```typescript
|
||||
const DEFAULT_METADATA: VideoMetadata = {
|
||||
title: 'Your Video Title Here',
|
||||
channelName: 'Your Channel',
|
||||
channelAvatarUrl: undefined,
|
||||
duration: '10:30',
|
||||
viewCount: 125000,
|
||||
publishedAt: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000) // 1 week ago
|
||||
};
|
||||
|
||||
const DEFAULT_STATE: PreviewState = {
|
||||
currentThumbnail: null,
|
||||
previewMode: 'desktop',
|
||||
themeMode: 'light',
|
||||
metadata: DEFAULT_METADATA,
|
||||
recentThumbnails: [],
|
||||
showMetadataEditor: false,
|
||||
lastSaved: new Date(),
|
||||
version: 1
|
||||
};
|
||||
```
|
||||
Reference in New Issue
Block a user