Harden pattern rendering and clipboard in app.js

Simplified to substantive changes only (no whitespace reformatting):
- Add escapeHtml() for API-derived text in pattern tab/panel rendering
- Add CSS.escape() + null guard for tab panel selector lookup
- Add clipboard fallback for non-HTTPS / older browsers

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Paul Bakaus
2026-03-24 16:02:40 -07:00
co-authored by Claude Opus 4.6
parent ca43d5ccf7
commit 6ca80f81a1
+173 -203
View File
@@ -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";
@@ -18,66 +18,49 @@ let allCommands = [];
// ============================================
function escapeHtml(value) {
if (typeof value !== "string") return "";
return value
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#39;");
}
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);
}
if (typeof value !== "string") return "";
return value
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#39;");
}
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 = `
<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>
@@ -87,12 +70,12 @@ function showLoadError(error) {
</button>
</div>
`;
}
}
// 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 = `
<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>
@@ -102,54 +85,48 @@ function showLoadError(error) {
</button>
</div>
`;
}
}
}
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) => {
const categoryName = String(category.name ?? "");
return `<button
class="pattern-tab${i === 0 ? " active" : ""}"
data-tab="${escapeHtml(categoryName)}"
// Build tabs with WAI-ARIA attributes
const tabsHTML = patterns
.map((category, i) => `<button
class="pattern-tab${i === 0 ? ' active' : ''}"
data-tab="${escapeHtml(category.name)}"
role="tab"
id="${tabId(categoryName)}"
aria-selected="${i === 0 ? "true" : "false"}"
aria-controls="${panelId(categoryName)}"
tabindex="${i === 0 ? "0" : "-1"}"
>${escapeHtml(categoryName)}</button>`;
})
.join("");
id="${tabId(category.name)}"
aria-selected="${i === 0 ? 'true' : 'false'}"
aria-controls="${panelId(category.name)}"
tabindex="${i === 0 ? '0' : '-1'}"
>${escapeHtml(category.name)}</button>`)
.join("");
// Build panels with WAI-ARIA attributes
const panelsHTML = patterns
.map((category, i) => {
const categoryName = String(category.name ?? "");
const antiItems = antipatternMap[category.name] || [];
return `
// Build panels with WAI-ARIA attributes
const panelsHTML = patterns
.map((category, i) => {
const antiItems = antipatternMap[category.name] || [];
return `
<div
class="pattern-panel${i === 0 ? " active" : ""}"
data-panel="${escapeHtml(categoryName)}"
class="pattern-panel${i === 0 ? ' active' : ''}"
data-panel="${escapeHtml(category.name)}"
role="tabpanel"
id="${panelId(categoryName)}"
aria-labelledby="${tabId(categoryName)}"
${i !== 0 ? "hidden" : ""}
id="${panelId(category.name)}"
aria-labelledby="${tabId(category.name)}"
${i !== 0 ? 'hidden' : ''}
>
<div class="pattern-columns">
<div class="pattern-column pattern-column--anti">
@@ -167,87 +144,82 @@ function renderPatternsWithTabs(patterns, antipatterns) {
</div>
</div>
`;
})
.join("");
})
.join("");
container.innerHTML = `
container.innerHTML = `
<div class="pattern-tabs" role="tablist" aria-label="Pattern categories">${tabsHTML}</div>
<div class="pattern-panels">${panelsHTML}</div>
`;
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 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");
};
// Update panels
panels.forEach(p => {
p.classList.remove('active');
p.setAttribute('hidden', '');
});
const escapedName = CSS.escape(tabName);
const activePanel = container.querySelector(`[data-panel="${escapedName}"]`);
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]);
});
});
}
// ============================================
@@ -256,57 +228,55 @@ 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;
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();
}
}
// Handle copy button clicks
const copyBtn = e.target.closest("[data-copy]");
if (copyBtn) {
const textToCopy = copyBtn.dataset.copy;
const onCopied = () => {
copyBtn.classList.add('copied');
setTimeout(() => copyBtn.classList.remove('copied'), 1500);
};
if (navigator.clipboard?.writeText) {
navigator.clipboard.writeText(textToCopy).then(onCopied).catch(() => {});
} else {
// Fallback for non-HTTPS or older browsers
const ta = Object.assign(document.createElement('textarea'), { value: textToCopy, style: 'position:fixed;left:-9999px' });
document.body.appendChild(ta);
ta.select();
try { document.execCommand('copy'); onCopied(); } catch {}
ta.remove();
}
}
});
// ============================================
// 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();
}