thibaud frere commited on
Commit
101af31
·
1 Parent(s): fee9c1e
app/.astro/astro/content.d.ts CHANGED
@@ -0,0 +1,164 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+
155
+ };
156
+
157
+ type DataEntryMap = {
158
+
159
+ };
160
+
161
+ type AnyEntryMap = ContentEntryMap & DataEntryMap;
162
+
163
+ export type ContentConfig = never;
164
+ }
app/package-lock.json CHANGED
Binary files a/app/package-lock.json and b/app/package-lock.json differ
 
app/package.json CHANGED
Binary files a/app/package.json and b/app/package.json differ
 
app/src/pages/article.mdx CHANGED
@@ -1,5 +1,5 @@
1
  ---
2
- title: "The science Template:\nCraft Beautiful Blogs"
3
  description: "A modern, MDX-first research article template with math, citations, and interactive figures."
4
  authors:
5
  - "Hynek Kydlíček"
@@ -29,12 +29,19 @@ In this guide, you’ll learn how to install the template, write content (math,
29
 
30
  ## Features
31
 
32
- - Math via KaTeX (remark-math/rehype-katex)
33
- - Citations and footnotes
34
- - Responsive images with `astro:assets`
35
- - Anchored headings and a generated table of contents
36
- - Dark/light theme toggle
37
- - HTML fragments for self-contained visualizations (e.g., Plotly)
 
 
 
 
 
 
 
38
 
39
  ## Getting Started
40
 
@@ -182,7 +189,7 @@ import audioDemo from '../assets/audio/audio-example.wav'
182
  Picking the right visualization depends on your goal (compare values, show distribution, part-to-whole, trends, relationships, etc.). The Visual Vocabulary poster below provides a concise mapping from analytical task to chart types.
183
 
184
  <figure>
185
- <Image src={visualPoster} alt="Visual Vocabulary: choosing the right chart by task" />
186
  <figcaption>
187
  Visual Vocabulary: a handy reference to select chart types by purpose (comparison, distribution, part-to-whole, correlation, and more).
188
  </figcaption>
 
1
  ---
2
+ title: "The science template:\nCraft Beautiful Blogs"
3
  description: "A modern, MDX-first research article template with math, citations, and interactive figures."
4
  authors:
5
  - "Hynek Kydlíček"
 
29
 
30
  ## Features
31
 
32
+ <div className="tag-list">
33
+ <span className="tag">Markdown based</span>
34
+ <span className="tag">KaTeX math</span>
35
+ <span className="tag">Citations & footnotes</span>
36
+ <span className="tag">Automatic build</span>
37
+ <span className="tag">Auto table of content</span>
38
+ <span className="tag">Dark theme</span>
39
+ <span className="tag">HTML fragments</span>
40
+ <span className="tag">Plotly ready</span>
41
+ <span className="tag">SEO Friendly</span>
42
+ <span className="tag">Lightweight bundle(\<500ko)</span>
43
+ <span className="tag">TO DO: Zoomable images</span>
44
+ </div>
45
 
46
  ## Getting Started
47
 
 
189
  Picking the right visualization depends on your goal (compare values, show distribution, part-to-whole, trends, relationships, etc.). The Visual Vocabulary poster below provides a concise mapping from analytical task to chart types.
190
 
191
  <figure>
192
+ <Image src={visualPoster} alt="Visual Vocabulary: choosing the right chart by task" data-zoomable />
193
  <figcaption>
194
  Visual Vocabulary: a handy reference to select chart types by purpose (comparison, distribution, part-to-whole, correlation, and more).
195
  </figcaption>
app/src/pages/index.astro CHANGED
@@ -6,6 +6,7 @@ import Footer from '../components/Footer.astro';
6
  import ThemeToggle from '../components/ThemeToggle.astro';
7
  import SeoHead from '../components/SeoHead.astro';
8
  import ogDefault from '../assets/images/visual-vocabulary-poster.png';
 
9
  import '../styles/global.scss';
10
  const docTitle = articleFM?.title ?? 'Untitled article';
11
  // Autoriser un retour à la ligne dans le titre via "\n" ou sauts de ligne YAML
@@ -28,7 +29,7 @@ const imageAbs = fmOg && fmOg.startsWith('http')
28
  <meta name="viewport" content="width=device-width, initial-scale=1" />
29
  <title>{docTitle}</title>
30
  <SeoHead title={docTitle} description={description} authors={authors} published={published} tags={tags} image={imageAbs} />
31
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css" crossorigin="anonymous" />
32
  <script src="https://cdn.plot.ly/plotly-3.0.0.min.js" charset="utf-8"></script>
33
  </head>
34
  <body>
@@ -71,47 +72,69 @@ const imageAbs = fmOg && fmOg.startsWith('http')
71
  <!-- Medium-like image zoom (lightbox) -->
72
  <script src="https://cdn.jsdelivr.net/npm/medium-zoom@1.1.0/dist/medium-zoom.min.js"></script>
73
  <script>
74
- // Init zoom on data-zoomable images, close on scroll/move like Medium
75
- const initMediumZoom = () => {
76
- if (!window.mediumZoom) return;
77
- const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
78
- const background = isDark ? 'rgba(0,0,0,.9)' : 'rgba(0,0,0,.85)';
79
- const zoom = window.mediumZoom('section.content-grid main img[data-zoomable]', {
80
- background,
81
- margin: 24,
82
- scrollOffset: 0
83
- });
84
-
85
- let onMove;
86
- const attachCloseOnMove = () => {
87
- if (onMove) return;
88
- onMove = () => { zoom.close(); };
89
- window.addEventListener('mousemove', onMove, { passive: true });
90
- window.addEventListener('wheel', onMove, { passive: true });
91
- window.addEventListener('touchmove', onMove, { passive: true });
92
- };
93
- const detachCloseOnMove = () => {
94
- if (!onMove) return;
95
- window.removeEventListener('mousemove', onMove);
96
- window.removeEventListener('wheel', onMove);
97
- window.removeEventListener('touchmove', onMove);
98
- onMove = null;
99
  };
100
 
101
- zoom.on('open', attachCloseOnMove);
102
- zoom.on('close', detachCloseOnMove);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
103
 
104
- // React to theme toggle
105
- const observer = new MutationObserver(() => {
106
- const dark = document.documentElement.getAttribute('data-theme') === 'dark';
107
- zoom.update({ background: dark ? 'rgba(0,0,0,.9)' : 'rgba(0,0,0,.85)' });
 
 
 
 
108
  });
109
- observer.observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] });
110
- };
111
 
