Spaces:
Running
Running
thibaud frere
commited on
Commit
Β·
b8e1b6c
1
Parent(s):
a153fdf
Move assets into content/assets; update imports; clean .gitattributes; fix LFS tracking
Browse files- .gitattributes +0 -2
- app/.astro/astro/content.d.ts +204 -0
- app/astro.config.mjs +24 -4
- app/src/components/{FullBleed.astro β FullWidth.astro} +1 -1
- app/src/components/Hero.astro +1 -1
- app/src/components/HtmlEmbed.astro +6 -6
- app/src/components/{Aside.astro β Sidenote.astro} +2 -0
- app/src/components/ThemeToggle.astro +2 -2
- app/src/content/article.mdx +11 -11
- app/src/{assets β content/assets}/audio/audio-example.wav +0 -0
- app/src/{assets β content/assets}/icones/moon.svg +0 -0
- app/src/{assets β content/assets}/icones/sun.svg +0 -0
- app/src/{assets β content/assets}/images/placeholder.png +0 -0
- app/src/{assets β content/assets}/images/visual-vocabulary-poster.png +0 -0
- app/src/content/chapters/available-blocks.mdx +12 -7
- app/src/content/chapters/best-pratices.mdx +3 -3
- app/src/content/chapters/getting-started.mdx +3 -3
- app/src/content/chapters/writing-your-content.mdx +25 -25
- app/src/content/{fragments β embeds}/banner.html +0 -0
- app/src/content/{fragments β embeds}/bar.html +0 -0
- app/src/content/{fragments β embeds}/color-picker.html +0 -0
- app/src/content/{fragments β embeds}/d3-bar.html +0 -0
- app/src/content/{fragments β embeds}/d3-line.html +0 -0
- app/src/content/{fragments β embeds}/heatmap.html +0 -0
- app/src/content/{fragments β embeds}/line.html +0 -0
- {fragments β app/src/content/embeds/original_embeds}/d3js/banner.html +0 -0
- {fragments β app/src/content/embeds/original_embeds}/d3js/line.html +0 -0
- {fragments β app/src/content/embeds/original_embeds}/plotly/banner.py +0 -0
- {fragments β app/src/content/embeds/original_embeds}/plotly/bar.py +0 -0
- {fragments β app/src/content/embeds/original_embeds}/plotly/heatmap.py +0 -0
- {fragments β app/src/content/embeds/original_embeds}/plotly/line.py +0 -0
- {fragments β app/src/content/embeds/original_embeds}/plotly/poetry.lock +0 -0
- {fragments β app/src/content/embeds/original_embeds}/plotly/pyproject.toml +0 -0
- app/src/content/{fragments β embeds}/palettes.html +0 -0
- app/src/pages/index.astro +100 -1
- app/src/styles/_base.css +15 -2
- app/src/styles/_layout.css +14 -9
- app/src/styles/_variables.css +8 -0
- app/src/styles/components/_code.css +43 -0
- app/src/styles/global.css +24 -0
.gitattributes
CHANGED
@@ -1,5 +1,3 @@
|
|
1 |
-
app/assets/images/*.png filter=lfs diff=lfs merge=lfs -text
|
2 |
-
app/src/assets/images/*.png filter=lfs diff=lfs merge=lfs -text
|
3 |
*.png filter=lfs diff=lfs merge=lfs -text
|
4 |
*.jpg filter=lfs diff=lfs merge=lfs -text
|
5 |
*.wav filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
1 |
*.png filter=lfs diff=lfs merge=lfs -text
|
2 |
*.jpg filter=lfs diff=lfs merge=lfs -text
|
3 |
*.wav filter=lfs diff=lfs merge=lfs -text
|
app/.astro/astro/content.d.ts
CHANGED
@@ -0,0 +1,204 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
declare module 'astro:content' {
|
2 |
+
interface Render {
|
3 |
+
'.mdx': Promise<{
|
4 |
+
Content: import('astro').MarkdownInstance<{}>['Content'];
|
5 |
+
headings: import('astro').MarkdownHeading[];
|
6 |
+
remarkPluginFrontmatter: Record<string, any>;
|
7 |
+
components: import('astro').MDXInstance<{}>['components'];
|
8 |
+
}>;
|
9 |
+
}
|
10 |
+
}
|
11 |
+
|
12 |
+
declare module 'astro:content' {
|
13 |
+
interface RenderResult {
|
14 |
+
Content: import('astro/runtime/server/index.js').AstroComponentFactory;
|
15 |
+
headings: import('astro').MarkdownHeading[];
|
16 |
+
remarkPluginFrontmatter: Record<string, any>;
|
17 |
+
}
|
18 |
+
interface Render {
|
19 |
+
'.md': Promise<RenderResult>;
|
20 |
+
}
|
21 |
+
|
22 |
+
export interface RenderedContent {
|
23 |
+
html: string;
|
24 |
+
metadata?: {
|
25 |
+
imagePaths: Array<string>;
|
26 |
+
[key: string]: unknown;
|
27 |
+
};
|
28 |
+
}
|
29 |
+
}
|
30 |
+
|
31 |
+
declare module 'astro:content' {
|
32 |
+
type Flatten<T> = T extends { [K: string]: infer U } ? U : never;
|
33 |
+
|
34 |
+
export type CollectionKey = keyof AnyEntryMap;
|
35 |
+
export type CollectionEntry<C extends CollectionKey> = Flatten<AnyEntryMap[C]>;
|
36 |
+
|
37 |
+
export type ContentCollectionKey = keyof ContentEntryMap;
|
38 |
+
export type DataCollectionKey = keyof DataEntryMap;
|
39 |
+
|
40 |
+
type AllValuesOf<T> = T extends any ? T[keyof T] : never;
|
41 |
+
type ValidContentEntrySlug<C extends keyof ContentEntryMap> = AllValuesOf<
|
42 |
+
ContentEntryMap[C]
|
43 |
+
>['slug'];
|
44 |
+
|
45 |
+
/** @deprecated Use `getEntry` instead. */
|
46 |
+
export function getEntryBySlug<
|
47 |
+
C extends keyof ContentEntryMap,
|
48 |
+
E extends ValidContentEntrySlug<C> | (string & {}),
|
49 |
+
>(
|
50 |
+
collection: C,
|
51 |
+
// Note that this has to accept a regular string too, for SSR
|
52 |
+
entrySlug: E,
|
53 |
+
): E extends ValidContentEntrySlug<C>
|
54 |
+
? Promise<CollectionEntry<C>>
|
55 |
+
: Promise<CollectionEntry<C> | undefined>;
|
56 |
+
|
57 |
+
/** @deprecated Use `getEntry` instead. */
|
58 |
+
export function getDataEntryById<C extends keyof DataEntryMap, E extends keyof DataEntryMap[C]>(
|
59 |
+
collection: C,
|
60 |
+
entryId: E,
|
61 |
+
): Promise<CollectionEntry<C>>;
|
62 |
+
|
63 |
+
export function getCollection<C extends keyof AnyEntryMap, E extends CollectionEntry<C>>(
|
64 |
+
collection: C,
|
65 |
+
filter?: (entry: CollectionEntry<C>) => entry is E,
|
66 |
+
): Promise<E[]>;
|
67 |
+
export function getCollection<C extends keyof AnyEntryMap>(
|
68 |
+
collection: C,
|
69 |
+
filter?: (entry: CollectionEntry<C>) => unknown,
|
70 |
+
): Promise<CollectionEntry<C>[]>;
|
71 |
+
|
72 |
+
export function getEntry<
|
73 |
+
C extends keyof ContentEntryMap,
|
74 |
+
E extends ValidContentEntrySlug<C> | (string & {}),
|
75 |
+
>(entry: {
|
76 |
+
collection: C;
|
77 |
+
slug: E;
|
78 |
+
}): E extends ValidContentEntrySlug<C>
|
79 |
+
? Promise<CollectionEntry<C>>
|
80 |
+
: Promise<CollectionEntry<C> | undefined>;
|
81 |
+
export function getEntry<
|
82 |
+
C extends keyof DataEntryMap,
|
83 |
+
E extends keyof DataEntryMap[C] | (string & {}),
|
84 |
+
>(entry: {
|
85 |
+
collection: C;
|
86 |
+
id: E;
|
87 |
+
}): E extends keyof DataEntryMap[C]
|
88 |
+
? Promise<DataEntryMap[C][E]>
|
89 |
+
: Promise<CollectionEntry<C> | undefined>;
|
90 |
+
export function getEntry<
|
91 |
+
C extends keyof ContentEntryMap,
|
92 |
+
E extends ValidContentEntrySlug<C> | (string & {}),
|
93 |
+
>(
|
94 |
+
collection: C,
|
95 |
+
slug: E,
|
96 |
+
): E extends ValidContentEntrySlug<C>
|
97 |
+
? Promise<CollectionEntry<C>>
|
98 |
+
: Promise<CollectionEntry<C> | undefined>;
|
99 |
+
export function getEntry<
|
100 |
+
C extends keyof DataEntryMap,
|
101 |
+
E extends keyof DataEntryMap[C] | (string & {}),
|
102 |
+
>(
|
103 |
+
collection: C,
|
104 |
+
id: E,
|
105 |
+
): E extends keyof DataEntryMap[C]
|
106 |
+
? Promise<DataEntryMap[C][E]>
|
107 |
+
: Promise<CollectionEntry<C> | undefined>;
|
108 |
+
|
109 |
+
/** Resolve an array of entry references from the same collection */
|
110 |
+
export function getEntries<C extends keyof ContentEntryMap>(
|
111 |
+
entries: {
|
112 |
+
collection: C;
|
113 |
+
slug: ValidContentEntrySlug<C>;
|
114 |
+
}[],
|
115 |
+
): Promise<CollectionEntry<C>[]>;
|
116 |
+
export function getEntries<C extends keyof DataEntryMap>(
|
117 |
+
entries: {
|
118 |
+
collection: C;
|
119 |
+
id: keyof DataEntryMap[C];
|
120 |
+
}[],
|
121 |
+
): Promise<CollectionEntry<C>[]>;
|
122 |
+
|
123 |
+
export function render<C extends keyof AnyEntryMap>(
|
124 |
+
entry: AnyEntryMap[C][string],
|
125 |
+
): Promise<RenderResult>;
|
126 |
+
|
127 |
+
export function reference<C extends keyof AnyEntryMap>(
|
128 |
+
collection: C,
|
129 |
+
): import('astro/zod').ZodEffects<
|
130 |
+
import('astro/zod').ZodString,
|
131 |
+
C extends keyof ContentEntryMap
|
132 |
+
? {
|
133 |
+
collection: C;
|
134 |
+
slug: ValidContentEntrySlug<C>;
|
135 |
+
}
|
136 |
+
: {
|
137 |
+
collection: C;
|
138 |
+
id: keyof DataEntryMap[C];
|
139 |
+
}
|
140 |
+
>;
|
141 |
+
// Allow generic `string` to avoid excessive type errors in the config
|
142 |
+
// if `dev` is not running to update as you edit.
|
143 |
+
// Invalid collection names will be caught at build time.
|
144 |
+
export function reference<C extends string>(
|
145 |
+
collection: C,
|
146 |
+
): import('astro/zod').ZodEffects<import('astro/zod').ZodString, never>;
|
147 |
+
|
148 |
+
type ReturnTypeOrOriginal<T> = T extends (...args: any[]) => infer R ? R : T;
|
149 |
+
type InferEntrySchema<C extends keyof AnyEntryMap> = import('astro/zod').infer<
|
150 |
+
ReturnTypeOrOriginal<Required<ContentConfig['collections'][C]>['schema']>
|
151 |
+
>;
|
152 |
+
|
153 |
+
type ContentEntryMap = {
|
154 |
+
"chapters": {
|
155 |
+
"available-blocks.mdx": {
|
156 |
+
id: "available-blocks.mdx";
|
157 |
+
slug: "available-blocks";
|
158 |
+
body: string;
|
159 |
+
collection: "chapters";
|
160 |
+
data: any
|
161 |
+
} & { render(): Render[".mdx"] };
|
162 |
+
"best-pratices.mdx": {
|
163 |
+
id: "best-pratices.mdx";
|
164 |
+
slug: "best-pratices";
|
165 |
+
body: string;
|
166 |
+
collection: "chapters";
|
167 |
+
data: any
|
168 |
+
} & { render(): Render[".mdx"] };
|
169 |
+
"getting-started.mdx": {
|
170 |
+
id: "getting-started.mdx";
|
171 |
+
slug: "getting-started";
|
172 |
+
body: string;
|
173 |
+
collection: "chapters";
|
174 |
+
data: any
|
175 |
+
} & { render(): Render[".mdx"] };
|
176 |
+
"writing-your-content.mdx": {
|
177 |
+
id: "writing-your-content.mdx";
|
178 |
+
slug: "writing-your-content";
|
179 |
+
body: string;
|
180 |
+
collection: "chapters";
|
181 |
+
data: any
|
182 |
+
} & { render(): Render[".mdx"] };
|
183 |
+
};
|
184 |
+
|
185 |
+
};
|
186 |
+
|
187 |
+
type DataEntryMap = {
|
188 |
+
"assets": Record<string, {
|
189 |
+
id: string;
|
190 |
+
collection: "assets";
|
191 |
+
data: any;
|
192 |
+
}>;
|
193 |
+
"embeds": Record<string, {
|
194 |
+
id: string;
|
195 |
+
collection: "embeds";
|
196 |
+
data: any;
|
197 |
+
}>;
|
198 |
+
|
199 |
+
};
|
200 |
+
|
201 |
+
type AnyEntryMap = ContentEntryMap & DataEntryMap;
|
202 |
+
|
203 |
+
export type ContentConfig = never;
|
204 |
+
}
|
app/astro.config.mjs
CHANGED
@@ -34,7 +34,25 @@ function rehypeCodeCopyAndLabel() {
|
|
34 |
fromClass(preClasses),
|
35 |
];
|
36 |
let lang = candidates.find(Boolean) || '';
|
37 |
-
const
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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');
|
@@ -81,14 +99,16 @@ function rehypeCodeCopyAndLabel() {
|
|
81 |
node.__forceSingle = true;
|
82 |
}
|
83 |
}
|
84 |
-
// Ensure CSS-only label works: set data-language on
|
85 |
code.properties = code.properties || {};
|
86 |
-
if (
|
|
|
|
|
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':
|
92 |
children: (isSingleLine || node.__forceSingle) ? [ node ] : [
|
93 |
{
|
94 |
type: 'element',
|
|
|
34 |
fromClass(preClasses),
|
35 |
];
|
36 |
let lang = candidates.find(Boolean) || '';
|
37 |
+
const lower = String(lang).toLowerCase();
|
38 |
+
const toExt = (s) => {
|
39 |
+
switch (String(s).toLowerCase()) {
|
40 |
+
case 'typescript': case 'ts': return 'ts';
|
41 |
+
case 'tsx': return 'tsx';
|
42 |
+
case 'javascript': case 'js': case 'node': return 'js';
|
43 |
+
case 'jsx': return 'jsx';
|
44 |
+
case 'python': case 'py': return 'py';
|
45 |
+
case 'bash': case 'shell': case 'sh': return 'sh';
|
46 |
+
case 'markdown': case 'md': return 'md';
|
47 |
+
case 'yaml': case 'yml': return 'yml';
|
48 |
+
case 'html': return 'html';
|
49 |
+
case 'css': return 'css';
|
50 |
+
case 'json': return 'json';
|
51 |
+
default: return lower || '';
|
52 |
+
}
|
53 |
+
};
|
54 |
+
const ext = toExt(lower);
|
55 |
+
const displayLang = ext ? String(ext).toUpperCase() : '';
|
56 |
// Determine if single-line block: prefer Shiki lines, then text content
|
57 |
const countLinesFromShiki = () => {
|
58 |
const isLineEl = (el) => el && el.type === 'element' && el.tagName === 'span' && Array.isArray(el.properties?.className) && el.properties.className.includes('line');
|
|
|
99 |
node.__forceSingle = true;
|
100 |
}
|
101 |
}
|
102 |
+
// Ensure CSS-only label works: set data-language on <code> and <pre>, and wrapper
|
103 |
code.properties = code.properties || {};
|
104 |
+
if (ext) code.properties['data-language'] = ext;
|
105 |
+
node.properties = node.properties || {};
|
106 |
+
if (ext) node.properties['data-language'] = ext;
|
107 |
// Replace <pre> with wrapper div.code-card containing button + pre
|
108 |
const wrapper = {
|
109 |
type: 'element',
|
110 |
tagName: 'div',
|
111 |
+
properties: { className: ['code-card'].concat((isSingleLine || node.__forceSingle) ? ['no-copy'] : []), 'data-language': ext },
|
112 |
children: (isSingleLine || node.__forceSingle) ? [ node ] : [
|
113 |
{
|
114 |
type: 'element',
|
app/src/components/{FullBleed.astro β FullWidth.astro}
RENAMED
@@ -1,6 +1,6 @@
|
|
1 |
---
|
2 |
const { class: className, ...props } = Astro.props;
|
3 |
-
const wrapperClass = ["full-
|
4 |
---
|
5 |
<div class={wrapperClass} {...props}>
|
6 |
<slot />
|
|
|
1 |
---
|
2 |
const { class: className, ...props } = Astro.props;
|
3 |
+
const wrapperClass = ["full-width", className].filter(Boolean).join(" ");
|
4 |
---
|
5 |
<div class={wrapperClass} {...props}>
|
6 |
<slot />
|
app/src/components/Hero.astro
CHANGED
@@ -87,7 +87,7 @@ const pdfFilename = `${slugify(pdfBase)}.pdf`;
|
|
87 |
</script>
|
88 |
|
89 |
<style>
|
90 |
-
/* Hero (full-
|
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; }
|
|
|
87 |
</script>
|
88 |
|
89 |
<style>
|
90 |
+
/* Hero (full-width) */
|
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; }
|
app/src/components/HtmlEmbed.astro
CHANGED
@@ -2,14 +2,14 @@
|
|
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
|
6 |
-
const
|
7 |
|
8 |
function resolveFragment(requested: string): string | null {
|
9 |
-
// Allow both "banner.html" and "
|
10 |
const needle = requested.replace(/^\/*/, '');
|
11 |
-
for (const [key, html] of Object.entries(
|
12 |
-
if (key.endsWith('/' + needle) || key.endsWith('/' + needle.replace(/^
|
13 |
return html;
|
14 |
}
|
15 |
}
|
@@ -58,7 +58,7 @@ const mountId = `frag-${Math.random().toString(36).slice(2)}`;
|
|
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
|
62 |
if (window.Plotly || window.d3 || document.readyState === 'complete') execute();
|
63 |
else window.addEventListener('load', execute, { once: true });
|
64 |
</script>
|
|
|
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 embeds under src/content/embeds/** as strings (dev & build)
|
6 |
+
const embeds = (import.meta as any).glob('../content/embeds/**/*.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 "embeds/banner.html"
|
10 |
const needle = requested.replace(/^\/*/, '');
|
11 |
+
for (const [key, html] of Object.entries(embeds)) {
|
12 |
+
if (key.endsWith('/' + needle) || key.endsWith('/' + needle.replace(/^embeds\//, ''))) {
|
13 |
return html;
|
14 |
}
|
15 |
}
|
|
|
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 embeds
|
62 |
if (window.Plotly || window.d3 || document.readyState === 'complete') execute();
|
63 |
else window.addEventListener('load', execute, { once: true });
|
64 |
</script>
|
app/src/components/{Aside.astro β Sidenote.astro}
RENAMED
@@ -8,3 +8,5 @@
|
|
8 |
<slot name="aside" />
|
9 |
</aside>
|
10 |
</div>
|
|
|
|
|
|
8 |
<slot name="aside" />
|
9 |
</aside>
|
10 |
</div>
|
11 |
+
|
12 |
+
|
app/src/components/ThemeToggle.astro
CHANGED
@@ -1,6 +1,6 @@
|
|
1 |
---
|
2 |
-
import sunIconUrl from "../assets/icones/sun.svg?url";
|
3 |
-
import moonIconUrl from "../assets/icones/moon.svg?url";
|
4 |
---
|
5 |
<button id="theme-toggle" aria-label="Toggle color theme">
|
6 |
<img class="icon light" src={sunIconUrl} alt="light" width="20" height="20" />
|
|
|
1 |
---
|
2 |
+
import sunIconUrl from "../content/assets/icones/sun.svg?url";
|
3 |
+
import moonIconUrl from "../content/assets/icones/moon.svg?url";
|
4 |
---
|
5 |
<button id="theme-toggle" aria-label="Toggle color theme">
|
6 |
<img class="icon light" src={sunIconUrl} alt="light" width="20" height="20" />
|
app/src/content/article.mdx
CHANGED
@@ -17,35 +17,35 @@ ogImage: "/thumb.jpg"
|
|
17 |
|
18 |
import HtmlEmbed from "../components/HtmlEmbed.astro";
|
19 |
import Wide from "../components/Wide.astro";
|
20 |
-
import
|
21 |
import { Image } from 'astro:assets';
|
22 |
-
import placeholder from "
|
23 |
-
import audioDemo from "
|
24 |
-
import
|
25 |
-
import visualPoster from "
|
26 |
|
27 |
import BestPractices from "./chapters/best-pratices.mdx";
|
28 |
import WritingYourContent from "./chapters/writing-your-content.mdx";
|
29 |
import AvailableBlocks from "./chapters/available-blocks.mdx";
|
30 |
import GettingStarted from "./chapters/getting-started.mdx";
|
31 |
|
32 |
-
<
|
33 |
-
Welcome to this single-page research article template
|
34 |
Itβs designed to help you write clear, modern, and **interactive** technical articles with **minimal setup**.
|
35 |
Whether you cover machine learning, data science, physics, or software topics, this template keeps the authoring flow simple while offering robust features out of the box.
|
36 |
<Fragment slot="aside">
|
37 |
Reading time: 20β25 minutes.
|
38 |
</Fragment>
|
39 |
In this guide, youβll learn how to install the template,
|
40 |
-
write content (math, citations, images, code,
|
41 |
customize styles and behavior, and follow a few **best practices** for publishing.
|
42 |
|
43 |
-
</
|
44 |
|
45 |
|
46 |
#### Features
|
47 |
|
48 |
-
<
|
49 |
<div className="tag-list">
|
50 |
<span className="tag">Markdown based</span>
|
51 |
<span className="tag">KaTeX math</span>
|
@@ -69,7 +69,7 @@ import GettingStarted from "./chapters/getting-started.mdx";
|
|
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>!
|
71 |
</Fragment>
|
72 |
-
</
|
73 |
|
74 |
## Introduction
|
75 |
The web enables forms of explanation that static PDFs cannot: **reactive diagrams**, progressive notation, and **exploratory views** that reveal how ideas behave. Use **interactive fragments** to let readers hover, scrub, and inspectβbuilding **intuition**, not just reading results.
|
|
|
17 |
|
18 |
import HtmlEmbed from "../components/HtmlEmbed.astro";
|
19 |
import Wide from "../components/Wide.astro";
|
20 |
+
import FullWidth from "../components/FullWidth.astro";
|
21 |
import { Image } from 'astro:assets';
|
22 |
+
import placeholder from "./assets/images/placeholder.png";
|
23 |
+
import audioDemo from "./assets/audio/audio-example.wav";
|
24 |
+
import Sidenote from "../components/Sidenote.astro";
|
25 |
+
import visualPoster from "./assets/images/visual-vocabulary-poster.png";
|
26 |
|
27 |
import BestPractices from "./chapters/best-pratices.mdx";
|
28 |
import WritingYourContent from "./chapters/writing-your-content.mdx";
|
29 |
import AvailableBlocks from "./chapters/available-blocks.mdx";
|
30 |
import GettingStarted from "./chapters/getting-started.mdx";
|
31 |
|
32 |
+
<Sidenote>
|
33 |
+
Welcome to this single-page research article template.
|
34 |
Itβs designed to help you write clear, modern, and **interactive** technical articles with **minimal setup**.
|
35 |
Whether you cover machine learning, data science, physics, or software topics, this template keeps the authoring flow simple while offering robust features out of the box.
|
36 |
<Fragment slot="aside">
|
37 |
Reading time: 20β25 minutes.
|
38 |
</Fragment>
|
39 |
In this guide, youβll learn how to install the template,
|
40 |
+
write content (math, citations, images, code, sidenotes, interactive fragments),
|
41 |
customize styles and behavior, and follow a few **best practices** for publishing.
|
42 |
|
43 |
+
</Sidenote>
|
44 |
|
45 |
|
46 |
#### Features
|
47 |
|
48 |
+
<Sidenote>
|
49 |
<div className="tag-list">
|
50 |
<span className="tag">Markdown based</span>
|
51 |
<span className="tag">KaTeX math</span>
|
|
|
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>!
|
71 |
</Fragment>
|
72 |
+
</Sidenote>
|
73 |
|
74 |
## Introduction
|
75 |
The web enables forms of explanation that static PDFs cannot: **reactive diagrams**, progressive notation, and **exploratory views** that reveal how ideas behave. Use **interactive fragments** to let readers hover, scrub, and inspectβbuilding **intuition**, not just reading results.
|
app/src/{assets β content/assets}/audio/audio-example.wav
RENAMED
File without changes
|
app/src/{assets β content/assets}/icones/moon.svg
RENAMED
File without changes
|
app/src/{assets β content/assets}/icones/sun.svg
RENAMED
File without changes
|
app/src/{assets β content/assets}/images/placeholder.png
RENAMED
File without changes
|
app/src/{assets β content/assets}/images/visual-vocabulary-poster.png
RENAMED
File without changes
|
app/src/content/chapters/available-blocks.mdx
CHANGED
@@ -1,10 +1,10 @@
|
|
1 |
import { Image } from 'astro:assets';
|
2 |
-
import placeholder from '
|
3 |
-
import audioDemo from '
|
4 |
import HtmlEmbed from '../../components/HtmlEmbed.astro';
|
5 |
-
import
|
6 |
import Wide from '../../components/Wide.astro';
|
7 |
-
import
|
8 |
import Accordion from '../../components/Accordion.astro';
|
9 |
|
10 |
## Available blocks
|
@@ -58,11 +58,14 @@ $$
|
|
58 |
|
59 |
Props (optional)
|
60 |
- `data-zoomable`: adds a zoomable lightbox.
|
|
|
61 |
- `loading="lazy"`: lazy loads the image.
|
62 |
- `figcaption`: adds a caption and credit.
|
63 |
|
64 |
**Optional:** Zoomable (Medium-like lightbox): add `data-zoomable` to opt-in. Only images with this attribute will open full-screen on click.
|
65 |
|
|
|
|
|
66 |
**Optional:** Lazy loading: add `loading="lazy"` to opt-in.
|
67 |
|
68 |
**Optional:** Figcaption and credits: add a `figcaption` element with a `span` containing the credit.
|
@@ -73,6 +76,8 @@ Props (optional)
|
|
73 |
<Image
|
74 |
src={placeholder}
|
75 |
data-zoomable
|
|
|
|
|
76 |
alt="Tensor parallelism in a transformer block"
|
77 |
/>
|
78 |
<figcaption>
|
@@ -84,12 +89,12 @@ Props (optional)
|
|
84 |
<small className="muted">Example</small>
|
85 |
```mdx
|
86 |
import { Image } from 'astro:assets'
|
87 |
-
import myImage from '
|
88 |
|
89 |
<Image src={myImage} alt="Responsive, optimized example image" />
|
90 |
|
91 |
<figure>
|
92 |
-
<Image src={myImage} data-zoomable alt="Example with caption and credit" loading="lazy" />
|
93 |
<figcaption>
|
94 |
Optimized image with a descriptive caption.
|
95 |
<span className="image-credit">Credit: Photo by <a href="https://example.com">Author</a></span>
|
@@ -299,7 +304,7 @@ greet("Astro");
|
|
299 |
<br/>
|
300 |
<small className="muted">Example</small>
|
301 |
```mdx
|
302 |
-
import audioDemo from '
|
303 |
|
304 |
<audio controls src={audioDemo}/>
|
305 |
```
|
|
|
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 Sidenote from '../../components/Sidenote.astro';
|
6 |
import Wide from '../../components/Wide.astro';
|
7 |
+
import FullWidth from '../../components/FullWidth.astro';
|
8 |
import Accordion from '../../components/Accordion.astro';
|
9 |
|
10 |
## Available blocks
|
|
|
58 |
|
59 |
Props (optional)
|
60 |
- `data-zoomable`: adds a zoomable lightbox.
|
61 |
+
- `data-downloadable`: adds a small download button to fetch the image file.
|
62 |
- `loading="lazy"`: lazy loads the image.
|
63 |
- `figcaption`: adds a caption and credit.
|
64 |
|
65 |
**Optional:** Zoomable (Medium-like lightbox): add `data-zoomable` to opt-in. Only images with this attribute will open full-screen on click.
|
66 |
|
67 |
+
**Optional:** Download button: add `data-downloadable` to render a small button in the bottom-right corner that downloads the image. By default it tries to use the original filename; you can override via `data-download-name="myfile.png"` and `data-download-src="/path/to/original.png"`.
|
68 |
+
|
69 |
**Optional:** Lazy loading: add `loading="lazy"` to opt-in.
|
70 |
|
71 |
**Optional:** Figcaption and credits: add a `figcaption` element with a `span` containing the credit.
|
|
|
76 |
<Image
|
77 |
src={placeholder}
|
78 |
data-zoomable
|
79 |
+
data-downloadable
|
80 |
+
layout="fixed"
|
81 |
alt="Tensor parallelism in a transformer block"
|
82 |
/>
|
83 |
<figcaption>
|
|
|
89 |
<small className="muted">Example</small>
|
90 |
```mdx
|
91 |
import { Image } from 'astro:assets'
|
92 |
+
import myImage from './assets/images/placeholder.jpg'
|
93 |
|
94 |
<Image src={myImage} alt="Responsive, optimized example image" />
|
95 |
|
96 |
<figure>
|
97 |
+
<Image src={myImage} layout="fixed" data-zoomable data-downloadable alt="Example with caption and credit" loading="lazy" />
|
98 |
<figcaption>
|
99 |
Optimized image with a descriptive caption.
|
100 |
<span className="image-credit">Credit: Photo by <a href="https://example.com">Author</a></span>
|
|
|
304 |
<br/>
|
305 |
<small className="muted">Example</small>
|
306 |
```mdx
|
307 |
+
import audioDemo from './assets/audio/audio-example.wav'
|
308 |
|
309 |
<audio controls src={audioDemo}/>
|
310 |
```
|
app/src/content/chapters/best-pratices.mdx
CHANGED
@@ -1,6 +1,6 @@
|
|
1 |
|
2 |
import { Image } from 'astro:assets';
|
3 |
-
import visualPoster from '
|
4 |
import Note from '../../components/Note.astro';
|
5 |
|
6 |
|
@@ -39,7 +39,7 @@ Favor **concise captions** and callouts that clarify what to look at and why it
|
|
39 |
{/* ### Use the right color
|
40 |
A palette encodes **meaning** (categories, magnitudes, oppositions), preserves **readability** and **accessibility** (sufficient contrast, colorβvision safety), and ensures **perceptually smooth transitions**. The three families below illustrate when to use **categorical**, **sequential**, or **diverging** colors and how they evolve from the same **reference hue**.
|
41 |
|
42 |
-
<
|
43 |
<div className="">
|
44 |
<HtmlEmbed src="palettes.html" />
|
45 |
</div>
|
@@ -49,7 +49,7 @@ A palette encodes **meaning** (categories, magnitudes, oppositions), preserves *
|
|
49 |
<Fragment slot="aside">
|
50 |
It will be applied to the <a href="/" target="_blank">whole page</a>.
|
51 |
</Fragment>
|
52 |
-
</
|
53 |
|
54 |
### Use the right chart
|
55 |
|
|
|
1 |
|
2 |
import { Image } from 'astro:assets';
|
3 |
+
import visualPoster from '../assets/images/visual-vocabulary-poster.png';
|
4 |
import Note from '../../components/Note.astro';
|
5 |
|
6 |
|
|
|
39 |
{/* ### Use the right color
|
40 |
A palette encodes **meaning** (categories, magnitudes, oppositions), preserves **readability** and **accessibility** (sufficient contrast, colorβvision safety), and ensures **perceptually smooth transitions**. The three families below illustrate when to use **categorical**, **sequential**, or **diverging** colors and how they evolve from the same **reference hue**.
|
41 |
|
42 |
+
<Sidenote>
|
43 |
<div className="">
|
44 |
<HtmlEmbed src="palettes.html" />
|
45 |
</div>
|
|
|
49 |
<Fragment slot="aside">
|
50 |
It will be applied to the <a href="/" target="_blank">whole page</a>.
|
51 |
</Fragment>
|
52 |
+
</Sidenote> */}
|
53 |
|
54 |
### Use the right chart
|
55 |
|
app/src/content/chapters/getting-started.mdx
CHANGED
@@ -1,10 +1,10 @@
|
|
1 |
-
import
|
2 |
|
3 |
## Getting Started
|
4 |
|
5 |
### Installation
|
6 |
|
7 |
-
<
|
8 |
```bash
|
9 |
git lfs install
|
10 |
git lfs pull
|
@@ -14,7 +14,7 @@ npm install
|
|
14 |
<Fragment slot="aside">
|
15 |
You can also use yarn.
|
16 |
</Fragment>
|
17 |
-
</
|
18 |
|
19 |
|
20 |
### Development
|
|
|
1 |
+
import Sidenote from '../../components/Sidenote.astro';
|
2 |
|
3 |
## Getting Started
|
4 |
|
5 |
### Installation
|
6 |
|
7 |
+
<Sidenote>
|
8 |
```bash
|
9 |
git lfs install
|
10 |
git lfs pull
|
|
|
14 |
<Fragment slot="aside">
|
15 |
You can also use yarn.
|
16 |
</Fragment>
|
17 |
+
</Sidenote>
|
18 |
|
19 |
|
20 |
### Development
|
app/src/content/chapters/writing-your-content.mdx
CHANGED
@@ -1,11 +1,11 @@
|
|
1 |
{/* IMPORTS */}
|
2 |
import { Image } from 'astro:assets';
|
3 |
-
import placeholder from '
|
4 |
-
import
|
5 |
import Wide from '../../components/Wide.astro';
|
6 |
-
import
|
7 |
import HtmlEmbed from '../../components/HtmlEmbed.astro';
|
8 |
-
import audioDemo from '
|
9 |
|
10 |
## Writing Your Content
|
11 |
|
@@ -14,7 +14,7 @@ import audioDemo from '../../assets/audio/audio-example.wav';
|
|
14 |
Your article lives in two places
|
15 |
|
16 |
- `app/src/content/` β where you can find the `article.mdx`, `bibliography.bib` and html fragments.
|
17 |
-
- `app/src/assets/` β images, audio, and other static assets. (handled by git lfs)
|
18 |
|
19 |
The `article.mdx` file is the main file that contains your article.
|
20 |
|
@@ -39,7 +39,7 @@ ogImage: "https://example.com/your-og-image.png"
|
|
39 |
|
40 |
{/* IMPORTS */}
|
41 |
import { Image } from 'astro:assets';
|
42 |
-
import placeholder from '
|
43 |
|
44 |
{/* CONTENT */}
|
45 |
# Hello, world
|
@@ -57,14 +57,14 @@ MDX is a mix of Markdown and HTML/JSX: write regular Markdown, and embed interac
|
|
57 |
```mdx
|
58 |
{/* IMPORTS */}
|
59 |
import { Image } from 'astro:assets'
|
60 |
-
import placeholder from '
|
61 |
-
import
|
62 |
|
63 |
# Mixing Markdown and components
|
64 |
|
65 |
This paragraph is written in Markdown.
|
66 |
|
67 |
-
<
|
68 |
|
69 |
Below is an image imported via Astro and optimized at build time:
|
70 |
|
@@ -105,14 +105,14 @@ Use the **color picker** below to see how the primary color affects the theme.
|
|
105 |
|
106 |
#### Brand color
|
107 |
|
108 |
-
<
|
109 |
<HtmlEmbed frameless src="color-picker.html" />
|
110 |
<Fragment slot="aside">
|
111 |
You can use the color picker to select the right color.
|
112 |
|
113 |
Here is an example of an <a href="#">interactive element</a>.
|
114 |
</Fragment>
|
115 |
-
</
|
116 |
|
117 |
|
118 |
#### Color palettes
|
@@ -124,25 +124,25 @@ Here is a suggestion of **color palettes** for your **data visualizations** that
|
|
124 |
|
125 |
### Placement
|
126 |
|
127 |
-
Use these helpers when you need to step outside the main content flow: **
|
128 |
|
129 |
-
####
|
130 |
|
131 |
-
<
|
132 |
This paragraph presents a **key idea** concisely.
|
133 |
<Fragment slot="aside">
|
134 |
**Side note** for brief context or a definition.
|
135 |
</Fragment>
|
136 |
-
</
|
137 |
|
138 |
<small className="muted">Example</small>
|
139 |
```mdx
|
140 |
-
import
|
141 |
|
142 |
-
<
|
143 |
Main paragraph with the core idea.
|
144 |
<Fragment slot="aside">Short side note.</Fragment>
|
145 |
-
</
|
146 |
```
|
147 |
|
148 |
Use these helpers to expand content beyond the main column when needed. They will always be centered and displayed above every other content.
|
@@ -162,17 +162,17 @@ import Wide from '../components/Wide.astro'
|
|
162 |
</Wide>
|
163 |
```
|
164 |
|
165 |
-
#### Full-
|
166 |
|
167 |
-
<
|
168 |
-
<div className="demo-full-
|
169 |
-
</
|
170 |
|
171 |
<small className="muted">Example</small>
|
172 |
```mdx
|
173 |
-
import
|
174 |
|
175 |
-
<
|
176 |
Your content here...
|
177 |
-
</
|
178 |
```
|
|
|
1 |
{/* IMPORTS */}
|
2 |
import { Image } from 'astro:assets';
|
3 |
+
import placeholder from '../assets/images/placeholder.png';
|
4 |
+
import Sidenote from '../../components/Sidenote.astro';
|
5 |
import Wide from '../../components/Wide.astro';
|
6 |
+
import FullWidth from '../../components/FullWidth.astro';
|
7 |
import HtmlEmbed from '../../components/HtmlEmbed.astro';
|
8 |
+
import audioDemo from '../assets/audio/audio-example.wav';
|
9 |
|
10 |
## Writing Your Content
|
11 |
|
|
|
14 |
Your article lives in two places
|
15 |
|
16 |
- `app/src/content/` β where you can find the `article.mdx`, `bibliography.bib` and html fragments.
|
17 |
+
- `app/src/content/assets/` β images, audio, and other static assets. (handled by git lfs)
|
18 |
|
19 |
The `article.mdx` file is the main file that contains your article.
|
20 |
|
|
|
39 |
|
40 |
{/* IMPORTS */}
|
41 |
import { Image } from 'astro:assets';
|
42 |
+
import placeholder from './assets/images/placeholder.jpg';
|
43 |
|
44 |
{/* CONTENT */}
|
45 |
# Hello, world
|
|
|
57 |
```mdx
|
58 |
{/* IMPORTS */}
|
59 |
import { Image } from 'astro:assets'
|
60 |
+
import placeholder from './assets/images/placeholder.png'
|
61 |
+
import Sidenote from '../components/Sidenote.astro'
|
62 |
|
63 |
# Mixing Markdown and components
|
64 |
|
65 |
This paragraph is written in Markdown.
|
66 |
|
67 |
+
<Sidenote>A short callout inserted via a component.</Sidenote>
|
68 |
|
69 |
Below is an image imported via Astro and optimized at build time:
|
70 |
|
|
|
105 |
|
106 |
#### Brand color
|
107 |
|
108 |
+
<Sidenote>
|
109 |
<HtmlEmbed frameless src="color-picker.html" />
|
110 |
<Fragment slot="aside">
|
111 |
You can use the color picker to select the right color.
|
112 |
|
113 |
Here is an example of an <a href="#">interactive element</a>.
|
114 |
</Fragment>
|
115 |
+
</Sidenote>
|
116 |
|
117 |
|
118 |
#### Color palettes
|
|
|
124 |
|
125 |
### Placement
|
126 |
|
127 |
+
Use these helpers when you need to step outside the main content flow: **Sidenotes** for contextual side notes, **Wide** to extend beyond the main column, and **Full-width** for full-width, immersive sections.
|
128 |
|
129 |
+
#### Sidenotes
|
130 |
|
131 |
+
<Sidenote>
|
132 |
This paragraph presents a **key idea** concisely.
|
133 |
<Fragment slot="aside">
|
134 |
**Side note** for brief context or a definition.
|
135 |
</Fragment>
|
136 |
+
</Sidenote>
|
137 |
|
138 |
<small className="muted">Example</small>
|
139 |
```mdx
|
140 |
+
import Sidenote from '../components/Sidenote.astro'
|
141 |
|
142 |
+
<Sidenote>
|
143 |
Main paragraph with the core idea.
|
144 |
<Fragment slot="aside">Short side note.</Fragment>
|
145 |
+
</Sidenote>
|
146 |
```
|
147 |
|
148 |
Use these helpers to expand content beyond the main column when needed. They will always be centered and displayed above every other content.
|
|
|
162 |
</Wide>
|
163 |
```
|
164 |
|
165 |
+
#### Full-width example
|
166 |
|
167 |
+
<FullWidth>
|
168 |
+
<div className="demo-full-width">demo full-width</div>
|
169 |
+
</FullWidth>
|
170 |
|
171 |
<small className="muted">Example</small>
|
172 |
```mdx
|
173 |
+
import FullWidth from '../components/FullWidth.astro'
|
174 |
|
175 |
+
<FullWidth>
|
176 |
Your content here...
|
177 |
+
</FullWidth>
|
178 |
```
|
app/src/content/{fragments β embeds}/banner.html
RENAMED
File without changes
|
app/src/content/{fragments β embeds}/bar.html
RENAMED
File without changes
|
app/src/content/{fragments β embeds}/color-picker.html
RENAMED
File without changes
|
app/src/content/{fragments β embeds}/d3-bar.html
RENAMED
File without changes
|
app/src/content/{fragments β embeds}/d3-line.html
RENAMED
File without changes
|
app/src/content/{fragments β embeds}/heatmap.html
RENAMED
File without changes
|
app/src/content/{fragments β embeds}/line.html
RENAMED
File without changes
|
{fragments β app/src/content/embeds/original_embeds}/d3js/banner.html
RENAMED
File without changes
|
{fragments β app/src/content/embeds/original_embeds}/d3js/line.html
RENAMED
File without changes
|
{fragments β app/src/content/embeds/original_embeds}/plotly/banner.py
RENAMED
File without changes
|
{fragments β app/src/content/embeds/original_embeds}/plotly/bar.py
RENAMED
File without changes
|
{fragments β app/src/content/embeds/original_embeds}/plotly/heatmap.py
RENAMED
File without changes
|
{fragments β app/src/content/embeds/original_embeds}/plotly/line.py
RENAMED
File without changes
|
{fragments β app/src/content/embeds/original_embeds}/plotly/poetry.lock
RENAMED
File without changes
|
{fragments β app/src/content/embeds/original_embeds}/plotly/pyproject.toml
RENAMED
File without changes
|
app/src/content/{fragments β embeds}/palettes.html
RENAMED
File without changes
|
app/src/pages/index.astro
CHANGED
@@ -5,7 +5,7 @@ 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 ?? {};
|
@@ -178,6 +178,105 @@ const bibtex = `@misc{${bibKey},\n title={${titleFlat}},\n author={${authorsBi
|
|
178 |
</script>
|
179 |
|
180 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
181 |
<script>
|
182 |
// Open external links in a new tab; keep internal anchors in-page
|
183 |
const setExternalTargets = () => {
|
|
|
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 '../content/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 ?? {};
|
|
|
178 |
</script>
|
179 |
|
180 |
|
181 |
+
<script>
|
182 |
+
// Add a small download button to img[data-downloadable]
|
183 |
+
(() => {
|
184 |
+
const SELECTOR = 'section.content-grid main img[data-downloadable]';
|
185 |
+
|
186 |
+
/**
|
187 |
+
* @param {HTMLImageElement} img
|
188 |
+
*/
|
189 |
+
const injectDownloadButton = (img) => {
|
190 |
+
if (!img || img.dataset.__dlInjected) return;
|
191 |
+
const parentFigure = img.closest('figure');
|
192 |
+
const parent = img.parentElement;
|
193 |
+
if (!parent) return;
|
194 |
+
img.dataset.__dlInjected = '1';
|
195 |
+
|
196 |
+
// Wrap the image in a positioned inline-block so the button is on the image
|
197 |
+
const wrapper = document.createElement('span');
|
198 |
+
wrapper.className = 'img-dl-wrap';
|
199 |
+
parent.insertBefore(wrapper, img);
|
200 |
+
wrapper.appendChild(img);
|
201 |
+
if (parentFigure && !parentFigure.classList.contains('has-dl-btn')) {
|
202 |
+
parentFigure.classList.add('has-dl-btn');
|
203 |
+
}
|
204 |
+
|
205 |
+
// Determine download href and filename
|
206 |
+
const pickHrefAndName = () => {
|
207 |
+
const current = img.currentSrc || img.src || '';
|
208 |
+
let href = img.getAttribute('data-download-src') || current;
|
209 |
+
// Derive filename from the original source when possible
|
210 |
+
const deriveName = () => {
|
211 |
+
try {
|
212 |
+
const u = new URL(current, location.href);
|
213 |
+
// Prefer original href param if provided by Astro image service
|
214 |
+
const rawHref = u.searchParams.get('href');
|
215 |
+
const candidate = rawHref ? decodeURIComponent(rawHref) : u.pathname;
|
216 |
+
const last = String(candidate).split('/').pop() || '';
|
217 |
+
// Strip query/hash and any appended transform suffixes after extension
|
218 |
+
const base = last.split('?')[0].split('#')[0];
|
219 |
+
const m = base.match(/^(.+?\.(?:png|jpe?g|webp|avif|gif|svg))(?:[._-].*)?$/i);
|
220 |
+
if (m && m[1]) return m[1];
|
221 |
+
// If extension missing, fallback to base as-is
|
222 |
+
return base || 'image';
|
223 |
+
} catch {
|
224 |
+
return 'image';
|
225 |
+
}
|
226 |
+
};
|
227 |
+
const name = img.getAttribute('data-download-name') || deriveName();
|
228 |
+
return { href, name };
|
229 |
+
};
|
230 |
+
|
231 |
+
const { href, name } = pickHrefAndName();
|
232 |
+
const a = document.createElement('a');
|
233 |
+
a.className = 'button button--ghost img-dl-btn';
|
234 |
+
a.href = href;
|
235 |
+
if (name) a.download = name;
|
236 |
+
a.setAttribute('aria-label', 'Download image');
|
237 |
+
a.setAttribute('title', name ? `Download ${name}` : 'Download image');
|
238 |
+
a.innerHTML = '<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M12 16c-.26 0-.52-.11-.71-.29l-5-5a1 1 0 0 1 1.42-1.42L11 12.59V4a1 1 0 1 1 2 0v8.59l3.29-3.3a1 1 0 1 1 1.42 1.42l-5 5c-.19.18-.45.29-.71.29zM5 20a1 1 0 1 1 0-2h14a1 1 0 1 1 0 2H5z"/></svg>';
|
239 |
+
|
240 |
+
// Ensure href/name match currentSrc right before navigation
|
241 |
+
a.addEventListener('click', async (ev) => {
|
242 |
+
try {
|
243 |
+
ev.preventDefault();
|
244 |
+
const picked = pickHrefAndName();
|
245 |
+
const res = await fetch(picked.href, { credentials: 'same-origin' });
|
246 |
+
const blob = await res.blob();
|
247 |
+
const objectUrl = URL.createObjectURL(blob);
|
248 |
+
const tmp = document.createElement('a');
|
249 |
+
tmp.href = objectUrl;
|
250 |
+
tmp.download = picked.name || 'image';
|
251 |
+
document.body.appendChild(tmp);
|
252 |
+
tmp.click();
|
253 |
+
setTimeout(() => { URL.revokeObjectURL(objectUrl); tmp.remove(); }, 1000);
|
254 |
+
} catch {
|
255 |
+
// Fallback to native behavior if fetch fails
|
256 |
+
}
|
257 |
+
});
|
258 |
+
|
259 |
+
// Append inside wrapper so positioning is relative to the image box
|
260 |
+
wrapper.appendChild(a);
|
261 |
+
};
|
262 |
+
|
263 |
+
const scan = () => {
|
264 |
+
document.querySelectorAll(SELECTOR).forEach((el) => injectDownloadButton(el));
|
265 |
+
};
|
266 |
+
|
267 |
+
const bootstrap = () => {
|
268 |
+
scan();
|
269 |
+
const main = document.querySelector('section.content-grid main');
|
270 |
+
if (!main) return;
|
271 |
+
const mo = new MutationObserver(() => scan());
|
272 |
+
mo.observe(main, { childList: true, subtree: true, attributes: true, attributeFilter: ['src'] });
|
273 |
+
};
|
274 |
+
|
275 |
+
if (document.readyState === 'complete') bootstrap();
|
276 |
+
else window.addEventListener('load', bootstrap, { once: true });
|
277 |
+
})();
|
278 |
+
</script>
|
279 |
+
|
280 |
<script>
|
281 |
// Open external links in a new tab; keep internal anchors in-page
|
282 |
const setExternalTargets = () => {
|
app/src/styles/_base.css
CHANGED
@@ -81,7 +81,7 @@ html { font-size: 14px; line-height: 1.6; }
|
|
81 |
|
82 |
/* Demo blocks for width helpers */
|
83 |
.demo-wide,
|
84 |
-
.demo-full-
|
85 |
display: grid;
|
86 |
place-items: center;
|
87 |
min-height: 150px;
|
@@ -118,6 +118,8 @@ picture {
|
|
118 |
max-width: 100%;
|
119 |
height: auto;
|
120 |
display: block;
|
|
|
|
|
121 |
}
|
122 |
|
123 |
/* Inline feature tags */
|
@@ -146,6 +148,17 @@ figcaption { text-align: left; font-size: 0.9rem; color: var(--muted-color); mar
|
|
146 |
.image-credit { display: block; margin-top: 4px; font-size: 12px; color: var(--muted-color); }
|
147 |
.image-credit a { color: inherit; text-decoration: underline; text-underline-offset: 2px; }
|
148 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
149 |
/* ============================================================================ */
|
150 |
/* Buttons (minimal, clean) */
|
151 |
/* ============================================================================ */
|
@@ -248,7 +261,7 @@ button:disabled, .button:disabled {
|
|
248 |
table,
|
249 |
blockquote,
|
250 |
.wide,
|
251 |
-
.full-
|
252 |
break-inside: avoid;
|
253 |
page-break-inside: avoid;
|
254 |
}
|
|
|
81 |
|
82 |
/* Demo blocks for width helpers */
|
83 |
.demo-wide,
|
84 |
+
.demo-full-width {
|
85 |
display: grid;
|
86 |
place-items: center;
|
87 |
min-height: 150px;
|
|
|
118 |
max-width: 100%;
|
119 |
height: auto;
|
120 |
display: block;
|
121 |
+
position: relative;
|
122 |
+
z-index: var(--z-elevated);
|
123 |
}
|
124 |
|
125 |
/* Inline feature tags */
|
|
|
148 |
.image-credit { display: block; margin-top: 4px; font-size: 12px; color: var(--muted-color); }
|
149 |
.image-credit a { color: inherit; text-decoration: underline; text-underline-offset: 2px; }
|
150 |
|
151 |
+
/* Zoomable overlay container (if used by any lightbox implementation) */
|
152 |
+
[data-zoom-overlay],
|
153 |
+
.zoom-overlay {
|
154 |
+
position: fixed;
|
155 |
+
inset: 0;
|
156 |
+
z-index: var(--z-overlay);
|
157 |
+
}
|
158 |
+
|
159 |
+
/* Download link inside figures */
|
160 |
+
figure .download-link { position: relative; z-index: var(--z-elevated); }
|
161 |
+
|
162 |
/* ============================================================================ */
|
163 |
/* Buttons (minimal, clean) */
|
164 |
/* ============================================================================ */
|
|
|
261 |
table,
|
262 |
blockquote,
|
263 |
.wide,
|
264 |
+
.full-width {
|
265 |
break-inside: avoid;
|
266 |
page-break-inside: avoid;
|
267 |
}
|
app/src/styles/_layout.css
CHANGED
@@ -1,7 +1,11 @@
|
|
1 |
/* ============================================================================ */
|
2 |
/* Layout β 3-column grid (TOC / Article / Aside) */
|
3 |
/* ============================================================================ */
|
4 |
-
|
|
|
|
|
|
|
|
|
5 |
.content-grid > main { max-width: 100%; margin: 0; padding: 0; }
|
6 |
|
7 |
/* TOC (left column) */
|
@@ -69,11 +73,11 @@ main > nav:first-of-type { display: none; }
|
|
69 |
|
70 |
|
71 |
/* ============================================================================ */
|
72 |
-
/* Width helpers β slightly wider than main column, and full-
|
73 |
/* ---------------------------------------------------------------------------- */
|
74 |
/* Usage in MDX: */
|
75 |
/* <div className="wide"> ... </div> */
|
76 |
-
/* <div className="full-
|
77 |
/* These center the content relative to the viewport while keeping it responsive. */
|
78 |
/* */
|
79 |
/* Notes: */
|
@@ -81,7 +85,7 @@ main > nav:first-of-type { display: none; }
|
|
81 |
/* to be wider or fully span the viewport. On small screens, they fall back to 100%. */
|
82 |
/* - Adjust the target width in .wide if desired. */
|
83 |
.wide,
|
84 |
-
.full-
|
85 |
|
86 |
.wide {
|
87 |
/* Target up to ~1100px while staying within viewport minus page gutters */
|
@@ -90,18 +94,19 @@ main > nav:first-of-type { display: none; }
|
|
90 |
transform: translateX(-50%);
|
91 |
}
|
92 |
|
93 |
-
.full-
|
94 |
-
/* Span the full viewport width */
|
95 |
width: 100vw;
|
96 |
-
margin-left: 50
|
97 |
-
|
98 |
}
|
99 |
|
100 |
@media (max-width: 1100px) {
|
101 |
.wide,
|
102 |
-
.full-
|
103 |
width: 100%;
|
104 |
margin-left: 0;
|
|
|
105 |
transform: none;
|
106 |
}
|
107 |
}
|
|
|
1 |
/* ============================================================================ */
|
2 |
/* Layout β 3-column grid (TOC / Article / Aside) */
|
3 |
/* ============================================================================ */
|
4 |
+
:root {
|
5 |
+
--content-padding-x: 16px;
|
6 |
+
}
|
7 |
+
|
8 |
+
.content-grid { max-width: 1280px; margin: 0 auto; padding: 0 var(--content-padding-x); margin-top: 40px; display: grid; grid-template-columns: 220px minmax(0, 680px) 260px; gap: 32px; align-items: start; }
|
9 |
.content-grid > main { max-width: 100%; margin: 0; padding: 0; }
|
10 |
|
11 |
/* TOC (left column) */
|
|
|
73 |
|
74 |
|
75 |
/* ============================================================================ */
|
76 |
+
/* Width helpers β slightly wider than main column, and full-width to viewport */
|
77 |
/* ---------------------------------------------------------------------------- */
|
78 |
/* Usage in MDX: */
|
79 |
/* <div className="wide"> ... </div> */
|
80 |
+
/* <div className="full-width"> ... </div> */
|
81 |
/* These center the content relative to the viewport while keeping it responsive. */
|
82 |
/* */
|
83 |
/* Notes: */
|
|
|
85 |
/* to be wider or fully span the viewport. On small screens, they fall back to 100%. */
|
86 |
/* - Adjust the target width in .wide if desired. */
|
87 |
.wide,
|
88 |
+
.full-width { box-sizing: border-box; position: relative; z-index: var(--z-elevated); }
|
89 |
|
90 |
.wide {
|
91 |
/* Target up to ~1100px while staying within viewport minus page gutters */
|
|
|
94 |
transform: translateX(-50%);
|
95 |
}
|
96 |
|
97 |
+
.full-width {
|
98 |
+
/* Span the full viewport width and center relative to viewport */
|
99 |
width: 100vw;
|
100 |
+
margin-left: calc(50% - 50vw + var(--content-padding-x) * 2);
|
101 |
+
margin-right: calc(50% - 50vw);
|
102 |
}
|
103 |
|
104 |
@media (max-width: 1100px) {
|
105 |
.wide,
|
106 |
+
.full-width {
|
107 |
width: 100%;
|
108 |
margin-left: 0;
|
109 |
+
margin-right: 0;
|
110 |
transform: none;
|
111 |
}
|
112 |
}
|
app/src/styles/_variables.css
CHANGED
@@ -33,6 +33,14 @@
|
|
33 |
--spacing-8: 56px;
|
34 |
--spacing-9: 64px;
|
35 |
--spacing-10: 72px;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
36 |
}
|
37 |
/* Theme tokens for dark mode */
|
38 |
[data-theme="dark"] {
|
|
|
33 |
--spacing-8: 56px;
|
34 |
--spacing-9: 64px;
|
35 |
--spacing-10: 72px;
|
36 |
+
|
37 |
+
/* Z-index scale */
|
38 |
+
--z-base: 0; /* background/base */
|
39 |
+
--z-content: 1; /* regular content */
|
40 |
+
--z-elevated: 10; /* wide/full-width blocks, images */
|
41 |
+
--z-overlay: 1000; /* overlays/lightboxes */
|
42 |
+
--z-modal: 1100; /* modals/dialogs */
|
43 |
+
--z-tooltip: 1200; /* tooltips/popovers */
|
44 |
}
|
45 |
/* Theme tokens for dark mode */
|
46 |
[data-theme="dark"] {
|
app/src/styles/components/_code.css
CHANGED
@@ -13,6 +13,7 @@ code {
|
|
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; }
|
@@ -81,3 +82,45 @@ html[data-theme='light'] .astro-code {
|
|
81 |
|
82 |
/* Overrides inside Accordion: remove padding and border on code containers */
|
83 |
.accordion .astro-code { padding: 0; border: none; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
+
.astro-code { position: relative; }
|
17 |
|
18 |
/* Prevent code blocks from breaking layout on small screens */
|
19 |
.astro-code { overflow-x: auto; width: 100%; max-width: 100%; box-sizing: border-box; -webkit-overflow-scrolling: touch; }
|
|
|
82 |
|
83 |
/* Overrides inside Accordion: remove padding and border on code containers */
|
84 |
.accordion .astro-code { padding: 0; border: none; }
|
85 |
+
|
86 |
+
/* Language/extension vignette for Shiki blocks (bottom-right, discreet) */
|
87 |
+
.astro-code::after {
|
88 |
+
content: attr(data-language);
|
89 |
+
position: absolute;
|
90 |
+
right: 0;
|
91 |
+
bottom: 0;
|
92 |
+
font-size: 10px;
|
93 |
+
line-height: 1;
|
94 |
+
text-transform: uppercase;
|
95 |
+
color: var(--muted-color);
|
96 |
+
background: var(--surface-bg);
|
97 |
+
border-top: 1px solid var(--border-color);
|
98 |
+
border-left: 1px solid var(--border-color);
|
99 |
+
opacity: 0.5;
|
100 |
+
border-radius: 8px 0 0 0; /* round only top-left */
|
101 |
+
padding: 4px 6px;
|
102 |
+
pointer-events: none;
|
103 |
+
}
|
104 |
+
|
105 |
+
/* Fallback if Shiki uses data-lang instead of data-language */
|
106 |
+
.astro-code[data-lang]::after { content: attr(data-lang); }
|
107 |
+
|
108 |
+
/* Normalize to extensions for common languages */
|
109 |
+
.astro-code[data-language="typescript"]::after { content: "ts"; }
|
110 |
+
.astro-code[data-language="tsx"]::after { content: "tsx"; }
|
111 |
+
.astro-code[data-language="javascript"]::after,
|
112 |
+
.astro-code[data-language="node"]::after,
|
113 |
+
.astro-code[data-language="jsx"]::after { content: "js"; }
|
114 |
+
.astro-code[data-language="python"]::after { content: "py"; }
|
115 |
+
.astro-code[data-language="bash"]::after,
|
116 |
+
.astro-code[data-language="shell"]::after,
|
117 |
+
.astro-code[data-language="sh"]::after { content: "sh"; }
|
118 |
+
.astro-code[data-language="markdown"]::after { content: "md"; }
|
119 |
+
.astro-code[data-language="yaml"]::after,
|
120 |
+
.astro-code[data-language="yml"]::after { content: "yml"; }
|
121 |
+
.astro-code[data-language="html"]::after { content: "html"; }
|
122 |
+
.astro-code[data-language="css"]::after { content: "css"; }
|
123 |
+
.astro-code[data-language="json"]::after { content: "json"; }
|
124 |
+
|
125 |
+
/* In Accordions, keep same bottom-right placement */
|
126 |
+
.accordion .astro-code::after { right: 0; bottom: 0; }
|
app/src/styles/global.css
CHANGED
@@ -20,6 +20,30 @@
|
|
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 |
/* ============================================================================ */
|
|
|
20 |
img[data-zoomable] { cursor: zoom-in; }
|
21 |
.medium-zoom--opened img[data-zoomable] { cursor: zoom-out; }
|
22 |
|
23 |
+
/* Download button for img[data-downloadable] */
|
24 |
+
figure.has-dl-btn { position: relative; }
|
25 |
+
.dl-host { position: relative; }
|
26 |
+
.img-dl-wrap { position: relative; display: inline-block; }
|
27 |
+
.img-dl-btn {
|
28 |
+
position: absolute;
|
29 |
+
right: 8px;
|
30 |
+
bottom: 8px;
|
31 |
+
align-items: center;
|
32 |
+
justify-content: center;
|
33 |
+
width: 30px;
|
34 |
+
height: 30px;
|
35 |
+
border-radius: 6px;
|
36 |
+
color: white;
|
37 |
+
text-decoration: none;
|
38 |
+
border: 1px solid rgba(255,255,255,0.25);
|
39 |
+
z-index: var(--z-elevated);
|
40 |
+
display: none;
|
41 |
+
}
|
42 |
+
.img-dl-btn svg { width: 18px; height: 18px; fill: currentColor; }
|
43 |
+
.img-dl-wrap:hover .img-dl-btn { display: inline-flex; }
|
44 |
+
[data-theme="dark"] .img-dl-btn { background: rgba(255,255,255,0.15); color: white; border-color: rgba(255,255,255,0.25); }
|
45 |
+
[data-theme="dark"] .img-dl-btn:hover { background: rgba(255,255,255,0.25); }
|
46 |
+
|
47 |
/* ============================================================================ */
|
48 |
/* Theme Toggle button (moved from component) */
|
49 |
/* ============================================================================ */
|