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

add some new components

Browse files
app/src/components/Glossary.astro ADDED
@@ -0,0 +1,336 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ interface Props {
3
+ /** The word or term to define */
4
+ term: string;
5
+ /** The definition of the term */
6
+ definition: string;
7
+ /** Optional CSS class to apply to the term */
8
+ class?: string;
9
+ /** Optional style to apply to the term */
10
+ style?: string;
11
+ /** Tooltip position (top, bottom, left, right) */
12
+ position?: "top" | "bottom" | "left" | "right";
13
+ /** Delay before showing tooltip in ms */
14
+ delay?: number;
15
+ /** Disable tooltip on mobile */
16
+ disableOnMobile?: boolean;
17
+ }
18
+
19
+ const {
20
+ term,
21
+ definition,
22
+ class: className = "",
23
+ style: inlineStyle = "",
24
+ position = "top",
25
+ delay = 300,
26
+ disableOnMobile = false,
27
+ } = Astro.props as Props;
28
+
29
+ // Generate a unique ID for this component
30
+ const tooltipId = `glossary-${Math.random().toString(36).slice(2)}`;
31
+ ---
32
+
33
+ <div class="glossary-container" data-glossary-container-id={tooltipId}>
34
+ <span
35
+ class={`glossary-term ${className}`}
36
+ style={inlineStyle}
37
+ data-glossary-term={term}
38
+ data-glossary-definition={definition}
39
+ data-glossary-position={position}
40
+ data-glossary-delay={delay}
41
+ data-glossary-disable-mobile={disableOnMobile}
42
+ data-glossary-id={tooltipId}
43
+ tabindex="0"
44
+ role="button"
45
+ aria-describedby={`${tooltipId}-tooltip`}
46
+ >
47
+ {term}
48
+ </span>
49
+
50
+ <div
51
+ id={`${tooltipId}-tooltip`}
52
+ class="glossary-tooltip"
53
+ data-glossary-tooltip-id={tooltipId}
54
+ data-position={position}
55
+ role="tooltip"
56
+ aria-hidden="true"
57
+ >
58
+ <div class="glossary-tooltip__content">
59
+ <div class="glossary-tooltip__term">{term}</div>
60
+ <div class="glossary-tooltip__definition">{definition}</div>
61
+ </div>
62
+ <div class="glossary-tooltip__arrow"></div>
63
+ </div>
64
+ </div>
65
+
66
+ <script is:inline>
67
+ // Global script for all Glossary tooltips
68
+ if (!window.glossaryInitialized) {
69
+ window.glossaryInitialized = true;
70
+
71
+ function initAllGlossaryTooltips() {
72
+ const glossaryTerms = document.querySelectorAll(".glossary-term");
73
+
74
+ glossaryTerms.forEach((termElement) => {
75
+ const tooltipElement =
76
+ termElement.parentElement.querySelector(".glossary-tooltip");
77
+
78
+ if (!tooltipElement) return;
79
+
80
+ const term = termElement.getAttribute("data-glossary-term");
81
+ const definition = termElement.getAttribute("data-glossary-definition");
82
+
83
+ if (!term || !definition) return;
84
+
85
+ // Fonction pour afficher le tooltip au niveau de la souris
86
+ const showTooltip = (event) => {
87
+ tooltipElement.style.display = "block";
88
+ tooltipElement.style.opacity = "1";
89
+ tooltipElement.style.position = "fixed";
90
+ tooltipElement.style.top = event.clientY + 10 + "px";
91
+ tooltipElement.style.left = event.clientX + 10 + "px";
92
+ tooltipElement.style.zIndex = "9999";
93
+ tooltipElement.style.pointerEvents = "none";
94
+ };
95
+
96
+ const hideTooltip = () => {
97
+ tooltipElement.style.display = "none";
98
+ tooltipElement.style.opacity = "0";
99
+ };
100
+
101
+ // Add events
102
+ termElement.addEventListener("mouseenter", showTooltip);
103
+ termElement.addEventListener("mouseleave", hideTooltip);
104
+ termElement.addEventListener("mousemove", showTooltip);
105
+ });
106
+ }
107
+
108
+ // Initialize when DOM is ready
109
+ if (document.readyState === "loading") {
110
+ document.addEventListener("DOMContentLoaded", initAllGlossaryTooltips);
111
+ } else {
112
+ initAllGlossaryTooltips();
113
+ }
114
+
115
+ // Observe DOM changes for new elements
116
+ if (window.MutationObserver) {
117
+ const observer = new MutationObserver((mutations) => {
118
+ mutations.forEach((mutation) => {
119
+ if (mutation.type === "childList") {
120
+ mutation.addedNodes.forEach((node) => {
121
+ if (
122
+ node.nodeType === 1 &&
123
+ node.querySelector &&
124
+ node.querySelector(".glossary-term")
125
+ ) {
126
+ initAllGlossaryTooltips();
127
+ }
128
+ });
129
+ }
130
+ });
131
+ });
132
+
133
+ observer.observe(document.body, {
134
+ childList: true,
135
+ subtree: true,
136
+ });
137
+ }
138
+ }
139
+ </script>
140
+
141
+ <style>
142
+ /* ============================================================================ */
143
+ /* Glossary Component */
144
+ /* ============================================================================ */
145
+
146
+ .glossary-container {
147
+ display: inline;
148
+ position: relative;
149
+ }
150
+
151
+ .glossary-term {
152
+ color: var(--primary-color) !important;
153
+ text-decoration: none !important;
154
+ background: color-mix(in srgb, var(--primary-color) 15%, transparent);
155
+ border-bottom: 1px dashed
156
+ color-mix(in srgb, var(--primary-color) 100%, transparent) !important;
157
+ cursor: help;
158
+ transition: all 0.2s ease;
159
+ border-radius: 3px;
160
+ margin: 0 2px;
161
+ padding: 4px 8px;
162
+ }
163
+
164
+ .glossary-term:hover,
165
+ .glossary-term:focus {
166
+ color: var(--primary-color-hover) !important;
167
+ text-decoration: none !important;
168
+ background: color-mix(in srgb, var(--primary-color) 20%, transparent);
169
+ outline: none;
170
+ }
171
+
172
+ .glossary-term:focus {
173
+ box-shadow: 0 0 0 2px
174
+ color-mix(in srgb, var(--primary-color) 20%, transparent);
175
+ }
176
+
177
+ .glossary-tooltip {
178
+ position: fixed;
179
+ top: -9999px;
180
+ left: -9999px;
181
+ z-index: var(--z-tooltip);
182
+ opacity: 0;
183
+ transform: translateY(-4px);
184
+ transition:
185
+ opacity 0.2s ease,
186
+ transform 0.2s ease;
187
+ pointer-events: none;
188
+ max-width: 300px;
189
+ min-width: 200px;
190
+ }
191
+
192
+ .glossary-tooltip.is-visible {
193
+ opacity: 1;
194
+ transform: translateY(0);
195
+ pointer-events: auto;
196
+ }
197
+
198
+ .glossary-tooltip__content {
199
+ background: var(--surface-bg);
200
+ border: 1px solid var(--border-color);
201
+ border-radius: 8px;
202
+ padding: 12px 16px;
203
+ box-shadow:
204
+ 0 8px 32px rgba(0, 0, 0, 0.12),
205
+ 0 2px 8px rgba(0, 0, 0, 0.06);
206
+ backdrop-filter: saturate(1.12) blur(8px);
207
+ }
208
+
209
+ .glossary-tooltip__term {
210
+ font-weight: 600;
211
+ font-size: 14px;
212
+ color: var(--primary-color);
213
+ margin-bottom: 4px;
214
+ line-height: 1.3;
215
+ }
216
+
217
+ .glossary-tooltip__definition {
218
+ font-size: 13px;
219
+ color: var(--text-color);
220
+ line-height: 1.4;
221
+ margin: 0;
222
+ }
223
+
224
+ .glossary-tooltip__arrow {
225
+ position: absolute;
226
+ width: 0;
227
+ height: 0;
228
+ border: 6px solid transparent;
229
+ }
230
+
231
+ /* Arrow positioning */
232
+ .glossary-tooltip[data-position="top"] .glossary-tooltip__arrow {
233
+ bottom: -6px;
234
+ left: 50%;
235
+ transform: translateX(-50%);
236
+ border-top-color: var(--border-color);
237
+ }
238
+
239
+ .glossary-tooltip[data-position="top"] .glossary-tooltip__arrow::after {
240
+ content: "";
241
+ position: absolute;
242
+ top: -7px;
243
+ left: -6px;
244
+ border: 6px solid transparent;
245
+ border-top-color: var(--surface-bg);
246
+ }
247
+
248
+ .glossary-tooltip[data-position="bottom"] .glossary-tooltip__arrow {
249
+ top: -6px;
250
+ left: 50%;
251
+ transform: translateX(-50%);
252
+ border-bottom-color: var(--border-color);
253
+ }
254
+
255
+ .glossary-tooltip[data-position="bottom"] .glossary-tooltip__arrow::after {
256
+ content: "";
257
+ position: absolute;
258
+ top: -5px;
259
+ left: -6px;
260
+ border: 6px solid transparent;
261
+ border-bottom-color: var(--surface-bg);
262
+ }
263
+
264
+ .glossary-tooltip[data-position="left"] .glossary-tooltip__arrow {
265
+ right: -6px;
266
+ top: 50%;
267
+ transform: translateY(-50%);
268
+ border-left-color: var(--border-color);
269
+ }
270
+
271
+ .glossary-tooltip[data-position="left"] .glossary-tooltip__arrow::after {
272
+ content: "";
273
+ position: absolute;
274
+ top: -6px;
275
+ left: -7px;
276
+ border: 6px solid transparent;
277
+ border-left-color: var(--surface-bg);
278
+ }
279
+
280
+ .glossary-tooltip[data-position="right"] .glossary-tooltip__arrow {
281
+ left: -6px;
282
+ top: 50%;
283
+ transform: translateY(-50%);
284
+ border-right-color: var(--border-color);
285
+ }
286
+
287
+ .glossary-tooltip[data-position="right"] .glossary-tooltip__arrow::after {
288
+ content: "";
289
+ position: absolute;
290
+ top: -6px;
291
+ left: -5px;
292
+ border: 6px solid transparent;
293
+ border-right-color: var(--surface-bg);
294
+ }
295
+
296
+ /* Mode sombre */
297
+ [data-theme="dark"] .glossary-tooltip__content {
298
+ background: var(--surface-bg);
299
+ border-color: var(--border-color);
300
+ }
301
+
302
+ [data-theme="dark"] .glossary-tooltip__term {
303
+ color: var(--primary-color);
304
+ }
305
+
306
+ [data-theme="dark"] .glossary-tooltip__definition {
307
+ color: var(--text-color);
308
+ }
309
+
310
+ /* Responsive - hide on mobile if disabled */
311
+ @media (max-width: 768px) {
312
+ .glossary-term[data-glossary-disable-mobile="true"] {
313
+ border-bottom: none;
314
+ color: inherit;
315
+ cursor: default;
316
+ }
317
+
318
+ .glossary-term[data-glossary-disable-mobile="true"]:hover,
319
+ .glossary-term[data-glossary-disable-mobile="true"]:focus {
320
+ background: none;
321
+ color: inherit;
322
+ border-bottom: none;
323
+ }
324
+ }
325
+
326
+ /* Accessibility improvement */
327
+ @media (prefers-reduced-motion: reduce) {
328
+ .glossary-tooltip {
329
+ transition: none;
330
+ }
331
+
332
+ .glossary-term {
333
+ transition: none;
334
+ }
335
+ }
336
+ </style>
app/src/components/Hero.astro CHANGED
@@ -91,21 +91,37 @@ const pdfBase = titleRaw ? stripHtml(titleRaw) : stripHtml(title);
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 &&
@@ -114,8 +130,17 @@ const pdfFilename = `${slugify(pdfBase)}.pdf`;
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 && ", "}
121
  </li>
