#!/usr/bin/env node /** * Extract all SCSS variable values and derived colors for each theme. * Outputs JSON with pre-calculated hex values for CSS custom properties. */ const sass = require("sass"); const path = require("path"); const stylesDir = path.resolve(__dirname, "../app/javascript/flavours/glitch/styles"); // All SCSS variables we need to extract const baseVars = [ "black", "white", "red-600", "red-500", "blurple-600", "blurple-500", "blurple-300", "grey-600", "grey-100", "success-green", "error-red", "warning-red", "gold-star", "red-bookmark", "classic-base-color", "classic-primary-color", "classic-secondary-color", "classic-highlight-color", "base-shadow-color", "base-overlay-background", "base-border-color", "simple-background-color", "valid-value-color", "error-value-color", "ui-base-color", "ui-base-lighter-color", "ui-primary-color", "ui-secondary-color", "ui-highlight-color", "ui-button-color", "ui-button-background-color", "ui-button-focus-background-color", "ui-button-secondary-color", "ui-button-secondary-border-color", "ui-button-secondary-focus-background-color", "ui-button-secondary-focus-color", "ui-button-tertiary-color", "ui-button-tertiary-border-color", "ui-button-tertiary-focus-background-color", "ui-button-tertiary-focus-color", "ui-button-destructive-background-color", "ui-button-destructive-focus-background-color", "primary-text-color", "darker-text-color", "dark-text-color", "secondary-text-color", "highlight-text-color", "action-button-color", "action-button-focus-color", "passive-text-color", "active-passive-text-color", "inverted-text-color", "lighter-text-color", "light-text-color", ]; // Derived colors: [variable, function, amount] // ALL unique lighten/darken/rgba calls found across the SCSS codebase const derivedColors = [ // === lighten calls (61 unique) === // action-button-color ["action-button-color", "lighten", 7], ["action-button-color", "lighten", 13], // darker-text-color ["darker-text-color", "lighten", 4], ["darker-text-color", "lighten", 7], ["darker-text-color", "lighten", 8], ["darker-text-color", "lighten", 10], // dark-text-color ["dark-text-color", "lighten", 4], ["dark-text-color", "lighten", 7], // error-red ["error-red", "lighten", 4], ["error-red", "lighten", 12], // error-value-color ["error-value-color", "lighten", 12], // gold-star ["gold-star", "lighten", 6], ["gold-star", "lighten", 16], // highlight-text-color ["highlight-text-color", "lighten", 4], ["highlight-text-color", "lighten", 6], ["highlight-text-color", "lighten", 8], ["highlight-text-color", "lighten", 13], // inverted-text-color ["inverted-text-color", "lighten", 4], ["inverted-text-color", "lighten", 10], ["inverted-text-color", "lighten", 15], ["inverted-text-color", "lighten", 16], // lighter-text-color ["lighter-text-color", "lighten", 7], ["lighter-text-color", "lighten", 20], // secondary-text-color ["secondary-text-color", "lighten", 4], ["secondary-text-color", "lighten", 7], ["secondary-text-color", "lighten", 8], // ui-base-color ["ui-base-color", "lighten", 2], ["ui-base-color", "lighten", 3], ["ui-base-color", "lighten", 4], ["ui-base-color", "lighten", 5], ["ui-base-color", "lighten", 6], ["ui-base-color", "lighten", 8], ["ui-base-color", "lighten", 11], ["ui-base-color", "lighten", 12], ["ui-base-color", "lighten", 13], ["ui-base-color", "lighten", 14], ["ui-base-color", "lighten", 16], ["ui-base-color", "lighten", 20], ["ui-base-color", "lighten", 26], ["ui-base-color", "lighten", 27], ["ui-base-color", "lighten", 29], ["ui-base-color", "lighten", 30], ["ui-base-color", "lighten", 33], ["ui-base-color", "lighten", 34], ["ui-base-color", "lighten", 50], // ui-base-lighter-color ["ui-base-lighter-color", "lighten", 4], ["ui-base-lighter-color", "lighten", 7], // ui-highlight-color ["ui-highlight-color", "lighten", 4], ["ui-highlight-color", "lighten", 8], ["ui-highlight-color", "lighten", 10], ["ui-highlight-color", "lighten", 12], // ui-primary-color ["ui-primary-color", "lighten", 8], ["ui-primary-color", "lighten", 12], ["ui-primary-color", "lighten", 20], // ui-secondary-color ["ui-secondary-color", "lighten", 6], ["ui-secondary-color", "lighten", 8], // valid-value-color ["valid-value-color", "lighten", 8], ["valid-value-color", "lighten", 15], // warning-red ["warning-red", "lighten", 12], // white ["white", "lighten", 4], ["white", "lighten", 7], // === darken calls (33 unique) === // action-button-color ["action-button-color", "darken", 13], // darker-text-color ["darker-text-color", "darken", 13], // highlight-text-color ["highlight-text-color", "darken", 4], // lighter-text-color ["lighter-text-color", "darken", 4], ["lighter-text-color", "darken", 7], // simple-background-color ["simple-background-color", "darken", 2], ["simple-background-color", "darken", 8], ["simple-background-color", "darken", 14], ["simple-background-color", "darken", 24], // ui-base-color ["ui-base-color", "darken", 2], ["ui-base-color", "darken", 4], ["ui-base-color", "darken", 5], ["ui-base-color", "darken", 6], ["ui-base-color", "darken", 7], ["ui-base-color", "darken", 8], ["ui-base-color", "darken", 10], ["ui-base-color", "darken", 12], ["ui-base-color", "darken", 13], ["ui-base-color", "darken", 14], ["ui-base-color", "darken", 20], // ui-highlight-color ["ui-highlight-color", "darken", 2], ["ui-highlight-color", "darken", 3], ["ui-highlight-color", "darken", 5], ["ui-highlight-color", "darken", 8], // ui-primary-color ["ui-primary-color", "darken", 5], ["ui-primary-color", "darken", 14], ["ui-primary-color", "darken", 40], // ui-secondary-color ["ui-secondary-color", "darken", 8], ["ui-secondary-color", "darken", 10], ["ui-secondary-color", "darken", 16], ["ui-secondary-color", "darken", 18], ["ui-secondary-color", "darken", 24], // === rgba calls (74 unique) === // action-button-color ["action-button-color", "rgba", 0.15], ["action-button-color", "rgba", 0.3], // base-overlay-background ["base-overlay-background", "rgba", 0], ["base-overlay-background", "rgba", 0.1], ["base-overlay-background", "rgba", 0.3], ["base-overlay-background", "rgba", 0.5], ["base-overlay-background", "rgba", 0.6], ["base-overlay-background", "rgba", 0.7], ["base-overlay-background", "rgba", 0.8], ["base-overlay-background", "rgba", 0.9], // base-shadow-color ["base-shadow-color", "rgba", 0.1], ["base-shadow-color", "rgba", 0.2], ["base-shadow-color", "rgba", 0.25], ["base-shadow-color", "rgba", 0.3], ["base-shadow-color", "rgba", 0.35], ["base-shadow-color", "rgba", 0.4], ["base-shadow-color", "rgba", 0.45], ["base-shadow-color", "rgba", 0.6], ["base-shadow-color", "rgba", 0.65], ["base-shadow-color", "rgba", 0.75], ["base-shadow-color", "rgba", 0.8], ["base-shadow-color", "rgba", 0.85], // black ["black", "rgba", 0.45], ["black", "rgba", 0.65], ["black", "rgba", 0.85], ["black", "rgba", 0.9], // darker-text-color ["darker-text-color", "rgba", 0.15], ["darker-text-color", "rgba", 0.3], // dark-text-color ["dark-text-color", "rgba", 0.1], // error-red ["error-red", "rgba", 0.5], // error-value-color ["error-value-color", "rgba", 0.1], ["error-value-color", "rgba", 0.5], // gold-star ["gold-star", "rgba", 0.15], ["gold-star", "rgba", 0.25], ["gold-star", "rgba", 0.3], ["gold-star", "rgba", 0.5], // highlight-text-color ["highlight-text-color", "rgba", 0.15], ["highlight-text-color", "rgba", 0.25], ["highlight-text-color", "rgba", 0.3], ["highlight-text-color", "rgba", 0.4], // lighter-text-color ["lighter-text-color", "rgba", 0.15], ["lighter-text-color", "rgba", 0.3], // primary-text-color ["primary-text-color", "rgba", 0.7], ["primary-text-color", "rgba", 0.8], // success-green ["success-green", "rgba", 0.1], ["success-green", "rgba", 0.5], // ui-base-color ["ui-base-color", "rgba", 0], ["ui-base-color", "rgba", 0.15], ["ui-base-color", "rgba", 0.25], ["ui-base-color", "rgba", 0.3], ["ui-base-color", "rgba", 0.85], ["ui-base-color", "rgba", 1], // ui-base-lighter-color ["ui-base-lighter-color", "rgba", 0.6], // ui-highlight-color ["ui-highlight-color", "rgba", 0], ["ui-highlight-color", "rgba", 0.1], ["ui-highlight-color", "rgba", 0.15], ["ui-highlight-color", "rgba", 0.23], ["ui-highlight-color", "rgba", 0.4], ["ui-highlight-color", "rgba", 0.5], // ui-secondary-color ["ui-secondary-color", "rgba", 0.1], ["ui-secondary-color", "rgba", 0.3], ["ui-secondary-color", "rgba", 0.4], ["ui-secondary-color", "rgba", 0.5], ["ui-secondary-color", "rgba", 0.7], // valid-value-color ["valid-value-color", "rgba", 0.25], ["valid-value-color", "rgba", 0.5], // warning-red ["warning-red", "rgba", 0.15], // white ["white", "rgba", 0.15], ["white", "rgba", 0.2], ["white", "rgba", 0.3], ["white", "rgba", 0.35], ["white", "rgba", 0.7], ["white", "rgba", 0.75], ["white", "rgba", 0.8], ]; // Mix calls: [name, scssExpression] const mixColors = [ ["mix-ui-base-highlight-95", "mix($ui-base-color, $ui-highlight-color, 95%)"], ["mix-white-highlight-80", "mix($white, $ui-highlight-color, 80%)"], ["mix-ui-base-lighten4-highlight-95", "mix(lighten($ui-base-color, 4%), $ui-highlight-color, 95%)"], ["mix-ui-base-lighten8-highlight-95", "mix(lighten($ui-base-color, 8%), $ui-highlight-color, 95%)"], ["mix-ui-base-lighten12-highlight-80", "mix(lighten($ui-base-color, 12%), $ui-highlight-color, 80%)"], ]; function generateScss(themeImport) { let scss = ""; if (themeImport) { scss += `@import '${themeImport}';\n`; } scss += `@import 'variables';\n\n`; // Output base variables scss += `:root {\n`; for (const v of baseVars) { scss += ` --extract-${v}: #{$${v}};\n`; } // Output derived colors for (const [varName, fn, amount] of derivedColors) { if (fn === "lighten") { scss += ` --extract-${varName}-lighten-${amount}: #{lighten($${varName}, ${amount}%)};\n`; } else if (fn === "darken") { scss += ` --extract-${varName}-darken-${amount}: #{darken($${varName}, ${amount}%)};\n`; } else if (fn === "rgba") { scss += ` --extract-${varName}-a${Math.round(amount * 100)}: #{rgba($${varName}, ${amount})};\n`; } } // Output mix colors for (const [name, expr] of mixColors) { scss += ` --extract-${name}: #{${expr}};\n`; } scss += `}\n`; return scss; } function extractValues(css) { const values = {}; const regex = /--extract-([\w-]+):\s*([^;]+);/g; let match; while ((match = regex.exec(css)) !== null) { values[match[1]] = match[2].trim(); } return values; } const themes = { dark: null, light: "mastodon-light/variables", contrast: "contrast/variables", }; const results = {}; for (const [name, themeImport] of Object.entries(themes)) { const scss = generateScss(themeImport); try { const result = sass.compileString(scss, { loadPaths: [stylesDir, path.resolve(__dirname, "../app/javascript")], }); results[name] = extractValues(result.css); } catch (err) { console.error(`Error compiling ${name} theme:`, err.message); results[name] = { error: err.message }; } } console.log(JSON.stringify(results, null, 2));