thibaud frere commited on
Commit
72cfb5a
·
1 Parent(s): e904bd4
CLAUDE.md ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ # Project Working Notes (CLAUDE)
2
+
3
+ This document summarizes recent implementation details and conventions. Written in English per your preference for written content and code comments.
README.md CHANGED
@@ -9,10 +9,3 @@ header: mini
9
  app_port: 8080
10
  thumbnail: https://huggingface.co/spaces/tfrere/research-paper-template/thumb.jpg
11
  ---
12
-
13
- TO DO :
14
-
15
- - rename le titre ?
16
- - Vérifier la biliographie comment elle marche
17
-
18
- - deploy
 
9
  app_port: 8080
10
  thumbnail: https://huggingface.co/spaces/tfrere/research-paper-template/thumb.jpg
11
  ---
 
 
 
 
 
 
 
app/astro.config.mjs CHANGED
@@ -10,6 +10,116 @@ import rehypeAutolinkHeadings from 'rehype-autolink-headings';
10
  import rehypeCitation from 'rehype-citation';
11
  // Built-in Shiki (dual themes) — no rehype-pretty-code
12
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
  export default defineConfig({
14
  output: 'static',
15
  integrations: [
@@ -45,7 +155,8 @@ export default defineConfig({
45
  [rehypeCitation, {
46
  bibliography: 'src/content/bibliography.bib',
47
  linkCitations: true
48
- }]
 
49
  ]
50
  }
51
  });
 
10
  import rehypeCitation from 'rehype-citation';
11
  // Built-in Shiki (dual themes) — no rehype-pretty-code
12
 
13
+ // Minimal rehype plugin to wrap code blocks with a copy button and a language label
14
+ function rehypeCodeCopyAndLabel() {
15
+ return (tree) => {
16
+ // Walk the tree; lightweight visitor to find <pre><code>
17
+ const visit = (node, parent) => {
18
+ if (!node || typeof node !== 'object') return;
19
+ const children = Array.isArray(node.children) ? node.children : [];
20
+ if (node.tagName === 'pre' && children.some(c => c.tagName === 'code')) {
21
+ // Find code child and guess language
22
+ const code = children.find(c => c.tagName === 'code');
23
+ const collectClasses = (val) => Array.isArray(val) ? val.map(String) : (typeof val === 'string' ? String(val).split(/\s+/) : []);
24
+ const fromClass = (names) => {
25
+ const hit = names.find((n) => /^language-/.test(String(n)));
26
+ return hit ? String(hit).replace(/^language-/, '') : '';
27
+ };
28
+ const codeClasses = collectClasses(code?.properties?.className);
29
+ const preClasses = collectClasses(node?.properties?.className);
30
+ const candidates = [
31
+ code?.properties?.['data-language'],
32
+ fromClass(codeClasses),
33
+ node?.properties?.['data-language'],
34
+ fromClass(preClasses),
35
+ ];
36
+ let lang = candidates.find(Boolean) || '';
37
+ const displayLang = lang ? String(lang).toUpperCase() : '';
38
+ // Determine if single-line block: prefer Shiki lines, then text content
39
+ const countLinesFromShiki = () => {
40
+ const isLineEl = (el) => el && el.type === 'element' && el.tagName === 'span' && Array.isArray(el.properties?.className) && el.properties.className.includes('line');
41
+ const hasNonWhitespaceText = (node) => {
42
+ if (!node) return false;
43
+ if (node.type === 'text') return /\S/.test(String(node.value || ''));
44
+ const kids = Array.isArray(node.children) ? node.children : [];
45
+ return kids.some(hasNonWhitespaceText);
46
+ };
47
+ const collectLines = (node, acc) => {
48
+ if (!node || typeof node !== 'object') return;
49
+ if (isLineEl(node)) acc.push(node);
50
+ const kids = Array.isArray(node.children) ? node.children : [];
51
+ kids.forEach((k) => collectLines(k, acc));
52
+ };
53
+ const lines = [];
54
+ collectLines(code, lines);
55
+ const nonEmpty = lines.filter((ln) => hasNonWhitespaceText(ln)).length;
56
+ return nonEmpty || 0;
57
+ };
58
+ const countLinesFromText = () => {
59
+ // Parse raw text content of the <code> node including nested spans
60
+ const extractText = (node) => {
61
+ if (!node) return '';
62
+ if (node.type === 'text') return String(node.value || '');
63
+ const kids = Array.isArray(node.children) ? node.children : [];
64
+ return kids.map(extractText).join('');
65
+ };
66
+ const raw = extractText(code);
67
+ if (!raw || !/\S/.test(raw)) return 0;
68
+ return raw.split('\n').filter(line => /\S/.test(line)).length;
69
+ };
70
+ const lines = countLinesFromShiki() || countLinesFromText();
71
+ const isSingleLine = lines <= 1;
72
+ // Also treat code blocks shorter than a threshold as single-line (defensive)
73
+ if (!isSingleLine) {
74
+ const approxChars = (() => {
75
+ const extract = (n) => Array.isArray(n?.children) ? n.children.map(extract).join('') : (n?.type === 'text' ? String(n.value||'') : '');
76
+ return extract(code).length;
77
+ })();
78
+ if (approxChars < 6) {
79
+ // e.g., "npm i" alone
80
+ // downgrade to single-line behavior
81
+ node.__forceSingle = true;
82
+ }
83
+ }
84
+ // Ensure CSS-only label works: set data-language on the <code> element
85
+ code.properties = code.properties || {};
86
+ if (displayLang) code.properties['data-language'] = displayLang;
87
+ // Replace <pre> with wrapper div.code-card containing button + pre
88
+ const wrapper = {
89
+ type: 'element',
90
+ tagName: 'div',
91
+ properties: { className: ['code-card'].concat((isSingleLine || node.__forceSingle) ? ['no-copy'] : []), 'data-language': displayLang },
92
+ children: (isSingleLine || node.__forceSingle) ? [ node ] : [
93
+ {
94
+ type: 'element',
95
+ tagName: 'button',
96
+ properties: { className: ['code-copy', 'button--ghost'], type: 'button', 'aria-label': 'Copy code' },
97
+ children: [
98
+ {
99
+ type: 'element',
100
+ tagName: 'svg',
101
+ properties: { viewBox: '0 0 24 24', 'aria-hidden': 'true', focusable: 'false' },
102
+ children: [
103
+ { type: 'element', tagName: 'path', properties: { d: 'M16 1H4c-1.1 0-2 .9-2 2v12h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z' }, children: [] }
104
+ ]
105
+ }
106
+ ]
107
+ },
108
+ node
109
+ ]
110
+ };
111
+ if (parent && Array.isArray(parent.children)) {
112
+ const idx = parent.children.indexOf(node);
113
+ if (idx !== -1) parent.children[idx] = wrapper;
114
+ }
115
+ return; // don't visit nested
116
+ }
117
+ children.forEach((c) => visit(c, node));
118
+ };
119
+ visit(tree, null);
120
+ };
121
+ }
122
+
123
  export default defineConfig({
124
  output: 'static',
125
  integrations: [
 
155
  [rehypeCitation, {
156
  bibliography: 'src/content/bibliography.bib',
157
  linkCitations: true
158
+ }],
159
+ rehypeCodeCopyAndLabel
160
  ]
161
  }
162
  });
app/package-lock.json CHANGED
Binary files a/app/package-lock.json and b/app/package-lock.json differ
 
app/package.json CHANGED
Binary files a/app/package.json and b/app/package.json differ
 
app/src/components/Header.astro DELETED
@@ -1,17 +0,0 @@
1
- ---
2
- import HtmlFragment from "./HtmlFragment.astro";
3
-
4
- interface Props {
5
- title: string;
6
- description?: string;
7
- }
8
- const { title, description } = Astro.props as Props;
9
- ---
10
- <section class="hero">
11
- <h1 class="hero-title" set:html={title}></h1>
12
- <div class="hero-banner">
13
- <HtmlFragment src="banner.html" />
14
- {description && <p class="hero-desc">{description}</p>}
15
- </div>
16
- </section>
17
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/src/components/{Meta.astro → Hero.astro} RENAMED
@@ -1,11 +1,21 @@
1
  ---
 
 
2
  interface Props {
3
- title: string;
 
 
4
  authors?: string[];
5
  affiliation?: string;
6
  published?: string;
7
  }
8
- const { title, authors = [], affiliation, published } = Astro.props as Props;
 
 
 
 
 
 
9
  function slugify(text: string): string {
10
  return String(text || '')
11
  .normalize('NFKD')
@@ -15,8 +25,18 @@ function slugify(text: string): string {
15
  .replace(/^-+|-+$/g, '')
16
  .slice(0, 120) || 'article';
17
  }
18
- const pdfFilename = `${slugify(title)}.pdf`;
 
 
19
  ---
 
 
 
 
 
 
 
 
20
  <header class="meta">
21
  <div class="meta-container">
22
  {authors.length > 0 && (
@@ -64,5 +84,21 @@ const pdfFilename = `${slugify(title)}.pdf`;
64
  document.addEventListener('DOMContentLoaded', ready, { once: true });
65
  } else { ready(); }
66
  })();
67
- </script>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
 
 
1
  ---
2
+ import HtmlEmbed from "./HtmlEmbed.astro";
3
+
4
  interface Props {
5
+ title: string; // may contain HTML (e.g., <br/>)
6
+ titleRaw?: string; // plain title for slug/PDF (optional)
7
+ description?: string;
8
  authors?: string[];
9
  affiliation?: string;
10
  published?: string;
11
  }
12
+
13
+ const { title, titleRaw, description, authors = [], affiliation, published } = Astro.props as Props;
14
+
15
+ function stripHtml(text: string): string {
16
+ return String(text || '').replace(/<[^>]*>/g, '');
17
+ }
18
+
19
  function slugify(text: string): string {
20
  return String(text || '')
21
  .normalize('NFKD')
 
25
  .replace(/^-+|-+$/g, '')
26
  .slice(0, 120) || 'article';
27
  }