@@ -127,14 +152,21 @@ const pdfFilename = `${slugify(pdfBase)}.pdf`;
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,12 +176,13 @@ const pdfFilename = `${slugify(pdfBase)}.pdf`;
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,17 +196,21 @@ const pdfFilename = `${slugify(pdfBase)}.pdf`;
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,11 +220,17 @@ const pdfFilename = `${slugify(pdfBase)}.pdf`;
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,113 +243,18 @@ const pdfFilename = `${slugify(pdfBase)}.pdf`;
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
- margin-right: 4px;
286
- }
287
- .affiliations {
288
- margin: 0;
289
- padding-left: 1.25em;
290
- }
291
- .affiliations li {
292
- margin: 0;
293
- }
294
-
295
- header.meta .meta-container {
296
- flex-wrap: wrap;
297
- row-gap: 12px;
298
- }
299
-
300
- @media (max-width: 768px) {
301
- .meta-container-cell--affiliations,
302
- .meta-container-cell--pdf {
303
- text-align: right;
304
- }
305
  }
306
 
307
  @media print {
308
- .meta-container-cell--pdf {
309
- display: none !important;
310
  }
311
  }
312
  </style>
 
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 &&
 
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>
 
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
  ))}
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
  }
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
  <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
  </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>
app/src/components/Image.astro CHANGED
@@ -25,6 +25,8 @@ interface Props {
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 <AstroImage> */
29
  [key: string]: any;
30
  }
