diff --git a/public/app.js b/public/app.js
index 62bcb5896..6ae7f41c0 100644
--- a/public/app.js
+++ b/public/app.js
@@ -1,6 +1,6 @@
import {
- initGlassTerminal,
- renderTerminalLayout,
+ initGlassTerminal,
+ renderTerminalLayout,
} from "./js/components/glass-terminal.js";
import { initLensEffect } from "./js/components/lens.js";
import { initFrameworkViz } from "./js/components/framework-viz.js";
@@ -17,40 +17,67 @@ let allCommands = [];
// CONTENT LOADING
// ============================================
+function escapeHtml(value) {
+ if (typeof value !== "string") return "";
+ return value
+ .replaceAll("&", "&")
+ .replaceAll("<", "<")
+ .replaceAll(">", ">")
+ .replaceAll('"', """)
+ .replaceAll("'", "'");
+}
+
+function copyTextFallback(text) {
+ const textArea = document.createElement("textarea");
+ textArea.value = text;
+ textArea.setAttribute("readonly", "");
+ textArea.style.position = "absolute";
+ textArea.style.left = "-9999px";
+ document.body.appendChild(textArea);
+ textArea.select();
+ try {
+ return document.execCommand("copy");
+ } catch {
+ return false;
+ } finally {
+ document.body.removeChild(textArea);
+ }
+}
+
async function loadContent() {
- try {
- const [commandsRes, patternsRes] = await Promise.all([
- fetch("/api/commands"),
- fetch("/api/patterns"),
- ]);
+ try {
+ const [commandsRes, patternsRes] = await Promise.all([
+ fetch("/api/commands"),
+ fetch("/api/patterns"),
+ ]);
- // 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}`);
- }
+ // 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();
+ allCommands = await commandsRes.json();
+ const patternsData = await patternsRes.json();
- // Render commands (Glass Terminal)
- renderTerminalLayout(allCommands);
+ // Render commands (Glass Terminal)
+ renderTerminalLayout(allCommands);
- // Render patterns with tabbed navigation
- renderPatternsWithTabs(patternsData.patterns, patternsData.antipatterns);
- } catch (error) {
- console.error("Failed to load content:", error);
- showLoadError(error);
- }
+ // 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 = `
+ // Show error in commands section
+ const commandsGallery = document.querySelector(".commands-gallery");
+ if (commandsGallery) {
+ commandsGallery.innerHTML = `
⚠
Failed to load commands
@@ -60,12 +87,12 @@ function showLoadError(error) {
`;
- }
+ }
- // Show error in patterns section
- const patternsContainer = document.getElementById("patterns-categories");
- if (patternsContainer) {
- patternsContainer.innerHTML = `
+ // Show error in patterns section
+ const patternsContainer = document.getElementById("patterns-categories");
+ if (patternsContainer) {
+ patternsContainer.innerHTML = `
⚠
Failed to load patterns
@@ -75,139 +102,152 @@ function showLoadError(error) {
`;
- }
+ }
}
function renderPatternsWithTabs(patterns, antipatterns) {
- const container = document.getElementById("patterns-categories");
- if (!container || !patterns || !antipatterns) return;
+ const container = document.getElementById("patterns-categories");
+ if (!container || !patterns || !antipatterns) return;
- // Create a map of antipatterns by category name
- const antipatternMap = {};
- antipatterns.forEach(cat => {
- antipatternMap[cat.name] = cat.items;
- });
+ // Create a map of antipatterns by category name
+ const antipatternMap = {};
+ antipatterns.forEach((cat) => {
+ antipatternMap[cat.name] = cat.items;
+ });
- // Generate unique IDs for tabs
- const tabId = (name) => `pattern-tab-${name.toLowerCase().replace(/\s+/g, '-')}`;
- const panelId = (name) => `pattern-panel-${name.toLowerCase().replace(/\s+/g, '-')}`;
+ // Generate unique IDs for tabs
+ const tabId = (name) =>
+ `pattern-tab-${name.toLowerCase().replace(/\s+/g, "-")}`;
+ const panelId = (name) =>
+ `pattern-panel-${name.toLowerCase().replace(/\s+/g, "-")}`;
- // Build tabs with WAI-ARIA attributes
- const tabsHTML = patterns
- .map((category, i) => ``;
+ })
+ .join("");
- // Build panels with WAI-ARIA attributes
- const panelsHTML = patterns
- .map((category, i) => {
- const antiItems = antipatternMap[category.name] || [];
- return `
+ // Build panels with WAI-ARIA attributes
+ const panelsHTML = patterns
+ .map((category, i) => {
+ const categoryName = String(category.name ?? "");
+ const antiItems = antipatternMap[category.name] || [];
+ return `
Don't
- ${antiItems.map((item) => `- ${item}
`).join("")}
+ ${antiItems.map((item) => `- ${escapeHtml(item)}
`).join("")}
Do
- ${category.items.map((item) => `- ${item}
`).join("")}
+ ${category.items.map((item) => `- ${escapeHtml(item)}
`).join("")}
`;
- })
- .join("");
+ })
+ .join("");
- container.innerHTML = `
+ container.innerHTML = `
${tabsHTML}
${panelsHTML}
`;
- const tabs = container.querySelectorAll('.pattern-tab');
- const panels = container.querySelectorAll('.pattern-panel');
+ const tabs = container.querySelectorAll(".pattern-tab");
+ const panels = container.querySelectorAll(".pattern-panel");
- // Function to switch tabs
- const switchTab = (newTab) => {
- const tabName = newTab.dataset.tab;
+ // Function to switch tabs
+ const switchTab = (newTab) => {
+ const tabName = newTab.dataset.tab;
- // Update ARIA attributes on all tabs
- tabs.forEach(t => {
- t.classList.remove('active');
- t.setAttribute('aria-selected', 'false');
- t.setAttribute('tabindex', '-1');
- });
+ // Update ARIA attributes on all tabs
+ tabs.forEach((t) => {
+ t.classList.remove("active");
+ t.setAttribute("aria-selected", "false");
+ t.setAttribute("tabindex", "-1");
+ });
- // Activate the new tab
- newTab.classList.add('active');
- newTab.setAttribute('aria-selected', 'true');
- newTab.setAttribute('tabindex', '0');
- newTab.focus();
+ // Activate the new tab
+ newTab.classList.add("active");
+ newTab.setAttribute("aria-selected", "true");
+ newTab.setAttribute("tabindex", "0");
+ newTab.focus();
- // Update panels
- panels.forEach(p => {
- p.classList.remove('active');
- p.setAttribute('hidden', '');
- });
- const activePanel = container.querySelector(`[data-panel="${tabName}"]`);
- activePanel.classList.add('active');
- activePanel.removeAttribute('hidden');
- };
+ // Update panels
+ panels.forEach((p) => {
+ p.classList.remove("active");
+ p.setAttribute("hidden", "");
+ });
+ const escapedTabName =
+ typeof CSS !== "undefined" && CSS.escape
+ ? CSS.escape(tabName)
+ : tabName.replaceAll('"', '\\"');
+ const activePanel = container.querySelector(
+ `[data-panel="${escapedTabName}"]`,
+ );
+ if (!activePanel) return;
+ activePanel.classList.add("active");
+ activePanel.removeAttribute("hidden");
+ };
- // Tab click handling
- tabs.forEach(tab => {
- tab.addEventListener('click', () => switchTab(tab));
- });
+ // Tab click handling
+ tabs.forEach((tab) => {
+ tab.addEventListener("click", () => switchTab(tab));
+ });
- // Keyboard navigation (Arrow keys, Home, End)
- tabs.forEach((tab, index) => {
- tab.addEventListener('keydown', (e) => {
- let targetIndex = index;
+ // Keyboard navigation (Arrow keys, Home, End)
+ tabs.forEach((tab, index) => {
+ tab.addEventListener("keydown", (e) => {
+ let targetIndex = index;
- switch (e.key) {
- case 'ArrowLeft':
- case 'ArrowUp':
- e.preventDefault();
- targetIndex = index === 0 ? tabs.length - 1 : index - 1;
- break;
- case 'ArrowRight':
- case 'ArrowDown':
- e.preventDefault();
- targetIndex = index === tabs.length - 1 ? 0 : index + 1;
- break;
- case 'Home':
- e.preventDefault();
- targetIndex = 0;
- break;
- case 'End':
- e.preventDefault();
- targetIndex = tabs.length - 1;
- break;
- default:
- return;
- }
+ switch (e.key) {
+ case "ArrowLeft":
+ case "ArrowUp":
+ e.preventDefault();
+ targetIndex = index === 0 ? tabs.length - 1 : index - 1;
+ break;
+ case "ArrowRight":
+ case "ArrowDown":
+ e.preventDefault();
+ targetIndex = index === tabs.length - 1 ? 0 : index + 1;
+ break;
+ case "Home":
+ e.preventDefault();
+ targetIndex = 0;
+ break;
+ case "End":
+ e.preventDefault();
+ targetIndex = tabs.length - 1;
+ break;
+ default:
+ return;
+ }
- switchTab(tabs[targetIndex]);
- });
- });
+ switchTab(tabs[targetIndex]);
+ });
+ });
}
// ============================================
@@ -216,45 +256,57 @@ function renderPatternsWithTabs(patterns, antipatterns) {
// Handle bundle download clicks via event delegation
document.addEventListener("click", (e) => {
- const bundleBtn = e.target.closest("[data-bundle]");
- if (bundleBtn) {
- const provider = bundleBtn.dataset.bundle;
- const prefixToggle = document.getElementById('prefix-toggle');
- const usePrefixed = prefixToggle && prefixToggle.checked;
- const bundleName = usePrefixed ? `${provider}-prefixed` : provider;
- window.location.href = `/api/download/bundle/${bundleName}`;
- }
+ const bundleBtn = e.target.closest("[data-bundle]");
+ if (bundleBtn) {
+ const provider = bundleBtn.dataset.bundle;
+ const prefixToggle = document.getElementById("prefix-toggle");
+ const usePrefixed = prefixToggle && prefixToggle.checked;
+ const bundleName = usePrefixed ? `${provider}-prefixed` : provider;
+ window.location.href = `/api/download/bundle/${bundleName}`;
+ }
- // Handle copy button clicks
- const copyBtn = e.target.closest("[data-copy]");
- if (copyBtn) {
- const textToCopy = copyBtn.dataset.copy;
- navigator.clipboard.writeText(textToCopy).then(() => {
- copyBtn.classList.add('copied');
- setTimeout(() => copyBtn.classList.remove('copied'), 1500);
- });
- }
+ // Handle copy button clicks
+ const copyBtn = e.target.closest("[data-copy]");
+ if (copyBtn) {
+ const textToCopy = copyBtn.dataset.copy;
+ const onCopySuccess = () => {
+ copyBtn.classList.add("copied");
+ setTimeout(() => copyBtn.classList.remove("copied"), 1500);
+ };
+
+ if (navigator.clipboard?.writeText) {
+ navigator.clipboard
+ .writeText(textToCopy)
+ .then(onCopySuccess)
+ .catch(() => {
+ if (copyTextFallback(textToCopy)) {
+ onCopySuccess();
+ }
+ });
+ } else if (copyTextFallback(textToCopy)) {
+ onCopySuccess();
+ }
+ }
});
-
// ============================================
// STARTUP
// ============================================
function init() {
- initAnchorScroll();
- initHashTracking();
- initLensEffect();
- initScrollReveal();
- initGlassTerminal();
- initFrameworkViz();
- loadContent();
+ initAnchorScroll();
+ initHashTracking();
+ initLensEffect();
+ initScrollReveal();
+ initGlassTerminal();
+ initFrameworkViz();
+ loadContent();
- document.body.classList.add("loaded");
+ document.body.classList.add("loaded");
}
if (document.readyState === "loading") {
- document.addEventListener("DOMContentLoaded", init);
+ document.addEventListener("DOMContentLoaded", init);
} else {
- init();
+ init();
}