#!/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));