@@ -39,11 +41,13 @@ const {
39
  linkHref,
40
  linkTarget,
41
  linkRel,
 
42
  ...imgProps
43
  } = Astro.props as Props;
44
  const hasCaptionSlot = Astro.slots.has("caption");
45
  const hasCaption =
46
  hasCaptionSlot || (typeof caption === "string" && caption.length > 0);
 
47
  const uid = `ri_${Math.random().toString(36).slice(2)}`;
48
  const dataZoomable =
49
  zoomable === true || (imgProps as any)["data-zoomable"] ? "1" : undefined;
@@ -56,7 +60,12 @@ const resolvedTarget = hasLink ? linkTarget || "_blank" : undefined;
56
  const resolvedRel = hasLink ? linkRel || "noopener noreferrer" : undefined;
57
  ---
58
 
59
- <div class="ri-root" data-ri-root={uid}>
 
 
 
 
 
60
  {
61
  hasCaption ? (
62
  <figure
@@ -165,10 +174,18 @@ const resolvedRel = hasLink ? linkRel || "noopener noreferrer" : undefined;
165
  target={resolvedTarget}
166
  rel={resolvedRel}
167
  >
168
- <AstroImage {...imgProps} data-zoomable={dataZoomable} />
 
 
 
 
169
  </a>
170
  ) : (
171
- <AstroImage {...imgProps} data-zoomable={dataZoomable} />
 
 
 
 
172
  )
173
  }
