tfrere HF Staff commited on
Commit
ab0c87a
·
1 Parent(s): eff408b

add stack and glossary | remove multi image ( replaced by stack ) | update doc

Browse files
app/.astro/settings.json CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:5cd907e18678e90f342986642cb5fcdca79f1c4a61fe1d75caa2df036e69070e
3
  size 58
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:2698c64dc5f43414f1e5c9baf32bc19408ed3ef10b9d21165027ec49d593a35b
3
  size 58
app/package-lock.json CHANGED
Binary files a/app/package-lock.json and b/app/package-lock.json differ
 
app/src/components/Hero.astro CHANGED
@@ -91,58 +91,33 @@ const pdfBase = titleRaw ? stripHtml(titleRaw) : stripHtml(title);
91
  const pdfFilename = `${slugify(pdfBase)}.pdf`;
92
  ---
93
 
94
- <section class="w-full pt-12 pb-4 px-4 text-center">
95
- <h1
96
- class="text-3xl sm:text-4xl lg:text-5xl font-extrabold leading-tight m-0 mb-2 max-w-full mx-auto"
97
- set:html={title}
98
- />
99
- <div class="max-w-4xl mx-auto">
100
  <HtmlEmbed src="banner.html" frameless />
101
- {
102
- description && (
103
- <p class="text-gray-500 dark:text-gray-400 italic m-0 mb-4">
104
- {description}
105
- </p>
106
- )
107
- }
108
  </div>
109
  </section>
110
 
