Spaces:
Running
Running
thibaud frere
commited on
Commit
·
72cfb5a
1
Parent(s):
e904bd4
update
Browse files- CLAUDE.md +3 -0
- README.md +0 -7
- app/astro.config.mjs +112 -1
- app/package-lock.json +0 -0
- app/package.json +0 -0
- app/src/components/Header.astro +0 -17
- app/src/components/{Meta.astro → Hero.astro} +40 -4
- app/src/components/{HtmlFragment.astro → HtmlEmbed.astro} +45 -5
- app/src/components/MermaidDemo.astro +0 -11
- app/src/components/{SeoHead.astro → Seo.astro} +0 -0
- app/src/content/article.mdx +6 -5
- app/src/content/chapters/available-blocks.mdx +99 -67
- app/src/content/chapters/best-pratices.mdx +1 -1
- app/src/content/chapters/getting-started.mdx +2 -9
- app/src/content/chapters/writing-your-content.mdx +4 -4
- app/src/content/fragments/palettes.html +19 -9
- app/src/env.d.ts +9 -1
- app/src/pages/index.astro +46 -50
- app/src/styles/_base.css +19 -28
- app/src/styles/components/_code.css +29 -14
- app/src/styles/components/_poltly.css +17 -18
- app/src/styles/global.css +1 -46
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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
-
|
|
|
|
|
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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
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('
|
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: "
|
5 |
-
description: "A modern, MDX-first research article template with math, citations
|
6 |
authors:
|
7 |
- "John Doe"
|
8 |
- "Alice Martin"
|
9 |
- "Robert Brown"
|
10 |
affiliation: "Hugging Face"
|
11 |
-
published: "
|
12 |
tags:
|
13 |
- research
|
14 |
- template
|
15 |
ogImage: "/thumb.jpg"
|
16 |
---
|
17 |
|
18 |
-
import
|
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
|
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 |
-
###
|
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
|
95 |
|
96 |
Use fenced code blocks with a language for syntax highlighting.
|
97 |
|
@@ -112,30 +117,85 @@ greet("Astro")
|
|
112 |
```
|
113 |
````
|
114 |
|
115 |
-
### Mermaid
|
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 |
-
|
121 |
-
|
122 |
-
|
123 |
-
|
124 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
125 |
```
|
126 |
|
127 |
<small className="muted">Example</small>
|
128 |
````mdx
|
129 |
```mermaid
|
130 |
-
|
131 |
-
|
132 |
-
|
133 |
-
|
134 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
135 |
```
|
136 |
````
|
137 |
|
138 |
-
###
|
|
|
|
|
|
|
|
|
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 |
-
|
272 |
|
273 |
-
The main purpose of the ```
|
274 |
|
275 |
They exist in the `app/src/content/fragments` folder.
|
276 |
|
277 |
-
|
278 |
-
|
279 |
-
Here are some examples of the two **libraries** in the template:
|
280 |
|
281 |
-
|
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 |
-
|
|
|
|
|
|
|
295 |
|
296 |
-
<
|
297 |
-
|
298 |
-
|
299 |
-
|
|
|
|
|
|
|
|
|
|
|
300 |
|
301 |
<small className="muted">Example</small>
|
302 |
```mdx
|
303 |
-
import
|
304 |
|
305 |
-
<
|
306 |
```
|
307 |
|
308 |
-
|
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="
|
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 |
-
|
324 |
|
325 |
You can also embed **gradio** apps.
|
326 |
|
327 |
-
<gradio-app theme_mode="light" space="
|
328 |
|
329 |
|
330 |
|
331 |
<small className="muted">Example</small>
|
332 |
```mdx
|
333 |
-
<gradio-app theme_mode="light" space="
|
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 |
-
<
|
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.
|
43 |
-
|
44 |
-
Then push your changes to your new Space repo:
|
45 |
|
46 |
```bash
|
47 |
-
git clone
|
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
|
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 |
-
<
|
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 |
-
<
|
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:
|
5 |
/* removed circular badge */
|
6 |
-
.palettes .palette-card__swatches { display:
|
7 |
-
.palettes .palette-card__swatches .sw {
|
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 .
|
11 |
-
.palettes .copy-btn
|
12 |
-
.palettes .copy-btn:
|
|
|
|
|
13 |
@media (max-width: 640px) {
|
14 |
.palettes .palette-card { grid-template-columns: 1fr; align-items: stretch; gap: 10px; }
|
15 |
-
.palettes .palette-card__swatches {
|
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
|
|
|
166 |
btn.addEventListener('click', async () => {
|
167 |
const json = JSON.stringify(colors, null, 2);
|
168 |
-
try {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|
3 |
-
import
|
4 |
import Footer from '../components/Footer.astro';
|
5 |
-
import Header from '../components/Header.astro';
|
6 |
import ThemeToggle from '../components/ThemeToggle.astro';
|
7 |
-
import
|
8 |
-
import
|
|
|
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 ??
|
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 |
-
<
|
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 |
-
<
|
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 |
-
|
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 |
-
.
|
|
|
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:
|
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:
|
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-
|
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: .
|
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 |
-
|
55 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
56 |
position: absolute;
|
57 |
-
top:
|
58 |
-
|
59 |
-
|
60 |
-
|
61 |
-
|
62 |
-
|
|
|
|
|
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 |
-
.
|
5 |
-
.
|
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"] .
|
24 |
-
[data-theme="dark"] .
|
25 |
-
[data-theme="dark"] .
|
26 |
-
[data-theme="dark"] .
|
27 |
-
[data-theme="dark"] .
|
28 |
-
[data-theme="dark"] .
|
29 |
-
[data-theme="dark"] .
|
30 |
|
31 |
-
[data-theme="dark"] .
|
32 |
-
[data-theme="dark"] .
|
33 |
-
[data-theme="dark"] .
|
34 |
-
[data-theme="dark"] .
|
35 |
|
36 |
-
[data-theme="dark"] .
|
37 |
|
38 |
/* Legend and hover backgrounds */
|
39 |
-
[data-theme="dark"] .
|
40 |
-
[data-theme="dark"] .
|
41 |
|
42 |
/* Colorbar background (keep gradient intact) */
|
43 |
-
[data-theme="dark"] .
|
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"] .
|
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 |
/* ============================================================================ */
|