174
  </div>
@@ -472,4 +489,20 @@ const resolvedRel = hasLink ? linkRel || "noopener noreferrer" : undefined;
472
  [data-theme="dark"] .img-dl-btn:hover {
473
  background: var(--primary-color-hover);
474
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
475
  </style>
 
25
  linkTarget?: string;
26
  /** Optional rel for the link (default: noopener noreferrer when linkHref provided) */
27
  linkRel?: string;
28
+ /** Make the image span full width */
29
+ fullWidth?: boolean;
30
  /** Any additional attributes should be forwarded to the underlying <AstroImage> */
31
  [key: string]: any;
32
  }
 
41
  linkHref,
42
  linkTarget,
43
  linkRel,
44
+ fullWidth,
45
  ...imgProps
46
  } = Astro.props as Props;
47
  const hasCaptionSlot = Astro.slots.has("caption");
48
  const hasCaption =
49
  hasCaptionSlot || (typeof caption === "string" && caption.length > 0);
50
+ const hasTitle = Astro.slots.has("title");
51
  const uid = `ri_${Math.random().toString(36).slice(2)}`;
52
  const dataZoomable =
53
  zoomable === true || (imgProps as any)["data-zoomable"] ? "1" : undefined;
 
60
  const resolvedRel = hasLink ? linkRel || "noopener noreferrer" : undefined;
61
  ---
62
 
63
+ <div
64
+ class={`ri-root`}
65
+ data-ri-root={uid}
66
+ data-has-title={hasTitle}
67
+ data-has-caption={hasCaption}
68
+ >
69
  {
70
  hasCaption ? (
71
  <figure
 
174
  target={resolvedTarget}
175
  rel={resolvedRel}
176
  >
177
+ <AstroImage
178
+ {...imgProps}
179
+ data-zoomable={dataZoomable}
180
+ class={fullWidth ? "full" : ""}
181
+ />
182
  </a>
183
  ) : (
184
+ <AstroImage
185
+ {...imgProps}
186
+ data-zoomable={dataZoomable}
187
+ class={fullWidth ? "full" : ""}
188
+ />
189
  )
190
  }
191
  </div>
 
489
  [data-theme="dark"] .img-dl-btn:hover {
490
  background: var(--primary-color-hover);
491
  }
492
+
493
+ /* Conditional margins based on title and caption presence */
494
+ .ri-root:not([data-has-title="true"]) {
495
+ margin-top: 20px;
496
+ }
497
+
498
+ .ri-root:not([data-has-caption="true"]) {
499
+ margin-bottom: 20px;
500
+ }
501
+
502
+ /* full image styles */
503
+ img.full {
504
+ width: 100% !important;
505
+ min-width: 100%;
506
+ max-width: 100%;
507
+ }
508
  </style>
