Add user.css, minified files, and unit tests for AssetMinifier

Co-authored-by: jmiller-moko <230051081+jmiller-moko@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-01-24 02:43:52 +00:00
parent a2f68d7f66
commit 739afc08f0
20 changed files with 250 additions and 44 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

1
src/media/css/editor.min.css vendored Normal file
View File

@@ -0,0 +1 @@
@charset "UTF-8";body{font-size:1rem;font-weight:400;line-height:1.5;color:#22262a;background-color:#fff}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:0.5rem;font-weight:700;line-height:1.2}h1{font-size:calc(1.375rem + 1.5vw)}h2{font-size:calc(1.325rem + 0.9vw)}h3{font-size:calc(1.3rem + 0.6vw)}h4{font-size:calc(1.275rem + 0.3vw)}h5{font-size:1.25rem}h6{font-size:1rem}a{text-decoration:none}a:link{color:#224faa}a:hover{color:#424077}p{margin-top:0;margin-bottom:1rem}hr#system-readmore{color:#f00;border:#f00 dashed 1px}span[lang]{padding:2px;border:1px dashed #bbb}span[lang]:after{font-size:smaller;color:#f00;vertical-align:super;content:attr(lang)}

1
src/media/css/template.min.css vendored Normal file

File diff suppressed because one or more lines are too long

19
src/media/css/user.css Normal file
View File

@@ -0,0 +1,19 @@
/* Copyright (C) 2025 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: Moko-Cassiopeia
REPO: https://github.com/mokoconsulting-tech/moko-cassiopeia
PATH: ./media/css/user.css
VERSION: 03.05.00
BRIEF: User custom styles - add your custom CSS here
*/
/**
* This file is intentionally empty and available for user customizations.
* Add your custom CSS rules here to override template styles.
*/

0
src/media/css/user.min.css vendored Normal file
View File

1
src/media/js/darkmode-toggle.min.js vendored Normal file
View File

@@ -0,0 +1 @@
(function(){'use strict';var STORAGE_KEY='theme';var docEl=document.documentElement;var mql=window.matchMedia('(prefers-color-scheme: dark)');function getStored(){try{return localStorage.getItem(STORAGE_KEY);}catch(e){return null;}}function setStored(v){try{localStorage.setItem(STORAGE_KEY,v);}catch(e){}}function clearStored(){try{localStorage.removeItem(STORAGE_KEY);}catch(e){}}function systemTheme(){return mql.matches ? 'dark' : 'light';}function applyTheme(theme){docEl.setAttribute('data-bs-theme',theme);docEl.setAttribute('data-aria-theme',theme);var meta=document.querySelector('meta[name="theme-color"]');if(meta){meta.setAttribute('content',theme==='dark' ? '#0f1115' : '#ffffff');}var sw=document.getElementById('mokoThemeSwitch');if(sw){sw.setAttribute('aria-checked',theme==='dark' ? 'true' : 'false');}}function initTheme(){var stored=getStored();applyTheme(stored ? stored : systemTheme());}function posClassFromBody(){var pos=(document.body.getAttribute('data-theme-fab-pos')||'br').toLowerCase();if(!/^(br|bl|tr|tl)$/.test(pos))pos='br';return 'pos-'+pos;}function buildToggle(){if(document.getElementById('mokoThemeFab'))return;var wrap=document.createElement('div');wrap.id='mokoThemeFab';wrap.className=posClassFromBody();var lblL=document.createElement('span');lblL.className='label';lblL.textContent='Light';var switchWrap=document.createElement('button');switchWrap.id='mokoThemeSwitch';switchWrap.type='button';switchWrap.setAttribute('role','switch');switchWrap.setAttribute('aria-label','Toggle dark mode');switchWrap.setAttribute('aria-checked','false');var track=document.createElement('span');track.className='switch';var knob=document.createElement('span');knob.className='knob';track.appendChild(knob);switchWrap.appendChild(track);var lblD=document.createElement('span');lblD.className='label';lblD.textContent='Dark';var auto=document.createElement('button');auto.id='mokoThemeAuto';auto.type='button';auto.className='btn btn-sm btn-link text-decoration-none px-2';auto.setAttribute('aria-label','Follow system theme');auto.textContent='Auto';switchWrap.addEventListener('click',function(){var current=(docEl.getAttribute('data-bs-theme')||'light').toLowerCase();var next=current==='dark' ? 'light' : 'dark';applyTheme(next);setStored(next);});auto.addEventListener('click',function(){clearStored();applyTheme(systemTheme());});var onMql=function(){if(!getStored())applyTheme(systemTheme());};if(typeof mql.addEventListener==='function')mql.addEventListener('change',onMql);else if(typeof mql.addListener==='function')mql.addListener(onMql);var initial=getStored()||systemTheme();switchWrap.setAttribute('aria-checked',initial==='dark' ? 'true' : 'false');wrap.appendChild(lblL);wrap.appendChild(switchWrap);wrap.appendChild(lblD);wrap.appendChild(auto);document.body.appendChild(wrap);window.mokoThemeFabStatus=function(){var el=document.getElementById('mokoThemeFab');if(!el)return{mounted: false};var r=el.getBoundingClientRect();return{mounted: true,rect:{top: r.top,left: r.left,width: r.width,height: r.height},zIndex: window.getComputedStyle(el).zIndex,posClass: el.className};};setTimeout(function(){var r=wrap.getBoundingClientRect();if(r.width<10||r.height<10){wrap.classList.add('debug-outline');console.warn('[moko]Theme FAB mounted but appears too small — check CSS collisions.');}},50);}function init(){initTheme();buildToggle();}if(document.readyState==='loading'){document.addEventListener('DOMContentLoaded',init);}else{init();}})();

1
src/media/js/gtm.min.js vendored Normal file

File diff suppressed because one or more lines are too long

1
src/media/js/template.min.js vendored Normal file
View File

@@ -0,0 +1 @@
(function(win,doc){"use strict";function backToTop(){win.scrollTo({top: 0,behavior: "smooth"});}function handleScroll(){if(win.scrollY>50){doc.body.classList.add("scrolled");}else{doc.body.classList.remove("scrolled");}}function initTOC(){if(typeof win.Toc==="function"&&doc.querySelector("#toc")){win.Toc.init({$nav: $("#toc"),$scope: $("main")});}}function initDrawers(){var leftBtn=doc.querySelector(".drawer-toggle-left");var rightBtn=doc.querySelector(".drawer-toggle-right");if(leftBtn){leftBtn.addEventListener("click",function(){var target=doc.querySelector(leftBtn.getAttribute("data-bs-target"));if(target)new bootstrap.Offcanvas(target).show();});}if(rightBtn){rightBtn.addEventListener("click",function(){var target=doc.querySelector(rightBtn.getAttribute("data-bs-target"));if(target)new bootstrap.Offcanvas(target).show();});}}function initBackTop(){var backTop=doc.getElementById("back-top");if(backTop){backTop.addEventListener("click",function(e){e.preventDefault();backToTop();});}}function init(){handleScroll();win.addEventListener("scroll",handleScroll);initTOC();initDrawers();initBackTop();}if(doc.readyState==="loading"){doc.addEventListener("DOMContentLoaded",init);}else{init();}})(window,document);

1
src/media/js/theme-init.min.js vendored Normal file
View File

@@ -0,0 +1 @@
(function(win,doc){"use strict";var storageKey="theme";var mql=win.matchMedia("(prefers-color-scheme: dark)");var root=doc.documentElement;function applyTheme(theme){root.setAttribute("data-bs-theme",theme);root.setAttribute("data-aria-theme",theme);try{localStorage.setItem(storageKey,theme);}catch(e){}}function clearStored(){try{localStorage.removeItem(storageKey);}catch(e){}}function systemTheme(){return mql.matches ? "dark" : "light";}function init(){var stored=null;try{stored=localStorage.getItem(storageKey);}catch(e){}var theme=stored ? stored : systemTheme();applyTheme(theme);var onChange=function(){if(!localStorage.getItem(storageKey)){applyTheme(systemTheme());}};if(typeof mql.addEventListener==="function"){mql.addEventListener("change",onChange);}else if(typeof mql.addListener==="function"){mql.addListener(onChange);}var switchEl=doc.getElementById("themeSwitch");var autoBtn=doc.getElementById("themeAuto");if(switchEl){switchEl.checked=(theme==="dark");switchEl.addEventListener("change",function(){var choice=switchEl.checked ? "dark" : "light";applyTheme(choice);});}if(autoBtn){autoBtn.addEventListener("click",function(){clearStored();applyTheme(systemTheme());});}}if(doc.readyState==="loading"){doc.addEventListener("DOMContentLoaded",init);}else{init();}})(window,document);

View File

@@ -1,4 +0,0 @@
/*!
* Bootstrap Table of Contents v1.0.1 (http://afeld.github.io/bootstrap-toc/)
* Copyright 2015 Aidan Feldman
* Licensed under MIT (https://github.com/afeld/bootstrap-toc/blob/gh-pages/LICENSE.md) */nav[data-toggle=toc] .nav>li>a{display:block;padding:4px 20px;font-size:13px;font-weight:500;color:#767676}nav[data-toggle=toc] .nav>li>a:focus,nav[data-toggle=toc] .nav>li>a:hover{padding-left:19px;color:#563d7c;text-decoration:none;background-color:transparent;border-left:1px solid #563d7c}nav[data-toggle=toc] .nav-link.active,nav[data-toggle=toc] .nav-link.active:focus,nav[data-toggle=toc] .nav-link.active:hover{padding-left:18px;font-weight:700;color:#563d7c;background-color:transparent;border-left:2px solid #563d7c}nav[data-toggle=toc] .nav-link+ul{display:none;padding-bottom:10px}nav[data-toggle=toc] .nav .nav>li>a{padding-top:1px;padding-bottom:1px;padding-left:30px;font-size:12px;font-weight:400}nav[data-toggle=toc] .nav .nav>li>a:focus,nav[data-toggle=toc] .nav .nav>li>a:hover{padding-left:29px}nav[data-toggle=toc] .nav .nav>li>.active,nav[data-toggle=toc] .nav .nav>li>.active:focus,nav[data-toggle=toc] .nav .nav>li>.active:hover{padding-left:28px;font-weight:500}nav[data-toggle=toc] .nav-link.active+ul{display:block}

View File

@@ -1,5 +0,0 @@
/*!
* Bootstrap Table of Contents v1.0.1 (http://afeld.github.io/bootstrap-toc/)
* Copyright 2015 Aidan Feldman
* Licensed under MIT (https://github.com/afeld/bootstrap-toc/blob/gh-pages/LICENSE.md) */
!function(a){"use strict";window.Toc={helpers:{findOrFilter:function(e,t){var n=e.find(t);return e.filter(t).add(n).filter(":not([data-toc-skip])")},generateUniqueIdBase:function(e){return a(e).text().trim().replace(/\'/gi,"").replace(/[& +$,:;=?@"#{}|^~[`%!'<>\]\.\/\(\)\*\\\n\t\b\v]/g,"-").replace(/-{2,}/g,"-").substring(0,64).replace(/^-+|-+$/gm,"").toLowerCase()||e.tagName.toLowerCase()},generateUniqueId:function(e){for(var t=this.generateUniqueIdBase(e),n=0;;n++){var r=t;if(0<n&&(r+="-"+n),!document.getElementById(r))return r}},generateAnchor:function(e){if(e.id)return e.id;var t=this.generateUniqueId(e);return e.id=t},createNavList:function(){return a('<ul class="nav navbar-nav"></ul>')},createChildNavList:function(e){var t=this.createNavList();return e.append(t),t},generateNavEl:function(e,t){var n=a('<a class="nav-link"></a>');n.attr("href","#"+e),n.text(t);var r=a("<li></li>");return r.append(n),r},generateNavItem:function(e){var t=this.generateAnchor(e),n=a(e),r=n.data("toc-text")||n.text();return this.generateNavEl(t,r)},getTopLevel:function(e){for(var t=1;t<=6;t++){if(1<this.findOrFilter(e,"h"+t).length)return t}return 1},getHeadings:function(e,t){var n="h"+t,r="h"+(t+1);return this.findOrFilter(e,n+","+r)},getNavLevel:function(e){return parseInt(e.tagName.charAt(1),10)},populateNav:function(r,a,e){var i,s=r,c=this;e.each(function(e,t){var n=c.generateNavItem(t);c.getNavLevel(t)===a?s=r:i&&s===r&&(s=c.createChildNavList(i)),s.append(n),i=n})},parseOps:function(e){var t;return(t=e.jquery?{$nav:e}:e).$scope=t.$scope||a(document.body),t}},init:function(e){(e=this.helpers.parseOps(e)).$nav.attr("data-toggle","toc");var t=this.helpers.createChildNavList(e.$nav),n=this.helpers.getTopLevel(e.$scope),r=this.helpers.getHeadings(e.$scope,n);this.helpers.populateNav(t,n,r)}},a(function(){a('nav[data-toggle="toc"]').each(function(e,t){var n=a(t);Toc.init(n)})})}(jQuery);

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,6 +0,0 @@
/*!
* Font Awesome Free 7.1.0 by @fontawesome - https://fontawesome.com
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
* Copyright 2025 Fonticons, Inc.
*/
:host,:root{--fa-family-classic:"Font Awesome 7 Free";--fa-font-regular:normal 400 1em/1 var(--fa-family-classic);--fa-style-family-classic:var(--fa-family-classic)}@font-face{font-family:"Font Awesome 7 Free";font-style:normal;font-weight:400;font-display:block;src:url(../webfonts/fa-regular-400.woff2)}.far{--fa-style:400}.fa-classic,.far{--fa-family:var(--fa-family-classic)}.fa-regular{--fa-style:400}

