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 |
/* ============================================================================ */
|