112
- if (document.readyState === 'loading') {
113
- document.addEventListener('DOMContentLoaded', initMediumZoom, { once: true });
114
- } else { initMediumZoom(); }
115
  </script>
116
  <script>
117
  // Open external links in a new tab; keep internal anchors in-page
 
6
  import ThemeToggle from '../components/ThemeToggle.astro';
7
  import SeoHead from '../components/SeoHead.astro';
8
  import ogDefault from '../assets/images/visual-vocabulary-poster.png';
9
+ import 'katex/dist/katex.min.css';
10
  import '../styles/global.scss';
11
  const docTitle = articleFM?.title ?? 'Untitled article';
12
  // Autoriser un retour à la ligne dans le titre via "\n" ou sauts de ligne YAML
 
29
  <meta name="viewport" content="width=device-width, initial-scale=1" />
30
  <title>{docTitle}</title>
31
  <SeoHead title={docTitle} description={description} authors={authors} published={published} tags={tags} image={imageAbs} />
32
+
33
  <script src="https://cdn.plot.ly/plotly-3.0.0.min.js" charset="utf-8"></script>
34
  </head>
35
  <body>
 
72
  <!-- Medium-like image zoom (lightbox) -->
73
  <script src="https://cdn.jsdelivr.net/npm/medium-zoom@1.1.0/dist/medium-zoom.min.js"></script>
74
  <script>
