thibaud frere commited on
Commit
06ee7eb
·
1 Parent(s): 1565c58

add grettings | hf-user component | update responsiveImage

Browse files
app/.astro/astro/content.d.ts CHANGED
@@ -180,6 +180,13 @@ declare module 'astro:content' {
180
  collection: "chapters";
181
  data: any
182
  } & { render(): Render[".mdx"] };
 
 
 
 
 
 
 
183
  "introduction.mdx": {
184
  id: "introduction.mdx";
185
  slug: "introduction";
 
180
  collection: "chapters";
181
  data: any
182
  } & { render(): Render[".mdx"] };
183
+ "greetings.mdx": {
184
+ id: "greetings.mdx";
185
+ slug: "greetings";
186
+ body: string;
187
+ collection: "chapters";
188
+ data: any
189
+ } & { render(): Render[".mdx"] };
190
  "introduction.mdx": {
191
  id: "introduction.mdx";
192
  slug: "introduction";
app/src/components/HfUser.astro ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ interface Props {
3
+ username: string;
4
+ name?: string;
5
+ url?: string;
6
+ avatarUrl?: string;
7
+ }
8
+
9
+ const { username, name, url, avatarUrl } = Astro.props as Props;
10
+ const profileUrl = url ?? `https://huggingface.co/${encodeURIComponent(username)}`;
11
+ const displayName = name ?? username;
12
+ const imgSrc = avatarUrl ?? `https://huggingface.co/api/users/${encodeURIComponent(username)}/avatar`;
13
+ ---
14
+ <div class="hf-user">
15
+ <div class="hf-user__left">
16
+ <img
17
+ class="hf-user__avatar"
18
+ src={imgSrc}
19
+ alt={`${displayName} avatar`}
20
+ width="44"
21
+ height="44"
22
+ loading="lazy"
23
+ decoding="async"
24
+ referrerpolicy="no-referrer"
25
+ />
26
+ <span class="hf-user__text">
27
+ <a class="hf-user__name" href={profileUrl} target="_blank" rel="noopener noreferrer">{displayName}</a>
28
+ <span class="hf-user__row">
29
+ <a class="hf-user__username" href={profileUrl} target="_blank" rel="noopener noreferrer">@{username}</a>
30
+ </span>
31
+ </span>
32
+ </div>
33
+ </div>
34
+
35
+ <style>
36
+ .hf-user {
37
+ display: inline-flex;
38
+ align-items: center;
39
+ gap: 10px;
40
+ padding: 10px 10px 10px 12px;
41
+ border-radius: 12px;
42
+ background: linear-gradient(180deg, rgba(255, 255, 255, 0.04), rgba(255, 255, 255, 0.02));
43
+ border: 1px solid rgba(255, 255, 255, 0.08);
44
+ box-shadow: none;
45
+ }
46
+ .hf-user__left {
47
+ display: flex;
48
+ align-items: center;
49
+ gap: 10px;
50
+ text-decoration: none;
51
+ color: inherit;
52
+ }
53
+ .hf-user__avatar {
54
+ display: block;
55
+ width: 44px;
56
+ height: 44px;
57
+ border-radius: 50%;
58
+ object-fit: cover;
59
+ box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.12) inset;
60
+ }
61
+ .hf-user__text {
62
+ display: flex;
63
+ flex-direction: column;
64
+ line-height: 1.1;
65
+ }
66
+ .hf-user__row {
67
+ display: inline-flex;
68
+ align-items: center;
69
+ white-space: nowrap;
70
+ }
71
+ .hf-user__name {
72
+ font-size: 14px;
73
+ font-weight: 700;
74
+ }
75
+ .hf-user__username {
76
+ font-size: 12px;
77
+ color: var(--muted-color);
78
+ text-decoration: underline!important;
79
+ text-underline-offset: 2px;
80
+ text-decoration-thickness: 0.06em;
81
+ text-decoration-color: var(--link-underline);
82
+ }
83
+
84
+ .hf-user a {
85
+ color: inherit;
86
+ text-decoration: none;
87
+ border-bottom: none;
88
+ }
89
+ </style>
90
+
91
+ <style is:global>
92
+ .hf-user-list {
93
+ display: flex;
94
+ flex-wrap: wrap;
95
+ gap: 12px;
96
+ }
97
+ </style>
98
+
app/src/components/ResponsiveImage.astro CHANGED
@@ -19,29 +19,50 @@ interface Props {
19
  downloadName?: string;
20
  /** Optional explicit source URL to download instead of currentSrc */
21
  downloadSrc?: string;
 
 
 
 
 
 
22
  /** Any additional attributes should be forwarded to the underlying <Image> */
23
  [key: string]: any;
24
  }
