feat: port all template categories to JSON format

- Ported Minimalist templates to JSON (Swiss Grid, Brutalist, etc.)
- Ported Tech templates to JSON (SaaS, Terminal, Cyberpunk, etc.)
- Ported Creative templates to JSON (Art Gallery, Zine, Pop Art, etc.)
- Ported Industrial templates to JSON (Blueprint, Factory, Schematic, etc.)
- Ported Nature templates to JSON (Botanical, Ocean, Mountain, etc.)
- Ported Lifestyle templates to JSON (Cookbook, Travel, Coffee House, etc.)
- Ported Vintage templates to JSON (Art Deco, Medieval, Retro 80s, etc.)
- Updated README.md to reflect the new JSON-based style system (example configuration and contribution workflow)
- Completed migration of over 150 styles to the new architecture
This commit is contained in:
TypoGenie
2026-02-01 18:51:43 +02:00
parent da335734d3
commit a6f664088c
405 changed files with 69134 additions and 5936 deletions

View File

@@ -0,0 +1,247 @@
import { invoke } from '@tauri-apps/api/core';
import { StyleOption, DocxStyleConfig, ElementStyle, FontConfig, ColorPalette } from '../types';
import { generatePreviewCss, generateWordConfig } from './templateRenderer';
interface RawTemplate {
id: string;
name: string;
category?: string;
description?: string;
vibe?: string;
googleFontsImport?: string;
typography?: {
fonts?: FontConfig;
colors?: ColorPalette;
};
elements?: Record<string, ElementStyle>;
page?: Record<string, any>;
wordConfig?: Record<string, any>;
previewCss?: string;
[key: string]: any;
}
interface TemplateFile {
name: string;
content: string;
category: string; // Folder-based category from Rust
}
/**
* Resolves a color reference or returns the color directly
*/
function resolveColor(color: string | undefined, palette: ColorPalette): string {
if (!color) return '#000000';
if (palette[color as keyof ColorPalette]) {
return '#' + palette[color as keyof ColorPalette];
}
return color.startsWith('#') ? color : '#' + color;
}
/**
* Resolves a font reference or returns the font directly
*/
function resolveFont(font: string | undefined, fonts: FontConfig): string {
if (!font) return 'serif';
if (fonts[font as keyof FontConfig]) {
return fonts[font as keyof FontConfig]!;
}
return font;
}
/**
* Generates preview CSS from unified template structure
*/
/**
/**
* Validate that a parsed object conforms to the StyleOption interface
*/
function validateTemplate(raw: RawTemplate, filename: string): StyleOption | null {
if (!raw.id || typeof raw.id !== 'string') {
console.error(`Invalid template in ${filename}: missing or invalid 'id'`);
return null;
}
if (!raw.name || typeof raw.name !== 'string') {
console.error(`Invalid template in ${filename}: missing or invalid 'name'`);
return null;
}
const hasUnifiedStructure = raw.elements && raw.typography;
let wordConfig: StyleOption['wordConfig'];
let previewCss: string;
if (hasUnifiedStructure) {
const fonts = raw.typography?.fonts || {};
const palette = raw.typography?.colors || {};
const elements = raw.elements || {};
// Only generate wordConfig if it's missing or incomplete in the raw template
// Prefer the raw wordConfig if it exists (allows manual overrides)
if (raw.wordConfig && Object.keys(raw.wordConfig).length > 0) {
wordConfig = raw.wordConfig as StyleOption['wordConfig'];
} else {
// Use the imported generator which now has better logic
wordConfig = generateWordConfig(elements, fonts, palette) as StyleOption['wordConfig'];
}
// Similarly for previewCss - prefer explicit override
if (raw.previewCss) {
previewCss = raw.previewCss;
} else {
previewCss = generatePreviewCss(elements, fonts, palette);
}
} else {
const defaultHeading1: DocxStyleConfig = {
font: 'Arial', size: 24, color: '000000', align: 'left', spacing: { before: 240, after: 120, line: 240 }
};
const defaultHeading2: DocxStyleConfig = {
font: 'Arial', size: 18, color: '333333', align: 'left', spacing: { before: 200, after: 100, line: 240 }
};
const defaultBody: DocxStyleConfig = {
font: 'Arial', size: 11, color: '333333', align: 'left', spacing: { before: 0, after: 120, line: 276 }
};
wordConfig = raw.wordConfig && raw.wordConfig.heading1 && raw.wordConfig.heading2 && raw.wordConfig.body
? {
heading1: { ...defaultHeading1, ...raw.wordConfig.heading1 },
heading2: { ...defaultHeading2, ...raw.wordConfig.heading2 },
body: { ...defaultBody, ...raw.wordConfig.body },
accentColor: raw.wordConfig.accentColor || '000000'
}
: {
heading1: defaultHeading1,
heading2: defaultHeading2,
body: defaultBody,
accentColor: '000000'
};
previewCss = raw.previewCss || 'font-family: Arial, sans-serif;';
}
const template: StyleOption = {
id: raw.id,
name: raw.name,
category: raw.category || 'Other',
description: raw.description || '',
vibe: raw.vibe || '',
googleFontsImport: raw.googleFontsImport || '',
wordConfig,
previewCss,
// Preserve unified template structure if present
...(raw.typography && { typography: raw.typography }),
...(raw.elements && { elements: raw.elements }),
...(raw.page && { page: raw.page })
};
const extraFields = Object.entries(raw).reduce((acc, [key, val]) => {
if (!['id', 'name', 'category', 'description', 'vibe', 'googleFontsImport', 'wordConfig', 'previewCss', 'typography', 'elements', 'page'].includes(key)) {
acc[key] = val;
}
return acc;
}, {} as Record<string, any>);
return { ...template, ...extraFields };
}
/**
* Debug function to check paths
*/
export async function debugPaths(): Promise<string> {
try {
return await invoke<string>('debug_paths');
} catch (e) {
return `Error: ${e}`;
}
}
/**
* Load all templates from the portable folder (next to EXE)
* Uses Rust backend for file operations since BaseDirectory.Executable doesn't work in JS
*/
export async function loadTemplates(): Promise<{ templates: StyleOption[]; error?: string }> {
const templates: StyleOption[] = [];
let error: string | undefined;
try {
// Use Rust command to read all templates
console.log('Calling read_templates command...');
const files = await invoke<TemplateFile[]>('read_templates');
console.log(`Received ${files.length} templates from Rust`);
for (const file of files) {
try {
const raw = JSON.parse(file.content) as RawTemplate;
const validated = validateTemplate(raw, file.name);
if (validated) {
// Use folder-based category from Rust, fallback to 'Other'
const category = file.category || 'Other';
templates.push({ ...validated, category });
}
} catch (e) {
console.error(`Failed to parse template ${file.name}:`, e);
}
}
if (templates.length === 0) {
error = 'No templates found.';
}
} catch (e) {
console.error('Error in loadTemplates:', e);
error = e instanceof Error ? e.message : String(e);
}
// Sort by category then name
templates.sort((a, b) => {
const catCompare = a.category.localeCompare(b.category);
if (catCompare !== 0) return catCompare;
return a.name.localeCompare(b.name);
});
// Deduplicate by ID (in case of duplicate files)
const seen = new Set<string>();
const deduplicated = templates.filter(t => {
if (seen.has(t.id)) {
console.warn(`Duplicate template ID found: ${t.id}, skipping duplicate`);
return false;
}
seen.add(t.id);
return true;
});
console.log(`Returning ${deduplicated.length} templates (deduplicated from ${templates.length}), error: ${error}`);
return { templates: deduplicated, error };
}
/**
* Open the templates folder in the system file explorer
*/
export async function openTemplatesFolder(): Promise<{ success: boolean; error?: string }> {
try {
await invoke('open_templates_folder');
return { success: true };
} catch (e) {
const errorMsg = e instanceof Error ? e.message : String(e);
return { success: false, error: errorMsg };
}
}
/**
* Load a single template by ID
*/
export async function loadTemplateById(id: string): Promise<StyleOption | null> {
const { templates } = await loadTemplates();
return templates.find(t => t.id === id) || null;
}
/**
* Get all available categories from templates
*/
export function getCategories(templates: StyleOption[]): string[] {
const categories = new Set<string>();
templates.forEach(t => categories.add(t.category));
return Array.from(categories).sort();
}