View File

@@ -1,6 +0,0 @@
/*!
* Font Awesome Free 7.1.0 by @fontawesome - https://fontawesome.com
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
* Copyright 2025 Fonticons, Inc.
*/
:host,:root{--fa-family-classic:"Font Awesome 7 Free";--fa-font-solid:normal 900 1em/1 var(--fa-family-classic);--fa-style-family-classic:var(--fa-family-classic)}@font-face{font-family:"Font Awesome 7 Free";font-style:normal;font-weight:900;font-display:block;src:url(../webfonts/fa-solid-900.woff2)}.fas{--fa-style:900}.fa-classic,.fas{--fa-family:var(--fa-family-classic)}.fa-solid{--fa-style:900}

View File

@@ -0,0 +1,221 @@
<?php
namespace Tests\Unit;
use Codeception\Test\Unit;
use Tests\Support\UnitTester;
/**
* Unit tests for AssetMinifier class
*/
class AssetMinifierTest extends Unit
{
protected UnitTester $tester;
private string $testDir;
private string $testCssFile;
private string $testJsFile;
protected function _before()
{
// Create temporary test directory
$this->testDir = sys_get_temp_dir() . '/moko-cassiopeia-test-' . uniqid();
mkdir($this->testDir, 0777, true);
$this->testCssFile = $this->testDir . '/test.css';
$this->testJsFile = $this->testDir . '/test.js';
// Load the AssetMinifier class
require_once __DIR__ . '/../../src/templates/AssetMinifier.php';
}
protected function _after()
{
// Clean up test directory
if (is_dir($this->testDir)) {
$this->deleteDirectory($this->testDir);
}
}
/**
* Helper to recursively delete a directory
*/
private function deleteDirectory(string $dir): void
{
if (!is_dir($dir)) {
return;
}
$files = array_diff(scandir($dir), ['.', '..']);
foreach ($files as $file) {
$path = $dir . '/' . $file;
is_dir($path) ? $this->deleteDirectory($path) : unlink($path);
}
rmdir($dir);
}
/**
* Test CSS minification
*/
public function testMinifyCSS()
{
$css = "
/* This is a comment */
body {
margin: 0;
padding: 0;
background-color: #ffffff;
}
.container {
width: 100%;
max-width: 1200px;
}
";
$minified = \AssetMinifier::minifyCSS($css);
// Should remove comments
$this->assertStringNotContainsString('/* This is a comment */', $minified);
// Should remove whitespace
$this->assertStringNotContainsString("\n", $minified);
$this->assertStringNotContainsString(" ", $minified);
// Should still contain the actual CSS
$this->assertStringContainsString('body{', $minified);
$this->assertStringContainsString('margin:0', $minified);
}
/**
* Test JavaScript minification
*/
public function testMinifyJS()
{
$js = "
// This is a single-line comment
function hello() {
/* Multi-line
comment */
console.log('Hello World');
return true;
}
";
$minified = \AssetMinifier::minifyJS($js);
// Should remove comments
$this->assertStringNotContainsString('// This is a single-line comment', $minified);
$this->assertStringNotContainsString('/* Multi-line', $minified);
// Should still contain the function
$this->assertStringContainsString('function hello()', $minified);
$this->assertStringContainsString("console.log('Hello World')", $minified);
}
/**
* Test minifying CSS file
*/
public function testMinifyCSSFile()
{
$css = "body { margin: 0; padding: 0; }";
file_put_contents($this->testCssFile, $css);
$minFile = $this->testDir . '/test.min.css';
$result = \AssetMinifier::minifyFile($this->testCssFile, $minFile);
$this->assertTrue($result, 'Minification should succeed');
$this->assertFileExists($minFile, 'Minified file should exist');
$content = file_get_contents($minFile);
$this->assertNotEmpty($content, 'Minified file should not be empty');
}
/**
* Test minifying JavaScript file
*/
public function testMinifyJSFile()
{
$js = "function test() { return true; }";
file_put_contents($this->testJsFile, $js);
$minFile = $this->testDir . '/test.min.js';
$result = \AssetMinifier::minifyFile($this->testJsFile, $minFile);
$this->assertTrue($result, 'Minification should succeed');
$this->assertFileExists($minFile, 'Minified file should exist');
$content = file_get_contents($minFile);
$this->assertNotEmpty($content, 'Minified file should not be empty');
$this->assertStringContainsString('function test()', $content);
}
/**
* Test minifying non-existent file
*/
public function testMinifyNonExistentFile()
{
$result = \AssetMinifier::minifyFile(
$this->testDir . '/nonexistent.css',
$this->testDir . '/output.min.css'
);
$this->assertFalse($result, 'Should return false for non-existent file');
}
/**
* Test deleting minified files
*/
public function testDeleteMinifiedFiles()
{
// Create some test files
file_put_contents($this->testDir . '/file1.css', 'body{}');
file_put_contents($this->testDir . '/file1.min.css', 'body{}');
file_put_contents($this->testDir . '/file2.js', 'var x=1;');
file_put_contents($this->testDir . '/file2.min.js', 'var x=1;');
// Create subdirectory with minified files
$subDir = $this->testDir . '/sub';
mkdir($subDir);
file_put_contents($subDir . '/sub.min.css', 'div{}');
$deleted = \AssetMinifier::deleteMinifiedFiles($this->testDir);
$this->assertGreaterThanOrEqual(3, $deleted, 'Should delete at least 3 minified files');
$this->assertFileDoesNotExist($this->testDir . '/file1.min.css');
$this->assertFileDoesNotExist($this->testDir . '/file2.min.js');
$this->assertFileDoesNotExist($subDir . '/sub.min.css');
// Non-minified files should still exist
$this->assertFileExists($this->testDir . '/file1.css');
$this->assertFileExists($this->testDir . '/file2.js');
}
/**
* Test process assets in development mode
*/
public function testProcessAssetsDevelopmentMode()
{
// Create some minified files
file_put_contents($this->testDir . '/test.min.css', 'body{}');
file_put_contents($this->testDir . '/test.min.js', 'var x=1;');
$result = \AssetMinifier::processAssets($this->testDir, true);
$this->assertEquals('development', $result['mode']);
$this->assertGreaterThanOrEqual(2, $result['deleted'], 'Should delete minified files');
$this->assertFileDoesNotExist($this->testDir . '/test.min.css');
$this->assertFileDoesNotExist($this->testDir . '/test.min.js');
}
/**
* Test process assets returns error for non-existent directory
*/
public function testProcessAssetsNonExistentDirectory()
{
$result = \AssetMinifier::processAssets('/nonexistent/path', false);
$this->assertNotEmpty($result['errors']);
$this->assertStringContainsString('does not exist', $result['errors'][0]);
}
}