Files
MokoCassiopeia/scripts/minify.js
Jonathan Miller 97d3133843 03.09.01 — mod_custom hero override, palette starter files, updated descriptions (#84)
* 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>
2026-03-24 16:48:07 -05:00

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);
})();