app/src/components/Stack.astro ADDED
@@ -0,0 +1,159 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ interface Props {
3
+ /** Layout mode: number of columns or 'auto' for responsive */
4
+ layout?: "2-column" | "3-column" | "4-column" | "auto";
5
+ /** Gap between items - can be a predefined size or custom value (e.g., "2rem", "20px", "1.5em") */
6
+ gap?: "small" | "medium" | "large" | string;
7
+ /** Optional class to apply on the wrapper */
8
+ class?: string;
9
+ /** Optional ID for the stack */
10
+ id?: string;
11
+ }
12
+
13
+ const {
14
+ layout = "2-column",
15
+ gap = "medium",
16
+ class: className,
17
+ id,
18
+ } = Astro.props as Props;
19
+
20
+ // Generate flex properties based on layout
21
+ const getFlexProperties = () => {
22
+ switch (layout) {
23
+ case "2-column":
24
+ return { flexBasis: "50%", maxWidth: "50%" };
25
+ case "3-column":
26
+ return { flexBasis: "33.333%", maxWidth: "33.333%" };
27
+ case "4-column":
28
+ return { flexBasis: "25%", maxWidth: "25%" };
29
+ case "auto":
30
+ return { flexBasis: "auto", maxWidth: "none" };
31
+ default:
32
+ // By default, all children on one line with equal width
33
+ return { flexBasis: "auto", maxWidth: "none" };
34
+ }
35
+ };
36
+
37
+ const getGapSize = () => {
38
+ // If it's a predefined size, return the corresponding value
39
+ switch (gap) {
40
+ case "small":
41
+ return "0.5rem";
42
+ case "medium":
43
+ return "1rem";
44
+ case "large":
45
+ return "1.5rem";
46
+ default:
47
+ // If it's a custom value, return it as-is (e.g., "2rem", "20px", "1.5em")
48
+ return gap;
49
+ }
50
+ };
51
+
52
+ const flexProps = getFlexProperties();
53
+ const gapSize = getGapSize();
54
+ ---
55
+
56
+ <div
57
+ class={`stack ${className || ""}`}
58
+ data-layout={layout}
59
+ data-gap={gap}
60
+ {id}
61
+ style={`gap: ${gapSize}`}
62
+ >
63
+ <slot />
64
+ </div>
65
+
66
+ <style>
67
+ .stack {
68
+ display: grid;
69
+ gap: 1rem;
70
+ margin: var(--block-spacing-y) 0;
71
+ width: 100%;
72
+ max-width: 100%;
73
+ box-sizing: border-box;
74
+ }
75
+
76
+ /* Layout configurations */
77
+ .stack[data-layout="2-column"] {
78
+ grid-template-columns: repeat(2, 1fr);
79
+ }
80
+
81
+ .stack[data-layout="3-column"] {
82
+ grid-template-columns: repeat(3, 1fr);
83
+ }
84
+
85
+ .stack[data-layout="4-column"] {
86
+ grid-template-columns: repeat(4, 1fr);
87
+ }
88
+
89
+ .stack[data-layout="auto"] {
90
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
91
+ }
92
+
93
+ /* Default layout (2-column) */
94
+ .stack:not([data-layout]) {
95
+ grid-template-columns: repeat(2, 1fr);
96
+ }
97
+
98
+ /* Ensure child elements don't overflow */
99
+ .stack :global(> *) {
100
+ min-width: 0 !important;
101
+ max-width: 100% !important;
102
+ box-sizing: border-box !important;
103
+ word-wrap: break-word !important;
104
+ overflow-wrap: break-word !important;
105
+ overflow: hidden !important;
106
+ }
107
+
108
+ /* Handle code blocks inside stack */
109
+ .stack pre {
110
+ overflow-x: auto;
111
+ max-width: 100%;
112
+ width: 100%;
113
+ word-wrap: break-word;
114
+ white-space: pre-wrap;
115
+ box-sizing: border-box;
116
+ min-width: 0 !important;
117
+ }
118
+
119
+ .stack code {
120
+ word-wrap: break-word;
121
+ white-space: pre-wrap;
122
+ max-width: 100%;
123
+ box-sizing: border-box;
124
+ min-width: 0 !important;
125
+ }
126
+
127
+ /* Override the min-width: 100% from _code.css */
128
+ .stack pre code {
129
+ min-width: 0 !important;
130
+ }
131
+
132
+ /* Override section.content-grid pre code min-width rule */
133
+ .stack section.content-grid pre code {
134
+ min-width: 0 !important;
135
+ }
136
+
137
+ /* Responsive behavior */
138
+ @media (max-width: 768px) {
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
+ }
145
+ }
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
+ }
153
+
154
+ @media (min-width: 1101px) and (max-width: 1400px) {
155
+ .stack[data-layout="4-column"] {
156
+ grid-template-columns: repeat(2, 1fr) !important;
157
+ }
158
+ }
159
+ </style>