File size: 8,465 Bytes
b565d01
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e903a32
b565d01
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e903a32
b565d01
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
---
interface Props { src: string; title?: string; desc?: string; frameless?: boolean; align?: 'left' | 'center' | 'right'; id?: string, data?: string | string[], config?: any }
const { src, title, desc, frameless = false, align = 'left', id, data, config } = 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)}`;
const dataAttr = Array.isArray(data) ? JSON.stringify(data) : (typeof data === 'string' ? data : undefined);
const configAttr = typeof config === 'string' ? config : (config != null ? JSON.stringify(config) : undefined);

// Apply the ID to the HTML content if provided
const htmlWithId = id && html ? html.replace(/<div class="([^"]*)"[^>]*>/, `<div class="$1" id="${id}">`) : html;
---
{ 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} data-datafiles={dataAttr} data-config={configAttr} set:html={htmlWithId} />
    </div>
    {desc && <figcaption class="html-embed__desc" style={`text-align:${align}`} set:html={desc}></figcaption>}
  </figure>
 ) : (
  <div><!-- Fragment not found: {src} --></div>
 ) }



<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);
    z-index: var(--z-elevated);
    position: relative;
  }
  .html-embed__title { 
    text-align: left; 
    font-weight: 600; 
    font-size: 0.95rem; 
    color: var(--text-color);
    margin: 0;
    padding: 0;
    padding-bottom: var(--spacing-1);
    position: relative;
    display: block;
    width: 100%;
    background: var(--page-bg);
    z-index: var(--z-elevated);
  }
  .html-embed__card {
    background: var(--code-bg);
    border: 1px solid var(--border-color);
    border-radius: 10px;
    padding: 24px;
    z-index: calc(var(--z-elevated) + 1);
    position: relative;
  }
  .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: 0;
    padding-top: var(--spacing-1);
    position: relative;
    z-index: var(--z-elevated);
    display: block;
    width: 100%;
    background: var(--page-bg);
  }
  /* Plotly – fragments & controls */
  .html-embed__card svg text { fill: var(--text-color); }
  .html-embed__card label { color: var(--text-color); }
  .plotly-graph-div { width: 100%; 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>