feat(child-theme): scaffold asterion-bricks v0.1.0
Initial child theme structure inheriting from Bricks parent. Token-driven, RGPD-native, mobile-first foundation ready for component and template work. What's in this commit - style.css: WP theme metadata (Template: bricks, v0.1.0) - functions.php: enqueue cascade tokens -> utilities -> components, defer-loaded main.js, font preload, theme support, image sizes (av-hero 2400x1350, av-card 800x600, av-thumb 400x300), Gutenberg default styles dequeued - theme.json: Gutenberg color/typo palette mirroring tokens - assets/css/tokens.css: full design system per Design Handoff section 2 (brand colors, neutrals, semantics, type scale + line-heights, 8pt spacing, radii, shadows, z-index, motion, prefers-reduced-motion) - assets/css/utilities.css: modern reset + .av-container, .av-skip-link, .av-sr-only, .av-stack, .av-section helpers, opinionated heading scale - assets/css/components.css: index of planned components + buttons (5 variants x 4 sizes per Design Handoff section 3.1) and links - assets/js/main.js: vanilla skeleton with header scroll, IntersectionObserver reveal, count-up, mobile drawer; respects prefers-reduced-motion - inc/cpt.php: register_post_type case_study (slug /customers/) + scenario (slug /scenarios/) + shared 'industry' taxonomy - inc/seo.php: Organization JSON-LD on home; placeholders for Product, Article, FAQPage, BreadcrumbList - assets/fonts/: Inter Variable + Italic WOFF2 self-hosted (RGPD; no Google Fonts CDN). Inter Display @font-face commented — Inter 4+ uses opsz axis from the same file. Out of scope (intentionally deferred) - Component fills: cards, forms, navigation, alerts, modals, tabs (placeholders) - Page templates: home and beyond - Bricks JSON template exports - Schema.org Product / Article / FAQPage / BreadcrumbList implementations Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
149
wp-content/themes/asterion-bricks/assets/js/main.js
Normal file
149
wp-content/themes/asterion-bricks/assets/js/main.js
Normal file
@@ -0,0 +1,149 @@
|
||||
/**
|
||||
* Asterion Bricks — main JS
|
||||
*
|
||||
* Vanilla JS, defer-loaded. Wraps everything in DOMContentLoaded.
|
||||
* Performance budget : initial bundle < 200 KB gzipped.
|
||||
*
|
||||
* Modules planned :
|
||||
* - Header scroll behavior (background swap after 80px)
|
||||
* - Mega menu trigger (hover desktop, click touch, ESC closes)
|
||||
* - Mobile drawer (full-screen overlay, accordion sections)
|
||||
* - Scroll-triggered fade-in (IntersectionObserver, threshold 0.15)
|
||||
* - Stat callouts count-up (1000ms ease-out)
|
||||
* - Hero video lazy-load + reduced-motion fallback to poster
|
||||
*/
|
||||
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Detect reduced motion once and expose a flag for downstream modules.
|
||||
* Updated reactively if the user toggles the setting mid-session.
|
||||
*/
|
||||
const reducedMotionQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
|
||||
let prefersReducedMotion = reducedMotionQuery.matches;
|
||||
reducedMotionQuery.addEventListener('change', (e) => {
|
||||
prefersReducedMotion = e.matches;
|
||||
});
|
||||
|
||||
/**
|
||||
* Header scroll behavior.
|
||||
* After 80px scroll, header gets a `is-scrolled` class
|
||||
* (consumed by .av-header.is-scrolled rule in components.css).
|
||||
*/
|
||||
function initHeaderScroll() {
|
||||
const header = document.querySelector('.av-header');
|
||||
if (!header) return;
|
||||
|
||||
let ticking = false;
|
||||
const onScroll = () => {
|
||||
if (!ticking) {
|
||||
window.requestAnimationFrame(() => {
|
||||
header.classList.toggle('is-scrolled', window.scrollY > 80);
|
||||
ticking = false;
|
||||
});
|
||||
ticking = true;
|
||||
}
|
||||
};
|
||||
onScroll();
|
||||
window.addEventListener('scroll', onScroll, { passive: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Scroll-triggered fade-in (Design Handoff section 5.3).
|
||||
* Sections fade-in + slide-up 16px on entry, threshold 0.15, trigger once.
|
||||
* Skipped entirely if user prefers reduced motion.
|
||||
*/
|
||||
function initScrollReveal() {
|
||||
if (prefersReducedMotion) return;
|
||||
|
||||
const targets = document.querySelectorAll('[data-av-reveal]');
|
||||
if (!targets.length) return;
|
||||
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
entry.target.classList.add('is-revealed');
|
||||
observer.unobserve(entry.target);
|
||||
}
|
||||
});
|
||||
}, { threshold: 0.15, rootMargin: '0px 0px -10% 0px' });
|
||||
|
||||
targets.forEach((target) => observer.observe(target));
|
||||
}
|
||||
|
||||
/**
|
||||
* Stat count-up (Design Handoff section 5.3).
|
||||
* Element : <span data-av-countup="0|target|duration">target</span>
|
||||
* Animates from 0 to target value over duration ms, ease-out.
|
||||
*/
|
||||
function initStatCountUp() {
|
||||
if (prefersReducedMotion) return;
|
||||
|
||||
const stats = document.querySelectorAll('[data-av-countup]');
|
||||
if (!stats.length) return;
|
||||
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (!entry.isIntersecting) return;
|
||||
const el = entry.target;
|
||||
const target = parseFloat(el.dataset.avCountup) || 0;
|
||||
const duration = parseInt(el.dataset.avCountupDuration || '1000', 10);
|
||||
const start = performance.now();
|
||||
|
||||
const tick = (now) => {
|
||||
const elapsed = Math.min((now - start) / duration, 1);
|
||||
// ease-out cubic
|
||||
const eased = 1 - Math.pow(1 - elapsed, 3);
|
||||
el.textContent = Math.round(target * eased).toLocaleString();
|
||||
if (elapsed < 1) requestAnimationFrame(tick);
|
||||
};
|
||||
requestAnimationFrame(tick);
|
||||
observer.unobserve(el);
|
||||
});
|
||||
}, { threshold: 0.5 });
|
||||
|
||||
stats.forEach((s) => observer.observe(s));
|
||||
}
|
||||
|
||||
/**
|
||||
* Mobile drawer toggle.
|
||||
* Trigger : <button class="av-drawer-toggle" aria-controls="av-drawer">
|
||||
* Drawer : <div id="av-drawer" class="av-drawer">
|
||||
*/
|
||||
function initMobileDrawer() {
|
||||
const toggle = document.querySelector('.av-drawer-toggle');
|
||||
const drawer = document.getElementById('av-drawer');
|
||||
if (!toggle || !drawer) return;
|
||||
|
||||
const open = () => {
|
||||
drawer.classList.add('is-open');
|
||||
toggle.setAttribute('aria-expanded', 'true');
|
||||
document.body.style.overflow = 'hidden';
|
||||
};
|
||||
const close = () => {
|
||||
drawer.classList.remove('is-open');
|
||||
toggle.setAttribute('aria-expanded', 'false');
|
||||
document.body.style.overflow = '';
|
||||
};
|
||||
|
||||
toggle.addEventListener('click', () => {
|
||||
drawer.classList.contains('is-open') ? close() : open();
|
||||
});
|
||||
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape' && drawer.classList.contains('is-open')) close();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Boot.
|
||||
*/
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
initHeaderScroll();
|
||||
initScrollReveal();
|
||||
initStatCountUp();
|
||||
initMobileDrawer();
|
||||
});
|
||||
|
||||
})();
|
||||
Reference in New Issue
Block a user