Files
Conal Mullan 0d96c3b764 Add shared component library, unified commands, and examples
Major changes:
- Shared lib/ with reusable components (AnimatedBackground, SlideTransition,
  Label, Vignette, LogoWatermark, SplitScreen, NarratorPiP)
- Theme system (ThemeProvider, useTheme) in lib/theme/
- Multi-session project system with project.json schema
- Brand loader utility and brand.ts generation

Commands:
- /video - unified project management (replaced /new-video, /new-sprint-video)
- /brand - brand profiles (replaced /new-brand)
- /template - list templates
- /skills - list/create skills (renamed from /skill to avoid Claude Code conflict)
- /contribute - guided contribution workflow

Infrastructure:
- examples/ directory for shareable showcase projects
- CONTRIBUTORS.md for recognition
- FEEDBACK.md for capturing improvements
- Evolution narrative in all commands

Fix:
- Renamed /skill to /skills - naming collision with built-in Skill tool was
  silently preventing ALL custom commands from loading

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-10 19:03:56 +00:00

156 lines
4.0 KiB
TypeScript

#!/usr/bin/env npx ts-node
/**
* Generate brand.ts file for a project
*
* This script reads a brand.json file and generates a TypeScript file
* that can be imported by Remotion templates.
*
* Usage:
* npx ts-node lib/generate-brand-ts.ts <brand-name> <output-path>
*
* Example:
* npx ts-node lib/generate-brand-ts.ts digital-samba projects/my-video/src/config/brand.ts
*/
import * as fs from 'fs';
import * as path from 'path';
interface Brand {
name: string;
description?: string;
colors: Record<string, string>;
fonts: { primary: string; mono: string };
spacing: Record<string, number>;
borderRadius: Record<string, number>;
typography: Record<string, { size: number; weight: number; letterSpacing?: number }>;
assets?: Record<string, string>;
}
function findToolkitRoot(): string {
let current = __dirname;
const root = path.parse(current).root;
while (current !== root) {
const brandsPath = path.join(current, 'brands');
if (fs.existsSync(brandsPath)) {
return current;
}
current = path.dirname(current);
}
throw new Error('Could not find toolkit root');
}
function loadBrand(brandName: string): Brand {
const toolkitRoot = findToolkitRoot();
const brandPath = path.join(toolkitRoot, 'brands', brandName, 'brand.json');
if (!fs.existsSync(brandPath)) {
throw new Error(`Brand not found: ${brandPath}`);
}
return JSON.parse(fs.readFileSync(brandPath, 'utf-8'));
}
function generateBrandTs(brand: Brand, brandName: string): string {
const colorsStr = Object.entries(brand.colors)
.map(([key, value]) => ` ${key}: '${value}',`)
.join('\n');
const typographyStr = Object.entries(brand.typography)
.map(([key, value]) => {
const props = [`size: ${value.size}`, `weight: ${value.weight}`];
if (value.letterSpacing) props.push(`letterSpacing: ${value.letterSpacing}`);
return ` ${key}: { ${props.join(', ')} },`;
})
.join('\n');
const assetsStr = brand.assets
? Object.entries(brand.assets)
.map(([key, value]) => ` ${key}: '${value}',`)
.join('\n')
: ' logo: undefined as string | undefined,\n logoLight: undefined as string | undefined,';
return `/**
* Brand Configuration
*
* This file is generated by /new-video command.
* It contains the brand settings for this project.
*
* Brand: ${brand.name}
* Generated: ${new Date().toISOString().split('T')[0]}
*
* To change brands, either:
* 1. Run /new-video again with a different brand
* 2. Manually copy values from brands/{brand-name}/brand.json
*/
import type { Theme } from './types';
// Project brand name (for reference)
export const brandName = '${brandName}';
// Brand colors and styling
export const brand = {
name: '${brand.name}',
colors: {
${colorsStr}
},
fonts: {
primary: '${brand.fonts.primary}',
mono: '${brand.fonts.mono}',
},
spacing: {
xs: ${brand.spacing.xs},
sm: ${brand.spacing.sm},
md: ${brand.spacing.md},
lg: ${brand.spacing.lg},
xl: ${brand.spacing.xl},
xxl: ${brand.spacing.xxl},
},
borderRadius: {
sm: ${brand.borderRadius.sm},
md: ${brand.borderRadius.md},
lg: ${brand.borderRadius.lg},
},
typography: {
${typographyStr}
},
assets: {
${assetsStr}
},
};
// Theme derived from brand (for component use)
export const brandTheme: Theme = {
colors: brand.colors,
fonts: brand.fonts,
spacing: brand.spacing,
borderRadius: brand.borderRadius,
typography: brand.typography,
};
`;
}
// CLI execution
if (require.main === module) {
const [brandName, outputPath] = process.argv.slice(2);
if (!brandName || !outputPath) {
console.error('Usage: npx ts-node lib/generate-brand-ts.ts <brand-name> <output-path>');
process.exit(1);
}
try {
const brand = loadBrand(brandName);
const content = generateBrandTs(brand, brandName);
fs.writeFileSync(outputPath, content);
console.log(`Generated ${outputPath} from brand '${brandName}'`);
} catch (error) {
console.error('Error:', (error as Error).message);
process.exit(1);
}
}
export { loadBrand, generateBrandTs };