25
 
26
- const { caption, figureClass, zoomable, downloadable, downloadName, downloadSrc, ...imgProps } = Astro.props as Props;
27
  const hasCaptionSlot = Astro.slots.has('caption');
28
  const hasCaption = hasCaptionSlot || (typeof caption === 'string' && caption.length > 0);
29
  const uid = `ri_${Math.random().toString(36).slice(2)}`;
30
  const dataZoomable = (zoomable === true || (imgProps as any)['data-zoomable']) ? '1' : undefined;
31
  const dataDownloadable = (downloadable === true || (imgProps as any)['data-downloadable']) ? '1' : undefined;
 
 
 
32
  ---
33
  <div class="ri-root" data-ri-root={uid}>
34
  {hasCaption ? (
35
  <figure class={(figureClass || '') + (dataDownloadable ? ' has-dl-btn' : '')}>
36
  {dataDownloadable ? (
37
  <span class="img-dl-wrap">
38
- <Image {...imgProps} data-zoomable={dataZoomable} data-downloadable={dataDownloadable} data-download-name={downloadName} data-download-src={downloadSrc} />
 
 
 
 
 
 
39
  <button type="button" class="button button--ghost img-dl-btn" aria-label="Download image" title={downloadName ? `Download ${downloadName}` : 'Download image'}>
40
  <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>
41
  </button>
42
  </span>
43
  ) : (
44
- <Image {...imgProps} data-zoomable={dataZoomable} />
 
 
 
 
 
 
45
  )}
46
  <figcaption>
47
  {hasCaptionSlot ? (
@@ -54,13 +75,25 @@ const dataDownloadable = (downloadable === true || (imgProps as any)['data-downl
54
  ) : (
55
  dataDownloadable ? (
56
  <span class="img-dl-wrap">
57
- <Image {...imgProps} data-zoomable={dataZoomable} data-downloadable={dataDownloadable} data-download-name={downloadName} data-download-src={downloadSrc} />
 
 
 
 
 
 
58
  <button type="button" class="button button--ghost img-dl-btn" aria-label="Download image" title={downloadName ? `Download ${downloadName}` : 'Download image'}>
59
  <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>
60
  </button>
61
  </span>
62
  ) : (
63
- <Image {...imgProps} data-zoomable={dataZoomable} />
 
 
 
 
 
 
64
  )
65
  )}
66
  </div>
 
19
  downloadName?: string;
20
  /** Optional explicit source URL to download instead of currentSrc */
21
  downloadSrc?: string;
22
+ /** Optional link that wraps the image (not the caption) */
23
+ linkHref?: string;
24
+ /** Optional target for the link (default: _blank when linkHref provided) */
25
+ linkTarget?: string;
26
+ /** Optional rel for the link (default: noopener noreferrer when linkHref provided) */
27
+ linkRel?: string;
28
  /** Any additional attributes should be forwarded to the underlying <Image> */
29
  [key: string]: any;
30
  }
31
 
32
+ const { caption, figureClass, zoomable, downloadable, downloadName, downloadSrc, linkHref, linkTarget, linkRel, ...imgProps } = Astro.props as Props;
33
  const hasCaptionSlot = Astro.slots.has('caption');
34
  const hasCaption = hasCaptionSlot || (typeof caption === 'string' && caption.length > 0);
35
  const uid = `ri_${Math.random().toString(36).slice(2)}`;
36
  const dataZoomable = (zoomable === true || (imgProps as any)['data-zoomable']) ? '1' : undefined;
37
  const dataDownloadable = (downloadable === true || (imgProps as any)['data-downloadable']) ? '1' : undefined;
38
+ const hasLink = typeof linkHref === 'string' && linkHref.length > 0;
39
+ const resolvedTarget = hasLink ? (linkTarget || '_blank') : undefined;
40
+ const resolvedRel = hasLink ? (linkRel || 'noopener noreferrer') : undefined;
41
  ---
42
  <div class="ri-root" data-ri-root={uid}>
43
  {hasCaption ? (
44
  <figure class={(figureClass || '') + (dataDownloadable ? ' has-dl-btn' : '')}>
45
  {dataDownloadable ? (
46
  <span class="img-dl-wrap">
47
+ {hasLink ? (
48
+ <a class="ri-link" href={linkHref} target={resolvedTarget} rel={resolvedRel}>
49
+ <Image {...imgProps} data-zoomable={dataZoomable} data-downloadable={dataDownloadable} data-download-name={downloadName} data-download-src={downloadSrc} />
50
+ </a>
51
+ ) : (
52
+ <Image {...imgProps} data-zoomable={dataZoomable} data-downloadable={dataDownloadable} data-download-name={downloadName} data-download-src={downloadSrc} />
53
+ )}
54
  <button type="button" class="button button--ghost img-dl-btn" aria-label="Download image" title={downloadName ? `Download ${downloadName}` : 'Download image'}>
55
  <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>
56
  </button>
57
  </span>
58
  ) : (
59
+ hasLink ? (
60
+ <a class="ri-link" href={linkHref} target={resolvedTarget} rel={resolvedRel}>
61
+ <Image {...imgProps} data-zoomable={dataZoomable} />
62
+ </a>
63
+ ) : (
64
+ <Image {...imgProps} data-zoomable={dataZoomable} />
65
+ )
66
  )}
67
  <figcaption>
68
  {hasCaptionSlot ? (
 
75
  ) : (
76
  dataDownloadable ? (
77
  <span class="img-dl-wrap">
78
+ {hasLink ? (
79
+ <a class="ri-link" href={linkHref} target={resolvedTarget} rel={resolvedRel}>
80
+ <Image {...imgProps} data-zoomable={dataZoomable} data-downloadable={dataDownloadable} data-download-name={downloadName} data-download-src={downloadSrc} />
81
+ </a>
82
+ ) : (
83
+ <Image {...imgProps} data-zoomable={dataZoomable} data-downloadable={dataDownloadable} data-download-name={downloadName} data-download-src={downloadSrc} />
84
+ )}
85
  <button type="button" class="button button--ghost img-dl-btn" aria-label="Download image" title={downloadName ? `Download ${downloadName}` : 'Download image'}>
86
  <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>
87
  </button>
88
  </span>
89
  ) : (
90
+ hasLink ? (
91
+ <a class="ri-link" href={linkHref} target={resolvedTarget} rel={resolvedRel}>
92
+ <Image {...imgProps} data-zoomable={dataZoomable} />
93
+ </a>
94
+ ) : (
95
+ <Image {...imgProps} data-zoomable={dataZoomable} />
96
+ )
97
  )
98
  )}
99
  </div>
app/src/content/article.mdx CHANGED
@@ -28,6 +28,7 @@ import AvailableBlocks from "./chapters/markdown.mdx";
28
  import GettingStarted from "./chapters/getting-started.mdx";
29
  import Markdown from "./chapters/markdown.mdx";
30
  import Components from "./chapters/components.mdx";
 
31
 
32
  <Introduction />
33
 
@@ -41,3 +42,6 @@ import Components from "./chapters/components.mdx";
41
 
42
  <BestPractices />
43
 
 
 
 
 
28
  import GettingStarted from "./chapters/getting-started.mdx";
29
  import Markdown from "./chapters/markdown.mdx";
30
  import Components from "./chapters/components.mdx";
31
+ import Greetings from "./chapters/greetings.mdx";
32
 
33
  <Introduction />
34
 
 
42
 
43
  <BestPractices />
44
 
45
+ <Greetings />
46
+
47
+
app/src/content/chapters/best-pratices.mdx CHANGED
@@ -56,10 +56,9 @@ A palette encodes **meaning** (categories, magnitudes, oppositions), preserves *
56
  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**.
57
 
58
 
59
- <a href={"https://raw.githubusercontent.com/Financial-Times/chart-doctor/refs/heads/main/visual-vocabulary/poster.png"} target="_blank" rel="noopener noreferrer">
60
  <ResponsiveImage
61
  src={visualPoster}
62
  alt="Visual Vocabulary: choosing the right chart by task"
63
- caption={'A handy reference to select chart types by purpose — click to enlarge.'}
 
64
  />
65
- </a>
 
56
  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**.
57
 
58
 
 
59
  <ResponsiveImage
60
  src={visualPoster}
61
  alt="Visual Vocabulary: choosing the right chart by task"
62
+ linkHref="https://raw.githubusercontent.com/Financial-Times/chart-doctor/refs/heads/main/visual-vocabulary/poster.png"
63
+ caption={'Credits <a href="https://www.ft.com/" target="_blank" rel="noopener noreferrer">Financial-Times/chart-doctor</a> <br/>A handy reference to select chart types by purpose — click to enlarge.'}
64
  />
 
app/src/content/chapters/greetings.mdx ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ## Greetings
2
+
3
+ ### Inspiration
4
+
5
+ This project stands in the direct continuity of [Distill](https://distill.pub/) (2016–2021). Our goal is to carry that spirit forward and push it even further: accessible scientific writing, high‑quality interactive explanations, and reproducible, production‑ready demos.
6
+
7
+ <div className="tag-list">
8
+ <span className="tag">Astro.js</span>
9
+ <span className="tag">MDX</span>
10
+ <span className="tag">D3.js</span>
11
+ <span className="tag">Plotly.js</span>
12
+ </div>
13
+
14
+ ### Feedback
15
+
16
+ Huge thanks to the following people for their precious feedbacks!
17
+
18
+ import HfUser from '../../components/HfUser.astro';
19
+
20
+ <div className="hf-user-list">
21
+ <HfUser username="lvwerra" name="Leandro von Werra" />
22
+ <HfUser username="clefourrier" name="Clémentine Fourrier" />
23
+ <HfUser username="hynky" name="Hynek Kydlíček" />
24
+ <HfUser username="lusxvr" name="Luis Wiedmann" />
25
+ <HfUser username="molbap" name="Pablo Montalvo-Leroux" />
26
+ <HfUser username="lewtun" name="Lewis Tunstall" />
27
+ <HfUser username="guipenedo" name="Guilherme Penedo" />
28
+ </div>
app/src/pages/index.astro CHANGED
@@ -208,71 +208,6 @@ const licence = (articleFM as any)?.licence ?? (articleFM as any)?.license ?? (a
208
  });
209
  </script>
210
 
211
- <script is:inline>
212
- // Minimal HF viewer badge: detect if signed in and Pro status (client-side only)
213
- (async () => {
214
- const showBadge = (text) => {
215
- try {
216
- const el = document.createElement('div');
217
- el.id = 'hf-viewer-status';
218
- el.style.position = 'fixed';
219
- el.style.top = '0.75rem';
220
- el.style.right = '0.75rem';
221
- el.style.zIndex = '9999';
222
- el.style.fontFamily = 'system-ui, -apple-system, Segoe UI, Roboto, sans-serif';
223
- el.style.fontSize = '12px';
224
- el.style.lineHeight = '1.2';
225
- el.style.background = 'rgba(0, 0, 0, 0.6)';
226
- el.style.color = '#fff';
227
- el.style.padding = '6px 8px';
228
- el.style.borderRadius = '6px';
229
- el.textContent = text;
230
- document.body.appendChild(el);
231
- } catch {}
232
- };
233
- try {
234
- const isHFHost = (() => {
235
- try {
236
- const h = location.hostname;
237
- return (
238
- h === 'huggingface.co' ||
239
- h.endsWith('.huggingface.co') ||
240
- h === 'hf.space' ||
241
- h.endsWith('.hf.space')
242
- );
243
- } catch { return false; }
244
- })();
245
-
246
- if (!isHFHost) {
247
- showBadge('HF: status only on huggingface.co/Spaces');
248
- return;
249
- }
250
-
251
- const res = await fetch('https://huggingface.co/api/whoami-v2', { credentials: 'include' });
252
- if (!res.ok) {
253
- showBadge('HF: not logged in');
254
- return;
255
- }
256
- const data = await res.json();
257
- const username = data?.name || data?.username || data?.user || '';
258
- let isPro = (data && (data.is_pro ?? data.isPro));
259
- if ((isPro === undefined || isPro === null) && username) {
260
- try {
261
- const r2 = await fetch(`https://huggingface.co/api/users/${encodeURIComponent(username)}`, { credentials: 'include' });
262
- if (r2.ok) {
263
- const u = await r2.json();
264
- isPro = u?.is_pro ?? u?.isPro ?? false;
265
- }
266
- } catch {}
267
- }
268
- showBadge(`HF: ${username || 'signed in'}${isPro ? ' (Pro)' : ''}`);
269
- } catch {
270
- // On error (CORS, network, blocked 3rd-party cookies), still show a fallback badge
271
- showBadge('HF: status unavailable');
272
- }
273
- })();
274
- </script>
275
-
276
 
277
  </body>
278
  </html>
 
208
  });
209
  </script>
210
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
211
 
212
  </body>
213
  </html>