Files
whyrating/.context/turbostarter-framework-context/wireframes/apply-theme.js
2026-02-04 01:55:00 +01:00

311 lines
8.7 KiB
JavaScript

#!/usr/bin/env node
/**
* Excalidraw Theme Applicator
*
* This script applies color themes to Excalidraw wireframe files by replacing
* color tokens (like $background, $primary, etc.) with actual hex values.
*
* Usage:
* node apply-theme.js <input.excalidraw> <theme-name> [output.excalidraw]
*
* Example:
* node apply-theme.js wireframe.excalidraw orange-light themed-wireframe.excalidraw
*
* The script expects a wireframe-themes.json file in the same directory with
* the following structure:
*
* {
* "themes": {
* "orange-light": {
* "background": "#ffffff",
* "primary": "#f97316",
* "secondary": "#fed7aa",
* "accent": "#ea580c",
* "text": "#1f2937",
* "muted": "#9ca3af",
* "border": "#e5e7eb",
* "surface": "#f9fafb"
* },
* "blue-dark": {
* "background": "#0f172a",
* ...
* }
* }
* }
*/
const fs = require("fs");
const path = require("path");
// ---------------------------------------------------------------------------
// Configuration
// ---------------------------------------------------------------------------
const SCRIPT_DIR = __dirname;
const THEMES_FILE = path.join(SCRIPT_DIR, "wireframe-themes.json");
// Color properties to search for in Excalidraw elements
const COLOR_PROPERTIES = ["strokeColor", "backgroundColor"];
// ---------------------------------------------------------------------------
// Helper Functions
// ---------------------------------------------------------------------------
/**
* Load and parse the themes configuration file
* @returns {Object} The themes configuration object
*/
function loadThemes() {
if (!fs.existsSync(THEMES_FILE)) {
console.error(`Error: Themes file not found at ${THEMES_FILE}`);
console.error("\nPlease create a wireframe-themes.json file with the following structure:");
console.error(`
{
"themes": {
"theme-name": {
"background": "#ffffff",
"primary": "#f97316",
"secondary": "#fed7aa",
"accent": "#ea580c",
"text": "#1f2937",
"muted": "#9ca3af",
"border": "#e5e7eb",
"surface": "#f9fafb"
}
}
}
`);
process.exit(1);
}
try {
const content = fs.readFileSync(THEMES_FILE, "utf-8");
return JSON.parse(content);
} catch (error) {
console.error(`Error: Failed to parse themes file: ${error.message}`);
process.exit(1);
}
}
/**
* List all available themes from the configuration
* @param {Object} themesConfig - The themes configuration object
*/
function listAvailableThemes(themesConfig) {
const themes = Object.keys(themesConfig.themes || {});
if (themes.length === 0) {
console.error("No themes found in wireframe-themes.json");
return;
}
console.log("\nAvailable themes:");
themes.forEach((theme) => {
console.log(` - ${theme}`);
});
console.log("");
}
/**
* Replace a color token with the corresponding theme color
* @param {string} value - The color value (may be a token like "$primary")
* @param {Object} themeColors - The color mapping for the selected theme
* @returns {string} The resolved hex color or the original value if not a token
*/
function resolveColorToken(value, themeColors) {
// Check if the value is a color token (starts with $)
if (typeof value !== "string" || !value.startsWith("$")) {
return value;
}
// Extract the token name (remove the $ prefix)
const tokenName = value.substring(1);
// Look up the color in the theme
if (themeColors.hasOwnProperty(tokenName)) {
return themeColors[tokenName];
}
// Token not found in theme - warn but keep original
console.warn(`Warning: Color token "${value}" not found in theme`);
return value;
}
/**
* Recursively process an element and its children, replacing color tokens
* @param {Object} element - An Excalidraw element
* @param {Object} themeColors - The color mapping for the selected theme
* @param {Object} stats - Statistics object to track replacements
* @returns {Object} The element with colors replaced
*/
function processElement(element, themeColors, stats) {
if (!element || typeof element !== "object") {
return element;
}
// Handle arrays (like the elements array or grouped elements)
if (Array.isArray(element)) {
return element.map((item) => processElement(item, themeColors, stats));
}
// Process the current object
const processed = { ...element };
// Check and replace color properties
for (const prop of COLOR_PROPERTIES) {
if (processed.hasOwnProperty(prop) && typeof processed[prop] === "string") {
const originalValue = processed[prop];
const newValue = resolveColorToken(originalValue, themeColors);
if (originalValue !== newValue) {
processed[prop] = newValue;
stats.replacements++;
stats.details.push({
property: prop,
from: originalValue,
to: newValue,
});
}
}
}
// Recursively process nested objects and arrays
for (const key of Object.keys(processed)) {
if (typeof processed[key] === "object" && processed[key] !== null) {
processed[key] = processElement(processed[key], themeColors, stats);
}
}
return processed;
}
/**
* Generate a default output filename based on input and theme
* @param {string} inputFile - The input file path
* @param {string} themeName - The theme name
* @returns {string} The generated output file path
*/
function generateOutputFilename(inputFile, themeName) {
const dir = path.dirname(inputFile);
const ext = path.extname(inputFile);
const base = path.basename(inputFile, ext);
return path.join(dir, `${base}-${themeName}${ext}`);
}
/**
* Display usage information
*/
function showUsage() {
console.log(`
Excalidraw Theme Applicator
Usage:
node apply-theme.js <input.excalidraw> <theme-name> [output.excalidraw]
Arguments:
input.excalidraw - Path to the input Excalidraw JSON file
theme-name - Name of the theme to apply
output.excalidraw - Optional output file path (default: input-themename.excalidraw)
Examples:
node apply-theme.js wireframe.excalidraw orange-light
node apply-theme.js wireframe.excalidraw blue-dark themed-wireframe.excalidraw
Color Tokens:
The script replaces tokens like $background, $primary, etc. in strokeColor
and backgroundColor properties with hex values from the selected theme.
`);
}
// ---------------------------------------------------------------------------
// Main Script
// ---------------------------------------------------------------------------
function main() {
// Parse command line arguments
const args = process.argv.slice(2);
if (args.length < 2) {
showUsage();
// Try to show available themes if the file exists
if (fs.existsSync(THEMES_FILE)) {
const themesConfig = loadThemes();
listAvailableThemes(themesConfig);
}
process.exit(1);
}
const inputFile = args[0];
const themeName = args[1];
const outputFile = args[2] || generateOutputFilename(inputFile, themeName);
// Validate input file exists
if (!fs.existsSync(inputFile)) {
console.error(`Error: Input file not found: ${inputFile}`);
process.exit(1);
}
// Load themes configuration
const themesConfig = loadThemes();
// Validate theme exists
if (!themesConfig.themes || !themesConfig.themes[themeName]) {
console.error(`Error: Theme "${themeName}" not found`);
listAvailableThemes(themesConfig);
process.exit(1);
}
const themeColors = themesConfig.themes[themeName];
// Load and parse the Excalidraw file
let excalidrawData;
try {
const content = fs.readFileSync(inputFile, "utf-8");
excalidrawData = JSON.parse(content);
} catch (error) {
console.error(`Error: Failed to parse input file: ${error.message}`);
process.exit(1);
}
// Track statistics
const stats = {
replacements: 0,
details: [],
};
// Process the Excalidraw data
console.log(`Applying theme "${themeName}" to ${inputFile}...`);
const themedData = processElement(excalidrawData, themeColors, stats);
// Write the output file
try {
const outputContent = JSON.stringify(themedData, null, 2);
fs.writeFileSync(outputFile, outputContent, "utf-8");
} catch (error) {
console.error(`Error: Failed to write output file: ${error.message}`);
process.exit(1);
}
// Report results
console.log(`\nTheme applied successfully!`);
console.log(` Input: ${inputFile}`);
console.log(` Output: ${outputFile}`);
console.log(` Theme: ${themeName}`);
console.log(` Replacements: ${stats.replacements}`);
if (stats.replacements > 0 && args.includes("--verbose")) {
console.log("\nDetails:");
stats.details.forEach((detail) => {
console.log(` ${detail.property}: ${detail.from} -> ${detail.to}`);
});
}
}
// Run the script
main();