| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| export const DEFAULT_EMBED_HEIGHT = 400; |
|
|
| export const EMBED_MESSAGE_TYPES = { |
| resize: "embedResize", |
| setTheme: "setTheme", |
| updatePrimaryColor: "updatePrimaryColor", |
| ready: "embedReady", |
| } as const; |
|
|
| |
| const COLOR_PALETTES_POLYFILL = ` |
| (function(){ |
| var palettes = { |
| categorical: ['#4e79a7','#f28e2b','#e15759','#76b7b2','#59a14f','#edc948','#b07aa1','#ff9da7','#9c755f','#bab0ac'], |
| sequential: ['#084594','#2171b5','#4292c6','#6baed6','#9ecae1','#c6dbef','#deebf7','#f7fbff'], |
| diverging: ['#d73027','#f46d43','#fdae61','#fee090','#ffffbf','#e0f3f8','#abd9e9','#74add1','#4575b4'] |
| }; |
| window.ColorPalettes = { |
| getColors: function(type, n) { |
| var p = palettes[type] || palettes.categorical; |
| return p.slice(0, n != null ? n : p.length); |
| }, |
| getPrimary: function() { |
| return getComputedStyle(document.documentElement).getPropertyValue('--primary-color').trim() || '#4e79a7'; |
| }, |
| refresh: function() {}, |
| getTextStyleForBackground: function() { return { fill: '#ffffff' }; } |
| }; |
| })(); |
| `.trim(); |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| const HEIGHT_REPORTER = ` |
| (function(){ |
| var lastH = 0; |
| var scheduled = false; |
| // IMPORTANT: only measure <body>. document.documentElement.scrollHeight is |
| // clamped to at least the iframe's viewport height, so it would never allow |
| // the iframe to shrink (it would always report >= current iframe height). |
| // body.scrollHeight reflects the actual content height plus body padding. |
| function measure(){ |
| var b = document.body; |
| if (!b) return 0; |
| // getBoundingClientRect().bottom accounts for padding/margin precisely; |
| // fall back to scrollHeight if the rect reports 0 (detached layout). |
| var rectBottom = Math.ceil(b.getBoundingClientRect().bottom); |
| var scroll = Math.ceil(b.scrollHeight); |
| // body margin may push the rect down; scrollHeight ignores it. Take the |
| // max so we never under-report. |
| return Math.max(rectBottom, scroll); |
| } |
| function post(h){ |
| try { |
| window.parent.postMessage({ type: 'embedResize', height: h }, '*'); |
| } catch(e){} |
| } |
| function schedule(){ |
| if (scheduled) return; |
| scheduled = true; |
| requestAnimationFrame(function(){ |
| scheduled = false; |
| var h = measure(); |
| if (h > 0 && Math.abs(h - lastH) >= 1) { |
| lastH = h; |
| post(h); |
| } |
| }); |
| } |
| if (typeof ResizeObserver !== 'undefined') { |
| var ro = new ResizeObserver(schedule); |
| ro.observe(document.documentElement); |
| if (document.body) ro.observe(document.body); |
| } |
| if (typeof MutationObserver !== 'undefined' && document.body) { |
| var mo = new MutationObserver(schedule); |
| mo.observe(document.body, { childList: true, subtree: true, attributes: true, characterData: true }); |
| } |
| window.addEventListener('load', function(){ |
| schedule(); |
| setTimeout(schedule, 300); |
| setTimeout(schedule, 1000); |
| setTimeout(schedule, 3000); |
| }); |
| // Opt-in explicit ready signal for async charts |
| window.__embedReady = function(){ |
| schedule(); |
| try { window.parent.postMessage({ type: 'embedReady' }, '*'); } catch(e){} |
| }; |
| })(); |
| `.trim(); |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| const THEME_BOOTSTRAP = ` |
| (function(){ |
| try { |
| var theme = null; |
| var primary = null; |
| try { |
| var parentDoc = window.parent && window.parent.document; |
| if (parentDoc) { |
| theme = parentDoc.documentElement.getAttribute('data-theme') || null; |
| var pc = getComputedStyle(parentDoc.documentElement).getPropertyValue('--primary-color'); |
| if (pc) primary = pc.trim(); |
| } |
| } catch(e) {} |
| if (!theme) { |
| try { theme = window.localStorage && window.localStorage.getItem('theme'); } catch(e) {} |
| } |
| if (!theme) { |
| theme = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; |
| } |
| document.documentElement.setAttribute('data-theme', theme); |
| document.documentElement.style.colorScheme = theme === 'dark' ? 'dark' : 'light'; |
| if (primary) document.documentElement.style.setProperty('--primary-color', primary); |
| } catch(e) {} |
| })(); |
| `.trim(); |
|
|
| |
| |
| |
| |
| |
| const THEME_LISTENER = ` |
| (function(){ |
| window.addEventListener('message', function(e){ |
| if (!e.data) return; |
| if (e.data.type === 'setTheme') { |
| document.documentElement.setAttribute('data-theme', e.data.theme); |
| document.documentElement.style.colorScheme = e.data.theme === 'dark' ? 'dark' : 'light'; |
| if (e.data.primaryColor) { |
| document.documentElement.style.setProperty('--primary-color', e.data.primaryColor); |
| } |
| if (window.__chartRerender) try { window.__chartRerender(); } catch(err){} |
| } else if (e.data.type === 'updatePrimaryColor') { |
| document.documentElement.style.setProperty('--primary-color', e.data.color); |
| if (window.__chartRerender) try { window.__chartRerender(); } catch(err){} |
| } |
| }); |
| })(); |
| `.trim(); |
|
|
| const BASE_STYLES = ` |
| :root, [data-theme='light'] { |
| color-scheme: light; |
| --text-color: rgba(0,0,0,.85); |
| --surface-bg: #f9f9f9; |
| --page-bg: #fff; |
| --border-color: rgba(0,0,0,.1); |
| --muted-color: rgba(0,0,0,.6); |
| --axis-color: rgba(0,0,0,.6); |
| --tick-color: rgba(0,0,0,.85); |
| --grid-color: rgba(0,0,0,.08); |
| } |
| [data-theme='dark'] { |
| color-scheme: dark; |
| --text-color: rgba(255,255,255,.9); |
| --surface-bg: #07080a; |
| --page-bg: #0f1115; |
| --border-color: rgba(255,255,255,.15); |
| --muted-color: rgba(255,255,255,.7); |
| --axis-color: rgba(255,255,255,.7); |
| --tick-color: rgba(255,255,255,.7); |
| --grid-color: rgba(255,255,255,.10); |
| } |
| *, *::before, *::after { box-sizing: border-box; } |
| /* IMPORTANT: do NOT set min-height:100% on <body>. Doing so creates a |
| lower bound on scrollHeight equal to the iframe's rendered height, so |
| the reporter can never signal a smaller height and the iframe can only |
| ever grow (never shrink when the chart re-renders narrower on resize). */ |
| html, body { margin: 0; padding: 0; background: transparent; } |
| body { |
| color: var(--text-color); |
| font-family: -apple-system, BlinkMacSystemFont, 'Inter', 'Segoe UI', sans-serif; |
| font-size: 13px; |
| padding: 16px; |
| /* overflow: clip clips runaway horizontal content without creating a |
| scroll container that would trap scroll events or inflate scrollHeight. */ |
| overflow: clip; |
| } |
| `.trim(); |
|
|
| |
| |
| |
| |
| |
| function escapeScriptTags(html: string): string { |
| const escaped = html.replace(/<\/script>/gi, "<\\/script>"); |
| const lastIdx = escaped.lastIndexOf("<\\/script>"); |
| if (lastIdx === -1) return html; |
| return escaped.slice(0, lastIdx) + "</script>" + escaped.slice(lastIdx + 10); |
| } |
|
|
| export interface BuildEmbedSrcdocOptions { |
| |
| isDark?: boolean; |
| |
| primaryColor?: string; |
| |
| |
| |
| |
| |
| |
| |
| fullBleed?: boolean; |
| } |
|
|
| |
| |
| |
| |
| |
| const FULL_BLEED_STYLES = ` |
| html, body { height: 100%; width: 100%; } |
| body { padding: 0 !important; overflow: hidden; } |
| body > :first-child { width: 100%; height: 100%; } |
| `.trim(); |
|
|
| |
| |
| |
| |
| |
| |
| export function buildEmbedSrcdoc(htmlFragment: string, opts: BuildEmbedSrcdocOptions = {}): string { |
| const isDark = !!opts.isDark; |
| const primaryColor = opts.primaryColor || "#4e79a7"; |
| const themeAttr = isDark ? "dark" : "light"; |
| const inlinePrimary = `:root{--primary-color:${primaryColor};}`; |
| const fullBleed = !!opts.fullBleed; |
| const extraStyles = fullBleed ? FULL_BLEED_STYLES : ""; |
|
|
| const parts: string[] = [ |
| "<!DOCTYPE html>", |
| `<html data-theme="${themeAttr}">`, |
| "<head>", |
| '<meta charset="UTF-8">', |
| |
| |
| |
| |
| `<script>${THEME_BOOTSTRAP}<\/script>`, |
| `<style>${BASE_STYLES}${inlinePrimary}${extraStyles}</style>`, |
| `<script>${COLOR_PALETTES_POLYFILL}<\/script>`, |
| "</head>", |
| "<body>", |
| escapeScriptTags(htmlFragment), |
| ]; |
|
|
| |
| |
| |
| if (!fullBleed) { |
| parts.push(`<script>${HEIGHT_REPORTER}<\/script>`); |
| } |
| parts.push(`<script>${THEME_LISTENER}<\/script>`); |
| parts.push("</body></html>"); |
|
|
| return parts.join("\n"); |
| } |
|
|