111
- <header
112
- class="border-t border-b border-gray-200 dark:border-gray-700 py-4 text-sm"
113
- aria-label="Article meta information"
114
- >
115
- <div
116
- class="max-w-4xl mx-auto flex flex-row justify-between px-4 gap-2 md:flex md:gap-2 max-md:flex-wrap"
117
- >
118
  {
119
  normalizedAuthors.length > 0 && (
120
- <div class="meta-cell flex flex-col gap-2 max-w-64 md:min-w-0 max-xs:text-center">
121
- <h3 class="m-0 text-xs font-normal text-gray-500 dark:text-gray-400 uppercase tracking-wide">
122
- Author{normalizedAuthors.length > 1 ? "s" : ""}
123
- </h3>
124
- <ul class="m-0 list-none p-0 flex flex-wrap">
125
  {normalizedAuthors.map((a, i) => {
126
  const supers =
127
  shouldShowAffiliationSupers &&
128
  Array.isArray(a.affiliationIndices) &&
129
  a.affiliationIndices.length ? (
130
- <sup>{a.affiliationIndices.join(",")}</sup>
131
  ) : null;
132
  return (
133
- <li class="whitespace-nowrap mr-1">
134
- {a.url ? (
135
- <a
136
- href={a.url}
137
- class="text-blue-600 dark:text-blue-400 underline underline-offset-2 decoration-1 decoration-blue-300 dark:decoration-blue-500 hover:decoration-blue-500 dark:hover:decoration-blue-400 transition-colors duration-150"
138
- >
139
- {a.name}
140
- </a>
141
- ) : (
142
- a.name
143
- )}
144
  {supers}
145
- {i < normalizedAuthors.length - 1 && ", "}
146
  </li>
147
  );
148
  })}
@@ -152,21 +127,14 @@ const pdfFilename = `${slugify(pdfBase)}.pdf`;
152
  }
153
  {
154
  Array.isArray(affiliations) && affiliations.length > 0 && (
155
- <div class="meta-cell flex flex-col gap-2 max-w-64 max-md:text-right md:min-w-0 max-xs:text-center">
156
- <h3 class="m-0 text-xs font-normal text-gray-500 dark:text-gray-400 uppercase tracking-wide">
157
- Affiliation{affiliations.length > 1 ? "s" : ""}
158
- </h3>
159
  {hasMultipleAffiliations ? (
160
- <ol class="m-0 pl-5 max-xs:list-inside max-xs:pl-0 max-xs:ml-0">
161
  {affiliations.map((af) => (
162
- <li value={af.id} class="m-0 max-xs:text-center">
163
  {af.url ? (
164
- <a
165
- href={af.url}
166
- target="_blank"
167
- rel="noopener noreferrer"
168
- class="text-blue-600 dark:text-blue-400 underline underline-offset-2 decoration-1 decoration-blue-300 dark:decoration-blue-500 hover:decoration-blue-500 dark:hover:decoration-blue-400 transition-colors duration-150"
169
- >
170
  {af.name}
171
  </a>
172
  ) : (
@@ -176,13 +144,12 @@ const pdfFilename = `${slugify(pdfBase)}.pdf`;
176
  ))}
177
  </ol>
178
  ) : (
179
- <p class="m-0">
180
  {affiliations[0]?.url ? (
181
  <a
182
  href={affiliations[0].url}
183
  target="_blank"
184
  rel="noopener noreferrer"
185
- class="text-blue-600 dark:text-blue-400 underline underline-offset-2 decoration-1 decoration-blue-300 dark:decoration-blue-500 hover:decoration-blue-500 dark:hover:decoration-blue-400 transition-colors duration-150"
186
  >
187
  {affiliations[0].name}
188
  </a>
@@ -196,21 +163,17 @@ const pdfFilename = `${slugify(pdfBase)}.pdf`;
196
  }
197
  {
198
  (!affiliations || affiliations.length === 0) && affiliation && (
199
- <div class="meta-cell flex flex-col gap-2 max-w-64 max-md:text-right md:min-w-0 max-xs:text-center">
200
- <h3 class="m-0 text-xs font-normal text-gray-500 dark:text-gray-400 uppercase tracking-wide">
201
- Affiliation
202
- </h3>
203
- <p class="m-0">{affiliation}</p>
204
  </div>
205
  )
206
  }
207
  {
208
  published && (
209
- <div class="meta-cell flex flex-col gap-2 max-w-64 md:min-w-0 max-xs:text-center">
210
- <h3 class="m-0 text-xs font-normal text-gray-500 dark:text-gray-400 uppercase tracking-wide">
211
- Published
212
- </h3>
213
- <p class="m-0">{published}</p>
214
  </div>
215
  )
216
  }
@@ -220,17 +183,11 @@ const pdfFilename = `${slugify(pdfBase)}.pdf`;
220
  <p><a href={`https://doi.org/${doi}`} target="_blank" rel="noopener noreferrer">{doi}</a></p>
221
  </div>
222
  )} -->
223
- <div
224
- class="meta-cell flex flex-col gap-2 max-w-64 max-md:text-right md:min-w-0 max-xs:text-center"
225
- >
226
- <h3
227
- class="m-0 text-xs font-normal text-gray-500 dark:text-gray-400 uppercase tracking-wide"
228
- >
229
- PDF
230
- </h3>
231
- <p class="m-0">
232
  <a
233
- class="inline-block px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors duration-150 no-underline"
234
  href={`/${pdfFilename}`}
235
  download={pdfFilename}
236
  aria-label={`Download PDF ${pdfFilename}`}
@@ -243,18 +200,112 @@ const pdfFilename = `${slugify(pdfBase)}.pdf`;
243
  </header>
244
 
245
  <style>
246
- /* Flex behavior for meta cells */
247
- .meta-cell {
248
- flex: 1 1 calc(50% - 8px);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
249
  }
250
 
251
- .meta-cell.max-xs\:flex-1 {
252
- flex: 1 1 100% !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
253
  }
254
 
255
  @media print {
256
- .max-md\\:text-right {
257
- @apply hidden;
258
  }
259
  }
260
  </style>
 
91
  const pdfFilename = `${slugify(pdfBase)}.pdf`;
92
  ---
93
 
94
+ <section class="hero">
95
+ <h1 class="hero-title" set:html={title} />
96
+ <div class="hero-banner">
 
 
 
97
  <HtmlEmbed src="banner.html" frameless />
98
+ {description && <p class="hero-desc">{description}</p>}
 
 
 
 
 
 
99
  </div>
100
  </section>
101
 
102
+ <header class="meta" aria-label="Article meta information">
103
+ <div class="meta-container">
 
 
 
 
 
104
  {
105
  normalizedAuthors.length > 0 && (
106
+ <div class="meta-container-cell">
107
+ <h3>Author{normalizedAuthors.length > 1 ? "s" : ""}</h3>
108
+ <ul class="authors">
 
 
109
  {normalizedAuthors.map((a, i) => {
110
  const supers =
111
  shouldShowAffiliationSupers &&
112
  Array.isArray(a.affiliationIndices) &&
113
  a.affiliationIndices.length ? (
114
+ <sup>{a.affiliationIndices.join(", ")}</sup>
115
  ) : null;
116
  return (
117
+ <li>
118
+ {a.url ? <a href={a.url}>{a.name}</a> : a.name}
 
 
 
 
 
 
 
 
 
119
  {supers}
120
+ {i < normalizedAuthors.length - 1 && <span set:html=",&nbsp;" />}
121
  </li>
122
  );
123
  })}
 
127
  }
128
  {
129
  Array.isArray(affiliations) && affiliations.length > 0 && (
130
+ <div class="meta-container-cell meta-container-cell--affiliations">
131
+ <h3>Affiliation{affiliations.length > 1 ? "s" : ""}</h3>
 
 
132
  {hasMultipleAffiliations ? (
133
+ <ol class="affiliations">
134
  {affiliations.map((af) => (
135
+ <li value={af.id}>
136
  {af.url ? (
137
+ <a href={af.url} target="_blank" rel="noopener noreferrer">
 
 
 
 
 
138
  {af.name}
139
  </a>
140
  ) : (
 
144
  ))}
145
  </ol>
146
  ) : (
147
+ <p>
148
  {affiliations[0]?.url ? (
149
  <a
150
  href={affiliations[0].url}
151
  target="_blank"
152
  rel="noopener noreferrer"
 
153
  >
154
  {affiliations[0].name}
155
  </a>
 
163
  }
164
  {
165
  (!affiliations || affiliations.length === 0) && affiliation && (
166
+ <div class="meta-container-cell meta-container-cell--affiliations">
167
+ <h3>Affiliation</h3>
168
+ <p>{affiliation}</p>
 
 
169
  </div>
170
  )
171
  }
172
  {
173
  published && (
174
+ <div class="meta-container-cell meta-container-cell--published">
175
+ <h3>Published</h3>
176
+ <p>{published}</p>
 
 
177
  </div>
178
  )
179
  }
 
183
  <p><a href={`https://doi.org/${doi}`} target="_blank" rel="noopener noreferrer">{doi}</a></p>
184
  </div>
185
  )} -->
186
+ <div class="meta-container-cell meta-container-cell--pdf">
187
+ <h3>PDF</h3>
188
+ <p>
 
 
 
 
 
 
189
  <a
190
+ class="button"
191
  href={`/${pdfFilename}`}
192
  download={pdfFilename}
193
  aria-label={`Download PDF ${pdfFilename}`}
 
200
  </header>
201
 
202
  <style>
203
+ /* Hero (full-width) */
204
+ .hero {
205
+ width: 100%;
206
+ padding: 48px 16px 16px;
207
+ text-align: center;
208
+ }
209
+ .hero-title {
210
+ font-size: clamp(28px, 4vw, 48px);
211
+ font-weight: 800;
212
+ line-height: 1.1;
213
+ margin: 0 0 8px;
214
+ max-width: 100%;
215
+ margin: auto;
216
+ }
217
+ .hero-banner {
218
+ max-width: 980px;
219
+ margin: 0 auto;
220
+ }
221
+ .hero-desc {
222
+ color: var(--muted-color);
223
+ font-style: italic;
224
+ margin: 0 0 16px 0;
225
  }
226
 
227
+ /* Meta (byline-like header) */
228
+ .meta {
229
+ border-top: 1px solid var(--border-color);
230
+ border-bottom: 1px solid var(--border-color);
231
+ padding: 1rem 0;
232
+ font-size: 0.9rem;
233
+ }
234
+ .meta-container {
235
+ max-width: 760px;
236
+ display: flex;
237
+ flex-direction: row;
238
+ justify-content: space-between;
239
+ margin: 0 auto;
240
+ padding: 0 var(--content-padding-x);
241
+ gap: 8px;
242
+ }
243
+ /* Subtle underline for links in meta; keep buttons without underline */
244
+ .meta-container a:not(.button) {
245
+ color: var(--primary-color);
246
+ text-decoration: underline;
247
+ text-underline-offset: 2px;
248
+ text-decoration-thickness: 0.06em;
249
+ text-decoration-color: var(--link-underline);
250
+ transition: text-decoration-color 0.15s ease-in-out;
251
+ }
252
+ .meta-container a:hover {
253
+ text-decoration-color: var(--link-underline-hover);
254
+ }
255
+ .meta-container a.button,
256
+ .meta-container .button {
257
+ text-decoration: none;
258
+ }
259
+ .meta-container-cell {
260
+ display: flex;
261
+ flex-direction: column;
262
+ gap: 8px;
263
+ max-width: 250px;
264
+ }
265
+ .meta-container-cell h3 {
266
+ margin: 0;
267
+ font-size: 12px;
268
+ font-weight: 400;
269
+ color: var(--muted-color);
270
+ text-transform: uppercase;
271
+ letter-spacing: 0.02em;
272
+ }
273
+ .meta-container-cell p {
274
+ margin: 0;
275
+ }
276
+ .authors {
277
+ margin: 0;
278
+ list-style-type: none;
279
+ padding-left: 0;
280
+ display: flex;
281
+ flex-wrap: wrap;
282
+ }
283
+ .authors li {
284
+ white-space: nowrap;
285
+ }
286
+ .affiliations {
287
+ margin: 0;
288
+ padding-left: 1.25em;
289
+ }
290
+ .affiliations li {
291
+ margin: 0;
292
+ }
293
+
294
+ header.meta .meta-container {
295
+ flex-wrap: wrap;
296
+ row-gap: 12px;
297
+ }
298
+
299
+ @media (max-width: 768px) {
300
+ .meta-container-cell--affiliations,
301
+ .meta-container-cell--pdf {
302
+ text-align: right;
303
+ }
304
  }
305
 
306
  @media print {
307
+ .meta-container-cell--pdf {
308
+ display: none !important;
309
  }
310
  }
311
  </style>
app/src/components/MultiImage.astro DELETED
@@ -1,342 +0,0 @@
1
- ---
2
- // @ts-ignore - types provided by Astro at runtime
3
- import Image from "./Image.astro";
4
-
5
- interface ImageItem {
6
- /** Source image imported via astro:assets */
7
- src: any;
8
- /** Alt text for accessibility */
9
- alt: string;
10
- /** Individual caption for this image */
11
- caption?: string;
12
- /** Optional individual image ID for referencing */
13
- id?: string;
14
- /** Enable zoom on this specific image (defaults to parent zoomable setting) */
15
- zoomable?: boolean;
16
- /** Enable download on this specific image (defaults to parent downloadable setting) */
17
- downloadable?: boolean;
18
- }
19
-
20
- interface Props {
21
- /** Array of images to display */
22
- images: ImageItem[];
23
- /** Global caption for the entire figure */
24
- caption?: string;
25
- /** Layout mode: number of columns or 'auto' for responsive */
26
- layout?: "2-column" | "3-column" | "4-column" | "auto";
27
- /** Enable medium-zoom behavior on all images (can be overridden per image) */
28
- zoomable?: boolean;
29
- /** Show download buttons on all images (can be overridden per image) */
30
- downloadable?: boolean;
31
- /** Optional class to apply on the wrapper */
32
- class?: string;
33
- /** Optional global ID for the multi-image figure */
34
- id?: string;
35
- }
36
-
37
- const {
38
- images,
39
- caption,
40
- layout = "3-column",
41
- zoomable = false,
42
- downloadable = false,
43
- class: className,
44
- id,
45
- } = Astro.props as Props;
46
-
47
- const hasCaptionSlot = Astro.slots.has("caption");
48
- const hasCaption =
49
- hasCaptionSlot || (typeof caption === "string" && caption.length > 0);
50
- const uid = `mi_${Math.random().toString(36).slice(2)}`;
51
-
52
- // Generate CSS grid columns based on layout
53
- const getGridColumns = () => {
54
- switch (layout) {
55
- case "2-column":
56
- return "repeat(2, 1fr)";
57
- case "3-column":
58
- return "repeat(3, 1fr)";
59
- case "4-column":
60
- return "repeat(4, 1fr)";
61
- case "auto":
62
- return "repeat(auto-fit, minmax(200px, 1fr))";
63
- default:
64
- return "repeat(3, 1fr)";
65
- }
66
- };
67
-
68
- const gridColumns = getGridColumns();
69
- ---
70
-
71
- <div
72
- class={`multi-image ${className || ""}`}
73
- data-mi-root={uid}
74
- data-layout={layout}
75
- {id}
76
- >
77
- {
78
- hasCaption ? (
79
- <figure class="multi-image-figure">
80
- <div
81
- class="multi-image-grid"
82
- style={`grid-template-columns: ${gridColumns}`}
83
- >
84
- {images.map((image, index) => (
85
- <div class="multi-image-item">
86
- <Image
87
- src={image.src}
88
- alt={image.alt}
89
- zoomable={image.zoomable ?? zoomable}
90
- downloadable={
91
- image.downloadable ?? downloadable
92
- }
93
- class="multi-image-img"
94
- />
95
- {image.caption && (
96
- <div class="multi-image-subcaption">
97
- {image.caption}
98
- </div>
99
- )}
100
- {image.id && (
101
- <span
102
- id={image.id}
103
- style="position: absolute;"
104
- />
105
- )}
106
- </div>
107
- ))}
108
- </div>
109
- <figcaption class="multi-image-caption">
110
- {hasCaptionSlot ? (
111
- <slot name="caption" />
112
- ) : (
113
- caption && <span set:html={caption} />
114
- )}
115
- </figcaption>
116
- </figure>
117
- ) : (
118
- <div
119
- class="multi-image-grid"
120
- style={`grid-template-columns: ${gridColumns}`}
121
- >
122
- {images.map((image, index) => (
123
- <div class="multi-image-item">
124
- <Image
125
- src={image.src}
126
- alt={image.alt}
127
- zoomable={image.zoomable ?? zoomable}
128
- downloadable={image.downloadable ?? downloadable}
129
- class="multi-image-img"
130
- />
131
- {image.caption && (
132
- <div class="multi-image-subcaption">
133
- {image.caption}
134
- </div>
135
- )}
136
- {image.id && (
137
- <span id={image.id} style="position: absolute;" />
138
- )}
139
- </div>
140
- ))}
141
- </div>
142
- )
143
- }
144
- </div>
145
-
146
- <style>
147
- .multi-image {
148
- margin: var(--block-spacing-y) 0;
149
- }
150
-
151
- .multi-image-figure {
152
- margin: 0;
153
- }
154
-
155
- .multi-image-grid {
156
- display: grid;
157
- gap: 1rem;
158
- align-items: start;
159
- }
160
-
161
- .multi-image-item {
162
- display: flex;
163
- flex-direction: column;
164
- text-align: center;
165
- position: relative;
166
- z-index: var(--z-content);
167
- transition: z-index 0.3s ease;
168
- }
169
-
170
- /* Quand medium-zoom est actif, masquer temporairement les autres images du multi-image */
171
- :global(.medium-zoom--opened) .multi-image-item {
172
- opacity: 0;
173
- z-index: calc(var(--z-base) - 1);
174
- transition:
175
- opacity 0.3s ease,
176
- z-index 0.3s ease;
177
- }
178
-
179
- /* Masquer spécifiquement les captions pendant le zoom - approche radicale */
180
- :global(.medium-zoom--opened) .multi-image-subcaption {
181
- display: none !important;
182
- }
183
-
184
- /* Masquer complètement tous les éléments multi-image pendant le zoom */
185
- :global(.medium-zoom--opened) .multi-image {
186
- z-index: -1 !important;
187
- }
188
-
189
- /* Masquer tous les textes de l'ensemble des multi-images */
190
- :global(.medium-zoom--opened) .multi-image-caption {
191
- display: none !important;
192
- }
193
-
194
- /* L'image actuellement zoomée reste visible */
195
- :global(.medium-zoom--opened)
196
- .multi-image-item:has(:global(.medium-zoom--opened)) {
197
- opacity: 1;
198
- z-index: var(--z-overlay);
199
- }
200
-
201
- /* Fallback for browsers without :has() support */
202
- :global(.medium-zoom--opened) .multi-image-item.zoom-active {
203
- opacity: 1 !important;
204
- z-index: var(--z-overlay) !important;
205
- }
206
-
207
- /* Garder la caption de l'image active visible */
208
- :global(.medium-zoom--opened)
209
- .multi-image-item.zoom-active
210
- .multi-image-subcaption {
211
- opacity: 1 !important;
212
- z-index: var(--z-overlay) !important;
213
- }
214
-
215
- .multi-image-item :global(.ri-root) {
216
- margin: 0;
217
- }
218
-
219
- .multi-image-item :global(figure) {
220
- margin: 0;
221
- }
222
-
223
- .multi-image-img {
224
- width: 100%;
225
- height: auto;
226
- object-fit: contain;
227
- }
228
-
229
- .multi-image-subcaption {
230
- font-size: 0.85rem;
231
- color: var(--muted-color);
232
- margin-top: 0.5rem;
233
- line-height: 1.4;
234
- }
235
-
236
- .multi-image-caption {
237
- text-align: left;
238
- font-size: 0.9rem;
239
- color: var(--muted-color);
240
- margin-top: 1rem;
241
- line-height: 1.4;
242
- }
243
-
244
- /* Responsive behavior */
245
- @media (max-width: 768px) {
246
- .multi-image-grid[style*="repeat(3, 1fr)"],
247
- .multi-image-grid[style*="repeat(4, 1fr)"] {
248
- grid-template-columns: 1fr !important;
249
- gap: 1.5rem;
250
- }
251
-
252
- .multi-image-grid[style*="repeat(2, 1fr)"] {
253
- grid-template-columns: 1fr !important;
254
- gap: 1.5rem;
255
- }
256
- }
257
-
258
- @media (min-width: 769px) and (max-width: 1024px) {
259
- .multi-image-grid[style*="repeat(4, 1fr)"] {
260
- grid-template-columns: repeat(2, 1fr) !important;
261
- }
262
- }
263
-
264
- /* Images maintain natural aspect ratio */
265
- .multi-image[data-layout*="column"] .multi-image-item :global(img) {
266
- height: auto;
267
- object-fit: contain;
268
- }
269
-
270
- /* Auto layout gets flexible heights */
271
- .multi-image[data-layout="auto"] .multi-image-item :global(img) {
272
- height: auto;
273
- }
274
-
275
- /* Ensure images maintain aspect ratio */
276
- .multi-image-item :global(img) {
277
- max-width: 100%;
278
- display: block;
279
- margin: 0 auto;
280
- }
281
- </style>
282
-
283
- <script>
284
- // Enhanced medium-zoom integration for MultiFigure
285
- document.addEventListener("DOMContentLoaded", () => {
286
- // Improve MultiFigure behavior with medium-zoom
287
- const multiImages = document.querySelectorAll(".multi-image");
288
-
289
- multiImages.forEach((multiImage) => {
290
- const items = multiImage.querySelectorAll(".multi-image-item");
291
- const zoomableImages = multiImage.querySelectorAll(
292
- 'img[data-zoomable="1"]',
293
- );
294
-
295
- zoomableImages.forEach((img) => {
296
- img.addEventListener("click", () => {
297
- // Trouver l'item parent de l'image cliquée et le ri-root
298
- const activeItem = img.closest(".multi-image-item");
299
- const riRoot = img.closest(".ri-root");
300
-
301
- // Nettoyer TOUS les zoom-active (MultiFigure items et Figure)
302
- document
303
- .querySelectorAll(
304
- ".multi-image-item.zoom-active, .ri-root.zoom-active",
305
- )
306
- .forEach((el) => el.classList.remove("zoom-active"));
307
-
308
- // Ajouter zoom-active aux éléments actifs
309
- if (activeItem) {
310
- activeItem.classList.add("zoom-active");
311
- }
312
- if (riRoot) {
313
- riRoot.classList.add("zoom-active");
314
- }
315
- });
316
- });
317
- });
318
-
319
- // Nettoyer TOUTES les classes lors de la fermeture du zoom
320
- document.addEventListener("click", (e) => {
321
- if (e.target.classList.contains("medium-zoom-overlay")) {
322
- // Zoom fermé, nettoyer toutes les classes zoom-active
323
- document
324
- .querySelectorAll(
325
- ".multi-image-item.zoom-active, .ri-root.zoom-active",
326
- )
327
- .forEach((item) => item.classList.remove("zoom-active"));
328
- }
329
- });
330
-
331
- // Listen for keyboard events to close zoom
332
- document.addEventListener("keydown", (e) => {
333
- if (e.key === "Escape") {
334
- document
335
- .querySelectorAll(
336
- ".multi-image-item.zoom-active, .ri-root.zoom-active",
337
- )
338
- .forEach((item) => item.classList.remove("zoom-active"));
339
- }
340
- });
341
- });
342
- </script>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/src/components/Stack.astro CHANGED
@@ -87,7 +87,7 @@ const gapSize = getGapSize();
87
  }
88
 
89
  .stack[data-layout="auto"] {
90
- grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
91
  }
92
 
93
  /* Default layout (2-column) */
@@ -139,6 +139,7 @@ const gapSize = getGapSize();
139
  .stack[data-layout="3-column"],
140
  .stack[data-layout="4-column"],
141
  .stack[data-layout="2-column"],
 
142
  .stack:not([data-layout]) {
143
  grid-template-columns: 1fr !important;
144
  }
@@ -146,7 +147,8 @@ const gapSize = getGapSize();
146
 
147
  @media (min-width: 769px) and (max-width: 1100px) {
148
  .stack[data-layout="3-column"],
149
- .stack[data-layout="4-column"] {
 
150
  grid-template-columns: repeat(2, 1fr) !important;
151
  }
152
  }
 
87
  }
88
 
89
  .stack[data-layout="auto"] {
90
+ grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
91
  }
92
 
93
  /* Default layout (2-column) */
 
139
  .stack[data-layout="3-column"],
140
  .stack[data-layout="4-column"],
141
  .stack[data-layout="2-column"],
142
+ .stack[data-layout="auto"],
143
  .stack:not([data-layout]) {
144
  grid-template-columns: 1fr !important;
145
  }
 
147
 
148
  @media (min-width: 769px) and (max-width: 1100px) {
149
  .stack[data-layout="3-column"],
150
+ .stack[data-layout="4-column"],
151
+ .stack[data-layout="auto"] {
152
  grid-template-columns: repeat(2, 1fr) !important;
153
  }
154
  }
app/src/content/article.mdx CHANGED
@@ -6,6 +6,9 @@ authors:
6
  - name: "Thibaud Frere"
7
  url: "https://huggingface.co/tfrere"
8
  affiliations: [1]
 
 
 
9
  affiliations:
10
  - name: "Hugging Face"
11
  url: "https://huggingface.co"
 
6
  - name: "Thibaud Frere"
7
  url: "https://huggingface.co/tfrere"
8
  affiliations: [1]
9
+ - name: "Thibaud Frere2"
10
+ url: "https://huggingface.co/tfrere"
11
+ affiliations: [1]
12
  affiliations:
13
  - name: "Hugging Face"
14
  url: "https://huggingface.co"
app/src/content/chapters/demo/components.mdx CHANGED
@@ -8,9 +8,10 @@ import Note from '../../../components/Note.astro';
8
  import FullWidth from '../../../components/FullWidth.astro';
9
  import Accordion from '../../../components/Accordion.astro';
10
  import Image from '../../../components/Image.astro';
11
- import MultiImage from '../../../components/MultiImage.astro';
12
  import Quote from '../../../components/Quote.astro';
13
  import Reference from '../../../components/Reference.astro';
 
 
14
 
15
  ## Components
16
 
@@ -22,7 +23,6 @@ To use any component in your MDX file, add the import statement at the top:
22
 
23
  ```mdx
24
  import Image from '../components/Image.astro';
25
- import MultiImage from '../components/MultiImage.astro';
26
  import Note from '../components/Note.astro';
27
 
28
  # Your content
@@ -39,10 +39,6 @@ Here are the components that are available:
39
  <strong>Image</strong>
40
  <span>Optimized images</span>
41
  </a>
42
- <a href="#multiimage" className="feature-card">
43
- <strong>MultiImage</strong>
44
- <span>Multi-panel images</span>
45
- </a>
46
  <a href="#placement" className="feature-card">
47
  <strong>Placement</strong>
48
  <span>Layout helpers</span>
@@ -71,6 +67,14 @@ Here are the components that are available:
71
  <strong>Iframe</strong>
72
  <span>Web embeds</span>
73
  </a>
 
 
 
 
 
 
 
 
74
  </div>
75
 
76
  ### Image
@@ -116,48 +120,6 @@ import myImage from './assets/image/placeholder.jpg'
116
  ```
117
  </Accordion>
118
 
119
- ### MultiImage
120
-
121
- **Display multiple images** in a grid layout with customizable columns and captions. Perfect for **multi-panel images**, **comparisons**, and **step-by-step visualizations** — just like LaTeX's `subfigure` environment.
122
-
123
- <MultiImage
124
- images={[
125
- { src: placeholder, alt: "Image 1", caption: "First image with caption" },
126
- { src: placeholder, alt: "Image 2", caption: "Second image" },
127
- { src: placeholder, alt: "Image 3", caption: "Third image description" }
128
- ]}
129
- layout="3-column"
130
- caption="Gallery of three images with individual captions"
131
- zoomable={true}
132
- />
133
-
134
- <Accordion title="Code example">
135
- ```mdx
136
- import placeholder from '../assets/image/placeholder.png';
137
-
138
- <MultiImage
139
- images={[
140
- { src: placeholder, alt: "Panel A", caption: "(a) First panel" },
141
- { src: placeholder, alt: "Panel B", caption: "(b) Second panel" },
142
- { src: placeholder, alt: "Panel C", caption: "(c) Third panel" }
143
- ]}
144
- layout="3-column"
145
- caption="Figure 1: Multi-panel comparison showing different conditions"
146
- zoomable={true}
147
- />
148
- ```
149
- </Accordion>
150
-
151
- | Prop | Required | Type | Description
152
- |------------------------|----------|-----------------------------------------------------------------------|-------------------------------------------------------
153
- | `images` | Yes | `ImageItem[]` | Array of image objects with `src`, `alt`, and optional `caption`, `id`, `zoomable`, `downloadable` properties
154
- | `layout` | No | `"2-column" \| "3-column" \| "4-column" \| "auto"` | Grid layout for the images (default: "3-column")
155
- | `caption` | No | `string` | Global caption for the entire multi-image figure
156
- | `zoomable` | No | `boolean` | Enable zoom functionality on all images (can be overridden per image)
157
- | `downloadable` | No | `boolean` | Show download buttons on all images (can be overridden per image)
158
- | `class` | No | `string` | Additional CSS classes to apply to the wrapper
159
- | `id` | No | `string` | Global ID for the multi-image figure (for deep-linking and cross-references)
160
-
161
  ### Placement
162
 
163
  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.
@@ -446,3 +408,153 @@ As long as your files are there, they will be served from the **`public/data`**
446
  You can fetch them with this address: **`[domain]/data/your-data.ext`**
447
 
448
  <Note emoji="⚠️" variant="danger"><b>Be careful</b>, unlike images, <b>data files are not optimized</b> by Astro. You need to optimize them manually.</Note>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
  import FullWidth from '../../../components/FullWidth.astro';
9
  import Accordion from '../../../components/Accordion.astro';
10
  import Image from '../../../components/Image.astro';
 
11
  import Quote from '../../../components/Quote.astro';
12
  import Reference from '../../../components/Reference.astro';
13
+ import Glossary from '../../../components/Glossary.astro';
14
+ import Stack from '../../../components/Stack.astro';
15
 
16
  ## Components
17
 
 
23
 
24
  ```mdx
25
  import Image from '../components/Image.astro';
 
26
  import Note from '../components/Note.astro';
27
 
28
  # Your content
 
39
  <strong>Image</strong>
40
  <span>Optimized images</span>
41
  </a>
 
 
 
 
42
  <a href="#placement" className="feature-card">
43
  <strong>Placement</strong>
44
  <span>Layout helpers</span>
 
67
  <strong>Iframe</strong>
68
  <span>Web embeds</span>
69
  </a>
70
+ <a href="#glossary" className="feature-card">
71
+ <strong>Glossary</strong>
72
+ <span>Interactive term definitions</span>
73
+ </a>
74
+ <a href="#stack" className="feature-card">
75
+ <strong>Stack</strong>
76
+ <span>Flexible grid layouts</span>
77
+ </a>
78
  </div>
79
 
80
  ### Image
 
120
  ```
121
  </Accordion>
122
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
123
  ### Placement
124
 
125
  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.
 
408
  You can fetch them with this address: **`[domain]/data/your-data.ext`**
409
 
410
  <Note emoji="⚠️" variant="danger"><b>Be careful</b>, unlike images, <b>data files are not optimized</b> by Astro. You need to optimize them manually.</Note>
411
+
412
+ ### Glossary
413
+
414
+ The **Glossary** component creates interactive term definitions with hover tooltips. Perfect for **technical terms**, **acronyms**, and **concepts** that need explanation without breaking the reading flow.
415
+
416
+ <Glossary term="Machine Learning" definition="A subset of artificial intelligence that enables computers to learn and improve from experience without being explicitly programmed." />
417
+
418
+ <Glossary term="Neural Network" definition="A computing system inspired by biological neural networks, consisting of interconnected nodes (neurons) that process information." position="bottom" />
419
+
420
+ <Glossary term="API" definition="Application Programming Interface - a set of protocols and tools for building software applications." position="right" disableOnMobile={true} />
421
+ <br/><br/>
422
+
423
+ | Prop | Required | Type | Description
424
+ |-------------------|----------|-----------------------------------------|-------------------------------------------------------
425
+ | `term` | Yes | `string` | The word or term to define
426
+ | `definition` | Yes | `string` | The definition of the term
427
+ | `class` | No | `string` | Optional CSS class to apply to the term
428
+ | `style` | No | `string` | Optional inline style to apply to the term
429
+ | `position` | No | `'top' \| 'bottom' \| 'left' \| 'right'` | Tooltip position (default: 'top')
430
+ | `delay` | No | `number` | Delay before showing tooltip in ms (default: 300)
431
+ | `disableOnMobile` | No | `boolean` | Disable tooltip on mobile devices (default: false)
432
+
433
+ <Accordion title="Code example">
434
+ ```mdx
435
+ import Glossary from '../../../components/Glossary.astro'
436
+
437
+ <Glossary term="Machine Learning" definition="A subset of artificial intelligence that enables computers to learn and improve from experience." />
438
+
439
+ <Glossary
440
+ term="Deep Learning"
441
+ definition="A subset of machine learning that uses neural networks with multiple layers to model and understand complex patterns in data."
442
+ position="bottom"
443
+ delay={500}
444
+ />
445
+
446
+ <Glossary
447
+ term="API"
448
+ definition="Application Programming Interface - a set of protocols and tools for building software applications."
449
+ disableOnMobile={true}
450
+ />
451
+
452
+
453
+ ```
454
+ </Accordion>
455
+
456
+ **Use cases:**
457
+ - **Technical terms** that need explanation
458
+ - **Acronyms** and abbreviations
459
+ - **Domain-specific concepts** for broader audiences
460
+ - **Interactive glossaries** for educational content
461
+
462
+ ### Stack
463
+
464
+ The **Stack** component provides flexible grid layouts for organizing content. Perfect for **comparisons**, **side-by-side examples**, **multi-column layouts**, and **responsive content organization**.
465
+
466
+ <Stack layout="auto" gap="large">
467
+ <div>
468
+ <h4>Auto Layout</h4>
469
+ <p>This layout automatically adjusts based on content and available space.</p>
470
+ </div>
471
+ <div>
472
+ <h4>Responsive</h4>
473
+ <p>Items will wrap to new lines as needed, with a minimum width of 300px.</p>
474
+ </div>
475
+ <div>
476
+ <h4>Flexible</h4>
477
+ <p>Perfect for varying content lengths and dynamic layouts.</p>
478
+ </div>
479
+ </Stack>
480
+
481
+ <Stack layout="2-column" gap="medium">
482
+ <Image
483
+ src={placeholder}
484
+ alt="First image in stack"
485
+ caption="Image 1: Example of using Image component within Stack"
486
+ zoomable
487
+ />
488
+ <Image
489
+ src={placeholder}
490
+ alt="Second image in stack"
491
+ caption="Image 2: Side-by-side comparison layout"
492
+ zoomable
493
+ />
494
+ </Stack>
495
+
496
+ | Prop | Required | Type | Description
497
+ |----------|----------|-----------------------------------------------------------------------|-------------------------------------------------------
498
+ | `layout` | No | `"2-column" \| "3-column" \| "4-column" \| "auto"` | Grid layout for the content (default: "2-column")
499
+ | `gap` | No | `"small" \| "medium" \| "large" \| string` | Gap between items - predefined size or custom value (e.g., "2rem", "20px")
500
+ | `class` | No | `string` | Optional CSS class to apply to the wrapper
501
+ | `id` | No | `string` | Optional ID for the stack
502
+
503
+ <Accordion title="Code example">
504
+ ```mdx
505
+ import Stack from '../../../components/Stack.astro'
506
+
507
+ <Stack layout="auto" gap="large">
508
+ <div>
509
+ <h4>Auto Layout</h4>
510
+ <p>This layout automatically adjusts based on content and available space.</p>
511
+ </div>
512
+ <div>
513
+ <h4>Responsive</h4>
514
+ <p>Items will wrap to new lines as needed, with a minimum width of 300px.</p>
515
+ </div>
516
+ <div>
517
+ <h4>Flexible</h4>
518
+ <p>Perfect for varying content lengths and dynamic layouts.</p>
519
+ </div>
520
+ </Stack>
521
+
522
+ <Stack layout="2-column" gap="medium">
523
+ <Image
524
+ src={placeholder}
525
+ alt="First image in stack"
526
+ caption="Image 1: Example of using Image component within Stack"
527
+ zoomable
528
+ />
529
+ <Image
530
+ src={placeholder}
531
+ alt="Second image in stack"
532
+ caption="Image 2: Side-by-side comparison layout"
533
+ zoomable
534
+ />
535
+ </Stack>
536
+ ```
537
+ </Accordion>
538
+
539
+ **Layout options:**
540
+ - **2-column**: Two equal-width columns
541
+ - **3-column**: Three equal-width columns
542
+ - **4-column**: Four equal-width columns
543
+ - **auto**: Responsive grid that adapts to content and screen size
544
+
545
+ **Gap options:**
546
+ - **small**: 0.5rem spacing
547
+ - **medium**: 1rem spacing (default)
548
+ - **large**: 1.5rem spacing
549
+ - **custom**: Any CSS value (e.g., "2rem", "20px", "1.5em")
550
+
551
+ **Responsive behavior:**
552
+ - **Mobile (≤768px)**: All layouts collapse to single column
553
+ - **Tablet (769px-1100px)**: 3-column and 4-column layouts become 2-column
554
+ - **Desktop (>1100px)**: Full layout as specified
555
+
556
+ **Use cases:**
557
+ - **Feature comparisons** and side-by-side content
558
+ - **Multi-column text** and documentation
559
+ - **Responsive card layouts** and grids
560
+ - **Flexible content organization** for varying screen sizes
app/src/styles/_base.css CHANGED
@@ -45,6 +45,23 @@ html {
45
  ;
46
  }
47
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
  .content-grid main a:hover {
49
  color: var(--primary-color-hover);
50
  border-bottom: 1px solid color-mix(in srgb, var(--primary-color, #007AFF) 40%, transparent);
 
45
  ;
46
  }
47
 
48
+ /* External links with arrow indicator */
49
+ .content-grid main a[href^="http"]:not([href*="localhost"]):not([href*="127.0.0.1"]):not([href*="0.0.0.0"])::after {
50
+ content: "↗";
51
+ display: inline-block;
52
+ margin-left: 6px;
53
+ font-size: 0.9em;
54
+ opacity: 0.8;
55
+ transition: opacity 0.2s ease, transform 0.2s ease;
56
+ transform: translateY(-1px);
57
+ font-weight: 600;
58
+ }
59
+
60
+ /* External links with arrow indicator */
61
+ .content-grid main a[href^="http"]:not([href*="localhost"]):not([href*="127.0.0.1"]):not([href*="0.0.0.0"]) {
62
+ white-space: nowrap;
63
+ }
64
+
65
  .content-grid main a:hover {
66
  color: var(--primary-color-hover);
67
  border-bottom: 1px solid color-mix(in srgb, var(--primary-color, #007AFF) 40%, transparent);