diff --git a/package.json b/package.json index 582fc0c..1fbede6 100755 --- a/package.json +++ b/package.json @@ -1,14 +1,14 @@ { "name": "astroplate", - "version": "5.9.0", + "version": "5.10.0", "description": "Astro and Tailwindcss boilerplate", "author": "zeon.studio", "license": "MIT", "packageManager": "yarn@1.22.22", "type": "module", "scripts": { - "dev": "yarn generate-json && astro dev", - "build": "yarn generate-json && astro build", + "dev": "concurrently \"node scripts/themeGenerator.js --watch\" \"yarn generate-json && astro dev\"", + "build": "node scripts/themeGenerator.js && yarn generate-json && astro build", "preview": "astro preview", "check": "astro check", "format": "prettier -w ./src", @@ -37,7 +37,8 @@ "remark-collapse": "^0.1.2", "remark-toc": "^9.0.0", "swiper": "^12.0.3", - "vite": "^7.2.2" + "vite": "^7.2.2", + "sharp": "^0.34.5" }, "devDependencies": { "@tailwindcss/forms": "^0.5.10", @@ -46,11 +47,12 @@ "@types/node": "24.10.1", "@types/react": "19.2.6", "@types/react-dom": "19.2.3", + "concurrently": "^9.2.1", "eslint": "^9.39.1", "prettier": "^3.6.2", "prettier-plugin-astro": "^0.14.1", "prettier-plugin-tailwindcss": "^0.7.1", - "sharp": "0.34.5", + "tailwind-bootstrap-grid": "^6.0.0", "tailwindcss": "^4.1.17", "typescript": "^5.9.3" } diff --git a/scripts/themeGenerator.js b/scripts/themeGenerator.js new file mode 100644 index 0000000..7962f94 --- /dev/null +++ b/scripts/themeGenerator.js @@ -0,0 +1,188 @@ +import fs from "fs"; +import path from "path"; +import { fileURLToPath } from "url"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +/** + * Determine the paths based on setup mode (theme vs project) + * @returns {Object} Configuration object with paths and mode info + */ +function determinePaths() { + const themePath = path.join(__dirname, "../src/config/theme.json"); + const outputPath = path.join(__dirname, "../src/styles/generated-theme.css"); + + if (!fs.existsSync(themePath)) { + throw new Error(`Could not find theme.json at: ${themePath}`); + } + + return { themePath, outputPath }; +} + +const { themePath, outputPath } = determinePaths(); + +// Helper to convert color name from snake_case to kebab-case +const toKebab = (str) => str.replace(/_/g, "-"); + +// Helper to extract a clean font name +const findFont = (fontStr) => + fontStr.replace(/\+/g, " ").replace(/:[^:]+/g, ""); + +/** + * Add color entries to CSS array + * @param {Array} cssLines - Array of CSS lines to append to + * @param {Object} colors - Color object to process + * @param {string} prefix - Optional prefix for color variable names + */ +function addColorsToCss(cssLines, colors, prefix = "") { + Object.entries(colors).forEach(([key, value]) => { + const colorName = prefix + ? `--color-${prefix}-${toKebab(key)}` + : `--color-${toKebab(key)}`; + cssLines.push(` ${colorName}: ${value};`); + }); +} + +/** + * Generate theme CSS from theme.json configuration + * @throws {Error} If theme.json is missing or invalid + */ +function generateThemeCSS() { + // Validate that theme.json exists + if (!fs.existsSync(themePath)) { + throw new Error(`Theme configuration not found: ${themePath}`); + } + + try { + // Read and parse theme configuration + const themeConfig = JSON.parse(fs.readFileSync(themePath, "utf8")); + + // Validate required theme structure + if (!themeConfig.colors || !themeConfig.fonts) { + throw new Error( + "Invalid theme.json: missing 'colors' or 'fonts' section", + ); + } + + // Build CSS using array for better performance + const cssLines = [ + "/**", + ' * Auto-generated from "src/config/theme.json"', + " * DO NOT EDIT THIS FILE MANUALLY", + " * Run: node scripts/themeGenerator.js", + " */", + "", + "@theme {", + " /* === Colors === */", + ]; + + // Add default theme colors + if (themeConfig.colors.default?.theme_color) { + addColorsToCss(cssLines, themeConfig.colors.default.theme_color); + } + + // Add default text colors + if (themeConfig.colors.default?.text_color) { + addColorsToCss(cssLines, themeConfig.colors.default.text_color); + } + + // Add darkmode colors (if available) + if (themeConfig.colors.darkmode) { + cssLines.push("", " /* === Darkmode Colors === */"); + + if (themeConfig.colors.darkmode.theme_color) { + addColorsToCss( + cssLines, + themeConfig.colors.darkmode.theme_color, + "darkmode", + ); + } + + if (themeConfig.colors.darkmode.text_color) { + addColorsToCss( + cssLines, + themeConfig.colors.darkmode.text_color, + "darkmode", + ); + } + } + + // Add font families + cssLines.push("", " /* === Font Families === */"); + const fontFamily = themeConfig.fonts.font_family || {}; + Object.entries(fontFamily) + .filter(([key]) => !key.includes("type")) + .forEach(([key, font]) => { + const fontFallback = fontFamily[`${key}_type`] || "sans-serif"; + const fontValue = `${findFont(font)}, ${fontFallback}`; + cssLines.push(` --font-${toKebab(key)}: ${fontValue};`); + }); + + // Add font sizes + cssLines.push("", " /* === Font Sizes === */"); + const baseSize = Number(themeConfig.fonts.font_size?.base || 16); + const scale = Number(themeConfig.fonts.font_size?.scale || 1.25); + + cssLines.push(` --text-base: ${baseSize}px;`); + cssLines.push(` --text-base-sm: ${baseSize * 0.8}px;`); + + let currentSize = scale; + for (let i = 6; i >= 1; i--) { + cssLines.push(` --text-h${i}: ${currentSize}rem;`); + cssLines.push(` --text-h${i}-sm: ${currentSize * 0.9}rem;`); + currentSize *= scale; + } + + cssLines.push("}"); + + // Ensure output directory exists + const outputDir = path.dirname(outputPath); + if (!fs.existsSync(outputDir)) { + fs.mkdirSync(outputDir, { recursive: true }); + } + + // Write the file + fs.writeFileSync(outputPath, cssLines.join("\n") + "\n"); + console.log("āœ… Theme CSS generated successfully at:", outputPath); + } catch (error) { + throw new Error(`Failed to generate theme CSS: ${error.message}`); + } +} + +// Generate CSS on startup +try { + generateThemeCSS(); +} catch (error) { + console.error("āŒ Error:", error.message); + process.exit(1); +} + +// Check for --watch flag +if (process.argv.includes("--watch")) { + let debounceTimer; + + const watcher = fs.watch(themePath, (eventType) => { + if (eventType === "change") { + // Debounce to avoid multiple triggers + clearTimeout(debounceTimer); + debounceTimer = setTimeout(() => { + try { + generateThemeCSS(); + } catch (error) { + console.error("āŒ Error regenerating theme CSS:", error.message); + } + }, 300); + } + }); + + // Handle graceful shutdown + process.on("SIGINT", () => { + clearTimeout(debounceTimer); + watcher.close(); + console.log("\nšŸ‘‹ Watcher stopped"); + process.exit(0); + }); + + console.log("šŸ‘ļø Watching for changes to:", themePath); +} diff --git a/src/styles/generated-theme.css b/src/styles/generated-theme.css new file mode 100644 index 0000000..f71a150 --- /dev/null +++ b/src/styles/generated-theme.css @@ -0,0 +1,47 @@ +/** + * Auto-generated from "src/config/theme.json" + * DO NOT EDIT THIS FILE MANUALLY + * Run: node scripts/themeGenerator.js + */ + +@theme { + /* === Colors === */ + --color-primary: #121212; + --color-body: #fff; + --color-border: #eaeaea; + --color-light: #f6f6f6; + --color-dark: #040404; + --color-text: #444444; + --color-text-dark: #040404; + --color-text-light: #717171; + + /* === Darkmode Colors === */ + --color-darkmode-primary: #fff; + --color-darkmode-body: #1c1c1c; + --color-darkmode-border: #3E3E3E; + --color-darkmode-light: #222222; + --color-darkmode-dark: #fff; + --color-darkmode-text: #B4AFB6; + --color-darkmode-text-dark: #fff; + --color-darkmode-text-light: #B4AFB6; + + /* === Font Families === */ + --font-primary: Heebo, sans-serif; + --font-secondary: Signika, sans-serif; + + /* === Font Sizes === */ + --text-base: 16px; + --text-base-sm: 12.8px; + --text-h6: 1.2rem; + --text-h6-sm: 1.08rem; + --text-h5: 1.44rem; + --text-h5-sm: 1.296rem; + --text-h4: 1.728rem; + --text-h4-sm: 1.5552rem; + --text-h3: 2.0736rem; + --text-h3-sm: 1.86624rem; + --text-h2: 2.48832rem; + --text-h2-sm: 2.239488rem; + --text-h1: 2.9859839999999997rem; + --text-h1-sm: 2.6873856rem; +} diff --git a/src/styles/main.css b/src/styles/main.css index 0d32c31..5aa27b6 100755 --- a/src/styles/main.css +++ b/src/styles/main.css @@ -1,11 +1,12 @@ @import "tailwindcss"; -@plugin "../tailwind-plugin/tw-theme"; -@plugin "../tailwind-plugin/tw-bs-grid"; @plugin "@tailwindcss/forms"; @plugin "@tailwindcss/typography"; - +@plugin 'tailwind-bootstrap-grid'; @custom-variant dark (&:where(.dark, .dark *)); +/* Auto-generated theme from "theme.json"*/ +@import "./generated-theme.css"; + @import "./safe.css"; @import "./utilities.css"; diff --git a/src/tailwind-plugin/tw-bs-grid.js b/src/tailwind-plugin/tw-bs-grid.js deleted file mode 100644 index f91675e..0000000 --- a/src/tailwind-plugin/tw-bs-grid.js +++ /dev/null @@ -1,122 +0,0 @@ -const plugin = require("tailwindcss/plugin"); - -module.exports = plugin.withOptions(() => { - return ({ addComponents }) => { - const gridColumns = 12; - const gridGutterWidth = "1.5rem"; - const gridGutters = { - 0: "0", - 1: "0.25rem", - 2: "0.5rem", - 3: "1rem", - 4: "1.5rem", - 5: "3rem", - }; - const respectImportant = true; - const columns = Array.from({ length: gridColumns }, (_, i) => i + 1); - const rowColsSteps = columns.slice(0, Math.floor(gridColumns / 2)); - - // Row - addComponents( - { - ".row": { - "--bs-gutter-x": gridGutterWidth, - "--bs-gutter-y": "0", - display: "flex", - flexWrap: "wrap", - marginTop: "calc(var(--bs-gutter-y) * -1)", - marginRight: "calc(var(--bs-gutter-x) / -2)", - marginLeft: "calc(var(--bs-gutter-x) / -2)", - "> *": { - boxSizing: "border-box", - flexShrink: "0", - width: "100%", - maxWidth: "100%", - paddingRight: "calc(var(--bs-gutter-x) / 2)", - paddingLeft: "calc(var(--bs-gutter-x) / 2)", - marginTop: "var(--bs-gutter-y)", - }, - }, - }, - { respectImportant }, - ); - - // Columns + helper row-cols - addComponents([ - { - ".col": { - flex: "1 0 0%", - width: "initial", - display: "initial", - }, - ".row-cols-auto": { - "> *": { - flex: "0 0 auto", - width: "auto", - }, - }, - }, - ...rowColsSteps.map((rowCol) => ({ - [`.row-cols-${rowCol}`]: { - "> *": { - flex: "0 0 auto", - width: `${100 / rowCol}%`, - display: "initial", - }, - }, - })), - { - ".col-auto": { - flex: "0 0 auto", - width: "auto", - }, - }, - // explicit sized columns - ...columns.map((size) => ({ - [`.col-${size}`]: { - flex: "0 0 auto", - width: `${(100 / gridColumns) * size}%`, - }, - })), - ]); - - // Offsets - addComponents( - [0, ...columns.slice(0, -1)].map((num) => ({ - [`.offset-${num}`]: { marginLeft: `${(100 / gridColumns) * num}%` }, - })), - { respectImportant }, - ); - - // Gutters - if (Object.keys(gridGutters).length) { - const gutterComponents = Object.entries(gridGutters).reduce( - (acc, [key, value]) => { - acc[`.g-${key}`] = { - "--bs-gutter-x": value, - "--bs-gutter-y": value, - }; - acc[`.gx-${key}`] = { "--bs-gutter-x": value }; - acc[`.gy-${key}`] = { "--bs-gutter-y": value }; - return acc; - }, - {}, - ); - addComponents(gutterComponents, { respectImportant }); - } - - // Ordering helpers - addComponents( - [ - { - ".order-first": { order: "-1" }, - ".order-last": { order: String(gridColumns + 1) }, - }, - ...[0, ...columns].map((num) => ({ - [`.order-${num}`]: { order: String(num) }, - })), - ], - { respectImportant }, - ); - }; -}); diff --git a/src/tailwind-plugin/tw-theme.js b/src/tailwind-plugin/tw-theme.js deleted file mode 100644 index 88a5529..0000000 --- a/src/tailwind-plugin/tw-theme.js +++ /dev/null @@ -1,139 +0,0 @@ -const plugin = require("tailwindcss/plugin"); -const themeConfig = require("../config/theme.json"); - -// Helper to extract a clean font name. -const findFont = (fontStr) => - fontStr.replace(/\+/g, " ").replace(/:[^:]+/g, ""); - -// Set font families dynamically, filtering out 'type' keys -const fontFamilies = Object.entries(themeConfig.fonts.font_family) - .filter(([key]) => !key.includes("type")) - .reduce((acc, [key, font]) => { - acc[key] = - `${findFont(font)}, ${themeConfig.fonts.font_family[`${key}_type`] || "sans-serif"}`; - return acc; - }, {}); - -const defaultColorGroups = [ - { colors: themeConfig.colors.default.theme_color, prefix: "" }, - { colors: themeConfig.colors.default.text_color, prefix: "" }, -]; -const darkColorGroups = []; -if (themeConfig.colors.darkmode?.theme_color) { - darkColorGroups.push({ - colors: themeConfig.colors.darkmode.theme_color, - prefix: "darkmode-", - }); -} -if (themeConfig.colors.darkmode?.text_color) { - darkColorGroups.push({ - colors: themeConfig.colors.darkmode.text_color, - prefix: "darkmode-", - }); -} - -const getVars = (groups) => { - const vars = {}; - groups.forEach(({ colors, prefix }) => { - Object.entries(colors).forEach(([k, v]) => { - const cssKey = k.replace(/_/g, "-"); - vars[`--color-${prefix}${cssKey}`] = v; - }); - }); - return vars; -}; - -const defaultVars = getVars(defaultColorGroups); -const darkVars = getVars(darkColorGroups); - -const baseSize = Number(themeConfig.fonts.font_size.base); -const scale = Number(themeConfig.fonts.font_size.scale); -const calculateFontSizes = (base, scale) => { - const sizes = {}; - let currentSize = scale; - for (let i = 6; i >= 1; i--) { - sizes[`h${i}`] = `${currentSize}rem`; - sizes[`h${i}-sm`] = `${currentSize * 0.9}rem`; - currentSize *= scale; - } - sizes.base = `${base}px`; - sizes["base-sm"] = `${base * 0.8}px`; - return sizes; -}; -const fontSizes = calculateFontSizes(baseSize, scale); - -const fontVars = {}; -Object.entries(fontSizes).forEach(([key, value]) => { - fontVars[`--text-${key}`] = value; -}); -Object.entries(fontFamilies).forEach(([key, font]) => { - fontVars[`--font-${key}`] = font; -}); - -const baseVars = { ...fontVars, ...defaultVars }; - -// Build a colorsMap including both sets -const colorsMap = {}; -[...defaultColorGroups, ...darkColorGroups].forEach(({ colors, prefix }) => { - Object.entries(colors).forEach(([key]) => { - const cssKey = key.replace(/_/g, "-"); - colorsMap[prefix + cssKey] = `var(--color-${prefix}${cssKey})`; - }); -}); - -module.exports = plugin.withOptions(() => { - return function ({ addBase, addUtilities, matchUtilities }) { - // Default vars on :root; dark vars on .dark - addBase({ - ":root": baseVars, - ".dark": darkVars, - }); - - const fontUtils = {}; - Object.keys(fontFamilies).forEach((key) => { - fontUtils[`.font-${key}`] = { fontFamily: `var(--font-${key})` }; - }); - Object.keys(fontSizes).forEach((key) => { - fontUtils[`.text-${key}`] = { fontSize: `var(--text-${key})` }; - }); - addUtilities(fontUtils, { - variants: ["responsive", "hover", "focus", "active", "disabled"], - }); - - matchUtilities( - { - bg: (value) => ({ backgroundColor: value }), - text: (value) => ({ color: value }), - border: (value) => ({ borderColor: value }), - fill: (value) => ({ fill: value }), - stroke: (value) => ({ stroke: value }), - }, - { values: colorsMap, type: "color" }, - ); - - matchUtilities( - { - from: (value) => ({ - "--tw-gradient-from": value, - "--tw-gradient-via-stops": - "var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))", - "--tw-gradient-stops": - "var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))", - }), - to: (value) => ({ - "--tw-gradient-to": value, - "--tw-gradient-via-stops": - "var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))", - "--tw-gradient-stops": - "var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))", - }), - via: (value) => ({ - "--tw-gradient-via": value, - "--tw-gradient-via-stops": - "var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position)", - }), - }, - { values: colorsMap, type: "color" }, - ); - }; -});