thibaud frere commited on
Commit
b8e1b6c
Β·
1 Parent(s): a153fdf

Move assets into content/assets; update imports; clean .gitattributes; fix LFS tracking

Browse files
Files changed (40) hide show
  1. .gitattributes +0 -2
  2. app/.astro/astro/content.d.ts +204 -0
  3. app/astro.config.mjs +24 -4
  4. app/src/components/{FullBleed.astro β†’ FullWidth.astro} +1 -1
  5. app/src/components/Hero.astro +1 -1
  6. app/src/components/HtmlEmbed.astro +6 -6
  7. app/src/components/{Aside.astro β†’ Sidenote.astro} +2 -0
  8. app/src/components/ThemeToggle.astro +2 -2
  9. app/src/content/article.mdx +11 -11
  10. app/src/{assets β†’ content/assets}/audio/audio-example.wav +0 -0
  11. app/src/{assets β†’ content/assets}/icones/moon.svg +0 -0
  12. app/src/{assets β†’ content/assets}/icones/sun.svg +0 -0
  13. app/src/{assets β†’ content/assets}/images/placeholder.png +0 -0
  14. app/src/{assets β†’ content/assets}/images/visual-vocabulary-poster.png +0 -0
  15. app/src/content/chapters/available-blocks.mdx +12 -7
  16. app/src/content/chapters/best-pratices.mdx +3 -3
  17. app/src/content/chapters/getting-started.mdx +3 -3
  18. app/src/content/chapters/writing-your-content.mdx +25 -25
  19. app/src/content/{fragments β†’ embeds}/banner.html +0 -0
  20. app/src/content/{fragments β†’ embeds}/bar.html +0 -0
  21. app/src/content/{fragments β†’ embeds}/color-picker.html +0 -0
  22. app/src/content/{fragments β†’ embeds}/d3-bar.html +0 -0
  23. app/src/content/{fragments β†’ embeds}/d3-line.html +0 -0
  24. app/src/content/{fragments β†’ embeds}/heatmap.html +0 -0
  25. app/src/content/{fragments β†’ embeds}/line.html +0 -0
  26. {fragments β†’ app/src/content/embeds/original_embeds}/d3js/banner.html +0 -0
  27. {fragments β†’ app/src/content/embeds/original_embeds}/d3js/line.html +0 -0
  28. {fragments β†’ app/src/content/embeds/original_embeds}/plotly/banner.py +0 -0
  29. {fragments β†’ app/src/content/embeds/original_embeds}/plotly/bar.py +0 -0
  30. {fragments β†’ app/src/content/embeds/original_embeds}/plotly/heatmap.py +0 -0
  31. {fragments β†’ app/src/content/embeds/original_embeds}/plotly/line.py +0 -0
  32. {fragments β†’ app/src/content/embeds/original_embeds}/plotly/poetry.lock +0 -0
  33. {fragments β†’ app/src/content/embeds/original_embeds}/plotly/pyproject.toml +0 -0
  34. app/src/content/{fragments β†’ embeds}/palettes.html +0 -0
  35. app/src/pages/index.astro +100 -1
  36. app/src/styles/_base.css +15 -2
  37. app/src/styles/_layout.css +14 -9
  38. app/src/styles/_variables.css +8 -0
  39. app/src/styles/components/_code.css +43 -0
  40. 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 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');
@@ -81,14 +99,16 @@ function rehypeCodeCopyAndLabel() {
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',
 
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-bleed", className].filter(Boolean).join(" ");
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-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; }
 
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 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"
10
  const needle = requested.replace(/^\/*/, '');
11
- for (const [key, html] of Object.entries(fragments)) {
12
- if (key.endsWith('/' + needle) || key.endsWith('/' + needle.replace(/^fragments\//, ''))) {
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 fragments
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 FullBleed from "../components/FullBleed.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 Aside from "../components/Aside.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
- <Aside>
33
- Welcome to this single-page research article template built with **Markdown**.
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, asides, interactive fragments),
41
  customize styles and behavior, and follow a few **best practices** for publishing.
42
 
43
- </Aside>
44
 
45
 
46
  #### Features
47
 
48
- <Aside>
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
- </Aside>
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 '../../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';
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 '../assets/images/placeholder.jpg'
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 '../assets/audio/audio-example.wav'
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 '../../assets/images/visual-vocabulary-poster.png';
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
- <Aside>
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
- </Aside> */}
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 Aside from '../../components/Aside.astro';
2
 
3
  ## Getting Started
4
 
5
  ### Installation
6
 
7
- <Aside>
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
- </Aside>
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 '../../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 HtmlEmbed from '../../components/HtmlEmbed.astro';
8
- import audioDemo from '../../assets/audio/audio-example.wav';
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 '../assets/images/placeholder.jpg';
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 '../assets/images/placeholder.png'
61
- 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
 
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
- <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
 
113
  Here is an example of an <a href="#">interactive element</a>.
114
  </Fragment>
115
- </Aside>
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: **Asides** for contextual side notes, **Wide** to extend beyond the main column, and **Full-bleed** for full-width, immersive sections.
128
 
129
- #### Asides
130
 
131
- <Aside>
132
  This paragraph presents a **key idea** concisely.
133
  <Fragment slot="aside">
134
  **Side note** for brief context or a definition.
135
  </Fragment>
136
- </Aside>
137
 
138
  <small className="muted">Example</small>
139
  ```mdx
140
- import Aside from '../components/Aside.astro'
141
 
142
- <Aside>
143
  Main paragraph with the core idea.
144
  <Fragment slot="aside">Short side note.</Fragment>
145
- </Aside>
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-bleed example
166
 
167
- <FullBleed>
168
- <div className="demo-full-bleed">demo full-bleed</div>
169
- </FullBleed>
170
 
171
  <small className="muted">Example</small>
172
  ```mdx
173
- import FullBleed from '../components/FullBleed.astro'
174
 
175
- <FullBleed>
176
  Your content here...
177
- </FullBleed>
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-bleed {
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-bleed {
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
- .content-grid { max-width: 1280px; margin: 0 auto; padding: 0 16px; margin-top: 40px; display: grid; grid-template-columns: 220px minmax(0, 680px) 260px; gap: 32px; align-items: start; }
 
 
 
 
5
  .content-grid > main { max-width: 100%; margin: 0; padding: 0; }
6
 
7
  /* TOC (left column) */
@@ -69,11 +73,11 @@ main > nav:first-of-type { display: none; }
69
 
70
 
71
  /* ============================================================================ */
72
- /* Width helpers – slightly wider than main column, and full-bleed to viewport */
73
  /* ---------------------------------------------------------------------------- */
74
  /* Usage in MDX: */
75
  /* <div className="wide"> ... </div> */
76
- /* <div className="full-bleed"> ... </div> */
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-bleed { box-sizing: border-box; }
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-bleed {
94
- /* Span the full viewport width */
95
  width: 100vw;
96
- margin-left: 50%;
97
- transform: translateX(-50%);
98
  }
99
 
100
  @media (max-width: 1100px) {
101
  .wide,
102
- .full-bleed {
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
  /* ============================================================================ */