/** * Shared CSS variables, animations, and utilities for all templates * Based on nicobailon/visual-explainer design system */ import type { Aesthetic, FontPairing, Palette } from "../types.js"; export declare const FONT_PAIRINGS: Record; export declare const PALETTES: Record; export declare const SHARED_CSS = "\n/* Reset */\n* { margin: 0; padding: 0; box-sizing: border-box; }\n\n/* Animation keyframes */\n@keyframes fadeUp {\n from { opacity: 0; transform: translateY(12px); }\n to { opacity: 1; transform: translateY(0); }\n}\n\n@keyframes fadeScale {\n from { opacity: 0; transform: scale(0.95); }\n to { opacity: 1; transform: scale(1); }\n}\n\n/* Reduced motion */\n@media (prefers-reduced-motion: reduce) {\n *, *::before, *::after {\n animation-duration: 0.01ms !important;\n animation-delay: 0ms !important;\n transition-duration: 0.01ms !important;\n }\n}\n\n/* Base styles */\nbody {\n min-height: 100vh;\n line-height: 1.6;\n}\n\n/* Section card base */\n.section {\n background: var(--surface);\n border: 1px solid var(--border);\n border-radius: 12px;\n padding: 20px 24px;\n animation: fadeUp 0.4s ease-out both;\n animation-delay: calc(var(--i, 0) * 0.06s);\n}\n\n.section--hero {\n background: var(--surface-elevated);\n border-color: color-mix(in srgb, var(--border) 50%, var(--accent) 50%);\n box-shadow: 0 4px 20px rgba(0,0,0,0.06);\n padding: 28px 32px;\n}\n\n.section--recessed {\n background: var(--surface2);\n box-shadow: inset 0 1px 3px rgba(0,0,0,0.04);\n}\n\n/* Section labels with dot indicators */\n.section-label {\n font-family: var(--font-mono);\n font-size: 11px;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 1.5px;\n margin-bottom: 16px;\n display: flex;\n align-items: center;\n gap: 8px;\n}\n\n.section-label .dot {\n width: 8px;\n height: 8px;\n border-radius: 50%;\n display: inline-block;\n}\n\n/* Flow arrows */\n.flow-arrow {\n display: flex;\n justify-content: center;\n align-items: center;\n gap: 8px;\n color: var(--text-dim);\n font-family: var(--font-mono);\n font-size: 12px;\n padding: 4px 0;\n animation: fadeUp 0.4s ease-out both;\n animation-delay: calc(var(--i, 0) * 0.06s);\n}\n\n.flow-arrow svg {\n width: 20px;\n height: 20px;\n fill: none;\n stroke: var(--border-bright);\n stroke-width: 2;\n stroke-linecap: round;\n stroke-linejoin: round;\n}\n\n/* Code styling */\ncode {\n font-family: var(--font-mono);\n font-size: 0.9em;\n background: var(--accent-dim);\n color: var(--accent);\n padding: 2px 6px;\n border-radius: 4px;\n}\n\n/* Responsive */\n@media (max-width: 768px) {\n body { padding: 20px; }\n .section { padding: 16px 20px; }\n .section--hero { padding: 20px 24px; }\n}\n"; export declare const MERMAID_SHELL_CSS = "\n.diagram-shell {\n position: relative;\n margin-bottom: 24px;\n}\n\n.diagram-shell__hint {\n font-family: var(--font-mono);\n font-size: 11px;\n color: var(--text-dim);\n margin-bottom: 8px;\n opacity: 0.7;\n}\n\n.mermaid-wrap {\n position: relative;\n background: var(--surface);\n border: 1px solid var(--border);\n border-radius: 12px;\n overflow: hidden;\n min-height: 360px;\n}\n\n.mermaid-viewport {\n position: relative;\n overflow: hidden;\n width: 100%;\n height: 100%;\n min-height: 300px;\n cursor: grab;\n}\n\n.mermaid-wrap.is-panning .mermaid-viewport {\n cursor: grabbing;\n user-select: none;\n}\n\n.mermaid-canvas {\n position: absolute;\n top: 0;\n left: 0;\n transform-origin: 0 0;\n}\n\n.mermaid-canvas svg {\n display: block;\n max-width: none;\n}\n\n.zoom-controls {\n position: absolute;\n top: 8px;\n right: 8px;\n display: flex;\n align-items: center;\n gap: 2px;\n z-index: 10;\n background: var(--surface);\n border: 1px solid var(--border);\n border-radius: 6px;\n padding: 2px 4px;\n}\n\n.zoom-controls button {\n width: 28px;\n height: 28px;\n border: none;\n background: transparent;\n color: var(--text-dim);\n font-family: var(--font-mono);\n font-size: 14px;\n cursor: pointer;\n border-radius: 4px;\n display: flex;\n align-items: center;\n justify-content: center;\n transition: background 0.15s ease, color 0.15s ease;\n}\n\n.zoom-controls button:hover {\n background: var(--border);\n color: var(--text);\n}\n\n.zoom-label {\n font-family: var(--font-mono);\n font-size: 10px;\n color: var(--text-dim);\n padding: 0 6px;\n white-space: nowrap;\n}\n\n.mermaid .nodeLabel {\n font-family: var(--font-body) !important;\n}\n\n.mermaid .edgeLabel {\n font-family: var(--font-mono) !important;\n}\n"; export declare const MERMAID_SHELL_JS = "\n(function initAllDiagrams() {\n const config = {\n fitPadding: 24,\n minHeight: 360,\n maxHeightPx: 960,\n maxHeightVh: 0.84,\n maxInitialZoom: 1.8,\n minZoom: 0.08,\n maxZoom: 6.5,\n zoomStep: 0.14,\n readabilityFloor: 0.58\n };\n\n const clamp = (n, lo, hi) => Math.max(lo, Math.min(hi, n));\n let activeDrag = null;\n\n addEventListener('mousemove', (e) => activeDrag && activeDrag.onMove(e));\n addEventListener('mouseup', () => {\n if (activeDrag) activeDrag.onEnd();\n activeDrag = null;\n });\n\n function initDiagram(shell) {\n const wrap = shell.querySelector('.mermaid-wrap');\n const viewport = shell.querySelector('.mermaid-viewport');\n const canvas = shell.querySelector('.mermaid-canvas');\n const source = shell.querySelector('.diagram-source');\n const label = shell.querySelector('.zoom-label');\n if (!wrap || !viewport || !canvas || !source || !label) {\n console.error('visual-explainer: missing required elements in', shell);\n return;\n }\n\n let zoom = 1;\n let fitMode = 'contain';\n let panX = 0;\n let panY = 0;\n let svgW = 0;\n let svgH = 0;\n let sx = 0;\n let sy = 0;\n let spx = 0;\n let spy = 0;\n let touchDist = 0;\n let touchCx = 0;\n let touchCy = 0;\n\n function constrainPan() {\n const vpW = viewport.clientWidth;\n const vpH = viewport.clientHeight;\n const rW = svgW * zoom;\n const rH = svgH * zoom;\n const pad = config.fitPadding;\n panX = (rW + pad * 2 <= vpW) ? (vpW - rW) / 2 : clamp(panX, vpW - rW - pad, pad);\n panY = (rH + pad * 2 <= vpH) ? (vpH - rH) / 2 : clamp(panY, vpH - rH - pad, pad);\n }\n\n function applyTransform() {\n const svg = canvas.querySelector('svg');\n if (!svg || !svgW) return;\n constrainPan();\n svg.style.width = (svgW * zoom) + 'px';\n svg.style.height = (svgH * zoom) + 'px';\n canvas.style.transform = 'translate(' + panX + 'px, ' + panY + 'px)';\n label.textContent = Math.round(zoom * 100) + '% \u2014 ' + fitMode;\n }\n\n function canPan() {\n const rW = svgW * zoom;\n const rH = svgH * zoom;\n return rW + config.fitPadding * 2 > viewport.clientWidth\n || rH + config.fitPadding * 2 > viewport.clientHeight;\n }\n\n function computeSmartFit() {\n const vpW = viewport.clientWidth;\n const vpH = viewport.clientHeight;\n const aW = Math.max(80, vpW - config.fitPadding * 2);\n const aH = Math.max(80, vpH - config.fitPadding * 2);\n const contain = Math.min(aW / svgW, aH / svgH);\n let z = contain;\n let mode = 'contain';\n if (contain < config.readabilityFloor) {\n const chartR = svgH / svgW;\n const vpR = vpH / Math.max(vpW, 1);\n if (chartR >= vpR) { z = aW / svgW; mode = 'width-priority'; }\n else { z = aH / svgH; mode = 'height-priority'; }\n }\n return { zoom: clamp(z, config.minZoom, config.maxInitialZoom), mode };\n }\n\n function fitDiagram() {\n if (!svgW) return;\n const fit = computeSmartFit();\n zoom = fit.zoom;\n fitMode = fit.mode;\n panX = (viewport.clientWidth - svgW * zoom) / 2;\n panY = (viewport.clientHeight - svgH * zoom) / 2;\n applyTransform();\n }\n\n function setOneToOne() {\n zoom = clamp(1, config.minZoom, config.maxZoom);\n fitMode = '1:1';\n panX = (viewport.clientWidth - svgW * zoom) / 2;\n panY = (viewport.clientHeight - svgH * zoom) / 2;\n applyTransform();\n }\n\n function zoomAround(factor, cx, cy) {\n const next = clamp(zoom * factor, config.minZoom, config.maxZoom);\n const ratio = next / zoom;\n panX = cx - ratio * (cx - panX);\n panY = cy - ratio * (cy - panY);\n zoom = next;\n fitMode = 'custom';\n applyTransform();\n }\n\n function readSvgNaturalSize(svg) {\n let w = 0;\n let h = 0;\n if (svg.viewBox && svg.viewBox.baseVal && svg.viewBox.baseVal.width > 0) {\n w = svg.viewBox.baseVal.width;\n h = svg.viewBox.baseVal.height;\n }\n if (!w) {\n w = parseFloat(svg.getAttribute('width')) || 0;\n h = parseFloat(svg.getAttribute('height')) || 0;\n }\n if (!w) {\n try { const b = svg.getBBox(); w = b.width; h = b.height; } catch {}\n }\n if (!w) {\n const r = svg.getBoundingClientRect();\n w = r.width || 1000;\n h = r.height || 700;\n }\n if (!svg.getAttribute('viewBox')) {\n svg.setAttribute('viewBox', '0 0 ' + w + ' ' + h);\n }\n return { w, h };\n }\n\n function setAdaptiveHeight() {\n if (!svgW) return;\n const usableW = Math.max(280, wrap.getBoundingClientRect().width - 2);\n const idealH = (svgH / svgW) * usableW + config.fitPadding * 2;\n const maxVp = Math.floor(innerHeight * config.maxHeightVh);\n const hardMax = Math.min(config.maxHeightPx, Math.max(config.minHeight + 40, maxVp));\n wrap.style.height = Math.round(clamp(idealH, config.minHeight, hardMax)) + 'px';\n }\n\n function openInNewTab() {\n const svg = canvas.querySelector('svg');\n if (!svg) return;\n const clone = svg.cloneNode(true);\n clone.style.width = '';\n clone.style.height = '';\n // Read the page background from the wrap's data-bg attribute (written\n // server-side from the current palette). This guarantees the new tab\n // matches the diagram's baked-in theme.\n const bg = wrap.getAttribute('data-bg') || '#ffffff';\n const html = ''\n + ''\n + 'Diagram' + clone.outerHTML + '';\n open(URL.createObjectURL(new Blob([html], { type: 'text/html' })), '_blank');\n }\n\n function waitForMermaid() {\n return new Promise((resolve, reject) => {\n if (window.mermaid && typeof window.mermaid.render === 'function') {\n resolve(window.mermaid);\n return;\n }\n const started = Date.now();\n const timer = setInterval(() => {\n if (window.mermaid && typeof window.mermaid.render === 'function') {\n clearInterval(timer);\n resolve(window.mermaid);\n } else if (Date.now() - started > 10000) {\n clearInterval(timer);\n reject(new Error('Mermaid failed to load from CDN (10s timeout)'));\n }\n }, 50);\n });\n }\n\n async function render() {\n try {\n const code = (source.textContent || '').trim();\n if (!code) {\n label.textContent = 'Error: empty source';\n return;\n }\n const mermaid = await waitForMermaid();\n const id = 'diagram-' + Date.now() + '-' + Math.random().toString(36).slice(2, 8);\n const { svg } = await mermaid.render(id, code);\n canvas.innerHTML = svg;\n const svgNode = canvas.querySelector('svg');\n if (!svgNode) {\n label.textContent = 'Error: no SVG';\n return;\n }\n const size = readSvgNaturalSize(svgNode);\n svgW = size.w;\n svgH = size.h;\n svgNode.removeAttribute('width');\n svgNode.removeAttribute('height');\n svgNode.style.maxWidth = 'none';\n svgNode.style.display = 'block';\n setAdaptiveHeight();\n fitDiagram();\n } catch (err) {\n console.error('visual-explainer mermaid render failed:', err);\n label.textContent = 'Error: ' + (err && err.message ? err.message : 'render failed');\n canvas.innerHTML = '
'\n          + String(err && err.message ? err.message : err).replace(/[&<>]/g, (c) => ({'&':'&','<':'<','>':'>'}[c]))\n          + '
';\n }\n }\n\n const actions = {\n 'zoom-in': () => zoomAround(1 + config.zoomStep, viewport.clientWidth / 2, viewport.clientHeight / 2),\n 'zoom-out': () => zoomAround(1 / (1 + config.zoomStep), viewport.clientWidth / 2, viewport.clientHeight / 2),\n 'zoom-fit': fitDiagram,\n 'zoom-one': setOneToOne,\n 'zoom-expand': openInNewTab\n };\n Object.keys(actions).forEach((action) => {\n const btn = wrap.querySelector('[data-action=\"' + action + '\"]');\n if (btn) btn.addEventListener('click', actions[action]);\n });\n\n viewport.addEventListener('dblclick', fitDiagram);\n\n viewport.addEventListener('wheel', (e) => {\n if (e.ctrlKey || e.metaKey) {\n e.preventDefault();\n const rect = viewport.getBoundingClientRect();\n const factor = e.deltaY < 0 ? 1 + config.zoomStep : 1 / (1 + config.zoomStep);\n zoomAround(factor, e.clientX - rect.left, e.clientY - rect.top);\n return;\n }\n if (canPan()) {\n e.preventDefault();\n panX -= e.deltaX;\n panY -= e.deltaY;\n applyTransform();\n }\n }, { passive: false });\n\n viewport.addEventListener('mousedown', (e) => {\n if (e.target.closest('.zoom-controls') || !canPan()) return;\n wrap.classList.add('is-panning');\n sx = e.clientX;\n sy = e.clientY;\n spx = panX;\n spy = panY;\n e.preventDefault();\n activeDrag = {\n onMove: (ev) => {\n panX = spx + (ev.clientX - sx);\n panY = spy + (ev.clientY - sy);\n applyTransform();\n },\n onEnd: () => { wrap.classList.remove('is-panning'); }\n };\n });\n\n viewport.addEventListener('touchstart', (e) => {\n if (e.touches.length === 1) {\n sx = e.touches[0].clientX;\n sy = e.touches[0].clientY;\n spx = panX;\n spy = panY;\n } else if (e.touches.length === 2) {\n const dx = e.touches[0].clientX - e.touches[1].clientX;\n const dy = e.touches[0].clientY - e.touches[1].clientY;\n touchDist = Math.sqrt(dx * dx + dy * dy);\n const r = viewport.getBoundingClientRect();\n touchCx = (e.touches[0].clientX + e.touches[1].clientX) / 2 - r.left;\n touchCy = (e.touches[0].clientY + e.touches[1].clientY) / 2 - r.top;\n }\n }, { passive: true });\n\n viewport.addEventListener('touchmove', (e) => {\n if (e.touches.length === 1 && canPan()) {\n if (touchDist > 0) {\n sx = e.touches[0].clientX;\n sy = e.touches[0].clientY;\n spx = panX;\n spy = panY;\n touchDist = 0;\n }\n e.preventDefault();\n panX = spx + (e.touches[0].clientX - sx);\n panY = spy + (e.touches[0].clientY - sy);\n applyTransform();\n } else if (e.touches.length === 2 && touchDist > 0) {\n e.preventDefault();\n const dx = e.touches[0].clientX - e.touches[1].clientX;\n const dy = e.touches[0].clientY - e.touches[1].clientY;\n const d = Math.sqrt(dx * dx + dy * dy);\n zoomAround(d / touchDist, touchCx, touchCy);\n touchDist = d;\n }\n }, { passive: false });\n\n new ResizeObserver(() => {\n if (svgW) { setAdaptiveHeight(); fitDiagram(); }\n }).observe(wrap);\n\n render();\n }\n\n function bootstrap() {\n document.querySelectorAll('.diagram-shell').forEach(initDiagram);\n }\n\n if (document.readyState === 'loading') {\n document.addEventListener('DOMContentLoaded', bootstrap);\n } else {\n bootstrap();\n }\n})();\n"; export declare function generateCSSVariables(palette: Palette, fonts: FontPairing, isDark: boolean): string; export declare function generateHtmlShell(title: string, bodyContent: string, _aesthetic: string, cssVariables: string, extraHead?: string, extraScripts?: string): string; export declare function escapeHtml(str: string): string; //# sourceMappingURL=shared.d.ts.map