* Add mod_custom hero layout override and bump version to 03.09.01 Adds src/html/mod_custom/hero.php — a banner-overlay style template override for mod_custom, mirroring Cassiopeia's banner layout pattern. Includes background image support via WebAssetManager and respects the Module Manager's moduleclass_sfx field. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Ignore and untrack .claude/settings.local.json Adds .claude/settings.local.json to .gitignore and removes it from version control to keep local Claude Code permissions out of the repo. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Ship custom palette starters and update template description - Add src/templates/light.custom.css and dark.custom.css as starter palette files that ship with the template, giving users a full variable reference to copy and customise - Register src/templates/ folder in templateDetails.xml <files> - Update <description> in templateDetails.xml: correct palette source paths, add Custom CSS & JavaScript section (user.css / user.js), link docs to GitHub repo docs/ directory - Sync en-GB and en-US tpl_mokocassiopeia.sys.ini with same changes, preserving British/American spelling variants; bump version to 03.09.01 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Add CSS Variables reference tab to template options Adds a new 'CSS Variables' tab to the template configuration with eight documented sections (brand, typography, navigation, header background, container backgrounds, borders/shadows, forms/focus) so site builders can reference all available custom properties without leaving Joomla admin. Also removes external docs links from descriptions in templateDetails.xml and both language files, replacing them with a pointer to the new tab. Fixes stale custom palette source paths in en-GB and en-US ini files. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Expand CSS Variables tab to full variable reference and add custom-hero class - Replace 8-field CSS Variables tab with 21 comprehensive sections covering all variables from light.standard.css and dark.standard.css - New sections: Links, Layout & Spacing, Breakpoints, Bootstrap Semantic Palette, Bootstrap State Colors, Alert & List Group Colors, Standard Colors/Grays/Opacity, Shadows & Shadow Tokens, Buttons, Cards, Component & Plugin Colors, VirtueMart, Gable - Add custom-hero class to hero.php outer div (always present) - Both en-GB and en-US language files updated Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Add Google Search Console verification and ensure all Google services coexist - Add googlesitekey param to Google fieldset in templateDetails.xml - Inject <meta name="google-site-verification"> via setMetaData() in index.php, component.php, and offline.php - GTM, GA, and Search Console verification can now all be active simultaneously - Add language strings for new field in en-US and en-GB Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Add header-aside module position to the right of the logo - New position renders inside .header-brand-wrap, right-aligned via margin-inline-start: auto on .container-header-aside - CSS: .header-brand-wrap uses flexbox so logo stays left, aside floats right - Registered in templateDetails.xml positions list - Language strings added to both en-US and en-GB sys.ini files Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Add minify build script and generate .min CSS/JS; rename position to brand-aside Build tooling: - Add package.json with clean-css and terser dev dependencies - Add scripts/minify.js: reads joomla.asset.json, auto-detects source/.min pairs, and minifies all template-owned CSS and JS files - Add node_modules/ to .gitignore Generated .min files (all 6 manifest pairs): - css/template.min.css (17.8% saved) - css/editor.min.css (49.4% saved) - css/theme/light.standard.min.css (13.1% saved) - css/theme/dark.standard.min.css (14.4% saved) - js/template.min.js (58.2% saved) - js/gtm.min.js (62.3% saved) Rename: header-aside → brand-aside (position, CSS class, language keys) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Add hero/banner-overlay CSS variables and wire template.css - Add HERO / BANNER OVERLAY section to light.standard.css and dark.standard.css: --hero-height, --hero-color, --hero-bg-repeat, --hero-bg-attachment, --hero-bg-position, --hero-bg-size, --hero-border-bottom, --hero-overlay-bg (light: 0.1 alpha / dark: 0.3 alpha), --hero-overlay-padding, --hero-overlay-text-align, --hero-overlay-text-color - Replace all hardcoded values in .container-banner .banner-overlay and .overlay with var() references (with fallbacks) - Fix background-position: comma syntax → correct space-separated single-bg value - Add css_vars_hero note field to CSS Variables tab in templateDetails.xml - Add TPL_MOKOCASSIOPEIA_CSS_VARS_HERO_LABEL/DESC to en-US and en-GB - Regenerate .min files Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Add smooth theme-switch transitions and restore hero .overlay wrapper - Add prefers-reduced-motion-scoped CSS transitions (bg, color, border) on :root, body, and key layout containers so light/dark theme switches animate smoothly instead of snapping - Restore <div class="overlay"> child in hero.php; slim .custom-hero rule to a customisation hook only — visual overlay styles are handled by .overlay child - Regenerate template.min.css Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Update template.css * Merge duplicate prefers-reduced-motion media queries Consolidate the two @media (prefers-reduced-motion: no-preference) blocks into one — scroll-behavior and theme-switch colour transitions share the same query and are cleaner in a single block. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Promote offcanvas variables to :root theme files and document in CSS Variables tab - Move --offcanvas-* definitions from component-scoped .offcanvas selector in template.css into :root[data-bs-theme] blocks in light.standard.css and dark.standard.css so they are overridable via user.css at root level - Fix two bugs in the old definitions: --offcanvas-bg was incorrectly set to var(--body-color) (text colour); corrected to var(--body-bg); and --offcanvas-color had a spurious 'color:' prefix - Dark theme uses a heavier box-shadow (0.3 alpha) for better depth perception - Add css_vars_offcanvas field to templateDetails.xml CSS Variables tab - Add en-US and en-GB language strings for the new Offcanvas Panel section - Rebuild all .min CSS files Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Promote Bootstrap component variables from template.css to :root theme files Move 16 component variable groups from component-scoped selectors in template.css into :root[data-bs-theme] blocks in light.standard.css and dark.standard.css: accordion, breadcrumb, pagination, badge, alert, progress, list-group, dropdown, toast, modal, tooltip, popover, spinner, nav, nav-tabs, nav-pills Dark theme values adapted for dark surfaces: semantic var() references, lighter SVG icon fill colours, heavier shadows, secondary-bg backgrounds. Component selectors in template.css retain only non-variable rules. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Promote table and backdrop variables to :root theme files Move --table-* and --backdrop-* base definitions from component selectors in template.css into :root[data-bs-theme] blocks in light/dark theme files. Dark table uses white-rgb-based striped/active overlays for proper contrast on dark surfaces. Deduplicate the double --table-active-* declarations that existed in template.css. Backdrop values are identical in both themes. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Add CSS Variables tab documentation for all promoted Bootstrap components Add LABEL/DESC language strings (en-US + en-GB) for all 17 Bootstrap component variable groups now living in the :root theme files: accordion, alert, badge, backdrop, breadcrumb, dropdown, list-group, modal, nav-tabs, nav-pills, pagination, popover, progress, spinner, table, toast, tooltip Each section documents variables with HTML subheadings (Dimensions, Colours, Typography, Stacking, Animation) and <code> tags for every variable name. British English spellings used throughout en-GB. Adds 34 new lines per language file (17 LABEL + 17 DESC pairs, 80 CSS_VARS_* keys total). XML fields were already present from the prior migration commit. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
189 lines
5.7 KiB
JavaScript
189 lines
5.7 KiB
JavaScript
#!/usr/bin/env node
|
|
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
*
|
|
* This file is part of a Moko Consulting project.
|
|
*
|
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
*
|
|
* # FILE INFORMATION
|
|
* DEFGROUP: Joomla.Template.Site
|
|
* INGROUP: MokoCassiopeia
|
|
* REPO: https://github.com/mokoconsulting-tech/MokoCassiopeia
|
|
* PATH: ./scripts/minify.js
|
|
* VERSION: 03.09.01
|
|
* BRIEF: Generates .min.css and .min.js files from the Joomla asset manifest
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
const CleanCSS = require('clean-css');
|
|
const { minify: terserMinify } = require('terser');
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Config
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const ROOT = path.resolve(__dirname, '..');
|
|
const SRC_MEDIA = path.join(ROOT, 'src', 'media');
|
|
const ASSET_JSON = path.join(ROOT, 'src', 'joomla.asset.json');
|
|
|
|
// URI prefix used in the manifest — maps to SRC_MEDIA on disk.
|
|
// e.g. "media/templates/site/mokocassiopeia/css/template.css"
|
|
const URI_PREFIX = 'media/templates/site/mokocassiopeia/';
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Helpers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Resolve a manifest URI to an absolute disk path under src/media/.
|
|
*
|
|
* @param {string} uri e.g. "media/templates/site/mokocassiopeia/css/foo.css"
|
|
* @returns {string|null}
|
|
*/
|
|
function uriToPath(uri) {
|
|
if (!uri.startsWith(URI_PREFIX)) return null;
|
|
return path.join(SRC_MEDIA, uri.slice(URI_PREFIX.length));
|
|
}
|
|
|
|
/**
|
|
* Return true if the filename looks like an already-minified file or belongs
|
|
* to a vendor bundle we don't own.
|
|
*/
|
|
function isVendorOrUserFile(filePath) {
|
|
const rel = filePath.replace(SRC_MEDIA + path.sep, '');
|
|
return rel.startsWith('vendor' + path.sep)
|
|
|| path.basename(filePath).startsWith('user.');
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Pair detection
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Read the asset manifest and return an array of { src, dest, type } pairs
|
|
* where dest is a minified version of src that doesn't already exist or is
|
|
* older than src.
|
|
*
|
|
* Pairing logic: for every non-.min asset, check whether the manifest also
|
|
* contains a corresponding .min asset. If so, that's our pair.
|
|
*/
|
|
function detectPairs(assets) {
|
|
// Build a lookup of all URIs in the manifest.
|
|
const uriSet = new Set(assets.map(a => a.uri));
|
|
|
|
const pairs = [];
|
|
|
|
for (const asset of assets) {
|
|
const { uri, type } = asset;
|
|
if (type !== 'style' && type !== 'script') continue;
|
|
|
|
// Skip already-minified entries.
|
|
if (/\.min\.(css|js)$/.test(uri)) continue;
|
|
|
|
// Derive the expected .min URI.
|
|
const minUri = uri.replace(/\.(css|js)$/, '.min.$1');
|
|
if (!uriSet.has(minUri)) continue;
|
|
|
|
const srcPath = uriToPath(uri);
|
|
const destPath = uriToPath(minUri);
|
|
if (!srcPath || !destPath) continue;
|
|
|
|
if (isVendorOrUserFile(srcPath)) continue;
|
|
|
|
if (!fs.existsSync(srcPath)) {
|
|
console.warn(` [skip] source missing: ${srcPath}`);
|
|
continue;
|
|
}
|
|
|
|
pairs.push({ src: srcPath, dest: destPath, type });
|
|
}
|
|
|
|
return pairs;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Minifiers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
async function minifyCSS(srcPath, destPath) {
|
|
const source = fs.readFileSync(srcPath, 'utf8');
|
|
const result = new CleanCSS({ level: 2, returnPromise: true });
|
|
const output = await result.minify(source);
|
|
|
|
if (output.errors && output.errors.length) {
|
|
throw new Error(output.errors.join('\n'));
|
|
}
|
|
|
|
fs.mkdirSync(path.dirname(destPath), { recursive: true });
|
|
fs.writeFileSync(destPath, output.styles, 'utf8');
|
|
|
|
const srcSize = Buffer.byteLength(source, 'utf8');
|
|
const destSize = Buffer.byteLength(output.styles, 'utf8');
|
|
const saving = (100 - (destSize / srcSize * 100)).toFixed(1);
|
|
|
|
return { srcSize, destSize, saving };
|
|
}
|
|
|
|
async function minifyJS(srcPath, destPath) {
|
|
const source = fs.readFileSync(srcPath, 'utf8');
|
|
const result = await terserMinify(source, {
|
|
compress: { drop_console: false },
|
|
mangle: true,
|
|
format: { comments: false }
|
|
});
|
|
|
|
if (!result.code) throw new Error('terser returned no output');
|
|
|
|
fs.mkdirSync(path.dirname(destPath), { recursive: true });
|
|
fs.writeFileSync(destPath, result.code, 'utf8');
|
|
|
|
const srcSize = Buffer.byteLength(source, 'utf8');
|
|
const destSize = Buffer.byteLength(result.code, 'utf8');
|
|
const saving = (100 - (destSize / srcSize * 100)).toFixed(1);
|
|
|
|
return { srcSize, destSize, saving };
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Main
|
|
// ---------------------------------------------------------------------------
|
|
|
|
(async () => {
|
|
const manifest = JSON.parse(fs.readFileSync(ASSET_JSON, 'utf8'));
|
|
const pairs = detectPairs(manifest.assets);
|
|
|
|
if (pairs.length === 0) {
|
|
console.log('No pairs found — nothing to minify.');
|
|
return;
|
|
}
|
|
|
|
console.log(`\nMinifying ${pairs.length} file(s)...\n`);
|
|
|
|
let ok = 0, fail = 0;
|
|
|
|
for (const { src, dest, type } of pairs) {
|
|
const label = path.relative(ROOT, src);
|
|
process.stdout.write(` ${label} ... `);
|
|
|
|
try {
|
|
const stats = type === 'style'
|
|
? await minifyCSS(src, dest)
|
|
: await minifyJS(src, dest);
|
|
|
|
const kb = n => (n / 1024).toFixed(1) + ' kB';
|
|
console.log(`${kb(stats.srcSize)} → ${kb(stats.destSize)} (${stats.saving}% saved)`);
|
|
ok++;
|
|
} catch (err) {
|
|
console.error(`FAILED\n ${err.message}`);
|
|
fail++;
|
|
}
|
|
}
|
|
|
|
console.log(`\nDone. ${ok} succeeded, ${fail} failed.\n`);
|
|
if (fail > 0) process.exit(1);
|
|
})();
|