thibaud frere commited on
Commit
fc7711a
·
1 Parent(s): 101af31
.gitattributes CHANGED
@@ -1,4 +1,5 @@
1
  app/assets/images/*.png filter=lfs diff=lfs merge=lfs -text
2
  app/src/assets/images/*.png filter=lfs diff=lfs merge=lfs -text
3
  *.png filter=lfs diff=lfs merge=lfs -text
 
4
  *.wav filter=lfs diff=lfs merge=lfs -text
 
1
  app/assets/images/*.png filter=lfs diff=lfs merge=lfs -text
2
  app/src/assets/images/*.png filter=lfs diff=lfs merge=lfs -text
3
  *.png filter=lfs diff=lfs merge=lfs -text
4
+ *.jpg filter=lfs diff=lfs merge=lfs -text
5
  *.wav filter=lfs diff=lfs merge=lfs -text
ASTRO_MIGRATION.md DELETED
@@ -1,272 +0,0 @@
1
- ## Migration vers Astro (Markdown/MDX) – Spécification et guide
2
-
3
- ### Objectif
4
- - Remplacer le template Distill par une pile moderne basée sur Markdown/MDX avec Astro, tout en conservant: front‑matter, maths, code, citations/bibliographie, table des matières, figures, sections de contenu, et composants interactifs (plots/tables).
5
- - Offrir une optimisation d’images de premier ordre (AVIF/WebP, `srcset`, lazy‑loading) et un déploiement statique via Nginx.
6
-
7
- ### Stack cible
8
- - Astro 4+ (SSG, sortie statique `dist/`)
9
- - Markdown/MDX (contenu principal)
10
- - Intégrations/Plugins:
11
- - `@astrojs/mdx` (MDX)
12
- - `remark-math` + `rehype-katex` (maths)
13
- - `rehype-slug` + `rehype-autolink-headings` (ancres H2/H3/H4)
14
- - `remark-toc` (table des matières auto)
15
- - `rehype-citation` (citations/bibliographie depuis BibTeX/CSL)
16
- - `@astrojs/image` avec `SquooshImageService` (optimisation d’images sans dépendances natives)
17
- - Surlignage code: Shiki par défaut d’Astro (fences ```lang)
18
-
19
- ### Mapping Distill → Astro
20
- - `d-front-matter` → YAML front‑matter en tête de fichier `.md/.mdx` (titre, auteurs, dates, cover/hero, tags…)
21
- - `d-title` → en‑tête de page dans un layout Astro (`src/layouts/BlogLayout.astro`) utilisant les métadonnées
22
- - `d-byline` → bloc auteurs/affiliations dans le layout
23
- - `d-article` → contenu Markdown principal
24
- - `d-figure` → `<Image />` Astro (optimisé) + `<figure>/<figcaption>` sémantique
25
- - `d-toc` → `remark-toc` + `rehype-slug/autolink` (TOC ancré)
26
- - `d-footnote`/`d-footnote-list` → notes de bas de page Markdown standard
27
- - `d-math` → Math Markdown via `remark-math`/`rehype-katex` (`$...$`, `$$...$$`)
28
- - `d-code` (Prism) → fences Markdown + Shiki
29
- - `d-cite`/`d-bibliography` → `rehype-citation` (génération des références + liens), section `## References` en bas
30
-
31
- ### Arborescence proposée
32
- ```
33
- astro-site/
34
- src/
35
- assets/ # images sources (ex: banner.png)
36
- content/
37
- bibliography.bib
38
- posts/
39
- finetasks.mdx # migration de index.html → Markdown/MDX
40
- components/
41
- TOC.astro # (optionnel) wrapper visuel autour du TOC
42
- PlotlyChart.jsx # composant interactif (client:visible)
43
- layouts/
44
- BlogLayout.astro
45
- public/ # assets statiques bruts si besoin
46
- astro.config.mjs
47
- package.json
48
- ```
49
-
50
- ### Configuration Astro (plugins)
51
- ```js
52
- // astro.config.mjs
53
- import { defineConfig } from 'astro/config';
54
- import mdx from '@astrojs/mdx';
55
- import image from '@astrojs/image';
56
- import { SquooshImageService } from '@astrojs/image/squoosh';
57
-
58
- // Remark/Rehype
59
- import remarkMath from 'remark-math';
60
- import remarkToc from 'remark-toc';
61
- import rehypeKatex from 'rehype-katex';
62
- import rehypeSlug from 'rehype-slug';
63
- import rehypeAutolinkHeadings from 'rehype-autolink-headings';
64
- import rehypeCitation from 'rehype-citation';
65
-
66
- export default defineConfig({
67
- integrations: [
68
- mdx(),
69
- image({ serviceEntryPoint: SquooshImageService() })
70
- ],
71
- markdown: {
72
- remarkPlugins: [
73
- [remarkToc, { heading: 'Table of contents', maxDepth: 3 }],
74
- remarkMath,
75
- ],
76
- rehypePlugins: [
77
- rehypeSlug,
78
- [rehypeAutolinkHeadings, { behavior: 'wrap' }],
79
- rehypeKatex,
80
- // Génère les citations et la biblio (insertion en bas si "## References" existe)
81
- [rehypeCitation, {
82
- bibliography: 'src/content/bibliography.bib',
83
- linkCitations: true,
84
- // CSL optionnel: csl: 'src/content/ieee.csl'
85
- }],
86
- ],
87
- shikiConfig: { theme: 'github-dark' },
88
- },
89
- build: {
90
- assets: 'assets',
91
- },
92
- output: 'static'
93
- });
94
- ```
95
-
96
- Ajouter les styles KaTeX (dans `src/layouts/BlogLayout.astro` ou global CSS):
97
- ```html
98
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css" crossorigin="anonymous" />
99
- ```
100
-
101
- ### Layout principal
102
- ```astro
103
- ---
104
- // src/layouts/BlogLayout.astro
105
- const { title, description, authors = [], published, hero } = Astro.props;
106
- ---
107
- <html lang="en">
108
- <head>
109
- <meta charset="utf-8" />
110
- <meta name="viewport" content="width=device-width, initial-scale=1" />
111
- <title>{title}</title>
112
- <meta name="description" content={description} />
113
- </head>
114
- <body>
115
- <header class="page-hero">
116
- {hero && <img src={hero} alt={title} loading="eager" fetchpriority="high" />}
117
- <h1>{title}</h1>
118
- {published && <p class="byline">{published}</p>}
119
- {authors.length > 0 && (
120
- <p class="authors">{authors.map(a => a.name || a).join(', ')}</p>
121
- )}
122
- </header>
123
- <main>
124
- <slot />
125
- </main>
126
- </body>
127
- <style>
128
- .page-hero img{width:100%;height:auto;}
129
- main{max-width:980px;margin:0 auto;padding:24px}
130
- </style>
131
- </html>
132
- ```
133
-
134
- ### Exemple de page MDX (migration de `index.html`)
135
- ```mdx
136
- ---
137
- layout: ../layouts/BlogLayout.astro
138
- title: "Scaling FineWeb to 1000+ languages: Step 1: finding signal in 100s of evaluation tasks"
139
- description: "Multilingual evaluation & FineTasks"
140
- published: "Oct 23, 2024"
141
- authors:
142
- - name: "Hynek Kydlíček"
143
- - name: "Guilherme Penedo"
144
- - name: "Clémentine Fourier"
145
- - name: "Nathan Habib"
146
- - name: "Thomas Wolf"
147
- hero: "../assets/images/banner.png"
148
- ---
149
-
150
- ## Table of contents
151
-
152
- <!-- Le TOC sera injecté ici par remark-toc -->
153
-
154
- ![FineTasks](../assets/images/banner.png)
155
-
156
- Du texte… équations $e^{i\pi}+1=0$ et un bloc:
157
-
158
- $$
159
- \int_a^b f(x)\,dx
160
- $$
161
-
162
- Citation: [@kydlicek2024finetasksmultilingualtasks].
163
-
164
- ## Results
165
-
166
- <PlotlyChart client:visible dataUrl="/data/..." />
167
-
168
- ## References
169
- ```
170
-
171
- ### Composants interactifs (exemples)
172
- - Plotly (client‑side, hydraté quand visible):
173
- ```jsx
174
- // src/components/PlotlyChart.jsx
175
- import { useEffect, useRef } from 'react';
176
- import Plotly from 'plotly.js-basic-dist-min';
177
-
178
- export default function PlotlyChart({ data, layout, config }) {
179
- const ref = useRef(null);
180
- useEffect(() => {
181
- if (!ref.current) return;
182
- Plotly.newPlot(ref.current, data || [], layout || {}, config || {responsive:true});
183
- return () => { Plotly.purge(ref.current); };
184
- }, [data, layout, config]);
185
- return <div ref={ref} style={{width:'100%'}} />;
186
- }
187
- ```
188
-
189
- - DataTables: préférer générer des tables Markdown; pour un tableau interactif, créer un composant MDX dédié et l’hydrater `client:visible`.
190
-
191
- ### Optimisation d’images (Astro)
192
- - Utiliser `@astrojs/image` (Squoosh) ou `astro:assets` pour images locales.
193
- - Exemple responsive (MDX):
194
- ```astro
195
- ---
196
- import { Image } from '@astrojs/image/components';
197
- import banner from '../assets/images/banner.png';
198
- ---
199
- <Image src={banner} alt="FineTasks" widths={[480,768,1080,1440]} formats={["avif","webp","png"]} sizes="(max-width: 768px) 100vw, 980px" loading="lazy" decoding="async" />
200
- ```
201
- - Bonnes pratiques: dimensions explicites `width/height` (automatiques avec `Image`), `loading="lazy"` hors hero, `fetchpriority="high"` sur le hero.
202
-
203
- ### Citations et bibliographie
204
- - Placer `src/content/bibliography.bib` (copie de `app/src/bibliography.bib`).
205
- - Citer en Markdown: `[@clé]` ou `[-@clé]`. Ajouter `## References` à la fin; `rehype-citation` génère la bibliographie.
206
-
207
- ### Commandes/installation
208
- ```bash
209
- npm create astro@latest astro-site -- --template minimal
210
- cd astro-site
211
- npm i -D @astrojs/mdx @astrojs/image remark-math rehype-katex rehype-slug rehype-autolink-headings remark-toc rehype-citation
212
- npm run dev
213
- # build
214
- npm run build
215
- ```
216
-
217
- ### Dockerfile (multi‑stage)
218
- ```Dockerfile
219
- FROM node:20 AS build
220
- WORKDIR /site
221
- COPY astro-site/package*.json ./
222
- RUN npm ci
223
- COPY astro-site/ .
224
- RUN npm run build
225
-
226
- FROM nginx:alpine
227
- COPY --from=build /site/dist /usr/share/nginx/html
228
- COPY nginx.conf /etc/nginx/nginx.conf
229
- EXPOSE 8080
230
- CMD ["nginx","-g","daemon off;"]
231
- ```
232
-
233
- ### Nginx (statique, cache + SPA fallback)
234
- ```nginx
235
- worker_processes auto;
236
- events { worker_connections 1024; }
237
- http {
238
- include /etc/nginx/mime.types;
239
- server {
240
- listen 8080;
241
- server_name localhost;
242
- root /usr/share/nginx/html;
243
- index index.html;
244
-
245
- location /assets/ {
246
- expires 30d;
247
- add_header Cache-Control "public, max-age=2592000, immutable";
248
- }
249
- location / {
250
- try_files $uri $uri/ /index.html;
251
- }
252
- location = /health { return 200 "ok"; add_header Content-Type text/plain; }
253
- }
254
- }
255
- ```
256
-
257
- ### Étapes de migration depuis ce dépôt
258
- 1) Créer `astro-site/` et installer la stack (ci‑dessus).
259
- 2) Copier `app/src/bibliography.bib` → `astro-site/src/content/bibliography.bib`.
260
- 3) Copier les images `app/assets/images/*` → `astro-site/src/assets/images/*`.
261
- 4) Convertir `app/src/index.html` → `astro-site/src/content/posts/finetasks.mdx` (reprendre sections H2/H3/H4, figures, équations, et remplacer les blocs Distill par Markdown/MDX).
262
- 5) Recréer les applets (plots/tables) via composants MDX (`PlotlyChart.jsx`, etc.).
263
- 6) Ajuster le layout (`BlogLayout.astro`) pour le titre, auteurs, date, hero.
264
- 7) Ajouter TOC, maths, citations (plugins configurés).
265
- 8) Vérifier le build (`npm run build`) et brancher le Docker/Nginx proposés.
266
-
267
- ### Notes
268
- - Pour éviter des dépendances natives (Sharp) en build Docker, on force le service image Squoosh (WASM).
269
- - Si besoin de SEO/sitemap/RSS, ajouter `@astrojs/sitemap`/`@astrojs/rss`.
270
- - Les composants interactifs doivent être idempotents et hydratés avec `client:visible`/`client:idle`.
271
-
272
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
README.md CHANGED
@@ -1,5 +1,5 @@
1
  ---
2
- title: 'Scaling FineWeb to 1000+ languages: Step 1: finding signal in 100s of evaluation tasks'
3
  emoji: 📝
4
  colorFrom: blue
5
  colorTo: indigo
 
1
  ---
2
+ title: 'The science article template'
3
  emoji: 📝
4
  colorFrom: blue
5
  colorTo: indigo
app/astro.config.mjs CHANGED
@@ -7,6 +7,7 @@ import remarkFootnotes from 'remark-footnotes';
7
  import rehypeSlug from 'rehype-slug';
8
  import rehypeAutolinkHeadings from 'rehype-autolink-headings';
9
  import rehypeCitation from 'rehype-citation';
 
10
 
11
  export default defineConfig({
12
  output: 'static',
@@ -22,6 +23,14 @@ export default defineConfig({
22
  rehypeSlug,
23
  [rehypeAutolinkHeadings, { behavior: 'wrap' }],
24
  rehypeKatex,
 
 
 
 
 
 
 
 
25
  [rehypeCitation, {
26
  bibliography: 'src/content/bibliography.bib',
27
  linkCitations: true
 
7
  import rehypeSlug from 'rehype-slug';
8
  import rehypeAutolinkHeadings from 'rehype-autolink-headings';
9
  import rehypeCitation from 'rehype-citation';
10
+ import rehypePrettyCode from 'rehype-pretty-code';
11
 
12
  export default defineConfig({
13
  output: 'static',
 
23
  rehypeSlug,
24
  [rehypeAutolinkHeadings, { behavior: 'wrap' }],
25
  rehypeKatex,
26
+ [rehypePrettyCode, {
27
+ theme: {
28
+ light: 'github-light',
29
+ dark: 'github-dark'
30
+ },
31
+ keepBackground: false,
32
+ defaultLang: 'text'
33
+ }],
34
  [rehypeCitation, {
35
  bibliography: 'src/content/bibliography.bib',
36
  linkCitations: true
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/assets/images/{placeholder.png → placeholder.jpg} RENAMED
File without changes
app/src/components/Aside.astro CHANGED
@@ -8,11 +8,3 @@
8
  <slot name="aside" />
9
  </aside>
10
  </div>
11
-
12
- <style>
13
- .margin-aside { display: grid; grid-template-columns: minmax(0, 1fr) 240px; gap: 16px; align-items: start; margin: 12px 0; }
14
- .margin-aside__aside { background: var(--surface-bg); border: 1px solid var(--border-color); border-radius: 8px; padding: 10px; font-size: 0.9rem; color: var(--text-color); }
15
- @media (max-width: 1100px) { .margin-aside { grid-template-columns: 1fr; } }
16
- </style>
17
-
18
-
 
8
  <slot name="aside" />
9
  </aside>
10
  </div>
 
 
 
 
 
 
 
 
app/src/components/Footer.astro CHANGED
@@ -26,7 +26,101 @@ const { citationText, bibtex } = Astro.props as Props;
26
  .footer-inner { max-width: 680px; margin: 0 auto; padding: 24px 16px; }
27
  .citation-block h3 { margin: 0 0 8px; }
28
  .citation-block h4 { margin: 16px 0 8px; font-size: 14px; text-transform: uppercase; color: var(--muted-color); }
29
- .citation-text, .citation-bibtex { width: 100%; min-height: 44px; border: 1px solid var(--border-color); border-radius: 6px; background: var(--surface-bg); padding: 8px; resize: none; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-size: 13px; color: var(--text-color); }
 
 
 
30
  </style>
31
 
32
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
  .footer-inner { max-width: 680px; margin: 0 auto; padding: 24px 16px; }
27
  .citation-block h3 { margin: 0 0 8px; }
28
  .citation-block h4 { margin: 16px 0 8px; font-size: 14px; text-transform: uppercase; color: var(--muted-color); }
29
+ .citation-text, .citation-bibtex { width: 100%; min-height: 44px; border: 1px solid var(--border-color); border-radius: 6px; background: var(--surface-bg); padding: 8px; resize: none; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-size: 13px; color: var(--text-color); white-space: pre-wrap; overflow-y: hidden; line-height: 1.4; }
30
+ .references-block h3 { margin: 24px 0 8px; }
31
+ .references-block .footnotes { margin-top: 8px; }
32
+ .references-block .bibliography { margin-top: 8px; }
33
  </style>
34
 
35
 
36
+ <script is:inline>
37
+ (() => {
38
+ const getFooter = () => document.currentScript?.closest('footer') || document.querySelector('footer.distill-footer');
39
+ const footer = getFooter();
40
+ if (!footer) return;
41
+ const target = footer.querySelector('.references-block');
42
+ if (!target) return;
43
+
44
+ const contentRoot = document.querySelector('section.content-grid main') || document.querySelector('main') || document.body;
45
+
46
+ const findFirstOutsideFooter = (selectors) => {
47
+ for (const sel of selectors) {
48
+ const el = contentRoot.querySelector(sel);
49
+ if (el && !footer.contains(el)) return el;
50
+ }
51
+ return null;
52
+ };
53
+
54
+ const ensureHeading = (text) => {
55
+ const exists = Array.from(target.children).some((c) => c.tagName === 'H3' && c.textContent.trim().toLowerCase() === text.toLowerCase());
56
+ if (!exists) {
57
+ const h = document.createElement('h3');
58
+ h.textContent = text;
59
+ target.appendChild(h);
60
+ }
61
+ };
62
+
63
+ const moveIntoFooter = (element, headingText) => {
64
+ if (!element) return false;
65
+ if (element.classList.contains('footnotes')) {
66
+ const hr = element.querySelector('hr');
67
+ if (hr) hr.remove();
68
+ }
69
+ // Supprimer un éventuel titre déjà inclus dans le bloc (pour éviter doublons)
70
+ const firstHeading = element.querySelector(':scope > h1, :scope > h2, :scope > h3');
71
+ if (firstHeading) {
72
+ const txt = (firstHeading.textContent || '').trim().toLowerCase();
73
+ const targetTxt = headingText.trim().toLowerCase();
74
+ if (txt === targetTxt || txt.includes('reference') || txt.includes('bibliograph')) {
75
+ firstHeading.remove();
76
+ }
77
+ }
78
+ ensureHeading(headingText);
79
+ target.appendChild(element);
80
+ return true;
81
+ };
82
+
83
+ const autoResizeTextareas = () => {
84
+ const areas = footer.querySelectorAll('.citation-text, .citation-bibtex');
85
+ areas.forEach((ta) => {
86
+ ta.style.height = 'auto';
87
+ const min = 44;
88
+ const next = Math.max(ta.scrollHeight, min);
89
+ ta.style.height = next + 'px';
90
+ });
91
+ };
92
+
93
+ const run = () => {
94
+ const referencesEl = findFirstOutsideFooter(['#references', '.references', '.bibliography']);
95
+ const footnotesEl = findFirstOutsideFooter(['.footnotes']);
96
+ const movedRefs = moveIntoFooter(referencesEl, 'References');
97
+ const movedNotes = moveIntoFooter(footnotesEl, 'Footnotes');
98
+ autoResizeTextareas();
99
+ return movedRefs || movedNotes;
100
+ };
101
+
102
+ // Try now; if not found yet, try again on DOM ready
103
+ const done = run();
104
+ if (!done) {
105
+ const onReady = () => run();
106
+ if (document.readyState === 'loading') {
107
+ document.addEventListener('DOMContentLoaded', onReady, { once: true });
108
+ } else {
109
+ setTimeout(onReady, 0);
110
+ }
111
+ }
112
+
113
+ // Resize on window changes (e.g., fonts, layout)
114
+ window.addEventListener('resize', () => {
115
+ // throttle via rAF
116
+ let raf = null;
117
+ if (raf) cancelAnimationFrame(raf);
118
+ raf = requestAnimationFrame(() => {
119
+ autoResizeTextareas();
120
+ raf = null;
121
+ });
122
+ }, { passive: true });
123
+ })();
124
+ </script>
125
+
126
+
app/src/pages/article.mdx CHANGED
@@ -2,9 +2,9 @@
2
  title: "The science template:\nCraft Beautiful Blogs"
3
  description: "A modern, MDX-first research article template with math, citations, and interactive figures."
4
  authors:
5
- - "Hynek Kydlíček"
6
- - "Guilherme Penedo"
7
- - "Clémentine Fourier"
8
  affiliation: "Hugging Face"
9
  published: "Feb 19, 2025"
10
  tags:
@@ -15,17 +15,26 @@ ogImage: "https://example.com/your-og-image.png"
15
 
16
  import HtmlFragment from "../components/HtmlFragment.astro";
17
  import { Image } from 'astro:assets';
18
- import placeholder from "../assets/images/placeholder.png";
19
  import audioDemo from "../assets/audio/audio-example.wav";
20
  import Aside from "../components/Aside.astro";
21
  import visualPoster from "../assets/images/visual-vocabulary-poster.png";
22
 
23
 
24
- Welcome to this single-page research article template built with Astro and MDX. It’s designed to help you write clear, modern, and interactive technical articles with minimal setup. Whether you cover machine learning, data science, physics, or software topics, this template keeps the authoring flow simple while offering robust features out of the box.
25
-
26
- Reading time: 10–15 minutes.
27
-
28
- In this guide, you’ll learn how to install the template, write content (math, citations, images, code, asides, interactive fragments), customize styles and behavior, and follow a few best practices for publishing.
 
 
 
 
 
 
 
 
 
29
 
30
  ## Features
31
 
@@ -34,13 +43,14 @@ In this guide, you’ll learn how to install the template, write content (math,
34
  <span className="tag">KaTeX math</span>
35
  <span className="tag">Citations & footnotes</span>
36
  <span className="tag">Automatic build</span>
37
- <span className="tag">Auto table of content</span>
38
  <span className="tag">Dark theme</span>
39
  <span className="tag">HTML fragments</span>
40
  <span className="tag">Plotly ready</span>
41
  <span className="tag">SEO Friendly</span>
42
  <span className="tag">Lightweight bundle(\<500ko)</span>
43
- <span className="tag">TO DO: Zoomable images</span>
 
44
  </div>
45
 
46
  ## Getting Started
@@ -66,6 +76,52 @@ Large assets: track binaries (e.g., `.png`, `.wav`) with Git LFS to keep the rep
66
 
67
  Author content in MDX. Below are minimal examples for the core elements.
68
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
69
  ### Math
70
 
71
  Inline example: $x^2 + y^2 = z^2$.
@@ -87,29 +143,114 @@ $$
87
 
88
  ### Images
89
 
90
- Responsive image using `astro:assets`:
 
 
91
 
92
- <Image src={placeholder} alt="Responsive example image" />
 
 
 
 
 
 
 
 
 
 
93
 
94
  ```mdx
95
  import { Image } from 'astro:assets'
96
- import myImage from '../assets/images/placeholder.png'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97
 
98
- <Image src={myImage} alt="Responsive example image" />
 
 
 
 
 
 
99
  ```
100
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
101
  ### Citations and notes
102
 
103
- A short citation [@example2023]. A footnote[^note].
 
 
 
 
104
 
105
- [^note]: Example footnote.
 
 
106
 
107
  ```mdx
108
- As shown in [@example2023]. A footnote[^note].
 
 
 
 
109
 
110
- [^note]: Example footnote.
111
  ```
112
 
 
113
  ### Interactive fragments
114
 
115
  Plotly example (line):
@@ -170,12 +311,19 @@ import audioDemo from '../assets/audio/audio-example.wav'
170
  </audio>
171
  ```
172
 
173
- ## Advanced Customization
 
 
174
 
175
- - Styling: customize `src/styles/global.scss` for layout, typography, color scheme, and dark mode variables.
176
- - Headings/TOC: tweak the TOC builder in `src/pages/index.astro` to match your preferred levels and behavior.
177
- - Components: extend `components/` with bespoke MDX-friendly blocks (callouts, figures, widgets).
178
- - Fragments: generate self-contained HTML fragments (e.g., Plotly) and include them via `<HtmlFragment src="..." />`.
 
 
 
 
 
179
 
180
  ## Best Practices
181
 
 
2
  title: "The science template:\nCraft Beautiful Blogs"
3
  description: "A modern, MDX-first research article template with math, citations, and interactive figures."
4
  authors:
5
+ - "John Doe"
6
+ - "Alice Martin"
7
+ - "Robert Brown"
8
  affiliation: "Hugging Face"
9
  published: "Feb 19, 2025"
10
  tags:
 
15
 
16
  import HtmlFragment from "../components/HtmlFragment.astro";
17
  import { Image } from 'astro:assets';
18
+ import placeholder from "../assets/images/placeholder.jpg";
19
  import audioDemo from "../assets/audio/audio-example.wav";
20
  import Aside from "../components/Aside.astro";
21
  import visualPoster from "../assets/images/visual-vocabulary-poster.png";
22
 
23
 
24
+ <Aside>
25
+ Welcome to this single-page research article template built with Astro and MDX.
26
+ It’s designed to help you write clear, modern, and interactive technical articles with minimal setup.
27
+ Whether you cover machine learning, data science, physics, or software topics, this template keeps the authoring flow simple while offering robust features out of the box.
28
+ <Fragment slot="aside">
29
+ Reading time: 10–15 minutes.
30
+ </Fragment>
31
+ In this guide, you’ll learn how to install the template,
32
+ write content (math, citations, images, code, asides, interactive fragments),
33
+ customize styles and behavior, and follow a few best practices for publishing.
34
+ <Fragment slot="aside">
35
+ If you have questions or remarks open a discussion on the <a href="https://huggingface.co/spaces/tfrere/science-blog-template/discussions?status=open&type=discussion">Community tab</a>!
36
+ </Fragment>
37
+ </Aside>
38
 
39
  ## Features
40
 
 
43
  <span className="tag">KaTeX math</span>
44
  <span className="tag">Citations & footnotes</span>
45
  <span className="tag">Automatic build</span>
46
+ <span className="tag">Table of content</span>
47
  <span className="tag">Dark theme</span>
48
  <span className="tag">HTML fragments</span>
49
  <span className="tag">Plotly ready</span>
50
  <span className="tag">SEO Friendly</span>
51
  <span className="tag">Lightweight bundle(\<500ko)</span>
52
+ <span className="tag">Aside notes</span>
53
+ <span className="tag">Medium like zoomable images</span>
54
  </div>
55
 
56
  ## Getting Started
 
76
 
77
  Author content in MDX. Below are minimal examples for the core elements.
78
 
79
+ ### MDX: frontmatter and imports (minimal example)
80
+
81
+ The initial skeleton of this article looks like this:
82
+
83
+ ```mdx
84
+ ---
85
+ title: "The science template:\nCraft Beautiful Blogs"
86
+ description: "A modern, MDX-first research article template with math, citations, and interactive figures."
87
+ authors:
88
+ - "John Doe"
89
+ - "Alice Martin"
90
+ - "Robert Brown"
91
+ affiliation: "Hugging Face"
92
+ published: "Feb 19, 2025"
93
+ tags:
94
+ - research
95
+ - template
96
+ ogImage: "https://example.com/your-og-image.png"
97
+ ---
98
+
99
+ import HtmlFragment from "../components/HtmlFragment.astro";
100
+ import { Image } from 'astro:assets';
101
+ import placeholder from "../assets/images/placeholder.png";
102
+ import audioDemo from "../assets/audio/audio-example.wav";
103
+ import Aside from "../components/Aside.astro";
104
+ import visualPoster from "../assets/images/visual-vocabulary-poster.png";
105
+
106
+
107
+ <Aside>
108
+ Welcome to this single-page research article template built with Astro and MDX.
109
+ It’s designed to help you write clear, modern, and interactive technical articles with minimal setup.
110
+ Whether you cover machine learning, data science, physics, or software topics, this template keeps the authoring flow simple while offering robust features out of the box.
111
+ <Fragment slot="aside">
112
+ Reading time: 10–15 minutes.
113
+ </Fragment>
114
+ In this guide, you’ll learn how to install the template,
115
+ write content (math, citations, images, code, asides, interactive fragments),
116
+ customize styles and behavior, and follow a few best practices for publishing.
117
+ <Fragment slot="aside">
118
+ If you have questions or remarks open a discussion on the <a href="https://huggingface.co/spaces/lvwerra/distill-blog-template/discussions?status=open&type=discussion">Community tab</a>!
119
+ </Fragment>
120
+ </Aside>
121
+
122
+
123
+ ```
124
+
125
  ### Math
126
 
127
  Inline example: $x^2 + y^2 = z^2$.
 
143
 
144
  ### Images
145
 
146
+ 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.
147
+
148
+ **Optional:** Zoomable (Medium-like lightbox): add `data-zoomable` to opt-in. Only images with this attribute will open full-screen on click.
149
 
150
+ <figure>
151
+ <Image
152
+ src={placeholder}
153
+ data-zoomable
154
+ alt="Example with caption and credit"
155
+ />
156
+ <figcaption>
157
+ Optimized image with a descriptive caption.
158
+ <span className="image-credit">Crédit: Photo by <a href="https://example.com">Author</a></span>
159
+ </figcaption>
160
+ </figure>
161
 
162
  ```mdx
163
  import { Image } from 'astro:assets'
164
+ import myImage from '../assets/images/placeholder.jpg'
165
+
166
+ <Image src={myImage} alt="Responsive, optimized example image" />
167
+
168
+ <figure>
169
+ <Image src={myImage} data-zoomable alt="Example with caption and credit" loading="lazy" />
170
+ <figcaption>
171
+ Optimized image with a descriptive caption.
172
+ <span className="image-credit">Crédit: Photo by <a href="https://example.com">Author</a></span>
173
+ </figcaption>
174
+ </figure>
175
+ ```
176
+
177
+
178
+ ### Code blocks
179
+
180
+ Use fenced code blocks with a language for syntax highlighting.
181
 
182
+ Python block example:
183
+
184
+ ```python
185
+ def greet(name: str) -> None:
186
+ print(f"Hello, {name}!")
187
+
188
+ greet("Astro")
189
  ```
190
 
191
+ How to write it in MDX:
192
+
193
+ ````mdx
194
+ ```python
195
+ def greet(name: str) -> None:
196
+ print(f"Hello, {name}!")
197
+
198
+ greet("Astro")
199
+ ```
200
+ ````
201
+
202
+
203
+ ### GitHub code embeds
204
+
205
+ Finally, if you want to include code from GitHub you can use emgithub.com and, for example, create a collapsible widget like this:
206
+
207
+ ```html
208
+ <details style="background: #f6f8fa; border: 1px solid #d0d7de; border-radius: 6px; margin: 1em 0;">
209
+ <summary style="padding: 12px; cursor: pointer; user-select: none; background: #f3f4f6; border-bottom: 1px solid #d0d7de;">
210
+ 👉 Naive DP implementation with overlap in Picotron (Click to expand)
211
+ </summary>
212
+ <div class="code-embed-container" style="margin: 0; border-radius: 0; overflow-x: scroll; width: max-content; min-width: 100%; font-size: 8px;"></div>
213
+ <script
214
+ src="https://emgithub.com/embed-v2.js?target=https%3A%2F%2Fgithub.com%2Fhuggingface%2Fpicotron%2Fblob%2F0035cce0e04afd6192763b11efe50010d8ad0f71%2Fpicotron%2Fdata_parallel%2Fdata_parallel.py%23L10-L60&style=github&type=code&showBorder=off&showLineNumbers=on&showFileMeta=on&showCopy=on&showFullPath=on">
215
+ </script>
216
+ </details>
217
+ ```
218
+
219
+ Which will display as follows:
220
+
221
+ <details style="background: #f6f8fa; border: 1px solid #d0d7de; border-radius: 6px; margin: 1em 0;">
222
+ <summary style="padding: 12px; cursor: pointer; user-select: none; background: #f3f4f6; border-bottom: 1px solid #d0d7de;">
223
+ 👉 Naive DP implementation with overlap in Picotron (Click to expand)
224
+ </summary>
225
+ <div class="code-embed-container" style="margin: 0; border-radius: 0; overflow-x: scroll; width: max-content; min-width: 100%; font-size: 8px;"></div>
226
+ <script
227
+ src="https://emgithub.com/embed-v2.js?target=https%3A%2F%2Fgithub.com%2Fhuggingface%2Fpicotron%2Fblob%2F0035cce0e04afd6192763b11efe50010d8ad0f71%2Fpicotron%2Fdata_parallel%2Fdata_parallel.py%23L10-L60&style=github&type=code&showBorder=off&showLineNumbers=on&showFileMeta=on&showCopy=on&showFullPath=on">
228
+ </script>
229
+ </details>
230
+
231
  ### Citations and notes
232
 
233
+ Here are a few variations using the same bibliography:
234
+
235
+ 1) In-text citation with brackets: [@example2023].
236
+
237
+ 2) Narrative citation: As shown by @vaswani2017attention, transformers enable efficient sequence modeling.
238
 
239
+ 3) Multiple citations and a footnote together: see [@vaswani2017attention; @example2023] for related work. Also note this footnote[^f1].
240
+
241
+ [^f1]: Footnote attached to the sentence above.
242
 
243
  ```mdx
244
+ 1) In-text citation with brackets: [@example2023].
245
+
246
+ 2) Narrative citation: As shown by @vaswani2017attention, transformers enable efficient sequence modeling.
247
+
248
+ 3) Multiple citations and a footnote together: see [@vaswani2017attention; @example2023] for related work. Also note this footnote[^f1].
249
 
250
+ [^f1]: Footnote attached to the sentence above.
251
  ```
252
 
253
+
254
  ### Interactive fragments
255
 
256
  Plotly example (line):
 
311
  </audio>
312
  ```
313
 
314
+ ### Tracking training metrics with TrackIO
315
+
316
+ TrackIO is a lightweight dashboard to monitor ML experiments. The embed below opens a demo project and shows a couple of metrics. You can customize the query parameters (`project`, `metrics`, `sidebar`) to fit your setup.
317
 
318
+ <div style="margin-top: 8px;">
319
+ <iframe src="https://trackio-documentation.hf.space/?project=fake-training-750735&metrics=train_loss,train_accuracy&sidebar=hidden" width="100%" height="600" frameborder="0"></iframe>
320
+ </div>
321
+
322
+ To embed TrackIO in your own page, copy the following HTML and adjust the `src` parameters:
323
+
324
+ ```html
325
+ <iframe src="https://org_name-space_name.hf.space/?project=fake-training&metrics=train_loss,train_accuracy&sidebar=hidden" width="100%" height="600" frameBorder="0"></iframe>
326
+ ```
327
 
328
  ## Best Practices
329
 
app/src/pages/index.astro CHANGED
@@ -22,6 +22,31 @@ const fmOg = articleFM?.ogImage as string | undefined;
22
  const imageAbs = fmOg && fmOg.startsWith('http')
23
  ? fmOg
24
  : (Astro.site ? new URL((fmOg ?? ogDefault.src), Astro.site).toString() : (fmOg ?? ogDefault.src));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
  ---
26
  <html lang="en">
27
  <head>
@@ -60,14 +85,7 @@ const imageAbs = fmOg && fmOg.startsWith('http')
60
  </main>
61
  </section>
62
 
63
- <Footer citationText='"The Distill Blog Template", 2025.' bibtex={`@misc{distill_blog_template,
64
- title={The Distill Blog Template},
65
- author={Some Authors et al},
66
- year={2025},
67
- }`}
68
- >
69
- <div class="references"><h3>References</h3></div>
70
- </Footer>
71
 
72
  <!-- Medium-like image zoom (lightbox) -->
73
  <script src="https://cdn.jsdelivr.net/npm/medium-zoom@1.1.0/dist/medium-zoom.min.js"></script>
@@ -212,6 +230,43 @@ const imageAbs = fmOg && fmOg.startsWith('http')
212
  document.addEventListener('DOMContentLoaded', buildTOC, { once: true });
213
  } else { buildTOC(); }
214
  </script>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
215
  </body>
216
  </html>
217
 
 
22
  const imageAbs = fmOg && fmOg.startsWith('http')
23
  ? fmOg
24
  : (Astro.site ? new URL((fmOg ?? ogDefault.src), Astro.site).toString() : (fmOg ?? ogDefault.src));
25
+
26
+ // ---- Build citation text & BibTeX from frontmatter ----
27
+ const rawTitle = articleFM?.title ?? 'Untitled article';
28
+ const titleFlat = String(rawTitle)
29
+ .replace(/\\n/g, ' ')
30
+ .replace(/\n/g, ' ')
31
+ .replace(/\s+/g, ' ')
32
+ .trim();
33
+ const extractYear = (val: string | undefined): number | undefined => {
34
+ if (!val) return undefined;
35
+ const d = new Date(val);
36
+ if (!Number.isNaN(d.getTime())) return d.getFullYear();
37
+ const m = String(val).match(/(19|20)\d{2}/);
38
+ return m ? Number(m[0]) : undefined;
39
+ };
40
+
41
+ const year = extractYear(published);
42
+ const citationAuthorsText = authors.join(', ');
43
+ const citationText = `${citationAuthorsText}${year ? ` (${year})` : ''}. "${titleFlat}".`;
44
+
45
+ const authorsBib = authors.join(' and ');
46
+ const keyAuthor = (authors[0] || 'article').split(/\s+/).slice(-1)[0].toLowerCase();
47
+ const keyTitle = titleFlat.toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_|_$/g, '').slice(0, 24);
48
+ const bibKey = `${keyAuthor}${year ?? ''}_${keyTitle}`;
49
+ const bibtex = `@misc{${bibKey},\n title={${titleFlat}},\n author={${authorsBib}},\n ${year ? `year={${year}}` : ''}\n}`;
50
  ---
51
  <html lang="en">
52
  <head>
 
85
  </main>
86
  </section>
87
 
88
+ <Footer citationText={citationText} bibtex={bibtex} />
 
 
 
 
 
 
 
89
 
90
  <!-- Medium-like image zoom (lightbox) -->
91
  <script src="https://cdn.jsdelivr.net/npm/medium-zoom@1.1.0/dist/medium-zoom.min.js"></script>
 
230
  document.addEventListener('DOMContentLoaded', buildTOC, { once: true });
231
  } else { buildTOC(); }
232
  </script>
233
+
234
+ <script>
235
+ // Inject visible language badges for code blocks when data-language is missing
236
+ const addCodeLangChips = () => {
237
+ const blocks = document.querySelectorAll('section.content-grid pre > code');
238
+ blocks.forEach(code => {
239
+ const pre = code.parentElement;
240
+ if (!pre || pre.querySelector('.code-lang-chip')) return;
241
+ // Try several places to detect language
242
+ const getLang = () => {
243
+ const direct = code.getAttribute('data-language') || code.dataset?.language;
244
+ if (direct) return direct;
245
+ const codeClass = (code.className || '').match(/language-([a-z0-9+\-]+)/i);
246
+ if (codeClass) return codeClass[1];
247
+ const preData = pre.getAttribute('data-language') || pre.dataset?.language;
248
+ if (preData) return preData;
249
+ const wrapper = pre.closest('.astro-code');
250
+ if (wrapper) {
251
+ const wrapData = wrapper.getAttribute('data-language') || wrapper.dataset?.language;
252
+ if (wrapData) return wrapData;
253
+ const wrapClass = (wrapper.className || '').match(/language-([a-z0-9+\-]+)/i);
254
+ if (wrapClass) return wrapClass[1];
255
+ }
256
+ return 'text';
257
+ };
258
+ const lang = getLang().toUpperCase();
259
+ const chip = document.createElement('span');
260
+ chip.className = 'code-lang-chip';
261
+ chip.textContent = lang;
262
+ pre.classList.add('has-lang-chip');
263
+ pre.appendChild(chip);
264
+ });
265
+ };
266
+ if (document.readyState === 'loading') {
267
+ document.addEventListener('DOMContentLoaded', addCodeLangChips, { once: true });
268
+ } else { addCodeLangChips(); }
269
+ </script>
270
  </body>
271
  </html>
272
 
app/src/styles/components/_base.scss ADDED
@@ -0,0 +1,153 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // ============================================================================
2
+ // Base / Reset
3
+ // ============================================================================
4
+ html { box-sizing: border-box; }
5
+ *, *::before, *::after { box-sizing: inherit; }
6
+ body { margin: 0; font-family: system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, Noto Sans, Helvetica Neue, Arial, Apple Color Emoji, Segoe UI Emoji; color: var(--text-color); }
7
+
8
+ /* Avoid constraining <main> inside grid; scope container sizing elsewhere if needed */
9
+ /* main { max-width: 980px; margin: 24px auto; padding: 16px; } */
10
+
11
+ // ============================================================================
12
+ // Typography (inspired by Distill)
13
+ // ============================================================================
14
+ html { font-size: 14px; line-height: 1.6; }
15
+ @media (min-width: 768px) { html { font-size: 16px; } }
16
+ @media (min-width: 1024px) { html { font-size: 17px; } }
17
+
18
+ .content-grid main { color: var(--text-color); }
19
+ .content-grid main p { margin: 0 0 var(--spacing-3); }
20
+
21
+ .content-grid main h2 {
22
+ font-weight: 600;
23
+ font-size: clamp(22px, 2.6vw, 32px);
24
+ line-height: 1.2;
25
+ margin: var(--spacing-5) 0 var(--spacing-3);
26
+ padding-bottom: var(--spacing-2);
27
+ border-bottom: 1px solid var(--border-color);
28
+ }
29
+
30
+ .content-grid main h3 {
31
+ font-weight: 700;
32
+ font-size: clamp(18px, 2.1vw, 22px);
33
+ line-height: 1.25;
34
+ margin: var(--spacing-4) 0 var(--spacing-2);
35
+ }
36
+
37
+ .content-grid main h4 {
38
+ font-weight: 600;
39
+ text-transform: uppercase;
40
+ font-size: 14px;
41
+ line-height: 1.2;
42
+ margin: var(--spacing-3) 0 var(--spacing-2);
43
+ }
44
+
45
+ .content-grid main a { color: inherit; text-decoration: none; border-bottom: 1px solid var(--link-underline); }
46
+ .content-grid main a:hover { border-bottom: 1px solid var(--link-underline-hover); }
47
+
48
+ /* Ne pas souligner les liens de titres dans l'article (pas le TOC) */
49
+ .content-grid main h2 a,
50
+ .content-grid main h3 a,
51
+ .content-grid main h4 a { border-bottom: none; text-decoration: none; }
52
+ .content-grid main h2 a:hover,
53
+ .content-grid main h3 a:hover,
54
+ .content-grid main h4 a:hover { border-bottom: none; text-decoration: none; }
55
+
56
+ .content-grid main ul,
57
+ .content-grid main ol { padding-left: 24px; margin: 0 0 var(--spacing-3); }
58
+ .content-grid main li { margin-bottom: var(--spacing-2); }
59
+ .content-grid main li:last-child { margin-bottom: 0; }
60
+
61
+ .content-grid main blockquote {
62
+ border-left: 2px solid var(--border-color);
63
+ padding-left: var(--spacing-4);
64
+ font-style: italic;
65
+ color: var(--muted-color);
66
+ margin: var(--spacing-4) 0;
67
+ }
68
+
69
+ .content-grid main pre { background: var(--code-bg); border: 1px solid var(--border-color); border-radius: 6px; padding: var(--spacing-3); font-size: 14px; overflow: auto; }
70
+ .content-grid main code { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; }
71
+
72
+ /* Pretty-code language label (visible chip at top-right) */
73
+ .content-grid main pre:has(code[data-language]),
74
+ .content-grid main pre:has(code[class*="language-"]) {
75
+ position: relative;
76
+ padding-top: 28px; /* space for the label */
77
+ }
78
+ .content-grid main pre > code[data-language]::after,
79
+ .content-grid main pre > code[class*="language-"]::after {
80
+ content: attr(data-language);
81
+ position: absolute;
82
+ top: 4px;
83
+ right: 6px;
84
+ font-size: 11px;
85
+ line-height: 1;
86
+ text-transform: uppercase;
87
+ color: var(--muted-color);
88
+ background: transparent;
89
+ border: none;
90
+ border-radius: 4px;
91
+ padding: 2px 4px;
92
+ pointer-events: none;
93
+ z-index: 1;
94
+ }
95
+
96
+ /* JS fallback chip */
97
+ .content-grid main pre.has-lang-chip { position: relative; padding-top: 22px; }
98
+ .content-grid main pre .code-lang-chip {
99
+ position: absolute;
100
+ top: 0px; right: 0px;
101
+ font-size: 10px; line-height: 1;
102
+ color: rgba(255,255,255,.5);
103
+ background: rgba(255,255,255,.1);
104
+ border: none;
105
+ border-radius: 0px; padding: 6px 6px 4px 4px; pointer-events: none; z-index: 1;
106
+ }
107
+
108
+ .content-grid main table { border-collapse: collapse; width: 100%; margin: 0 0 var(--spacing-4); }
109
+ .content-grid main th, .content-grid main td { border-bottom: 1px solid var(--border-color); padding: 6px 8px; text-align: left; font-size: 15px; }
110
+ .content-grid main thead th { border-bottom: 1px solid var(--border-color); }
111
+
112
+ .content-grid main hr { border: none; border-bottom: 1px solid var(--border-color); margin: var(--spacing-5) 0; }
113
+
114
+
115
+
116
+
117
+ // ============================================================================
118
+ // Media / Figures
119
+ // ============================================================================
120
+ :where(picture, img) {
121
+ max-width: 100%;
122
+ width: 100%;
123
+ height: auto;
124
+ display: block;
125
+ }
126
+
127
+ figure { margin: 16px 0; }
128
+ figcaption { color: var(--muted-color); font-size: 12px; }
129
+
130
+ // Inline feature tags
131
+ .tag-list { display: flex; flex-wrap: wrap; gap: 8px; margin: 8px 0 16px; }
132
+ .tag {
133
+ display: inline-flex;
134
+ align-items: center;
135
+ gap: 6px;
136
+ padding: 4px 8px;
137
+ font-size: 12px;
138
+ line-height: 1;
139
+ border-radius: 999px;
140
+ background: var(--surface-bg);
141
+ border: 1px solid var(--border-color);
142
+ color: var(--text-color);
143
+ }
144
+ [data-theme="dark"] .tag { background: #1a1f27; border-color: rgba(255,255,255,.15); }
145
+
146
+
147
+ // ============================================================================
148
+ // Figures, captions & image credits
149
+ // ============================================================================
150
+ figure { margin: 12px 0; }
151
+ figcaption { text-align: center; font-size: 0.9rem; color: var(--muted-color); margin-top: 6px; }
152
+ .image-credit { display: block; margin-top: 4px; font-size: 12px; color: var(--muted-color); }
153
+ .image-credit a { color: inherit; text-decoration: underline; text-underline-offset: 2px; }
app/src/styles/components/_layout.scss ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // ============================================================================
2
+ // Layout – 3-column grid (TOC / Article / Aside)
3
+ // ============================================================================
4
+ .content-grid { max-width: 1280px; margin: 0 auto; padding: 0 16px; margin-top: 40px; display: grid; grid-template-columns: 220px minmax(0, 680px) 260px; gap: 32px; align-items: start; }
5
+ .content-grid > main { max-width: 100%; margin: 0; padding: 0; }
6
+
7
+ // TOC (left column)
8
+ .toc { position: sticky; top: 24px; }
9
+ .toc nav { border-left: 1px solid var(--border-color); padding-left: 16px; font-size: 13px; }
10
+ .toc .title { font-weight: 600; font-size: 14px; margin-bottom: 8px; }
11
+
12
+ // Hide in-article TOC (duplicated by sticky aside)
13
+ main > nav:first-of-type { display: none; }
14
+
15
+ // TOC look & feel
16
+ .toc nav ul { margin: 0 0 6px; padding-left: 1em; }
17
+ .toc nav li { list-style: none; margin: .25em 0; }
18
+ .toc nav a { color: var(--text-color); text-decoration: none; border-bottom: none; }
19
+ .toc nav > ul > li > a { font-weight: 700; }
20
+ .toc nav a:hover { text-decoration: underline solid var(--muted-color); }
21
+ .toc nav a.active { text-decoration: underline; }
22
+
23
+ // Right aside (notes)
24
+ .right-aside { position: sticky; top: 24px; }
25
+ .right-aside .aside-card { background: var(--surface-bg); border: 1px solid var(--border-color); border-radius: 8px; padding: 10px; margin-bottom: 10px; font-size: 0.9rem; color: var(--text-color); }
26
+
27
+ // Responsive – collapse to single column
28
+ @media (max-width: 1100px) {
29
+ .content-grid { grid-template-columns: 1fr; }
30
+ .toc { position: static; }
31
+ .right-aside { display: none; }
32
+ main > nav:first-of-type { display: block; }
33
+ }
app/src/styles/components/_variables.scss ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // ============================================================================
2
+ // Theme Variables (inspired by Distill)
3
+ // ============================================================================
4
+ :root {
5
+ --distill-gray: rgb(107, 114, 128);
6
+ --distill-gray-light: rgb(185, 185, 185);
7
+ --distill-gray-lighter: rgb(228, 228, 228);
8
+ --distill-gray-lightest: rgb(245, 245, 245);
9
+ --distill-blue: #007BFF;
10
+
11
+ --text-color: rgba(0,0,0,.85);
12
+ --muted-color: rgba(0,0,0,.6);
13
+ --border-color: rgba(0,0,0,.1);
14
+
15
+ /* Light surfaces & links */
16
+ --surface-bg: #fafafa;
17
+ --code-bg: #f6f8fa;
18
+ --link-underline: rgba(0,0,0,.4);
19
+ --link-underline-hover: rgba(0,0,0,.8);
20
+
21
+ --spacing-1: 8px;
22
+ --spacing-2: 12px;
23
+ --spacing-3: 16px;
24
+ --spacing-4: 24px;
25
+ --spacing-5: 32px;
26
+ }
27
+ // Theme tokens for dark mode
28
+ [data-theme="dark"] {
29
+ --text-color: rgba(255,255,255,.9);
30
+ --muted-color: rgba(255,255,255,.7);
31
+ --border-color: rgba(255,255,255,.15);
32
+ --surface-bg: #12151b;
33
+ --code-bg: #12151b;
34
+ --link-underline: rgba(255,255,255,.5);
35
+ --link-underline-hover: rgba(255,255,255,.9);
36
+ color-scheme: dark;
37
+ background: #0f1115;
38
+ }
39
+
40
+
app/src/styles/global.scss CHANGED
@@ -1,41 +1,6 @@
1
- // ============================================================================
2
- // Theme Variables (inspired by Distill)
3
- // ============================================================================
4
- :root {
5
- --distill-gray: rgb(107, 114, 128);
6
- --distill-gray-light: rgb(185, 185, 185);
7
- --distill-gray-lighter: rgb(228, 228, 228);
8
- --distill-gray-lightest: rgb(245, 245, 245);
9
- --distill-blue: #007BFF;
10
-
11
- --text-color: rgba(0,0,0,.85);
12
- --muted-color: rgba(0,0,0,.6);
13
- --border-color: rgba(0,0,0,.1);
14
-
15
- /* Light surfaces & links */
16
- --surface-bg: #fafafa;
17
- --code-bg: #f6f8fa;
18
- --link-underline: rgba(0,0,0,.4);
19
- --link-underline-hover: rgba(0,0,0,.8);
20
-
21
- --spacing-1: 8px;
22
- --spacing-2: 12px;
23
- --spacing-3: 16px;
24
- --spacing-4: 24px;
25
- --spacing-5: 32px;
26
- }
27
- // Theme tokens for dark mode
28
- [data-theme="dark"] {
29
- --text-color: rgba(255,255,255,.9);
30
- --muted-color: rgba(255,255,255,.7);
31
- --border-color: rgba(255,255,255,.15);
32
- --surface-bg: #12151b;
33
- --code-bg: #12151b;
34
- --link-underline: rgba(255,255,255,.5);
35
- --link-underline-hover: rgba(255,255,255,.9);
36
- color-scheme: dark;
37
- background: #0f1115;
38
- }
39
  /* Dark-mode form tweak */
40
  [data-theme="dark"] .plotly_input_container > select { background-color: #1a1f27; border-color: var(--border-color); color: var(--text-color); }
41
 
@@ -47,116 +12,34 @@
47
  [data-theme="dark"] .citation-text,
48
  [data-theme="dark"] .citation-bibtex { background: #12151b; border-color: rgba(255,255,255,.15); color: var(--text-color); }
49
 
50
- // ============================================================================
51
- // Base / Reset
52
- // ============================================================================
53
- html { box-sizing: border-box; }
54
- *, *::before, *::after { box-sizing: inherit; }
55
- body { margin: 0; font-family: system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, Noto Sans, Helvetica Neue, Arial, Apple Color Emoji, Segoe UI Emoji; color: var(--text-color); }
56
-
57
- /* Avoid constraining <main> inside grid; scope container sizing elsewhere if needed */
58
- /* main { max-width: 980px; margin: 24px auto; padding: 16px; } */
59
-
60
- // ============================================================================
61
- // Typography (inspired by Distill)
62
- // ============================================================================
63
- html { font-size: 14px; line-height: 1.6; }
64
- @media (min-width: 768px) { html { font-size: 16px; } }
65
- @media (min-width: 1024px) { html { font-size: 17px; } }
66
-
67
- .content-grid main { color: var(--text-color); }
68
- .content-grid main p { margin: 0 0 var(--spacing-3); }
69
-
70
- .content-grid main h2 {
71
- font-weight: 600;
72
- font-size: clamp(22px, 2.6vw, 32px);
73
- line-height: 1.2;
74
- margin: var(--spacing-5) 0 var(--spacing-3);
75
- padding-bottom: var(--spacing-2);
76
- border-bottom: 1px solid var(--border-color);
77
- }
78
-
79
- .content-grid main h3 {
80
- font-weight: 700;
81
- font-size: clamp(18px, 2.1vw, 22px);
82
- line-height: 1.25;
83
- margin: var(--spacing-4) 0 var(--spacing-2);
84
- }
85
-
86
- .content-grid main h4 {
87
- font-weight: 600;
88
- text-transform: uppercase;
89
- font-size: 14px;
90
- line-height: 1.2;
91
- margin: var(--spacing-3) 0 var(--spacing-2);
92
- }
93
-
94
- .content-grid main a { color: inherit; text-decoration: none; border-bottom: 1px solid var(--link-underline); }
95
- .content-grid main a:hover { border-bottom: 1px solid var(--link-underline-hover); }
96
-
97
- /* Ne pas souligner les liens de titres dans l'article (pas le TOC) */
98
- .content-grid main h2 a,
99
- .content-grid main h3 a,
100
- .content-grid main h4 a { border-bottom: none; text-decoration: none; }
101
- .content-grid main h2 a:hover,
102
- .content-grid main h3 a:hover,
103
- .content-grid main h4 a:hover { border-bottom: none; text-decoration: none; }
104
 
105
- .content-grid main ul,
106
- .content-grid main ol { padding-left: 24px; margin: 0 0 var(--spacing-3); }
107
- .content-grid main li { margin-bottom: var(--spacing-2); }
108
- .content-grid main li:last-child { margin-bottom: 0; }
109
 
110
- .content-grid main blockquote {
111
- border-left: 2px solid var(--border-color);
112
- padding-left: var(--spacing-4);
113
- font-style: italic;
 
 
 
 
 
114
  color: var(--muted-color);
115
- margin: var(--spacing-4) 0;
116
  }
117
-
118
- .content-grid main pre { background: var(--code-bg); border: 1px solid var(--border-color); border-radius: 6px; padding: var(--spacing-3); font-size: 14px; overflow: auto; }
119
- .content-grid main code { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; }
120
-
121
- .content-grid main table { border-collapse: collapse; width: 100%; margin: 0 0 var(--spacing-4); }
122
- .content-grid main th, .content-grid main td { border-bottom: 1px solid var(--border-color); padding: 6px 8px; text-align: left; font-size: 15px; }
123
- .content-grid main thead th { border-bottom: 1px solid var(--border-color); }
124
-
125
- .content-grid main hr { border: none; border-bottom: 1px solid var(--border-color); margin: var(--spacing-5) 0; }
126
-
127
- // ============================================================================
128
- // Media / Figures
129
- // ============================================================================
130
- :where(picture, img) {
131
- max-width: 100%;
132
- width: 100%;
133
- height: auto;
134
- display: block;
135
  }
136
 
137
- figure { margin: 16px 0; }
138
- figcaption { color: var(--muted-color); font-size: 12px; }
139
-
140
- // Inline feature tags
141
- .tag-list { display: flex; flex-wrap: wrap; gap: 8px; margin: 8px 0 16px; }
142
- .tag {
143
- display: inline-flex;
144
- align-items: center;
145
- gap: 6px;
146
- padding: 4px 8px;
147
- font-size: 12px;
148
- line-height: 1;
149
- border-radius: 999px;
150
- background: var(--surface-bg);
151
- border: 1px solid var(--border-color);
152
- color: var(--text-color);
153
- }
154
- [data-theme="dark"] .tag { background: #1a1f27; border-color: rgba(255,255,255,.15); }
155
 
156
  /* Opt-in zoomable images */
157
  img[data-zoomable] { cursor: zoom-in; }
158
  .medium-zoom--opened img[data-zoomable] { cursor: zoom-out; }
159
 
 
160
  // ============================================================================
161
  // Hero (full-bleed)
162
  // ============================================================================
@@ -202,40 +85,6 @@ img[data-zoomable] { cursor: zoom-in; }
202
  }
203
  .meta-container-cell p { margin: 0; }
204
 
205
- // ============================================================================
206
- // Layout – 3-column grid (TOC / Article / Aside)
207
- // ============================================================================
208
- .content-grid { max-width: 1280px; margin: 0 auto; padding: 0 16px; margin-top: 40px; display: grid; grid-template-columns: 220px minmax(0, 680px) 260px; gap: 32px; align-items: start; }
209
- .content-grid > main { max-width: 100%; margin: 0; padding: 0; }
210
-
211
- // TOC (left column)
212
- .toc { position: sticky; top: 24px; }
213
- .toc nav { border-left: 1px solid var(--border-color); padding-left: 16px; font-size: 13px; }
214
- .toc .title { font-weight: 600; font-size: 14px; margin-bottom: 8px; }
215
-
216
- // Hide in-article TOC (duplicated by sticky aside)
217
- main > nav:first-of-type { display: none; }
218
-
219
- // TOC look & feel
220
- .toc nav ul { margin: 0 0 6px; padding-left: 1em; }
221
- .toc nav li { list-style: none; margin: .25em 0; }
222
- .toc nav a { color: var(--text-color); text-decoration: none; border-bottom: none; }
223
- .toc nav > ul > li > a { font-weight: 700; }
224
- .toc nav a:hover { text-decoration: underline solid var(--muted-color); }
225
- .toc nav a.active { text-decoration: underline; }
226
-
227
- // Right aside (notes)
228
- .right-aside { position: sticky; top: 24px; }
229
- .right-aside .aside-card { background: var(--surface-bg); border: 1px solid var(--border-color); border-radius: 8px; padding: 10px; margin-bottom: 10px; font-size: 0.9rem; color: var(--text-color); }
230
-
231
- // Responsive – collapse to single column
232
- @media (max-width: 1100px) {
233
- .content-grid { grid-template-columns: 1fr; }
234
- .toc { position: static; }
235
- .right-aside { display: none; }
236
- main > nav:first-of-type { display: block; }
237
- }
238
-
239
  // ============================================================================
240
  // Plotly – fragments & controls
241
  // ============================================================================
 
1
+ @import "./components/variables";
2
+ @import "./components/base";
3
+ @import "./components/layout";
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
  /* Dark-mode form tweak */
5
  [data-theme="dark"] .plotly_input_container > select { background-color: #1a1f27; border-color: var(--border-color); color: var(--text-color); }
6
 
 
12
  [data-theme="dark"] .citation-text,
13
  [data-theme="dark"] .citation-bibtex { background: #12151b; border-color: rgba(255,255,255,.15); color: var(--text-color); }
14
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
 
16
+ .margin-aside { position: relative; margin: 12px 0; }
 
 
 
17
 
18
+ .margin-aside__main { /* main text stays full width of the main column */ }
19
+ .margin-aside__aside {
20
+ position: absolute;
21
+ top: 0;
22
+ right: -260px; /* push into the right grid column (width 260 + gap 32) */
23
+ width: 260px;
24
+ border-radius: 8px;
25
+ padding: 10px;
26
+ font-size: 0.9rem;
27
  color: var(--muted-color);
 
28
  }
29
+ @media (max-width: 1100px) {
30
+ .margin-aside__aside {
31
+ position: static;
32
+ width: auto;
33
+ margin-top: 8px;
34
+ }
 
 
 
 
 
 
 
 
 
 
 
 
35
  }
36
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
 
38
  /* Opt-in zoomable images */
39
  img[data-zoomable] { cursor: zoom-in; }
40
  .medium-zoom--opened img[data-zoomable] { cursor: zoom-out; }
41
 
42
+
43
  // ============================================================================
44
  // Hero (full-bleed)
45
  // ============================================================================
 
85
  }
86
  .meta-container-cell p { margin: 0; }
87
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
88
  // ============================================================================
89
  // Plotly – fragments & controls
90
  // ============================================================================