FineVision / app /src /components /HtmlEmbed.astro
thibaud frere
fix charts, update html embed and responsive image description background, fix pdf generation
e063015
---
interface Props { src: string; title?: string; desc?: string; frameless?: boolean; align?: 'left' | 'center' | 'right'; id?: string }
const { src, title, desc, frameless = false, align = 'left', id } = Astro.props as Props;
// Load all .html embeds under src/content/embeds/** as strings (dev & build)
const embeds = (import.meta as any).glob('../content/embeds/**/*.html', { query: '?raw', import: 'default', eager: true }) as Record<string, string>;
function resolveFragment(requested: string): string | null {
// Allow both "banner.html" and "embeds/banner.html"
const needle = requested.replace(/^\/*/, '');
for (const [key, html] of Object.entries(embeds)) {
if (key.endsWith('/' + needle) || key.endsWith('/' + needle.replace(/^embeds\//, ''))) {
return html;
}
}
return null;
}
const html = resolveFragment(src);
const mountId = `frag-${Math.random().toString(36).slice(2)}`;
---
{ html ? (
<figure class="html-embed" id={id}>
{title && <figcaption class="html-embed__title" style={`text-align:${align}`}>{title}</figcaption>}
<div class={`html-embed__card${frameless ? ' is-frameless' : ''}`}>
<div id={mountId} set:html={html} />
</div>
{desc && <figcaption class="html-embed__desc" style={`text-align:${align}`} set:html={desc}></figcaption>}
</figure>
) : (
<div><!-- Fragment not found: {src} --></div>
) }
<script type="module" is:inline>
// Ensure global color palettes generator is loaded once per page
import '../scripts/color-palettes.js';
export {};
</script>
<script>
// Re-execute <script> tags inside the injected fragment (innerHTML doesn't run scripts)
const scriptEl = document.currentScript;
const mount = scriptEl ? scriptEl.previousElementSibling : null;
const execute = () => {
if (!mount) return;
const scripts = mount.querySelectorAll('script');
scripts.forEach(old => {
// ignore non-executable types (e.g., application/json)
if (old.type && old.type !== 'text/javascript' && old.type !== 'module' && old.type !== '') return;
if (old.dataset.executed === 'true') return;
old.dataset.executed = 'true';
if (old.src) {
const s = document.createElement('script');
Array.from(old.attributes).forEach(attr => s.setAttribute(attr.name, attr.value));
document.body.appendChild(s);
} else {
try {
// run inline
(0, eval)(old.text || '');
} catch (e) {
console.error('HtmlEmbed inline script error:', e);
}
}
});
};
// Execute after DOM is parsed (ensures deferred module scripts are executed first)
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', execute, { once: true });
else execute();
</script>
<style is:global>
.html-embed { margin: 0 0 var(--block-spacing-y); }
.html-embed__title {
text-align: left;
font-weight: 600;
font-size: 0.95rem;
color: var(--text-color);
margin: 0;
padding: var(--spacing-1);
background: var(--page-bg);
position: relative;
z-index: var(--z-elevated);
display: block;
width: 100%;
}
.html-embed__card {
background: var(--code-bg);
border: 1px solid var(--border-color);
border-radius: 10px;
padding: 8px;
}
.html-embed__card.is-frameless {
background: transparent;
border-color: transparent;
padding: 0;
}
.html-embed__desc {
text-align: left;
font-size: 0.9rem;
color: var(--muted-color);
margin: 0;
padding: var(--spacing-1);
background: var(--page-bg);
position: relative;
z-index: var(--z-elevated);
display: block;
width: 100%;
}
/* Plotly – fragments & controls */
.html-embed__card svg text { fill: var(--text-color) !important; }
.html-embed__card label { color: var(--text-color) !important; }
.plotly-graph-div { width: 100% !important; min-height: 320px; }
@media (max-width: 768px) { .plotly-graph-div { min-height: 260px; } }
[id^="plot-"] { display: flex; flex-direction: column; align-items: center; gap: 15px; }
.plotly_caption { font-style: italic; margin-top: 10px; }
.plotly_controls { display: flex; flex-wrap: wrap; justify-content: center; gap: 30px; }
.plotly_input_container { display: flex; align-items: center; flex-direction: column; gap: 10px; }
.plotly_input_container > select { padding: 2px 4px; line-height: 1.5em; text-align: center; border-radius: 4px; font-size: 12px; background-color: var(--neutral-200); outline: none; border: 1px solid var(--neutral-300); }
.plotly_slider { display: flex; align-items: center; gap: 10px; }
.plotly_slider > input[type="range"] { -webkit-appearance: none; appearance: none; height: 2px; background: var(--neutral-400); border-radius: 5px; outline: none; }
.plotly_slider > input[type="range"]::-webkit-slider-thumb { -webkit-appearance: none; width: 18px; height: 18px; border-radius: 50%; background: var(--primary-color); cursor: pointer; }
.plotly_slider > input[type="range"]::-moz-range-thumb { width: 18px; height: 18px; border-radius: 50%; background: var(--primary-color); cursor: pointer; }
.plotly_slider > span { font-size: 14px; line-height: 1.6em; min-width: 16px; }
/* Dark mode overrides for Plotly readability */
[data-theme="dark"] .html-embed__card:not(.is-frameless) { background: #12151b; border-color: rgba(255,255,255,.15); }
[data-theme="dark"] .html-embed__card .xaxislayer-above text,
[data-theme="dark"] .html-embed__card .yaxislayer-above text,
[data-theme="dark"] .html-embed__card .infolayer text,
[data-theme="dark"] .html-embed__card .legend text,
[data-theme="dark"] .html-embed__card .annotation text,
[data-theme="dark"] .html-embed__card .colorbar text,
[data-theme="dark"] .html-embed__card .hoverlayer text { fill: #fff !important; }
[data-theme="dark"] .html-embed__card .xaxislayer-above path,
[data-theme="dark"] .html-embed__card .yaxislayer-above path,
[data-theme="dark"] .html-embed__card .xlines-above,
[data-theme="dark"] .html-embed__card .ylines-above { stroke: rgba(255,255,255,.35) !important; }
[data-theme="dark"] .html-embed__card .gridlayer path { stroke: rgba(255,255,255,.15) !important; }
[data-theme="dark"] .html-embed__card .legend rect.bg { fill: rgba(0,0,0,.25) !important; stroke: rgba(255,255,255,.2) !important; }
[data-theme="dark"] .html-embed__card .hoverlayer .bg { fill: rgba(0,0,0,.8) !important; stroke: rgba(255,255,255,.2) !important; }
[data-theme="dark"] .html-embed__card .colorbar .cbbg { fill: rgba(0,0,0,.25) !important; stroke: rgba(255,255,255,.2) !important; }
@media print {
.html-embed, .html-embed__card { max-width: 100% !important; width: 100% !important; margin-left: 0 !important; margin-right: 0 !important; }
.html-embed__card { padding: 6px; }
.html-embed__card.is-frameless { padding: 0; }
.html-embed__card svg,
.html-embed__card canvas,
.html-embed__card img { max-width: 100% !important; height: auto !important; }
.html-embed__card > div[id^="frag-"] { width: 100% !important; }
}
@media print {
/* Avoid breaks inside embeds */
.html-embed, .html-embed__card { break-inside: avoid; page-break-inside: avoid; }
/* Constrain width and scale inner content */
.html-embed, .html-embed__card { max-width: 100% !important; width: 100% !important; }
.html-embed__card { padding: 6px; }
.html-embed__card.is-frameless { padding: 0; }
.html-embed__card svg,
.html-embed__card canvas,
.html-embed__card img,
.html-embed__card video,
.html-embed__card iframe { max-width: 100% !important; height: auto !important; }
.html-embed__card > div[id^="frag-"] { width: 100% !important; max-width: 100% !important; }
/* Center and constrain the banner (galaxy) when printing */
.html-embed .d3-galaxy { width: 100% !important; max-width: 980px !important; margin-left: auto !important; margin-right: auto !important; }
}
</style>