28
+
29
+ const pdfBase = titleRaw ? titleRaw : stripHtml(title);
30
+ const pdfFilename = `${slugify(pdfBase)}.pdf`;
31
  ---
32
+ <section class="hero">
33
+ <h1 class="hero-title" set:html={title}></h1>
34
+ <div class="hero-banner">
35
+ <HtmlEmbed src="banner.html" frameless />
36
+ {description && <p class="hero-desc">{description}</p>}
37
+ </div>
38
+ </section>
39
+
40
  <header class="meta">
41
  <div class="meta-container">
42
  {authors.length > 0 && (
 
84
  document.addEventListener('DOMContentLoaded', ready, { once: true });
85
  } else { ready(); }
86
  })();
87
+ </script>
88
+
89
+ <style>
90
+ /* Hero (full-bleed) */
91
+ .hero { width: 100%; padding: 48px 16px 16px; text-align: center; }
92
+ .hero-title { font-size: clamp(28px, 4vw, 48px); font-weight: 800; line-height: 1.1; margin: 0 0 8px; max-width: 60%; margin: auto; }
93
+ .hero-banner { max-width: 980px; margin: 0 auto; }
94
+ .hero-desc { color: var(--muted-color); font-style: italic; margin: 0 0 16px 0; }
95
+
96
+ /* Meta (byline-like header) */
97
+ .meta { border-top: 1px solid var(--border-color); border-bottom: 1px solid var(--border-color); padding: 1rem 0; font-size: 0.9rem; line-height: 1.8em; }
98
+ .meta-container { max-width: 720px; display: flex; flex-direction: row; justify-content: space-between; margin: 0 auto; gap: 8px; }
99
+ .meta-container-cell { display: flex; flex-direction: column; gap: 8px; }
100
+ .meta-container-cell h3 { margin: 0; font-size: 12px; font-weight: 400; color: var(--muted-color); text-transform: uppercase; letter-spacing: .02em; }
101
+ .meta-container-cell p { margin: 0; }
102
+ </style>
103
+
104
 
app/src/components/{HtmlFragment.astro → HtmlEmbed.astro} RENAMED
@@ -1,9 +1,9 @@
1
  ---
2
- interface Props { src: string }
3
- const { src } = Astro.props as Props;
4
 
5
  // Load all .html fragments under src/content/fragments/** as strings (dev & build)
6
- const fragments = import.meta.glob('../content/fragments/**/*.html', { query: '?raw', import: 'default', eager: true }) as Record<string, string>;
7
 
8
  function resolveFragment(requested: string): string | null {
9
  // Allow both "banner.html" and "fragments/banner.html"
@@ -20,7 +20,13 @@ const html = resolveFragment(src);
20
  const mountId = `frag-${Math.random().toString(36).slice(2)}`;
21
  ---
22
  { html ? (
23
- <div id={mountId} set:html={html} />
 
 
 
 
 
 
24
  ) : (
25
  <div><!-- Fragment not found: {src} --></div>
26
  ) }
@@ -46,14 +52,48 @@ const mountId = `frag-${Math.random().toString(36).slice(2)}`;
46
  // run inline
47
  (0, eval)(old.text || '');
48
  } catch (e) {
49
- console.error('HtmlFragment inline script error:', e);
50
  }
51
  }
52
  });
53
  };
54
  // Ensure execution when ready: run now if Plotly or D3 is present, or when document is ready; otherwise wait for 'load'
 
55
  if (window.Plotly || window.d3 || document.readyState === 'complete') execute();
56
  else window.addEventListener('load', execute, { once: true });
57
  </script>
58
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59
 
 
1
  ---
2
+ interface Props { src: string; title?: string; desc?: string; frameless?: boolean }
3
+ const { src, title, desc, frameless = false } = Astro.props as Props;
4
 
5
  // Load all .html fragments under src/content/fragments/** as strings (dev & build)
6
+ const fragments = (import.meta as any).glob('../content/fragments/**/*.html', { query: '?raw', import: 'default', eager: true }) as Record<string, string>;
7
 
8
  function resolveFragment(requested: string): string | null {
9
  // Allow both "banner.html" and "fragments/banner.html"
 
20
  const mountId = `frag-${Math.random().toString(36).slice(2)}`;
21
  ---
22
  { html ? (
23
+ <figure class="html-embed">
24
+ {title && <figcaption class="html-embed__title">{title}</figcaption>}
25
+ <div class={`html-embed__card${frameless ? ' is-frameless' : ''}`}>
26
+ <div id={mountId} set:html={html} />
27
+ </div>
28
+ {desc && <figcaption class="html-embed__desc" set:html={desc}></figcaption>}
29
+ </figure>
30
  ) : (
31
  <div><!-- Fragment not found: {src} --></div>
32
  ) }
 
52
  // run inline
53
  (0, eval)(old.text || '');
54
  } catch (e) {
55
+ console.error('HtmlEmbed inline script error:', e);
56
  }
57
  }
58
  });
59
  };
60
  // Ensure execution when ready: run now if Plotly or D3 is present, or when document is ready; otherwise wait for 'load'
61
+ // @ts-expect-error: Plotly/d3 are attached globally at runtime via fragments
62
  if (window.Plotly || window.d3 || document.readyState === 'complete') execute();
63
  else window.addEventListener('load', execute, { once: true });
64
  </script>
65
 
