Files
pbakaus_impeccable/site/scripts/app.js
T
Paul Bakaus ead6ddabe5 Preserve experimental Live app-server workstream
Snapshot the current app-server implementation, shared Live optimizations, generated harness output, and in-progress site work before restoring polling as the primary runtime path.

Prepared with Codex assistance under maintainer direction.
2026-07-15 16:07:34 -07:00

475 lines
15 KiB
JavaScript

import {
initGlassTerminal,
renderTerminalLayout,
} from "./components/glass-terminal.js";
import { initFrameworkViz } from "./components/framework-viz.js";
import { initScrollReveal } from "./utils/reveal.js";
import { initAnchorScroll, initHashTracking } from "./utils/scroll.js";
import { initCopyFeedback } from "./utils/copy-feedback.js";
import { initSectionNav } from "./components/section-nav.js";
import { initFoundationGrid } from "./components/foundation-grid.js";
import { initLiveDemo, initGbarPageChat } from "./components/live-demo.js";
// ============================================
// STATE
// ============================================
let allCommands = [];
// ============================================
// CONTENT LOADING
// ============================================
function escapeHtml(value) {
if (typeof value !== "string") return "";
return value
.replaceAll("&", "&")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#39;");
}
async function loadContent() {
try {
const [commandsRes, patternsRes] = await Promise.all([
fetch("/_data/api/commands.json"),
fetch("/_data/api/patterns.json"),
]);
// Check for HTTP errors
if (!commandsRes.ok) {
throw new Error(`Commands API failed: ${commandsRes.status}`);
}
if (!patternsRes.ok) {
throw new Error(`Patterns API failed: ${patternsRes.status}`);
}
allCommands = await commandsRes.json();
const patternsData = await patternsRes.json();
// Render commands (Glass Terminal)
renderTerminalLayout(allCommands);
// Initialize gallery card stack
initGalleryStack();
// Render patterns with tabbed navigation
renderPatternsWithTabs(patternsData.patterns, patternsData.antipatterns);
} catch (error) {
console.error("Failed to load content:", error);
showLoadError(error);
}
}
function showLoadError(error) {
// Show error in commands section
const commandsGallery = document.querySelector('.commands-gallery');
if (commandsGallery) {
commandsGallery.innerHTML = `
<div class="load-error" role="alert">
<div class="load-error-icon" aria-hidden="true">⚠</div>
<h3 class="load-error-title">Failed to load commands</h3>
<p class="load-error-text">There was a problem loading the content. Please check your connection and try again.</p>
<button class="btn btn-secondary load-error-retry" onclick="location.reload()">
Retry
</button>
</div>
`;
}
// Show error in patterns section
const patternsContainer = document.getElementById("patterns-categories");
if (patternsContainer) {
patternsContainer.innerHTML = `
<div class="load-error" role="alert">
<div class="load-error-icon" aria-hidden="true">⚠</div>
<h3 class="load-error-title">Failed to load patterns</h3>
<p class="load-error-text">There was a problem loading the content. Please check your connection and try again.</p>
<button class="btn btn-secondary load-error-retry" onclick="location.reload()">
Retry
</button>
</div>
`;
}
}
function initGalleryStack() {
const container = document.querySelector('.gallery-stack-container');
const stack = document.getElementById('gallery-stack');
if (!stack || !container) return;
const cards = stack.querySelectorAll('.gallery-stack-card');
const counter = container.querySelector('.gallery-stack-counter');
const total = cards.length;
let current = 0;
let lastScroll = 0;
function update() {
cards.forEach((card, i) => {
const offset = (i - current + total) % total;
card.dataset.offset = offset;
});
}
function next() { current = (current + 1) % total; update(); }
function prev() { current = (current - 1 + total) % total; update(); }
container.querySelector('.gallery-stack-prev').addEventListener('click', prev);
container.querySelector('.gallery-stack-next').addEventListener('click', next);
stack.addEventListener('wheel', (e) => {
e.preventDefault();
const now = Date.now();
if (now - lastScroll < 350) return;
lastScroll = now;
if (e.deltaY > 0) next(); else prev();
}, { passive: false });
update();
}
function renderPatternsWithTabs(patterns, antipatterns) {
const container = document.getElementById("patterns-categories");
if (!container || !patterns || !antipatterns) return;
const antipatternMap = {};
antipatterns.forEach(cat => { antipatternMap[cat.name] = cat.items; });
const tabsHTML = patterns.map((cat, i) =>
`<button class="patterns-tab${i === 0 ? ' is-active' : ''}" data-index="${i}">${escapeHtml(cat.name)}</button>`
).join('');
const panelsHTML = patterns.map((cat, i) => {
const antiItems = antipatternMap[cat.name] || [];
return `
<div class="patterns-content${i === 0 ? ' is-active' : ''}" data-index="${i}">
<div class="patterns-col patterns-col--dont">
<ul>${antiItems.map(item => `<li>${escapeHtml(item)}</li>`).join('')}</ul>
</div>
<div class="patterns-col patterns-col--do">
<ul>${cat.items.map(item => `<li>${escapeHtml(item)}</li>`).join('')}</ul>
</div>
</div>`;
}).join('');
container.innerHTML = `<div class="patterns-tabs-wrap"><div class="patterns-tabs" data-scroll="start">${tabsHTML}</div></div>${panelsHTML}`;
const tabsEl = container.querySelector('.patterns-tabs');
const tabsWrap = container.querySelector('.patterns-tabs-wrap');
container.addEventListener('click', (e) => {
const tab = e.target.closest('.patterns-tab');
if (!tab) return;
const index = tab.dataset.index;
container.querySelectorAll('.patterns-tab').forEach(t => t.classList.remove('is-active'));
container.querySelectorAll('.patterns-content').forEach(p => p.classList.remove('is-active'));
tab.classList.add('is-active');
container.querySelector(`.patterns-content[data-index="${index}"]`).classList.add('is-active');
// Center the clicked tab inside the tabs strip (not the page). Using
// scrollBy on the container keeps the page scroll untouched.
if (tabsEl) {
const tabRect = tab.getBoundingClientRect();
const stripRect = tabsEl.getBoundingClientRect();
const offset = (tabRect.left + tabRect.width / 2) - (stripRect.left + stripRect.width / 2);
tabsEl.scrollBy({ left: offset, behavior: 'smooth' });
}
});
// Track scroll position so the edge-fade mask only appears on sides where
// there's actually more content. At the start, no left fade; at the end,
// no right fade; if no overflow, no fade at all.
const updateScrollState = () => {
if (!tabsEl) return;
const { scrollLeft, scrollWidth, clientWidth } = tabsEl;
const max = scrollWidth - clientWidth;
let state;
if (max <= 1) state = 'none';
else if (scrollLeft <= 1) state = 'start';
else if (scrollLeft >= max - 1) state = 'end';
else state = 'middle';
tabsEl.dataset.scroll = state;
if (tabsWrap) tabsWrap.dataset.scroll = state;
};
tabsEl?.addEventListener('scroll', updateScrollState, { passive: true });
window.addEventListener('resize', updateScrollState);
updateScrollState();
}
// ============================================
// EVENT HANDLERS
// ============================================
// Handle bundle download clicks via event delegation.
// Each download button carries the full bundle name in data-bundle
// (currently just "universal") so the handler is just a redirect.
document.addEventListener("click", (e) => {
const bundleBtn = e.target.closest("[data-bundle]");
if (bundleBtn) {
const bundleName = bundleBtn.dataset.bundle;
window.location.href = `/api/download/bundle/${bundleName}`;
}
});
// ============================================
// STARTUP
// ============================================
// Fade the header's glass background in px-by-px as the user scrolls off the
// hero, by writing scroll progress (0 → 1 over RANGE px) to a --hp custom
// property the CSS interpolates against.
function initHeaderScroll() {
const header = document.querySelector("[data-site-header]");
if (!header) return;
const RANGE = 200;
let ticking = false;
const apply = () => {
const p = Math.min(1, window.scrollY / RANGE);
header.style.setProperty("--hp", p.toFixed(4));
ticking = false;
};
const onScroll = () => {
if (!ticking) {
ticking = true;
requestAnimationFrame(apply);
}
};
apply();
window.addEventListener("scroll", onScroll, { passive: true });
}
function initHeroProof() {
const root = document.querySelector("[data-hero-proof]");
if (!root) return;
const tabs = Array.from(root.querySelectorAll("[data-proof-tab]"));
const panels = Array.from(root.querySelectorAll("[data-proof-panel]"));
const result = root.querySelector("[data-hero-proof-result]");
if (!tabs.length || !panels.length) return;
panels.forEach((panel) => {
const range = panel.querySelector("[data-proof-range]");
if (!range) return;
const updatePosition = () => {
panel.style.setProperty("--proof-position", `${range.value}%`);
};
range.addEventListener("input", updatePosition);
updatePosition();
});
const activate = (index, moveFocus = false) => {
const nextTab = tabs[index];
const nextPanel = panels.find((panel) => panel.dataset.proofPanel === nextTab?.dataset.proofTab);
if (!nextTab || !nextPanel) return;
tabs.forEach((tab) => {
const active = tab === nextTab;
tab.classList.toggle("is-active", active);
tab.setAttribute("aria-selected", active ? "true" : "false");
tab.tabIndex = active ? 0 : -1;
});
panels.forEach((panel) => {
const active = panel === nextPanel;
panel.classList.toggle("is-active", active);
panel.hidden = !active;
});
if (result) result.textContent = nextPanel.dataset.summary || "Example updated";
if (moveFocus) nextTab.focus();
};
tabs.forEach((tab, index) => {
tab.addEventListener("click", () => activate(index));
tab.addEventListener("keydown", (event) => {
let next = index;
if (event.key === "ArrowRight") next = (index + 1) % tabs.length;
else if (event.key === "ArrowLeft") next = (index - 1 + tabs.length) % tabs.length;
else if (event.key === "Home") next = 0;
else if (event.key === "End") next = tabs.length - 1;
else return;
event.preventDefault();
activate(next, true);
});
});
}
function init() {
initAnchorScroll();
initHashTracking();
initCopyFeedback();
initHeaderScroll();
initHeroProof();
initScrollReveal();
initGlassTerminal();
initFrameworkViz();
initFoundationGrid();
initSectionNav();
initWhyTabs();
initLanguageTabs();
initLiveDemo();
initGbarPageChat();
loadContent();
document.body.classList.add("loaded");
}
function initLanguageTabs() {
const toggle = document.querySelector('.language-view-toggle');
if (!toggle) return;
const tabs = Array.from(toggle.querySelectorAll('.language-view-tab'));
const panels = Array.from(document.querySelectorAll('.language-view[data-view-panel]'));
if (!tabs.length || !panels.length) return;
tabs.forEach((tab) => {
tab.addEventListener('click', () => {
const view = tab.dataset.view;
tabs.forEach((t) => {
const on = t === tab;
t.classList.toggle('is-active', on);
t.setAttribute('aria-selected', on ? 'true' : 'false');
});
panels.forEach((p) => {
const on = p.dataset.viewPanel === view;
p.classList.toggle('is-active', on);
if (on) p.removeAttribute('hidden');
else p.setAttribute('hidden', '');
});
});
});
}
function initWhyTabs() {
const container = document.querySelector('.why-layout');
if (!container) return;
const tabs = Array.from(container.querySelectorAll('.why-tab'));
const panels = Array.from(container.querySelectorAll('.why-panel'));
if (!tabs.length || !panels.length) return;
const CYCLE_MS = 7000;
const reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
let current = Math.max(tabs.findIndex((tab) => tab.classList.contains('is-active')), 0);
let timer = null;
let autoRotate = !reducedMotion;
let visible = false;
const tabStrip = container.querySelector('.why-tabs');
const getPanelForTab = (tab) => {
const panelId = tab?.getAttribute('aria-controls');
return panelId ? container.querySelector(`#${CSS.escape(panelId)}`) : null;
};
const centerActiveInStrip = (active) => {
// On mobile the tab list is a horizontal scroll strip. Keep the
// active pill visible without touching the page scroll. Using
// scrollTo with behavior:auto + direct scrollLeft assignment,
// because smooth-scroll on this container is disabled by the
// parent's mask-image compositing and silently no-ops.
if (!tabStrip || tabStrip.scrollWidth <= tabStrip.clientWidth + 1) return;
const tabRect = active.getBoundingClientRect();
const stripRect = tabStrip.getBoundingClientRect();
const offset = (tabRect.left + tabRect.width / 2) - (stripRect.left + stripRect.width / 2);
if (Math.abs(offset) < 2) return;
tabStrip.scrollLeft += offset;
};
const activate = (index, fromAuto = false) => {
const targetTab = tabs[index];
const targetPanel = getPanelForTab(targetTab);
if (!targetTab || !targetPanel) return;
current = index;
tabs.forEach((tab, i) => {
const on = i === index;
tab.classList.toggle('is-active', on);
tab.setAttribute('aria-selected', on ? 'true' : 'false');
// Reset cycling class, re-add on the new active tab so the
// progress indicator restarts cleanly.
tab.classList.remove('is-cycling');
});
panels.forEach((panel) => {
const on = panel === targetPanel;
panel.classList.toggle('is-active', on);
if (on) panel.removeAttribute('hidden');
else panel.setAttribute('hidden', '');
});
if (autoRotate && visible) {
// Force reflow so the animation restart is picked up.
void targetTab.offsetWidth;
targetTab.classList.add('is-cycling');
}
centerActiveInStrip(targetTab);
};
const scheduleNext = () => {
clearTimeout(timer);
if (!autoRotate || !visible) return;
timer = setTimeout(() => {
const next = (current + 1) % tabs.length;
activate(next, true);
scheduleNext();
}, CYCLE_MS);
};
const stopAuto = () => {
autoRotate = false;
clearTimeout(timer);
tabs.forEach((t) => t.classList.remove('is-cycling'));
};
tabs.forEach((tab, index) => {
tab.addEventListener('click', () => {
stopAuto();
activate(index);
});
tab.addEventListener('keydown', (e) => {
if (e.key !== 'ArrowDown' && e.key !== 'ArrowUp') return;
e.preventDefault();
stopAuto();
const dir = e.key === 'ArrowDown' ? 1 : -1;
const next = (index + dir + tabs.length) % tabs.length;
tabs[next].focus();
activate(next);
});
});
container.addEventListener('mouseenter', () => {
// Pause auto-rotation on hover. Resume only if still allowed and
// user hasn't interacted (stopAuto flips autoRotate off).
clearTimeout(timer);
tabs.forEach((t) => t.classList.remove('is-cycling'));
});
container.addEventListener('mouseleave', () => {
if (autoRotate && visible) {
// Re-apply cycling class to current tab and resume the timer.
const active = tabs[current];
void active.offsetWidth;
active.classList.add('is-cycling');
scheduleNext();
}
});
// Observe visibility so we only rotate while the user can see it.
const io = new IntersectionObserver((entries) => {
entries.forEach((e) => {
visible = e.isIntersecting;
if (visible) {
if (autoRotate) {
const active = tabs[current];
void active.offsetWidth;
active.classList.add('is-cycling');
scheduleNext();
}
} else {
clearTimeout(timer);
tabs.forEach((t) => t.classList.remove('is-cycling'));
}
});
}, { threshold: 0.35 });
io.observe(container);
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init);
} else {
init();
}