75
+ // Init zoom sur img[data-zoomable] avec attente du script & contenu; close on scroll like Medium
76
+ (() => {
77
+ let zoomInstance = null;
78
+
79
+ const ensureMediumZoomReady = (cb) => {
80
+ if (window.mediumZoom) return cb();
81
+ const retry = () => (window.mediumZoom ? cb() : setTimeout(retry, 30));
82
+ retry();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83
  };
84
 
85
+ const collectTargets = () => Array.from(document.querySelectorAll('section.content-grid main img[data-zoomable]'));
86
+
87
+ const initOrUpdateZoom = () => {
88
+ const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
89
+ const background = isDark ? 'rgba(0,0,0,.9)' : 'rgba(0,0,0,.85)';
90
+ const targets = collectTargets();
91
+ if (!targets.length) return;
92
+
93
+ if (!zoomInstance) {
94
+ zoomInstance = window.mediumZoom(targets, { background, margin: 24, scrollOffset: 0 });
95
+
96
+ let onScrollLike;
97
+ const attachCloseOnScroll = () => {
98
+ if (onScrollLike) return;
99
+ onScrollLike = () => { zoomInstance && zoomInstance.close(); };
100
+ window.addEventListener('wheel', onScrollLike, { passive: true });
101
+ window.addEventListener('touchmove', onScrollLike, { passive: true });
102
+ window.addEventListener('scroll', onScrollLike, { passive: true });
103
+ };
104
+ const detachCloseOnScroll = () => {
105
+ if (!onScrollLike) return;
106
+ window.removeEventListener('wheel', onScrollLike);
107
+ window.removeEventListener('touchmove', onScrollLike);
108
+ window.removeEventListener('scroll', onScrollLike);
109
+ onScrollLike = null;
110
+ };
111
+ zoomInstance.on('open', attachCloseOnScroll);
112
+ zoomInstance.on('close', detachCloseOnScroll);
113
+
114
+ const themeObserver = new MutationObserver(() => {
115
+ const dark = document.documentElement.getAttribute('data-theme') === 'dark';
116
+ zoomInstance && zoomInstance.update({ background: dark ? 'rgba(0,0,0,.9)' : 'rgba(0,0,0,.85)' });
117
+ });
118
+ themeObserver.observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] });
119
+ } else {
120
+ zoomInstance.attach(targets);
121
+ zoomInstance.update({ background });
122
+ }
123
+ };
124
 
125
+ const bootstrap = () => ensureMediumZoomReady(() => {
126
+ initOrUpdateZoom();
127
+ setTimeout(initOrUpdateZoom, 0);
128
+ const main = document.querySelector('section.content-grid main');
129
+ if (main) {
130
+ const mo = new MutationObserver(() => initOrUpdateZoom());
131
+ mo.observe(main, { childList: true, subtree: true });
132
+ }
133
  });
 
 
134
 
135
+ if (document.readyState === 'complete') bootstrap();
136
+ else window.addEventListener('load', bootstrap, { once: true });
137
+ })();
138
  </script>
139
  <script>
140
  // Open external links in a new tab; keep internal anchors in-page
app/src/styles/global.scss CHANGED
@@ -137,6 +137,26 @@ html { font-size: 14px; line-height: 1.6; }
137
  figure { margin: 16px 0; }
138
  figcaption { color: var(--muted-color); font-size: 12px; }
139
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
140
  // ============================================================================
141
  // Hero (full-bleed)
142
  // ============================================================================
 
137
  figure { margin: 16px 0; }
138
  figcaption { color: var(--muted-color); font-size: 12px; }
139
 
140
+ // Inline feature tags
141
+ .tag-list { display: flex; flex-wrap: wrap; gap: 8px; margin: 8px 0 16px; }
142
+ .tag {
143
+ display: inline-flex;
144
+ align-items: center;
145
+ gap: 6px;
146
+ padding: 4px 8px;
147
+ font-size: 12px;
148
+ line-height: 1;
149
+ border-radius: 999px;
150
+ background: var(--surface-bg);
151
+ border: 1px solid var(--border-color);
152
+ color: var(--text-color);
153
+ }
154
+ [data-theme="dark"] .tag { background: #1a1f27; border-color: rgba(255,255,255,.15); }
155
+
156
+ /* Opt-in zoomable images */
157
+ img[data-zoomable] { cursor: zoom-in; }
158
+ .medium-zoom--opened img[data-zoomable] { cursor: zoom-out; }
159
+
160
  // ============================================================================
161
  // Hero (full-bleed)
162
  // ============================================================================
package-lock.json DELETED
Binary file (16.3 kB)
 
package.json DELETED
Binary file (53 Bytes)