66
+ <style>
67
+ .html-embed { margin: 12px 0; }
68
+ .html-embed__title {
69
+ text-align: left;
70
+ font-weight: 600;
71
+ font-size: 0.95rem;
72
+ color: var(--text-color);
73
+ margin: 0 0 6px 0;
74
+ }
75
+ .html-embed__card {
76
+ background: var(--code-bg);
77
+ border: 1px solid var(--border-color);
78
+ border-radius: 10px;
79
+ padding: 8px;
80
+ }
81
+ .html-embed__card.is-frameless {
82
+ background: transparent;
83
+ border-color: transparent;
84
+ }
85
+ .html-embed__desc {
86
+ text-align: left;
87
+ font-size: 0.9rem;
88
+ color: var(--muted-color);
89
+ margin: 6px 0 0 0;
90
+ }
91
+ @media (prefers-color-scheme: dark) {
92
+ [data-theme="dark"] .html-embed__card:not(.is-frameless) { background: #12151b; border-color: rgba(255,255,255,.15); }
93
+ }
94
+ @media print {
95
+ .html-embed, .html-embed__card { break-inside: avoid; page-break-inside: avoid; }
96
+ }
97
+ </style>
98
+
99
 
app/src/components/MermaidDemo.astro DELETED
@@ -1,11 +0,0 @@
1
- ---
2
- export interface Props {
3
- code?: string;
4
- }
5
- const { code = `graph TD\n A[Start] --> B{Is it working?}\n B -- Yes --> C[Great!]\n B -- No --> D[Fix it]\n D --> B` } = Astro.props;
6
- ---
7
-
8
- <pre class="mermaid">{code}</pre>
9
-
10
-
11
- <style>.mermaid { max-width: 100%; }</style>
 
 
 
 
 
 
 
 
 
 
 
 
app/src/components/{SeoHead.astro → Seo.astro} RENAMED
File without changes
app/src/content/article.mdx CHANGED
@@ -1,21 +1,21 @@
1
  ---
2
- title: "Bringing paper to life:\n A modern template for scientific writing
3
  "
4
- subtitle: "Markdown-first research article template with math, citations, and interactive figures."
5
- description: "A modern, MDX-first research article template with math, citations, and interactive figures."
6
  authors:
7
  - "John Doe"
8
  - "Alice Martin"
9
  - "Robert Brown"
10
  affiliation: "Hugging Face"
11
- published: "Feb 19, 2025"
12
  tags:
13
  - research
14
  - template
15
  ogImage: "/thumb.jpg"
16
  ---
17
 
18
- import HtmlFragment from "../components/HtmlFragment.astro";
19
  import Wide from "../components/Wide.astro";
20
  import FullBleed from "../components/FullBleed.astro";
21
  import { Image } from 'astro:assets';
@@ -64,6 +64,7 @@ import GettingStarted from "./chapters/getting-started.mdx";
64
  <span className="tag">Optimized images</span>
65
  <span className="tag">Automatic PDF export</span>
66
  <span className="tag">Dataviz color palettes</span>
 
67
  </div>
68
  <Fragment slot="aside">
69
  If you have questions or remarks open a discussion on the <a href="https://huggingface.co/spaces/tfrere/research-blog-template/discussions?status=open&type=discussion">Community tab</a>!
 
1
  ---
2
+ title: "Bringing paper to life:\n A modern template for\n scientific writing
3
  "
4
+ subtitle: "A modern, MDX-first research article template with math, citations and interactive figures."
5
+ description: "A modern, MDX-first research article template with math, citations and interactive figures."
6
  authors:
7
  - "John Doe"
8
  - "Alice Martin"
9
  - "Robert Brown"
10
  affiliation: "Hugging Face"
11
+ published: "Aug 28, 2025"
12
  tags:
13
  - research
14
  - template
15
  ogImage: "/thumb.jpg"
16
  ---
17
 
18
+ import HtmlEmbed from "../components/HtmlEmbed.astro";
19
  import Wide from "../components/Wide.astro";
20
  import FullBleed from "../components/FullBleed.astro";
21
  import { Image } from 'astro:assets';
 
64
  <span className="tag">Optimized images</span>
65
  <span className="tag">Automatic PDF export</span>
66
  <span className="tag">Dataviz color palettes</span>
67
+ <span className="tag">Embed gradio apps</span>
68
  </div>
69
  <Fragment slot="aside">
70
  If you have questions or remarks open a discussion on the <a href="https://huggingface.co/spaces/tfrere/research-blog-template/discussions?status=open&type=discussion">Community tab</a>!
app/src/content/chapters/available-blocks.mdx CHANGED
@@ -1,7 +1,7 @@
1
  import { Image } from 'astro:assets';
2
  import placeholder from '../../assets/images/placeholder.png';
3
  import audioDemo from '../../assets/audio/audio-example.wav';
4
- import HtmlFragment from '../../components/HtmlFragment.astro';
5
  import Aside from '../../components/Aside.astro';
6
  import Wide from '../../components/Wide.astro';
7
  import FullBleed from '../../components/FullBleed.astro';
@@ -50,10 +50,15 @@ $$
50
  $$
51
  ```
52
 
53
- ### Images
54
 
55
  **Responsive images** automatically generate an optimized `srcset` and `sizes` so the browser downloads the most appropriate file for the current viewport and DPR. You can also request multiple output formats (e.g., **AVIF**, **WebP**, fallback **PNG/JPEG**) and control **lazy loading/decoding** for better **performance**.
56
 
 
 
 
 
 
57
  **Optional:** Zoomable (Medium-like lightbox): add `data-zoomable` to opt-in. Only images with this attribute will open full-screen on click.
58
 
59
  **Optional:** Lazy loading: add `loading="lazy"` to opt-in.
@@ -91,7 +96,7 @@ import myImage from '../assets/images/placeholder.jpg'
91
  ```
92
 
93
 
94
- ### Code blocks
95
 
96
  Use fenced code blocks with a language for syntax highlighting.
97
 
@@ -112,30 +117,85 @@ greet("Astro")
112
  ```
113
  ````
114
 
115
- ### Mermaid diagrams
116
 
117
  Native mermaid diagrams are supported. You can use the <a target="_blank" href="https://mermaid.live/edit#pako:eNpVjUFPg0AQhf_KZk6a0AYsCywHE0u1lyZ66EnoYQMDSyy7ZFlSK_DfXWiMOqd58773ZoBcFQgxlGd1yQXXhhx3mSR2ntJE6LozDe9OZLV6HPdoSKMkXkeyvdsr0gnVtrWs7m_8doZIMhxmDIkRtfyYblay5F8ljmSXHnhrVHv66xwvaiTPaf0mbP1_R2i0qZe05HHJVznXJOF6QcCBStcFxEb36ECDuuGzhGF2MzACG8wgtmuBJe_PJoNMTjbWcvmuVPOT1KqvBNj6c2dV3xbc4K7mlea_CMoCdaJ6aSCm3lIB8QCfED94dM2o77ssjFzK3MiBq2WCNWUeiza-H26YvU8OfC0_3XVII9eLQuYFIaVBGEzfyTJ22g"> live editor</a> to create your diagram and copy the code to your article.
118
 
 
119
  ```mermaid
120
- graph TD
121
- A[Start] --> B{Is it working?}
122
- B -- Yes --> C[Great!]
123
- B -- No --> D[Fix it]
124
- D --> B
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
125
  ```
126
 
127
  <small className="muted">Example</small>
128
  ````mdx
129
  ```mermaid
130
- graph TD
131
- A[Start] --> B{Is it working?}
132
- B -- Yes --> C[Great!]
133
- B -- No --> D[Fix it]
134
- D --> B
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
135
  ```
136
  ````
137
 
138
- ### Citations and notes
 
 
 
 
139
 
140
  Here are a few variations using the same bibliography:
141
 
@@ -186,10 +246,6 @@ Accessible accordion based on `details/summary`. You can pass any children conte
186
  </ul>
187
  </Accordion>
188
 
189
- <Accordion title="Closed by default">
190
- <p>This one stays collapsed until the user clicks the summary.</p>
191
- </Accordion>
192
-
193
  <Accordion title="Accordion with code example">
194
  ```ts
195
  function greet(name: string) {
@@ -208,23 +264,6 @@ import Accordion from '../components/Accordion.astro'
208
  <p>Free content with <strong>markdown</strong> and MDX components.</p>
209
  </Accordion>
210
 
211
- <Accordion title="Another accordion">
212
- <ul>
213
- <li>Item A</li>
214
- <li>Item B</li>
215
- </ul>
216
- </Accordion>
217
-
218
- <Accordion title="Accordion with code example">
219
- ```ts
220
- function greet(name: string) {
221
- console.log(`Hello, ${name}`);
222
- }
223
-
224
- greet("Astro");
225
- ```
226
- </Accordion>
227
-
228
  <Accordion title="Accordion with code example">
229
  ```ts
230
  function greet(name: string) {
@@ -265,53 +304,46 @@ import audioDemo from '../assets/audio/audio-example.wav'
265
 
266
 
267
 
268
- ### Embeds
269
-
270
 
271
- #### Html Fragments
272
 
273
- The main purpose of the ```HtmlFragment``` component is to **embed** a **Plotly** or **D3.js** chart in your article. **Libraries** are already imported in the template.
274
 
275
  They exist in the `app/src/content/fragments` folder.
276
 
277
- Plotly and D3 are already supported by the template. For researchers who want to stay in **Python** while targeting **D3**, the [d3blocks](https://github.com/d3blocks/d3blocks) library lets you create interactive D3 charts with only a few lines of code. In **2025**, **D3** often provides more flexibility and a more web‑native rendering than **Plotly** for custom visualizations.
278
-
279
- Here are some examples of the two **libraries** in the template:
280
 
281
- <small class="muted">D3 version</small>
282
- <div className="plot-card">
283
- <HtmlFragment src="d3-line.html" />
284
- </div>
285
- <caption className="caption">D3 Line chart — simple time series example.</caption>
286
-
287
- <div className="plot-card">
288
- <HtmlFragment src="d3-bar.html" />
289
- </div>
290
- <caption className="caption">D3 Bar chart — categorical distribution example.</caption>
291
-
292
- ---
293
 
294
- <small class="muted">Plotly version</small>
 
 
 
295
 
296
- <div className="plot-card">
297
- <HtmlFragment src="line.html" />
298
- </div>
299
- <caption className="caption">Plotly Line chart — interactive time series with hover and zoom.</caption>
 
 
 
 
 
300
 
301
  <small className="muted">Example</small>
302
  ```mdx
303
- import HtmlFragment from '../components/HtmlFragment.astro'
304
 
305
- <HtmlFragment src="line.html" />
306
  ```
307
 
308
- #### Iframes
309
 
310
  You can embed external content in your article using **iframes**. For example, **TrackIO or github code embeds** can be used this way.
311
 
312
  <iframe frameborder="0" scrolling="no" style="width:100%; height:292px;" allow="clipboard-write" src="https://emgithub.com/iframe.html?target=https%3A%2F%2Fgithub.com%2Fhuggingface%2Fpicotron%2Fblob%2F1004ae37b87887cde597c9060fb067faa060bafe%2Fsetup.py&style=default&type=code&showBorder=on&showLineNumbers=on"></iframe>
313
 
314
- <iframe className="plot-card" src="https://trackio-documentation.hf.space/?project=fake-training-750735&metrics=train_loss,train_accuracy&sidebar=hidden&lang=en" width="100%" height="660" frameborder="0"></iframe>
315
 
316
 
317
  <small className="muted">Example</small>
@@ -320,15 +352,15 @@ You can embed external content in your article using **iframes**. For example, *
320
  <iframe src="https://trackio-documentation.hf.space/?project=fake-training-750735&metrics=train_loss,train_accuracy&sidebar=hidden&lang=en" width="100%" height="600" frameborder="0"></iframe>
321
  ```
322
 
323
- #### Gradio
324
 
325
  You can also embed **gradio** apps.
326
 
327
- <gradio-app theme_mode="light" space="hebrew-llm-leaderboard/leaderboard"></gradio-app>
328
 
329
 
330
 
331
  <small className="muted">Example</small>
332
  ```mdx
333
- <gradio-app theme_mode="light" space="hebrew-llm-leaderboard/leaderboard"></gradio-app>
334
  ```
 
1
  import { Image } from 'astro:assets';
2
  import placeholder from '../../assets/images/placeholder.png';
3
  import audioDemo from '../../assets/audio/audio-example.wav';
4
+ import HtmlEmbed from '../../components/HtmlEmbed.astro';
5
  import Aside from '../../components/Aside.astro';
6
  import Wide from '../../components/Wide.astro';
7
  import FullBleed from '../../components/FullBleed.astro';
 
50
  $$
51
  ```
52
 
53
+ ### Image
54
 
55
  **Responsive images** automatically generate an optimized `srcset` and `sizes` so the browser downloads the most appropriate file for the current viewport and DPR. You can also request multiple output formats (e.g., **AVIF**, **WebP**, fallback **PNG/JPEG**) and control **lazy loading/decoding** for better **performance**.
56
 
57
+ Props (optional)
58
+ - `data-zoomable`: adds a zoomable lightbox.
59
+ - `loading="lazy"`: lazy loads the image.
60
+ - `figcaption`: adds a caption and credit.
61
+
62
  **Optional:** Zoomable (Medium-like lightbox): add `data-zoomable` to opt-in. Only images with this attribute will open full-screen on click.
63
 
64
  **Optional:** Lazy loading: add `loading="lazy"` to opt-in.
 
96
  ```
97
 
98
 
99
+ ### Code
100
 
101
  Use fenced code blocks with a language for syntax highlighting.
102
 
 
117
  ```
118
  ````
119
 
120
+ ### Mermaid diagram
121
 
122
  Native mermaid diagrams are supported. You can use the <a target="_blank" href="https://mermaid.live/edit#pako:eNpVjUFPg0AQhf_KZk6a0AYsCywHE0u1lyZ66EnoYQMDSyy7ZFlSK_DfXWiMOqd58773ZoBcFQgxlGd1yQXXhhx3mSR2ntJE6LozDe9OZLV6HPdoSKMkXkeyvdsr0gnVtrWs7m_8doZIMhxmDIkRtfyYblay5F8ljmSXHnhrVHv66xwvaiTPaf0mbP1_R2i0qZe05HHJVznXJOF6QcCBStcFxEb36ECDuuGzhGF2MzACG8wgtmuBJe_PJoNMTjbWcvmuVPOT1KqvBNj6c2dV3xbc4K7mlea_CMoCdaJ6aSCm3lIB8QCfED94dM2o77ssjFzK3MiBq2WCNWUeiza-H26YvU8OfC0_3XVII9eLQuYFIaVBGEzfyTJ22g"> live editor</a> to create your diagram and copy the code to your article.
123
 
124
+
125
  ```mermaid
126
+ erDiagram
127
+ DATASET ||--o{ SAMPLE : contains
128
+ RUN }o--o{ SAMPLE : uses
129
+ RUN ||--|| MODEL : trains
130
+ RUN ||--o{ METRIC : logs
131
+
132
+ DATASET {
133
+ string id
134
+ string name
135
+ }
136
+
137
+ SAMPLE {
138
+ string id
139
+ string uri
140
+ }
141
+
142
+ MODEL {
143
+ string id
144
+ string framework
145
+ }
146
+
147
+ RUN {
148
+ string id
149
+ date startedAt
150
+ }
151
+
152
+ METRIC {
153
+ string name
154
+ float value
155
+ }
156
  ```
157
 
158
  <small className="muted">Example</small>
159
  ````mdx
160
  ```mermaid
161
+ erDiagram
162
+ DATASET ||--o{ SAMPLE : contains
163
+ RUN }o--o{ SAMPLE : uses
164
+ RUN ||--|| MODEL : trains
165
+ RUN ||--o{ METRIC : logs
166
+
167
+ DATASET {
168
+ string id
169
+ string name
170
+ }
171
+
172
+ SAMPLE {
173
+ string id
174
+ string uri
175
+ }
176
+
177
+ MODEL {
178
+ string id
179
+ string framework
180
+ }
181
+
182
+ RUN {
183
+ string id
184
+ date startedAt
185
+ }
186
+
187
+ METRIC {
188
+ string name
189
+ float value
190
+ }
191
  ```
192
  ````
193
 
194
+ ### Citation and footnote
195
+
196
+ **Citations** use the `@` syntax (e.g., `[@vaswani2017attention]` or `@vaswani2017attention` in narrative form) and are **automatically** collected to render the **bibliography** at the end of the article. The citation keys come from `app/src/content/bibliography.bib`.
197
+
198
+ **Footnotes** use an identifier like `[^f1]` and a definition anywhere in the document, e.g., `[^f1]: Your explanation`. They are **numbered** and **listed automatically** at the end of the article.
199
 
200
  Here are a few variations using the same bibliography:
201
 
 
246
  </ul>
247
  </Accordion>
248
 
 
 
 
 
249
  <Accordion title="Accordion with code example">
250
  ```ts
251
  function greet(name: string) {
 
264
  <p>Free content with <strong>markdown</strong> and MDX components.</p>
265
  </Accordion>
266
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
267
  <Accordion title="Accordion with code example">
268
  ```ts
269
  function greet(name: string) {
 
304
 
305
 
306
 
 
 
307
 
308
+ ### HtmlEmbed
309
 
310
+ The main purpose of the ```HtmlEmbed``` component is to **embed** a **Plotly** or **D3.js** chart in your article. **Libraries** are already imported in the template.
311
 
312
  They exist in the `app/src/content/fragments` folder.
313
 
314
+ For researchers who want to stay in **Python** while targeting **D3**, the [d3blocks](https://github.com/d3blocks/d3blocks) library lets you create interactive D3 charts with only a few lines of code. In **2025**, **D3** often provides more flexibility and a more web‑native rendering than **Plotly** for custom visualizations.
 
 
315
 
316
+ Here are some examples of the two **libraries** in the template
 
 
 
 
 
 
 
 
 
 
 
317
 
318
+ Props (optional)
319
+ - `title`: short title displayed above the card.
320
+ - `desc`: short description displayed below the card. Supports inline HTML (e.g., links).
321
+ - `frameless`: removes the card background and border for seamless embeds.
322
 
323
+ <HtmlEmbed src="d3-line.html" title="D3 Line" desc="Simple time series" />
324
+ ---
325
+ <HtmlEmbed
326
+ src="d3-bar.html"
327
+ title="Memory usage with recomputation"
328
+ desc={`Memory usage with recomputation — <a href="https://huggingface.co/spaces/nanotron/ultrascale-playbook?section=activation_recomputation" target="_blank">from the ultrascale playbook</a>`}
329
+ />
330
+ ---
331
+ <HtmlEmbed src="line.html" title="Plotly Line" desc="Interactive time series" />
332
 
333
  <small className="muted">Example</small>
334
  ```mdx
335
+ import HtmlEmbed from '../components/HtmlEmbed.astro'
336
 
337
+ <HtmlEmbed src="line.html" title="Plotly Line" desc="Interactive time series" />
338
  ```
339
 
340
+ ### Iframes
341
 
342
  You can embed external content in your article using **iframes**. For example, **TrackIO or github code embeds** can be used this way.
343
 
344
  <iframe frameborder="0" scrolling="no" style="width:100%; height:292px;" allow="clipboard-write" src="https://emgithub.com/iframe.html?target=https%3A%2F%2Fgithub.com%2Fhuggingface%2Fpicotron%2Fblob%2F1004ae37b87887cde597c9060fb067faa060bafe%2Fsetup.py&style=default&type=code&showBorder=on&showLineNumbers=on"></iframe>
345
 
346
+ <iframe className="html-embed__card" src="https://trackio-documentation.hf.space/?project=fake-training-750735&metrics=train_loss,train_accuracy&sidebar=hidden&lang=en" width="100%" height="660" frameborder="0"></iframe>
347
 
348
 
349
  <small className="muted">Example</small>
 
352
  <iframe src="https://trackio-documentation.hf.space/?project=fake-training-750735&metrics=train_loss,train_accuracy&sidebar=hidden&lang=en" width="100%" height="600" frameborder="0"></iframe>
353
  ```
354
 
355
+ ### Gradio
356
 
357
  You can also embed **gradio** apps.
358
 
359
+ <gradio-app theme_mode="light" space="gradio/hello_world"></gradio-app>
360
 
361
 
362
 
363
  <small className="muted">Example</small>
364
  ```mdx
365
+ <gradio-app theme_mode="light" space="gradio/hello_world"></gradio-app>
366
  ```
app/src/content/chapters/best-pratices.mdx CHANGED
@@ -41,7 +41,7 @@ A palette encodes **meaning** (categories, magnitudes, oppositions), preserves *
41
 
42
  <Aside>
43
  <div className="">
44
- <HtmlFragment src="palettes.html" />
45
  </div>
46
  <Fragment slot="aside">
47
  You can choose a color from the palette to update palettes and copy them to your clipboard.
 
41
 
42
  <Aside>
43
  <div className="">
44
+ <HtmlEmbed src="palettes.html" />
45
  </div>
46
  <Fragment slot="aside">
47
  You can choose a color from the palette to update palettes and copy them to your clipboard.
app/src/content/chapters/getting-started.mdx CHANGED
@@ -39,12 +39,10 @@ The recommended way is to **duplicate this Space on Hugging Face** rather than c
39
 
40
  1. Open the template Space: **[🤗 science-blog-template](https://huggingface.co/spaces/tfrere/science-blog-template)** and click "Duplicate this Space".
41
  2. Give it a name, choose visibility, and keep the SDK as **Docker** (this project includes a `Dockerfile`).
42
- 3. A new Space repository will be created under your account.
43
-
44
- Then push your changes to your new Space repo:
45
 
46
  ```bash
47
- git clone https://huggingface.co/spaces/<your-username>/<your-space>
48
  cd <your-space>
49
  # Make edits locally, then:
50
  git add .
@@ -52,8 +50,3 @@ git commit -m "Update content"
52
  git push
53
  ```
54
 
55
- **Every push automatically triggers a build and deploy** on Spaces.
56
-
57
- Notes for Docker Spaces:
58
- - This project exposes port `8080` via Nginx; the Space front matter includes `app_port: 8080` so the service can be probed correctly.
59
- - No extra configuration is required; the `Dockerfile` builds the Astro site and serves it.
 
39
 
40
  1. Open the template Space: **[🤗 science-blog-template](https://huggingface.co/spaces/tfrere/science-blog-template)** and click "Duplicate this Space".
41
  2. Give it a name, choose visibility, and keep the SDK as **Docker** (this project includes a `Dockerfile`).
42
+ 3. Then push your changes to your new Space repo. **Every push automatically triggers a build and deploy** on Spaces.
 
 
43
 
44
  ```bash
45
+ git clone git@hf.co:spaces/<your-username>/<your-space>.git
46
  cd <your-space>
47
  # Make edits locally, then:
48
  git add .
 
50
  git push
51
  ```
52
 
 
 
 
 
 
app/src/content/chapters/writing-your-content.mdx CHANGED
@@ -4,7 +4,7 @@ import placeholder from '../../assets/images/placeholder.png';
4
  import Aside from '../../components/Aside.astro';
5
  import Wide from '../../components/Wide.astro';
6
  import FullBleed from '../../components/FullBleed.astro';
7
- import HtmlFragment from '../../components/HtmlFragment.astro';
8
  import audioDemo from '../../assets/audio/audio-example.wav';
9
 
10
  ## Writing Your Content
@@ -62,7 +62,7 @@ import Aside from '../components/Aside.astro'
62
 
63
  # Mixing Markdown and components
64
 
65
- This paragraph is written in Markdown.
66
 
67
  <Aside>A short callout inserted via a component.</Aside>
68
 
@@ -106,7 +106,7 @@ Use the **color picker** below to see how the primary color affects the theme.
106
  #### Brand color
107
 
108
  <Aside>
109
- <HtmlFragment src="color-picker.html" />
110
  <Fragment slot="aside">
111
  You can use the color picker to select the right color.
112
 
@@ -119,7 +119,7 @@ Use the **color picker** below to see how the primary color affects the theme.
119
 
120
  Here is a suggestion of **color palettes** for your **data visualizations** that align with your **brand identity**. These palettes are generated from your `--primary-color`.
121
 
122
- <HtmlFragment src="palettes.html" />
123
 
124
 
125
  ### Placement
 
4
  import Aside from '../../components/Aside.astro';
5
  import Wide from '../../components/Wide.astro';
6
  import FullBleed from '../../components/FullBleed.astro';
7
+ import HtmlEmbed from '../../components/HtmlEmbed.astro';
8
  import audioDemo from '../../assets/audio/audio-example.wav';
9
 
10
  ## Writing Your Content
 
62
 
63
  # Mixing Markdown and components
64
 
65
+ This paragraph is written in Markdown.
66
 
67
  <Aside>A short callout inserted via a component.</Aside>
68
 
 
106
  #### Brand color
107
 
108
  <Aside>
109
+ <HtmlEmbed frameless src="color-picker.html" />
110
  <Fragment slot="aside">
111
  You can use the color picker to select the right color.
112
 
 
119
 
120
  Here is a suggestion of **color palettes** for your **data visualizations** that align with your **brand identity**. These palettes are generated from your `--primary-color`.
121
 
122
+ <HtmlEmbed frameless src="palettes.html" />
123
 
124
 
125
  ### Placement
app/src/content/fragments/palettes.html CHANGED
@@ -1,18 +1,20 @@
1
  <div class="palettes" style="width:100%; margin: 10px 0;">
2
  <style>
3
  .palettes .palettes__grid { display: grid; grid-template-columns: 1fr; gap: 12px; }
4
- .palettes .palette-card { position: relative; display: grid; grid-template-columns: 260px 1fr auto; align-items: center; gap: 14px; border: 1px solid var(--border-color); border-radius: 10px; background: var(--surface-bg); padding: 12px; transition: box-shadow .18s ease, transform .18s ease, border-color .18s ease; }
5
  /* removed circular badge */
6
- .palettes .palette-card__swatches { display: flex; gap: 0; margin: 0; height: 40px; overflow: hidden; border-radius: 8px; }
7
- .palettes .palette-card__swatches .sw { flex: 1 1 0; height: 100%; min-width: 0; border: none; }
8
  .palettes .palette-card__content { display: flex; flex-direction: column; gap: 6px; align-items: flex-start; justify-content: center; min-width: 0; padding-left: 12px; border-left: 1px solid var(--border-color); }
9
  .palettes .palette-card__actions { display: flex; align-items: center; justify-content: flex-end; justify-self: end; }
10
- .palettes .copy-btn { margin: 0; padding: 6px 12px; border-radius: 8px; border: 1px solid var(--border-color); background: var(--surface-bg); color: var(--text-color)!important; font-size: 12px; cursor: pointer; transition: background .18s ease, color .18s ease, border-color .18s ease; }
11
- .palettes .copy-btn:hover { background: var(--primary-color); color: var(--on-primary)!important; border-color: transparent; }
12
- .palettes .copy-btn:focus-visible { outline: 2px solid var(--primary-color); outline-offset: 2px; }
 
 
13
  @media (max-width: 640px) {
14
  .palettes .palette-card { grid-template-columns: 1fr; align-items: stretch; gap: 10px; }
15
- .palettes .palette-card__swatches { height: 36px; border-radius: 8px; }
16
  .palettes .palette-card__content { border-left: none; padding-left: 0; }
17
  .palettes .palette-card__actions { justify-self: start; }
18
  }
@@ -162,10 +164,18 @@
162
  const title = document.createElement('div'); title.className = 'palette-card__title'; title.style.textAlign = 'left'; title.style.fontWeight = '800'; title.style.fontSize = '15px'; title.textContent = c.title;
163
  const desc = document.createElement('div'); desc.className = 'palette-card__desc'; desc.style.textAlign = 'left'; desc.style.color = 'var(--muted-color)'; desc.style.lineHeight = '1.5'; desc.style.fontSize = '12px'; desc.innerHTML = c.desc;
164
  const actions = document.createElement('div'); actions.className = 'palette-card__actions';
165
- const btn = document.createElement('button'); btn.className = 'copy-btn'; btn.textContent = 'Copy';
 
166
  btn.addEventListener('click', async () => {
167
  const json = JSON.stringify(colors, null, 2);
168
- try { await navigator.clipboard.writeText(json); const old = btn.textContent; btn.textContent = 'Copied!'; setTimeout(() => btn.textContent = old, 900); } catch { window.prompt('Copy palette', json); }
 
 
 
 
 
 
 
169
  });
170
 
171
  content.appendChild(title); content.appendChild(desc);
 
1
  <div class="palettes" style="width:100%; margin: 10px 0;">
2
  <style>
3
  .palettes .palettes__grid { display: grid; grid-template-columns: 1fr; gap: 12px; }
4
+ .palettes .palette-card { position: relative; display: grid; grid-template-columns: 260px 1fr auto; align-items: stretch; gap: 14px; border: 1px solid var(--border-color); border-radius: 10px; background: var(--surface-bg); padding: 12px; transition: box-shadow .18s ease, transform .18s ease, border-color .18s ease; }
5
  /* removed circular badge */
6
+ .palettes .palette-card__swatches { display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); grid-auto-rows: 1fr; gap: 8px; margin: 0; }
7
+ .palettes .palette-card__swatches .sw { width: 100%; min-width: 0; min-height: 0; border-radius: 8px; border: 1px solid var(--border-color); }
8
  .palettes .palette-card__content { display: flex; flex-direction: column; gap: 6px; align-items: flex-start; justify-content: center; min-width: 0; padding-left: 12px; border-left: 1px solid var(--border-color); }
9
  .palettes .palette-card__actions { display: flex; align-items: center; justify-content: flex-end; justify-self: end; }
10
+ .palettes .palette-card__actions { align-self: stretch; }
11
+ /* .palettes .copy-btn { margin: 0; padding: 0 10px; height: 100%; border-radius: 8px; } */
12
+ /* .palettes .copy-btn:hover { background: var(--primary-color); color: var(--on-primary)!important; border-color: transparent; }
13
+ .palettes .copy-btn:focus-visible { outline: 2px solid var(--primary-color); outline-offset: 2px; } */
14
+ .palettes .copy-btn svg { width: 18px; height: 18px; fill: currentColor; display: block; }
15
  @media (max-width: 640px) {
16
  .palettes .palette-card { grid-template-columns: 1fr; align-items: stretch; gap: 10px; }
17
+ .palettes .palette-card__swatches { grid-template-columns: repeat(6, minmax(0, 1fr)); }
18
  .palettes .palette-card__content { border-left: none; padding-left: 0; }
19
  .palettes .palette-card__actions { justify-self: start; }
20
  }
 
164
  const title = document.createElement('div'); title.className = 'palette-card__title'; title.style.textAlign = 'left'; title.style.fontWeight = '800'; title.style.fontSize = '15px'; title.textContent = c.title;
165
  const desc = document.createElement('div'); desc.className = 'palette-card__desc'; desc.style.textAlign = 'left'; desc.style.color = 'var(--muted-color)'; desc.style.lineHeight = '1.5'; desc.style.fontSize = '12px'; desc.innerHTML = c.desc;
166
  const actions = document.createElement('div'); actions.className = 'palette-card__actions';
167
+ const btn = document.createElement('button'); btn.className = 'copy-btn button--ghost';
168
+ btn.innerHTML = '<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M16 1H4c-1.1 0-2 .9-2 2v12h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/></svg>';
169
  btn.addEventListener('click', async () => {
170
  const json = JSON.stringify(colors, null, 2);
171
+ try {
172
+ await navigator.clipboard.writeText(json);
173
+ const old = btn.innerHTML;
174
+ btn.innerHTML = '<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M9 16.2l-3.5-3.5-1.4 1.4L9 19 20.3 7.7l-1.4-1.4z"/></svg>';
175
+ setTimeout(() => btn.innerHTML = old, 900);
176
+ } catch {
177
+ window.prompt('Copy palette', json);
178
+ }
179
  });
180
 
181
  content.appendChild(title); content.appendChild(desc);
app/src/env.d.ts CHANGED
@@ -1 +1,9 @@
1
- /// <reference path="../.astro/types.d.ts" />
 
 
 
 
 
 
 
 
 
1
+ /// <reference path="../.astro/types.d.ts" />
2
+ /// <reference types="vite/client" />
3
+
4
+ declare module '*.png?url' {
5
+ const src: string;
6
+ export default src;
7
+ }
8
+
9
+ // (Global window typings for Plotly/D3 are intentionally omitted; components handle typing inline.)
app/src/pages/index.astro CHANGED
@@ -1,13 +1,15 @@
1
  ---
2
- import Article, { frontmatter as articleFM } from '../content/article.mdx';
3
- import Meta from '../components/Meta.astro';
4
  import Footer from '../components/Footer.astro';
5
- import Header from '../components/Header.astro';
6
  import ThemeToggle from '../components/ThemeToggle.astro';
7
- import SeoHead from '../components/SeoHead.astro';
8
- import ogDefault from '../assets/images/visual-vocabulary-poster.png';
 
9
  import 'katex/dist/katex.min.css';
10
  import '../styles/global.css';
 
 
11
  const docTitle = articleFM?.title ?? 'Untitled article';
12
  // Allow explicit line breaks in the title via "\n" or YAML newlines
13
  const docTitleHtml = (articleFM?.title ?? 'Untitled article')
@@ -20,9 +22,9 @@ const published = articleFM?.published ?? undefined;
20
  const tags = articleFM?.tags ?? [];
21
  // Prefer ogImage from frontmatter if provided
22
  const fmOg = articleFM?.ogImage as string | undefined;
23
- const imageAbs = fmOg && fmOg.startsWith('http')
24
  ? fmOg
25
- : (Astro.site ? new URL((fmOg ?? ogDefault.src), Astro.site).toString() : (fmOg ?? ogDefault.src));
26
 
27
  // ---- Build citation text & BibTeX from frontmatter ----
28
  const rawTitle = articleFM?.title ?? 'Untitled article';
@@ -54,7 +56,7 @@ const bibtex = `@misc{${bibKey},\n title={${titleFlat}},\n author={${authorsBi
54
  <meta charset="utf-8" />
55
  <meta name="viewport" content="width=device-width, initial-scale=1" />
56
  <title>{docTitle}</title>
57
- <SeoHead title={docTitle} description={description} authors={authors} published={published} tags={tags} image={imageAbs} />
58
  <script is:inline>
59
  (() => {
60
  try {
@@ -73,12 +75,7 @@ const bibtex = `@misc{${bibKey},\n title={${titleFlat}},\n author={${authorsBi
73
  </head>
74
  <body>
75
  <ThemeToggle />
76
- <Header title={docTitleHtml} description={subtitle}>
77
- </Header>
78
-
79
- <section class="article-header">
80
- <Meta title={docTitle} authors={articleFM?.authors} affiliation={articleFM?.affiliation} published={articleFM?.published} />
81
- </section>
82
 
83
  <section class="content-grid">
84
  <aside class="toc">
@@ -105,14 +102,19 @@ const bibtex = `@misc{${bibKey},\n title={${titleFlat}},\n author={${authorsBi
105
  <script>
106
  // Initialize zoom on img[data-zoomable]; wait for script & content; close on scroll like Medium
107
  (() => {
 
108
  let zoomInstance = null;
109
 
 
110
  const ensureMediumZoomReady = (cb) => {
 
111
  if (window.mediumZoom) return cb();
 
112
  const retry = () => (window.mediumZoom ? cb() : setTimeout(retry, 30));
113
  retry();
114
  };
115
 
 
116
  const collectTargets = () => Array.from(document.querySelectorAll('section.content-grid main img[data-zoomable]'));
117
 
118
  const initOrUpdateZoom = () => {
@@ -122,11 +124,13 @@ const bibtex = `@misc{${bibKey},\n title={${titleFlat}},\n author={${authorsBi
122
  if (!targets.length) return;
123
 
124
  if (!zoomInstance) {
 
125
  zoomInstance = window.mediumZoom(targets, { background, margin: 24, scrollOffset: 0 });
126
 
127
  let onScrollLike;
128
  const attachCloseOnScroll = () => {
129
  if (onScrollLike) return;
 
130
  onScrollLike = () => { zoomInstance && zoomInstance.close(); };
131
  window.addEventListener('wheel', onScrollLike, { passive: true });
132
  window.addEventListener('touchmove', onScrollLike, { passive: true });
@@ -139,16 +143,21 @@ const bibtex = `@misc{${bibKey},\n title={${titleFlat}},\n author={${authorsBi
139
  window.removeEventListener('scroll', onScrollLike);
140
  onScrollLike = null;
141
  };
 
142
  zoomInstance.on('open', attachCloseOnScroll);
 
143
  zoomInstance.on('close', detachCloseOnScroll);
144
 
145
  const themeObserver = new MutationObserver(() => {
146
  const dark = document.documentElement.getAttribute('data-theme') === 'dark';
 
147
  zoomInstance && zoomInstance.update({ background: dark ? 'rgba(0,0,0,.9)' : 'rgba(0,0,0,.85)' });
148
  });
149
  themeObserver.observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] });
150
  } else {
 
151
  zoomInstance.attach(targets);
 
152
  zoomInstance.update({ background });
153
  }
154
  };
@@ -191,6 +200,28 @@ const bibtex = `@misc{${bibKey},\n title={${titleFlat}},\n author={${authorsBi
191
  } else { setExternalTargets(); }
192
  </script>
193
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
194
  <script>
195
  // Build TOC from article headings (h2/h3/h4) and render into the sticky aside
196
  const buildTOC = () => {
@@ -303,42 +334,7 @@ const bibtex = `@misc{${bibKey},\n title={${titleFlat}},\n author={${authorsBi
303
  } else { buildTOC(); }
304
  </script>
305
 
306
- <script>
307
- // Inject visible language badges for code blocks when data-language is missing
308
- const addCodeLangChips = () => {
309
- const blocks = document.querySelectorAll('section.content-grid pre > code');
310
- blocks.forEach(code => {
311
- const pre = code.parentElement;
312
- if (!pre || pre.querySelector('.code-lang-chip')) return;
313
- // Try several places to detect language
314
- const getLang = () => {
315
- const direct = code.getAttribute('data-language') || code.dataset?.language;
316
- if (direct) return direct;
317
- const codeClass = (code.className || '').match(/language-([a-z0-9+\-]+)/i);
318
- if (codeClass) return codeClass[1];
319
- const preData = pre.getAttribute('data-language') || pre.dataset?.language;
320
- if (preData) return preData;
321
- const wrapper = pre.closest('.astro-code');
322
- if (wrapper) {
323
- const wrapData = wrapper.getAttribute('data-language') || wrapper.dataset?.language;
324
- if (wrapData) return wrapData;
325
- const wrapClass = (wrapper.className || '').match(/language-([a-z0-9+\-]+)/i);
326
- if (wrapClass) return wrapClass[1];
327
- }
328
- return 'text';
329
- };
330
- const lang = getLang().toUpperCase();
331
- const chip = document.createElement('span');
332
- chip.className = 'code-lang-chip';
333
- chip.textContent = lang;
334
- pre.classList.add('has-lang-chip');
335
- pre.appendChild(chip);
336
- });
337
- };
338
- if (document.readyState === 'loading') {
339
- document.addEventListener('DOMContentLoaded', addCodeLangChips, { once: true });
340
- } else { addCodeLangChips(); }
341
- </script>
342
  </body>
343
  </html>
344
 
 
1
  ---
2
+ import * as ArticleMod from '../content/article.mdx';
3
+ import Hero from '../components/Hero.astro';
4
  import Footer from '../components/Footer.astro';
 
5
  import ThemeToggle from '../components/ThemeToggle.astro';
6
+ import Seo from '../components/Seo.astro';
7
+ // @ts-ignore Astro asset import typed via env.d.ts
8
+ import ogDefaultUrl from '../assets/images/visual-vocabulary-poster.png?url';
9
  import 'katex/dist/katex.min.css';
10
  import '../styles/global.css';
11
+ const articleFM = (ArticleMod as any).frontmatter ?? {};
12
+ const Article = (ArticleMod as any).default;
13
  const docTitle = articleFM?.title ?? 'Untitled article';
14
  // Allow explicit line breaks in the title via "\n" or YAML newlines
15
  const docTitleHtml = (articleFM?.title ?? 'Untitled article')
 
22
  const tags = articleFM?.tags ?? [];
23
  // Prefer ogImage from frontmatter if provided
24
  const fmOg = articleFM?.ogImage as string | undefined;
25
+ const imageAbs: string = fmOg && fmOg.startsWith('http')
26
  ? fmOg
27
+ : (Astro.site ? new URL((fmOg ?? ogDefaultUrl), Astro.site).toString() : (fmOg ?? ogDefaultUrl));
28
 
29
  // ---- Build citation text & BibTeX from frontmatter ----
30
  const rawTitle = articleFM?.title ?? 'Untitled article';
 
56
  <meta charset="utf-8" />
57
  <meta name="viewport" content="width=device-width, initial-scale=1" />
58
  <title>{docTitle}</title>
59
+ <Seo title={docTitle} description={description} authors={authors} published={published} tags={tags} image={imageAbs} />
60
  <script is:inline>
61
  (() => {
62
  try {
 
75
  </head>
76
  <body>
77
  <ThemeToggle />
78
+ <Hero title={docTitleHtml} titleRaw={docTitle} description={subtitle} authors={articleFM?.authors} affiliation={articleFM?.affiliation} published={articleFM?.published} />
 
 
 
 
 
79
 
80
  <section class="content-grid">
81
  <aside class="toc">
 
102
  <script>
103
  // Initialize zoom on img[data-zoomable]; wait for script & content; close on scroll like Medium
104
  (() => {
105
+ /** @type {any} */
106
  let zoomInstance = null;
107
 
108
+ /** @param {() => void} cb */
109
  const ensureMediumZoomReady = (cb) => {
110
+ // @ts-ignore mediumZoom injected globally by external script
111
  if (window.mediumZoom) return cb();
112
+ // @ts-ignore mediumZoom injected globally by external script
113
  const retry = () => (window.mediumZoom ? cb() : setTimeout(retry, 30));
114
  retry();
115
  };
116
 
117
+ /** @returns {HTMLElement[]} */
118
  const collectTargets = () => Array.from(document.querySelectorAll('section.content-grid main img[data-zoomable]'));
119
 
120
  const initOrUpdateZoom = () => {
 
124
  if (!targets.length) return;
125
 
126
  if (!zoomInstance) {
127
+ // @ts-ignore medium-zoom injected globally by external script
128
  zoomInstance = window.mediumZoom(targets, { background, margin: 24, scrollOffset: 0 });
129
 
130
  let onScrollLike;
131
  const attachCloseOnScroll = () => {
132
  if (onScrollLike) return;
133
+ // @ts-ignore medium-zoom instance has close()
134
  onScrollLike = () => { zoomInstance && zoomInstance.close(); };
135
  window.addEventListener('wheel', onScrollLike, { passive: true });
136
  window.addEventListener('touchmove', onScrollLike, { passive: true });
 
143
  window.removeEventListener('scroll', onScrollLike);
144
  onScrollLike = null;
145
  };
146
+ // @ts-ignore medium-zoom instance has on()
147
  zoomInstance.on('open', attachCloseOnScroll);
148
+ // @ts-ignore medium-zoom instance has on()
149
  zoomInstance.on('close', detachCloseOnScroll);
150
 
151
  const themeObserver = new MutationObserver(() => {
152
  const dark = document.documentElement.getAttribute('data-theme') === 'dark';
153
+ // @ts-ignore medium-zoom instance has update()
154
  zoomInstance && zoomInstance.update({ background: dark ? 'rgba(0,0,0,.9)' : 'rgba(0,0,0,.85)' });
155
  });
156
  themeObserver.observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] });
157
  } else {
158
+ // @ts-ignore medium-zoom instance has attach()/update()
159
  zoomInstance.attach(targets);
160
+ // @ts-ignore medium-zoom instance has update()
161
  zoomInstance.update({ background });
162
  }
163
  };
 
200
  } else { setExternalTargets(); }
201
  </script>
202
 
203
+ <script>
204
+ // Delegate copy clicks for code blocks injected by rehypeCodeCopyAndLabel
205
+ document.addEventListener('click', async (e) => {
206
+ const target = e.target instanceof Element ? e.target : null;
207
+ const btn = target ? target.closest('.code-copy') : null;
208
+ if (!btn) return;
209
+ const card = btn.closest('.code-card');
210
+ const pre = card && card.querySelector('pre');
211
+ if (!pre) return;
212
+ const text = pre.textContent || '';
213
+ try {
214
+ await navigator.clipboard.writeText(text.trim());
215
+ const old = btn.innerHTML;
216
+ btn.innerHTML = '<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M9 16.2l-3.5-3.5-1.4 1.4L9 19 20.3 7.7l-1.4-1.4z"/></svg>';
217
+ setTimeout(() => (btn.innerHTML = old), 1200);
218
+ } catch {
219
+ btn.textContent = 'Error';
220
+ setTimeout(() => (btn.textContent = 'Copy'), 1200);
221
+ }
222
+ });
223
+ </script>
224
+
225
  <script>
226
  // Build TOC from article headings (h2/h3/h4) and render into the sticky aside
227
  const buildTOC = () => {
 
334
  } else { buildTOC(); }
335
  </script>
336
 
337
+ <!-- Removed JS fallback for language chips; labels handled by CSS/Shiki -->
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
338
  </body>
339
  </html>
340
 
app/src/styles/_base.css CHANGED
@@ -66,10 +66,7 @@ html { font-size: 14px; line-height: 1.6; }
66
  margin: var(--spacing-4) 0;
67
  }
68
 
69
- .content-grid main pre:not(.astro-code) { background: var(--code-bg); border: 1px solid var(--border-color); border-radius: 6px; padding: var(--spacing-3); font-size: 14px; overflow: auto; }
70
-
71
  /* Rely on Shiki's own token spans; no class remap */
72
- .content-grid main code { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; }
73
  /* Placeholder block (discreet centered text) */
74
  .placeholder-block {
75
  display: grid;
@@ -95,29 +92,6 @@ html { font-size: 14px; line-height: 1.6; }
95
  background: var(--surface-bg);
96
  }
97
 
98
- /* Pretty-code language label (visible chip at top-right) */
99
- .content-grid main pre:has(code[data-language]),
100
- .content-grid main pre:has(code[class*="language-"]) {
101
- position: relative;
102
- padding-top: 28px; /* space for the label */
103
- }
104
- .content-grid main pre > code[data-language]::after,
105
- .content-grid main pre > code[class*="language-"]::after {
106
- content: attr(data-language);
107
- position: absolute;
108
- top: 4px;
109
- right: 6px;
110
- font-size: 11px;
111
- line-height: 1;
112
- text-transform: uppercase;
113
- color: var(--muted-color);
114
- background: transparent;
115
- border: none;
116
- border-radius: 4px;
117
- padding: 2px 4px;
118
- pointer-events: none;
119
- z-index: 1;
120
- }
121
 
122
  .content-grid main table { border-collapse: collapse; width: 100%; margin: 0 0 var(--spacing-4); }
123
  .content-grid main th, .content-grid main td { border-bottom: 1px solid var(--border-color); padding: 6px 8px; text-align: left; font-size: 15px; }
@@ -141,7 +115,6 @@ html { font-size: 14px; line-height: 1.6; }
141
  /* ============================================================================ */
142
  img,
143
  picture {
144
- width: 100%;
145
  max-width: 100%;
146
  height: auto;
147
  display: block;
@@ -189,6 +162,11 @@ button, .button {
189
  display: inline-block;
190
  transition: background-color .15s ease, border-color .15s ease, box-shadow .15s ease, transform .02s ease;
191
  }
 
 
 
 
 
192
  button:hover, .button:hover {
193
  filter: brightness(96%);
194
  }
@@ -203,6 +181,18 @@ button:disabled, .button:disabled {
203
  cursor: not-allowed;
204
  }
205
 
 
 
 
 
 
 
 
 
 
 
 
 
206
  .button-group .button {
207
  margin: 5px;
208
  }
@@ -250,7 +240,8 @@ button:disabled, .button:disabled {
250
  .hero-banner,
251
  .d3-galaxy,
252
  .d3-galaxy svg,
253
- .plot-card,
 
254
  .js-plotly-plot,
255
  figure,
256
  pre,
 
66
  margin: var(--spacing-4) 0;
67
  }
68
 
 
 
69
  /* Rely on Shiki's own token spans; no class remap */
 
70
  /* Placeholder block (discreet centered text) */
71
  .placeholder-block {
72
  display: grid;
 
92
  background: var(--surface-bg);
93
  }
94
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
95
 
96
  .content-grid main table { border-collapse: collapse; width: 100%; margin: 0 0 var(--spacing-4); }
97
  .content-grid main th, .content-grid main td { border-bottom: 1px solid var(--border-color); padding: 6px 8px; text-align: left; font-size: 15px; }
 
115
  /* ============================================================================ */
116
  img,
117
  picture {
 
118
  max-width: 100%;
119
  height: auto;
120
  display: block;
 
162
  display: inline-block;
163
  transition: background-color .15s ease, border-color .15s ease, box-shadow .15s ease, transform .02s ease;
164
  }
165
+ /* Icon-only buttons: equal X/Y padding */
166
+ button:has(> svg:only-child),
167
+ .button:has(> svg:only-child) {
168
+ padding: 8px !important;
169
+ }
170
  button:hover, .button:hover {
171
  filter: brightness(96%);
172
  }
 
181
  cursor: not-allowed;
182
  }
183
 
184
+ /* Ghost/Muted button: subtle outline, primary color text/border */
185
+ .button--ghost {
186
+ background: transparent !important;
187
+ color: var(--primary-color) !important;
188
+ border-color: var(--primary-color) !important;
189
+ }
190
+ .button--ghost:hover {
191
+ color: var(--primary-color-hover) !important;
192
+ border-color: var(--primary-color-hover) !important;
193
+ filter: none;
194
+ }
195
+
196
  .button-group .button {
197
  margin: 5px;
198
  }
 
240
  .hero-banner,
241
  .d3-galaxy,
242
  .d3-galaxy svg,
243
+ .html-embed__card,
244
+ .html-embed__card,
245
  .js-plotly-plot,
246
  figure,
247
  pre,
app/src/styles/components/_code.css CHANGED
@@ -12,11 +12,11 @@ code {
12
 
13
  /* Sync Shiki variables with current theme */
14
  /* Standard wrapper look for code blocks */
15
- .astro-code { border: 1px solid var(--border-color); border-radius: 6px; padding: var(--spacing-3); font-size: 14px; --code-gutter-width: 2.5em; }
16
 
17
  /* Prevent code blocks from breaking layout on small screens */
18
  .astro-code { overflow-x: auto; width: 100%; max-width: 100%; box-sizing: border-box; -webkit-overflow-scrolling: touch; }
19
- section.content-grid pre { overflow-x: auto; width: 100%; max-width: 100%; box-sizing: border-box; -webkit-overflow-scrolling: touch; padding: var(--spacing-3); }
20
  section.content-grid pre code { display: inline-block; min-width: 100%; }
21
 
22
  /* Wrap long lines on mobile to avoid overflow (URLs, etc.) */
@@ -43,25 +43,40 @@ html[data-theme='light'] .astro-code {
43
  }
44
 
45
  /* Line numbers for Shiki-rendered code blocks */
46
- .astro-code code { counter-reset: astro-code-line; display: block; background: none; border: none; }
47
- .astro-code .line { display: inline-block; position: relative; padding-left: calc(var(--code-gutter-width) + var(--spacing-3)); min-height: 1.25em; }
48
- .astro-code .line::before { counter-increment: astro-code-line; content: counter(astro-code-line); position: absolute; left: 0; top: 0; bottom: 0; width: calc(var(--code-gutter-width)); text-align: right; color: var(--muted-color); opacity: .75; user-select: none; padding-right: var(--spacing-3); border-right: 1px solid var(--border-color); }
49
  .astro-code .line:empty::after { content: "\00a0"; }
50
  /* Hide trailing empty line added by parsers */
51
  .astro-code code > .line:last-child:empty { display: none; }
52
 
53
- /* JS fallback chip */
54
- .astro-code.has-lang-chip { position: relative; padding-top: 22px; }
55
- .astro-code .code-lang-chip {
 
 
 
 
 
 
 
 
 
 
56
  position: absolute;
57
- top: 0px; right: 0px;
58
- font-size: 10px; line-height: 1;
59
- color: rgba(255,255,255,.5);
60
- background: rgba(255,255,255,.1);
61
- border: none;
62
- border-radius: 0px; padding: 6px 6px 4px 4px; pointer-events: none; z-index: 1;
 
 
63
  }
64
 
 
 
 
65
 
66
 
67
  /* Overrides inside Accordion: remove padding and border on code containers */
 
12
 
13
  /* Sync Shiki variables with current theme */
14
  /* Standard wrapper look for code blocks */
15
+ .astro-code { border: 1px solid var(--border-color); border-radius: 6px; padding: 0; font-size: 14px; --code-gutter-width: 2.5em; }
16
 
17
  /* Prevent code blocks from breaking layout on small screens */
18
  .astro-code { overflow-x: auto; width: 100%; max-width: 100%; box-sizing: border-box; -webkit-overflow-scrolling: touch; }
19
+ section.content-grid pre { overflow-x: auto; width: 100%; max-width: 100%; box-sizing: border-box; -webkit-overflow-scrolling: touch; padding: 0; }
20
  section.content-grid pre code { display: inline-block; min-width: 100%; }
21
 
22
  /* Wrap long lines on mobile to avoid overflow (URLs, etc.) */
 
43
  }
44
 
45
  /* Line numbers for Shiki-rendered code blocks */
46
+ .astro-code code { counter-reset: astro-code-line; display: block; background: none; border: none; }
47
+ .astro-code .line { display: inline-block; position: relative; padding-left: calc(var(--code-gutter-width) + var(--spacing-1)); min-height: 1.25em; }
48
+ .astro-code .line::before { counter-increment: astro-code-line; content: counter(astro-code-line); position: absolute; left: 0; top: 0; bottom: 0; width: calc(var(--code-gutter-width)); text-align: right; color: var(--muted-color); opacity: .30; user-select: none; padding-right: var(--spacing-2); border-right: 1px solid var(--border-color); }
49
  .astro-code .line:empty::after { content: "\00a0"; }
50
  /* Hide trailing empty line added by parsers */
51
  .astro-code code > .line:last-child:empty { display: none; }
52
 
53
+ /* (Removed JS fallback chip: label handled via CSS in _base.css) */
54
+
55
+ /* Rehype-injected wrapper for non-Shiki pre blocks */
56
+ .code-card { position: relative; }
57
+ .code-card .code-copy {
58
+ position: absolute; top: 6px; right: 6px; z-index: 3; padding: 6px 12px;
59
+ }
60
+ .code-card .code-copy svg { width: 16px; height: 16px; display: block; fill: currentColor; }
61
+ .code-card pre { margin: 0; margin-bottom: var(--spacing-1);}
62
+
63
+ /* Discreet filetype/language label shown under the Copy button */
64
+ .code-card::after {
65
+ content: attr(data-language);
66
  position: absolute;
67
+ top: 8px; /* default, aligns with copy button */
68
+ right: 8px;
69
+ font-size: 10px;
70
+ line-height: 1;
71
+ text-transform: uppercase;
72
+ color: var(--muted-color);
73
+ pointer-events: none;
74
+ z-index: 2;
75
  }
76
 
77
+ /* When no copy button (single-line), keep the label in the top-right corner */
78
+ .code-card.no-copy::after { top: 8px; right: 8px; }
79
+
80
 
81
 
82
  /* Overrides inside Accordion: remove padding and border on code containers */
app/src/styles/components/_poltly.css CHANGED
@@ -1,9 +1,8 @@
1
  /* ============================================================================ */
2
  /* Plotly – fragments & controls */
3
  /* ============================================================================ */
4
- .plot-card { background: var(--code-bg); border: 1px solid var(--border-color); border-radius: 10px; padding: 8px; margin: 8px 0; }
5
- .plot-card svg text { fill: var(--text-color) !important; }
6
- .plot-card label { color: var(--text-color) !important; }
7
  .plotly-graph-div { width: 100% !important; min-height: 320px; }
8
  @media (max-width: 768px) { .plotly-graph-div { min-height: 260px; } }
9
  [id^="plot-"] { display: flex; flex-direction: column; align-items: center; gap: 15px; }
@@ -20,26 +19,26 @@
20
  /* ---------------------------------------------------------------------------- */
21
  /* Dark mode overrides for Plotly readability */
22
  /* ---------------------------------------------------------------------------- */
23
- [data-theme="dark"] .plot-card .xaxislayer-above text,
24
- [data-theme="dark"] .plot-card .yaxislayer-above text,
25
- [data-theme="dark"] .plot-card .infolayer text,
26
- [data-theme="dark"] .plot-card .legend text,
27
- [data-theme="dark"] .plot-card .annotation text,
28
- [data-theme="dark"] .plot-card .colorbar text,
29
- [data-theme="dark"] .plot-card .hoverlayer text { fill: #fff !important; }
30
 
31
- [data-theme="dark"] .plot-card .xaxislayer-above path,
32
- [data-theme="dark"] .plot-card .yaxislayer-above path,
33
- [data-theme="dark"] .plot-card .xlines-above,
34
- [data-theme="dark"] .plot-card .ylines-above { stroke: rgba(255,255,255,.35) !important; }
35
 
36
- [data-theme="dark"] .plot-card .gridlayer path { stroke: rgba(255,255,255,.15) !important; }
37
 
38
  /* Legend and hover backgrounds */
39
- [data-theme="dark"] .plot-card .legend rect.bg { fill: rgba(0,0,0,.25) !important; stroke: rgba(255,255,255,.2) !important; }
40
- [data-theme="dark"] .plot-card .hoverlayer .bg { fill: rgba(0,0,0,.8) !important; stroke: rgba(255,255,255,.2) !important; }
41
 
42
  /* Colorbar background (keep gradient intact) */
43
- [data-theme="dark"] .plot-card .colorbar .cbbg { fill: rgba(0,0,0,.25) !important; stroke: rgba(255,255,255,.2) !important; }
44
 
45
 
 
1
  /* ============================================================================ */
2
  /* Plotly – fragments & controls */
3
  /* ============================================================================ */
4
+ .html-embed__card svg text { fill: var(--text-color) !important; }
5
+ .html-embed__card label { color: var(--text-color) !important; }
 
6
  .plotly-graph-div { width: 100% !important; min-height: 320px; }
7
  @media (max-width: 768px) { .plotly-graph-div { min-height: 260px; } }
8
  [id^="plot-"] { display: flex; flex-direction: column; align-items: center; gap: 15px; }
 
19
  /* ---------------------------------------------------------------------------- */
20
  /* Dark mode overrides for Plotly readability */
21
  /* ---------------------------------------------------------------------------- */
22
+ [data-theme="dark"] .html-embed__card .xaxislayer-above text,
23
+ [data-theme="dark"] .html-embed__card .yaxislayer-above text,
24
+ [data-theme="dark"] .html-embed__card .infolayer text,
25
+ [data-theme="dark"] .html-embed__card .legend text,
26
+ [data-theme="dark"] .html-embed__card .annotation text,
27
+ [data-theme="dark"] .html-embed__card .colorbar text,
28
+ [data-theme="dark"] .html-embed__card .hoverlayer text { fill: #fff !important; }
29
 
30
+ [data-theme="dark"] .html-embed__card .xaxislayer-above path,
31
+ [data-theme="dark"] .html-embed__card .yaxislayer-above path,
32
+ [data-theme="dark"] .html-embed__card .xlines-above,
33
+ [data-theme="dark"] .html-embed__card .ylines-above { stroke: rgba(255,255,255,.35) !important; }
34
 
35
+ [data-theme="dark"] .html-embed__card .gridlayer path { stroke: rgba(255,255,255,.15) !important; }
36
 
37
  /* Legend and hover backgrounds */
38
+ [data-theme="dark"] .html-embed__card .legend rect.bg { fill: rgba(0,0,0,.25) !important; stroke: rgba(255,255,255,.2) !important; }
39
+ [data-theme="dark"] .html-embed__card .hoverlayer .bg { fill: rgba(0,0,0,.8) !important; stroke: rgba(255,255,255,.2) !important; }
40
 
41
  /* Colorbar background (keep gradient intact) */
42
+ [data-theme="dark"] .html-embed__card .colorbar .cbbg { fill: rgba(0,0,0,.25) !important; stroke: rgba(255,255,255,.2) !important; }
43
 
44
 
app/src/styles/global.css CHANGED
@@ -8,7 +8,7 @@
8
  /* Dark-mode form tweak */
9
  [data-theme="dark"] .plotly_input_container > select { background-color: #1a1f27; border-color: var(--border-color); color: var(--text-color); }
10
 
11
- [data-theme="dark"] .plot-card { background: #12151b; border-color: rgba(255,255,255,.15); }
12
  [data-theme="dark"] .right-aside .aside-card { background: #12151b; border-color: rgba(255,255,255,.15); }
13
  [data-theme="dark"] .content-grid main pre { background: #12151b; border-color: rgba(255,255,255,.15); }
14
  [data-theme="dark"] .toc nav { border-left-color: rgba(255,255,255,.15); }
@@ -20,51 +20,6 @@
20
  img[data-zoomable] { cursor: zoom-in; }
21
  .medium-zoom--opened img[data-zoomable] { cursor: zoom-out; }
22
 
23
- /* ============================================================================ */
24
- /* Hero (full-bleed) */
25
- /* ============================================================================ */
26
- .hero { width: 100%; padding: 48px 16px 16px; text-align: center; }
27
- .hero-title { font-size: clamp(28px, 4vw, 48px); font-weight: 800; line-height: 1.1; margin: 0 0 8px;
28
-
29
- max-width: 60%;
30
- margin: auto;}
31
- .hero-banner { max-width: 980px; margin: 0 auto; }
32
- .hero-desc { color: var(--muted-color); font-style: italic; margin: 0 0 16px 0; }
33
-
34
- /* ============================================================================ */
35
- /* Meta (byline-like header) */
36
- /* ============================================================================ */
37
-
38
- .meta {
39
- border-top: 1px solid var(--border-color);
40
- border-bottom: 1px solid var(--border-color);
41
- padding: 1rem 0;
42
- font-size: 0.9rem;
43
- line-height: 1.8em;
44
- }
45
- .meta-container {
46
- max-width: 720px;
47
- display: flex;
48
- flex-direction: row;
49
- justify-content: space-between;
50
- margin: 0 auto;
51
- gap: 8px;
52
- }
53
- .meta-container-cell {
54
- display: flex;
55
- flex-direction: column;
56
- gap: 8px;
57
- }
58
- .meta-container-cell h3 {
59
- margin: 0;
60
- font-size: 12px;
61
- font-weight: 400;
62
- color: var(--muted-color);
63
- text-transform: uppercase;
64
- letter-spacing: .02em;
65
- }
66
- .meta-container-cell p { margin: 0; }
67
-
68
  /* ============================================================================ */
69
  /* Theme Toggle button (moved from component) */
70
  /* ============================================================================ */
 
8
  /* Dark-mode form tweak */
9
  [data-theme="dark"] .plotly_input_container > select { background-color: #1a1f27; border-color: var(--border-color); color: var(--text-color); }
10
 
11
+ [data-theme="dark"] .html-embed__card:not(.is-frameless) { background: #12151b; border-color: rgba(255,255,255,.15); }
12
  [data-theme="dark"] .right-aside .aside-card { background: #12151b; border-color: rgba(255,255,255,.15); }
13
  [data-theme="dark"] .content-grid main pre { background: #12151b; border-color: rgba(255,255,255,.15); }
14
  [data-theme="dark"] .toc nav { border-left-color: rgba(255,255,255,.15); }
 
20
  img[data-zoomable] { cursor: zoom-in; }
21
  .medium-zoom--opened img[data-zoomable] { cursor: zoom-out; }
22
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
  /* ============================================================================ */
24
  /* Theme Toggle button (moved from component) */
25
  /* ============================================================================ */