@markuxt/markuxt 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +168 -0
  3. package/app.config.d.ts +33 -0
  4. package/nuxt.config.ts +170 -0
  5. package/package.json +43 -0
  6. package/src/components/AppFooter.vue +225 -0
  7. package/src/components/AppHeader.vue +342 -0
  8. package/src/components/Hero.vue +438 -0
  9. package/src/components/Icon.vue +131 -0
  10. package/src/components/LanguageSwitcher.vue +71 -0
  11. package/src/components/MemberCard.vue +198 -0
  12. package/src/components/MembersGrid.vue +129 -0
  13. package/src/components/MermaidDiagram.vue +99 -0
  14. package/src/components/NewsCard.vue +119 -0
  15. package/src/components/PublicationCard.vue +245 -0
  16. package/src/components/SectionTitle.vue +75 -0
  17. package/src/components/content/ProseImg.vue +29 -0
  18. package/src/components/content/ProsePre.vue +58 -0
  19. package/src/components/content/ProseVideo.vue +45 -0
  20. package/src/composables/resolveContentImage.ts +35 -0
  21. package/src/content-transformers/binary-assets.ts +20 -0
  22. package/src/error.vue +58 -0
  23. package/src/layouts/default.vue +37 -0
  24. package/src/middleware/navigation-guard.ts +22 -0
  25. package/src/pages/index.vue +232 -0
  26. package/src/pages/members/[...slug].vue +542 -0
  27. package/src/pages/members/index.vue +147 -0
  28. package/src/pages/news/[...slug].vue +280 -0
  29. package/src/pages/news/index.vue +102 -0
  30. package/src/pages/positions/[...slug].vue +425 -0
  31. package/src/pages/positions/index.vue +266 -0
  32. package/src/pages/projects/[...slug].vue +441 -0
  33. package/src/pages/projects/index.vue +499 -0
  34. package/src/pages/publications/[...slug].vue +435 -0
  35. package/src/pages/publications/index.vue +145 -0
  36. package/src/plugins/mathml-components.ts +33 -0
  37. package/src/plugins/suppress-warnings.ts +40 -0
  38. package/src/public/_markuxt/components/AppFooter.vue +225 -0
  39. package/src/public/_markuxt/components/AppHeader.vue +342 -0
  40. package/src/public/_markuxt/components/Hero.vue +430 -0
  41. package/src/public/_markuxt/components/Icon.vue +131 -0
  42. package/src/public/_markuxt/components/LanguageSwitcher.vue +71 -0
  43. package/src/public/_markuxt/components/MemberCard.vue +198 -0
  44. package/src/public/_markuxt/components/MembersGrid.vue +129 -0
  45. package/src/public/_markuxt/components/MermaidDiagram.vue +99 -0
  46. package/src/public/_markuxt/components/NewsCard.vue +119 -0
  47. package/src/public/_markuxt/components/PublicationCard.vue +245 -0
  48. package/src/public/_markuxt/components/SectionTitle.vue +75 -0
  49. package/src/public/_markuxt/components/content/ProseImg.vue +29 -0
  50. package/src/public/_markuxt/components/content/ProsePre.vue +58 -0
  51. package/src/public/_markuxt/components/content/ProseVideo.vue +45 -0
  52. package/src/public/_markuxt/composables/resolveContentImage.ts +35 -0
  53. package/src/public/_markuxt/content-transformers/binary-assets.ts +20 -0
  54. package/src/public/_markuxt/error.vue +58 -0
  55. package/src/public/_markuxt/layouts/default.vue +37 -0
  56. package/src/public/_markuxt/middleware/navigation-guard.ts +22 -0
  57. package/src/public/_markuxt/pages/index.vue +232 -0
  58. package/src/public/_markuxt/pages/members/[...slug].vue +542 -0
  59. package/src/public/_markuxt/pages/members/index.vue +147 -0
  60. package/src/public/_markuxt/pages/news/[...slug].vue +280 -0
  61. package/src/public/_markuxt/pages/news/index.vue +102 -0
  62. package/src/public/_markuxt/pages/positions/[...slug].vue +425 -0
  63. package/src/public/_markuxt/pages/positions/index.vue +266 -0
  64. package/src/public/_markuxt/pages/projects/[...slug].vue +441 -0
  65. package/src/public/_markuxt/pages/projects/index.vue +499 -0
  66. package/src/public/_markuxt/pages/publications/[...slug].vue +435 -0
  67. package/src/public/_markuxt/pages/publications/index.vue +145 -0
  68. package/src/public/_markuxt/plugins/mathml-components.ts +33 -0
  69. package/src/public/_markuxt/plugins/suppress-warnings.ts +40 -0
  70. package/src/public/_markuxt/server/plugins/content-locale.ts +47 -0
  71. package/src/public/_markuxt/server/plugins/fix-content-anchors.ts +63 -0
  72. package/src/public/_markuxt/styles/_animations.css +99 -0
  73. package/src/public/_markuxt/styles/_base.css +31 -0
  74. package/src/public/_markuxt/styles/_code.css +109 -0
  75. package/src/public/_markuxt/styles/_components.css +109 -0
  76. package/src/public/_markuxt/styles/_layout.css +220 -0
  77. package/src/public/_markuxt/styles/_markdown.css +52 -0
  78. package/src/public/_markuxt/styles/_tokens.css +62 -0
  79. package/src/public/_markuxt/styles/_typography.css +144 -0
  80. package/src/public/_markuxt/styles/_utilities.css +110 -0
  81. package/src/public/_markuxt/styles/main.css +784 -0
  82. package/src/public/images/logo.png +0 -0
  83. package/src/server/plugins/content-locale.ts +47 -0
  84. package/src/server/plugins/fix-content-anchors.ts +63 -0
  85. package/src/styles/_animations.css +99 -0
  86. package/src/styles/_base.css +31 -0
  87. package/src/styles/_code.css +109 -0
  88. package/src/styles/_components.css +109 -0
  89. package/src/styles/_layout.css +220 -0
  90. package/src/styles/_markdown.css +52 -0
  91. package/src/styles/_tokens.css +62 -0
  92. package/src/styles/_typography.css +144 -0
  93. package/src/styles/_utilities.css +110 -0
  94. package/src/styles/main.css +784 -0
@@ -0,0 +1,435 @@
1
+ <template>
2
+ <main class="publication-page" v-if="publication">
3
+ <!-- Decorative background -->
4
+ <div class="publication__bg">
5
+ <div class="publication__pattern"></div>
6
+ <div class="publication__shapes">
7
+ <span class="publication__shape publication__shape--1"></span>
8
+ <span class="publication__shape publication__shape--2"></span>
9
+ </div>
10
+ </div>
11
+
12
+ <div class="container publication-page__container">
13
+ <!-- Back button -->
14
+ <NuxtLink to="/publications" class="publication__back">
15
+ <ArrowLeft class="icon-inline" theme="outline" :size="16" fill="currentColor" :stroke-width="2" />
16
+ {{ t('publications.backTo') }}
17
+ </NuxtLink>
18
+
19
+ <!-- Publication Header -->
20
+ <div class="publication-header animate-fade-in-up">
21
+ <div class="publication-header__meta">
22
+ <span class="publication-header__year">{{ publication.year }}</span>
23
+ <span v-if="publication.venue" class="badge badge-accent">{{ publication.venue }}</span>
24
+ </div>
25
+ <h1 class="publication-header__title">{{ publication.title }}</h1>
26
+ <p class="publication-header__authors">{{ formattedAuthors }}</p>
27
+ </div>
28
+
29
+ <!-- Publication Content -->
30
+ <div class="publication-content">
31
+ <!-- Abstract -->
32
+ <div v-if="publication.abstract || publication.body" class="publication-section animate-fade-in-up delay-200">
33
+ <div class="publication-section__header">
34
+ <FileStaff class="icon-inline" theme="outline" :size="20" fill="white" :stroke-width="2.8" />
35
+ <h3>{{ t('publications.abstract') }}</h3>
36
+ </div>
37
+ <div class="publication-section__body publication-section__body--content">
38
+ <ContentRenderer :value="publication" />
39
+ </div>
40
+ </div>
41
+
42
+ <!-- Keywords -->
43
+ <div v-if="publication.keywords && publication.keywords.length" class="publication-section animate-fade-in-up delay-300">
44
+ <div class="publication-section__header">
45
+ <Key class="icon-inline" theme="outline" :size="20" fill="white" :stroke-width="2.8" />
46
+ <h3>{{ t('publications.keywords') }}</h3>
47
+ </div>
48
+ <div class="publication-section__body">
49
+ <div class="keyword-tags">
50
+ <span v-for="keyword in publication.keywords" :key="keyword" class="keyword-tag">
51
+ {{ keyword }}
52
+ </span>
53
+ </div>
54
+ </div>
55
+ </div>
56
+
57
+ <!-- DOI Link -->
58
+ <div v-if="publication.doi" class="animate-fade-in-up delay-400">
59
+ <a
60
+ :href="publication.doi"
61
+ target="_blank"
62
+ rel="noopener"
63
+ class="doi-link"
64
+ >
65
+ <LinkOut class="icon-inline" theme="outline" :size="18" fill="currentColor" :stroke-width="2.8" />
66
+ {{ t('publications.viewOnPublisherSite') }}
67
+ </a>
68
+ </div>
69
+ </div>
70
+ </div>
71
+ </main>
72
+
73
+ <!-- Not Found -->
74
+ <div v-else class="not-found-page">
75
+ <div class="container">
76
+ <div class="not-found">
77
+ <Help class="icon-inline" theme="outline" :size="80" fill="var(--color-accent)" :stroke-width="3" />
78
+ <h1>{{ t('publications.notFound') }}</h1>
79
+ <p>{{ t('publications.notFoundDesc') }}</p>
80
+ <NuxtLink to="/publications" class="btn btn-primary">{{ t('publications.browseAll') }}</NuxtLink>
81
+ </div>
82
+ </div>
83
+ </div>
84
+ </template>
85
+
86
+ <script setup lang="ts">
87
+ import ArrowLeft from '@icon-park/vue-next/es/icons/ArrowLeft'
88
+ import FileStaff from '@icon-park/vue-next/es/icons/FileStaff'
89
+ import Key from '@icon-park/vue-next/es/icons/Key'
90
+ import LinkOut from '@icon-park/vue-next/es/icons/LinkOut'
91
+ import Help from '@icon-park/vue-next/es/icons/Help'
92
+
93
+ const { t } = useI18n()
94
+ const route = useRoute()
95
+
96
+ // Get publication by file path
97
+ // For catch-all route [...slug], params.slug is an array like ['2022', 'sensors-sensor-fusion']
98
+ const slug = computed(() => {
99
+ const slugParam = route.params.slug
100
+ return Array.isArray(slugParam) ? slugParam.join('/') : slugParam
101
+ })
102
+
103
+ const { data: publicationData } = await useAsyncData(`publication-${slug.value}`, async () => {
104
+ try {
105
+ const fullPath = `/publications/${slug.value}`
106
+ return await queryContent(fullPath).findOne()
107
+ } catch (e) {
108
+ console.error('Error fetching publication:', e)
109
+ return null
110
+ }
111
+ }, {
112
+ watch: [slug]
113
+ })
114
+
115
+ const publication = computed(() => publicationData.value)
116
+
117
+ // Provide content ID for ProseImg/ProseVideo to resolve relative asset paths
118
+ provide('contentId', computed(() => publication.value?._id || ''))
119
+
120
+ const formattedAuthors = computed(() => {
121
+ if (!publication.value?.authors) return ''
122
+ const authors = publication.value.authors
123
+ if (authors.length <= 2) return authors.join(t('publications.authorSep'))
124
+ return authors.slice(0, authors.length - 1).join(', ') + ', ' + t('publications.authorSep') + authors[authors.length - 1]
125
+ })
126
+
127
+ useHead({
128
+ title: computed(() => publication.value ? `${publication.value.title} - ${t('site.shortName')}` : 'Publication Not Found'),
129
+ meta: computed(() => {
130
+ const description = publication.value?.abstract || publication.value?.body
131
+ ? (typeof (publication.value.abstract || publication.value.body) === 'string'
132
+ ? (publication.value.abstract || publication.value.body).substring(0, 160).replace(/<[^>]*>/g, '')
133
+ : `${t('site.shortName')} publication`)
134
+ : `${t('site.shortName')} publication page`
135
+ return [
136
+ { name: 'description', content: description }
137
+ ]
138
+ })
139
+ })
140
+ </script>
141
+
142
+ <style scoped>
143
+ .publication-page {
144
+ min-height: 100vh;
145
+ position: relative;
146
+ }
147
+
148
+ /* Page Container with top padding for fixed header */
149
+ .publication-page__container {
150
+ padding-top: 100px;
151
+ }
152
+
153
+ /* Background */
154
+ .publication__bg {
155
+ position: fixed;
156
+ inset: 0;
157
+ background: linear-gradient(180deg, var(--color-bg-alt) 0%, var(--color-bg) 100%);
158
+ z-index: -2;
159
+ pointer-events: none;
160
+ }
161
+
162
+ .publication__pattern {
163
+ position: absolute;
164
+ inset: 0;
165
+ background-image:
166
+ linear-gradient(rgba(10, 37, 64, 0.03) 1px, transparent 1px),
167
+ linear-gradient(90deg, rgba(10, 37, 64, 0.03) 1px, transparent 1px);
168
+ background-size: 40px 40px;
169
+ }
170
+
171
+ .publication__shapes {
172
+ position: absolute;
173
+ inset: 0;
174
+ overflow: hidden;
175
+ }
176
+
177
+ .publication__shape {
178
+ position: absolute;
179
+ border-radius: 50%;
180
+ animation: float 8s ease-in-out infinite;
181
+ }
182
+
183
+ .publication__shape--1 {
184
+ width: 200px;
185
+ height: 200px;
186
+ top: 10%;
187
+ right: 8%;
188
+ background: linear-gradient(135deg, var(--color-accent) 0%, var(--color-secondary) 100%);
189
+ opacity: 0.12;
190
+ animation-delay: 0s;
191
+ }
192
+
193
+ .publication__shape--2 {
194
+ width: 120px;
195
+ height: 120px;
196
+ bottom: 20%;
197
+ left: 5%;
198
+ background: var(--color-primary);
199
+ opacity: 0.08;
200
+ animation-delay: 2.5s;
201
+ }
202
+
203
+ @keyframes float {
204
+ 0%, 100% {
205
+ transform: translateY(0) rotate(0deg);
206
+ }
207
+ 50% {
208
+ transform: translateY(-20px) rotate(3deg);
209
+ }
210
+ }
211
+
212
+ /* Back Button */
213
+ .publication__back {
214
+ display: inline-flex;
215
+ align-items: center;
216
+ gap: var(--spacing-sm);
217
+ padding: var(--spacing-sm) var(--spacing-md);
218
+ background: rgba(255, 255, 255, 0.9);
219
+ border: 1px solid var(--color-border);
220
+ border-radius: var(--radius-full);
221
+ color: var(--color-text);
222
+ font-size: 0.875rem;
223
+ font-weight: 500;
224
+ text-decoration: none;
225
+ transition: all var(--transition-base);
226
+ margin-bottom: var(--spacing-xl);
227
+ }
228
+
229
+ .publication__back:hover {
230
+ background: var(--color-primary);
231
+ border-color: var(--color-primary);
232
+ color: white;
233
+ transform: translateX(-3px);
234
+ }
235
+
236
+ /* Publication Header */
237
+ .publication-header {
238
+ margin-bottom: var(--spacing-3xl);
239
+ }
240
+
241
+ .publication-header__meta {
242
+ display: flex;
243
+ align-items: center;
244
+ gap: var(--spacing-sm);
245
+ margin-bottom: var(--spacing-md);
246
+ }
247
+
248
+ .publication-header__year {
249
+ font-family: var(--font-display);
250
+ font-size: 0.875rem;
251
+ font-weight: 600;
252
+ color: var(--color-text-muted);
253
+ letter-spacing: 0.05em;
254
+ }
255
+
256
+ .badge {
257
+ display: inline-flex;
258
+ align-items: center;
259
+ padding: var(--spacing-xs) var(--spacing-sm);
260
+ font-size: 0.75rem;
261
+ font-weight: 600;
262
+ letter-spacing: 0.08em;
263
+ text-transform: uppercase;
264
+ border-radius: var(--radius-sm);
265
+ }
266
+
267
+ .badge-accent {
268
+ background: var(--color-accent);
269
+ color: white;
270
+ }
271
+
272
+ .publication-header__title {
273
+ font-family: var(--font-display);
274
+ font-size: clamp(1.75rem, 4vw, 2.25rem);
275
+ font-weight: 800;
276
+ line-height: 1.15;
277
+ color: var(--color-primary);
278
+ margin-bottom: var(--spacing-md);
279
+ }
280
+
281
+ .publication-header__authors {
282
+ font-size: 1rem;
283
+ color: var(--color-text-muted);
284
+ line-height: 1.6;
285
+ }
286
+
287
+ /* Content Area */
288
+ .publication-content {
289
+ display: flex;
290
+ flex-direction: column;
291
+ gap: var(--spacing-xl);
292
+ margin-bottom: var(--spacing-3xl);
293
+ }
294
+
295
+ /* Publication Sections */
296
+ .publication-section {
297
+ background: var(--color-bg-alt);
298
+ border-radius: var(--radius-xl);
299
+ border: 1px solid var(--color-border);
300
+ box-shadow: var(--shadow-md);
301
+ overflow: hidden;
302
+ transition: all var(--transition-base);
303
+ }
304
+
305
+ .publication-section:hover {
306
+ box-shadow: var(--shadow-lg);
307
+ border-color: var(--color-secondary);
308
+ }
309
+
310
+ .publication-section__header {
311
+ display: flex;
312
+ align-items: center;
313
+ gap: var(--spacing-sm);
314
+ padding: var(--spacing-lg) var(--spacing-xl);
315
+ background: linear-gradient(135deg, var(--color-primary) 0%, var(--color-primary-light) 100%);
316
+ border-bottom: 1px solid var(--color-border);
317
+ }
318
+
319
+ .publication-section__header h3 {
320
+ font-family: var(--font-display);
321
+ font-size: 1.125rem;
322
+ font-weight: 600;
323
+ color: white;
324
+ margin: 0;
325
+ }
326
+
327
+ .publication-section__body {
328
+ padding: var(--spacing-xl);
329
+ }
330
+
331
+ .publication-section__body--content {
332
+ padding: var(--spacing-xl);
333
+ background: white;
334
+ }
335
+
336
+ /* Keywords */
337
+ .keyword-tags {
338
+ display: flex;
339
+ flex-wrap: wrap;
340
+ gap: var(--spacing-sm);
341
+ }
342
+
343
+ .keyword-tag {
344
+ display: inline-flex;
345
+ align-items: center;
346
+ padding: var(--spacing-xs) var(--spacing-md);
347
+ font-size: 0.875rem;
348
+ font-weight: 500;
349
+ color: var(--color-text);
350
+ background: var(--color-bg);
351
+ border: 1px solid var(--color-border);
352
+ border-radius: var(--radius-lg);
353
+ transition: all var(--transition-fast);
354
+ }
355
+
356
+ .keyword-tag:hover {
357
+ background: var(--color-secondary);
358
+ border-color: var(--color-secondary);
359
+ color: white;
360
+ transform: translateY(-2px);
361
+ box-shadow: var(--shadow-sm);
362
+ }
363
+
364
+ /* DOI Link */
365
+ .doi-link {
366
+ display: inline-flex;
367
+ align-items: center;
368
+ gap: var(--spacing-sm);
369
+ padding: var(--spacing-md) var(--spacing-lg);
370
+ background: var(--color-secondary);
371
+ color: white;
372
+ text-decoration: none;
373
+ border-radius: var(--radius-lg);
374
+ font-weight: 600;
375
+ transition: all var(--transition-base);
376
+ }
377
+
378
+ .doi-link:hover {
379
+ background: var(--color-accent);
380
+ transform: translateY(-2px);
381
+ box-shadow: var(--shadow-md);
382
+ }
383
+
384
+ /* ContentRenderer Markdown Styling */
385
+ .publication-section__body--content :deep(h2) {
386
+ font-family: var(--font-display);
387
+ font-size: 1.5rem;
388
+ font-weight: 700;
389
+ color: var(--color-primary);
390
+ margin-top: var(--spacing-lg);
391
+ margin-bottom: var(--spacing-md);
392
+ }
393
+
394
+ .publication-section__body--content :deep(p) {
395
+ font-size: 1rem;
396
+ line-height: 1.8;
397
+ color: var(--color-text);
398
+ margin-bottom: var(--spacing-md);
399
+ }
400
+
401
+ /* Not Found */
402
+ .not-found-page {
403
+ min-height: 60vh;
404
+ display: flex;
405
+ align-items: center;
406
+ }
407
+
408
+ .not-found {
409
+ text-align: center;
410
+ padding: var(--spacing-4xl) 0;
411
+ max-width: 400px;
412
+ margin: 0 auto;
413
+ }
414
+
415
+ .not-found h1 {
416
+ font-family: var(--font-display);
417
+ font-size: clamp(2rem, 4vw, 2.5rem);
418
+ font-weight: 700;
419
+ color: var(--color-primary);
420
+ margin-bottom: var(--spacing-md);
421
+ }
422
+
423
+ .not-found p {
424
+ font-size: 1rem;
425
+ color: var(--color-text-muted);
426
+ margin-bottom: var(--spacing-xl);
427
+ }
428
+
429
+ /* Responsive */
430
+ @media (max-width: 768px) {
431
+ .publication-content {
432
+ gap: var(--spacing-lg);
433
+ }
434
+ }
435
+ </style>
@@ -0,0 +1,145 @@
1
+ <template>
2
+ <div class="publications-page">
3
+ <div class="section">
4
+ <div class="container">
5
+ <SectionTitle
6
+ :overline="t('home.research')"
7
+ :title="t('nav.publications')"
8
+ :description="t('publications.description')"
9
+ />
10
+
11
+ <!-- Publications List Grouped by Year -->
12
+ <div class="publications-list" v-if="publicationsByYear.length > 0">
13
+ <div
14
+ v-for="yearGroup in publicationsByYear"
15
+ :key="yearGroup.year"
16
+ class="publication-year-group"
17
+ >
18
+ <h3 class="publication-year">{{ yearGroup.year }}</h3>
19
+ <div class="publication-year__grid">
20
+ <PublicationCard
21
+ v-for="pub in yearGroup.publications"
22
+ :key="pub._path || pub.title"
23
+ :publication="pub"
24
+ />
25
+ </div>
26
+ </div>
27
+ </div>
28
+
29
+ <p v-else class="no-results">
30
+ {{ t('publications.noResults') }}
31
+ </p>
32
+ </div>
33
+ </div>
34
+ </div>
35
+ </template>
36
+
37
+ <script setup lang="ts">
38
+ interface Publication {
39
+ title: string
40
+ authors: string[]
41
+ year: number
42
+ doi?: string
43
+ venue?: string
44
+ keywords?: string[]
45
+ abstract?: string
46
+ _path?: string
47
+ }
48
+
49
+ const { t } = useI18n()
50
+
51
+ // Fetch all publications
52
+ const { data: publications } = await useAsyncData('publications', () =>
53
+ queryContent('/publications')
54
+ .where({ _hidden: { $ne: true } })
55
+ .where({ _extension: 'md' }).find()
56
+ )
57
+
58
+ const processedPublications = computed(() => {
59
+ return (publications.value || []).map(pub => ({
60
+ ...pub,
61
+ title: pub.title ?? t('publications.untitled'),
62
+ authors: pub.authors || [],
63
+ year: pub.year || new Date().getFullYear(),
64
+ keywords: pub.keywords || [],
65
+ abstract: pub.description || pub.abstract
66
+ }))
67
+ })
68
+
69
+ // Group by year
70
+ const publicationsByYear = computed(() => {
71
+ const grouped: Record<number, Publication[]> = {}
72
+
73
+ for (const pub of processedPublications.value) {
74
+ if (!grouped[pub.year]) {
75
+ grouped[pub.year] = []
76
+ }
77
+ grouped[pub.year].push(pub)
78
+ }
79
+
80
+ return Object.entries(grouped)
81
+ .map(([year, pubs]) => ({
82
+ year: parseInt(year),
83
+ publications: pubs.sort((a, b) => a.title.localeCompare(b.title))
84
+ }))
85
+ .sort((a, b) => b.year - a.year)
86
+ })
87
+
88
+ useHead({
89
+ title: t('publications.pageTitle'),
90
+ meta: [
91
+ { name: 'description', content: t('publications.pageDescription') }
92
+ ]
93
+ })
94
+ </script>
95
+
96
+ <style scoped>
97
+ .publications-page {
98
+ padding-top: var(--spacing-xl);
99
+ }
100
+
101
+ .publications-list {
102
+ display: flex;
103
+ flex-direction: column;
104
+ gap: var(--spacing-3xl);
105
+ }
106
+
107
+ .publication-year-group {
108
+ display: flex;
109
+ flex-direction: column;
110
+ gap: var(--spacing-lg);
111
+ }
112
+
113
+ .publication-year {
114
+ font-family: var(--font-display);
115
+ font-size: 1.75rem;
116
+ font-weight: 700;
117
+ color: var(--color-primary);
118
+ padding-bottom: var(--spacing-sm);
119
+ border-bottom: 2px solid var(--color-border);
120
+ }
121
+
122
+ .publication-year__grid {
123
+ display: grid;
124
+ grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
125
+ gap: var(--spacing-lg);
126
+ margin-top: var(--spacing-lg);
127
+ }
128
+
129
+ .no-results {
130
+ text-align: center;
131
+ padding: var(--spacing-3xl);
132
+ font-size: 1.125rem;
133
+ color: var(--color-text-muted);
134
+ }
135
+
136
+ @media (max-width: 768px) {
137
+ .publication-year {
138
+ font-size: 1.5rem;
139
+ }
140
+
141
+ .publication-year__grid {
142
+ grid-template-columns: 1fr;
143
+ }
144
+ }
145
+ </style>
@@ -0,0 +1,33 @@
1
+ import { h, defineComponent } from 'vue'
2
+
3
+ /**
4
+ * KaTeX renders math as HTML + MathML. The MathML tags (<mi>, <mrow>, …)
5
+ * appear as PascalCase in MDC's rendered output (Mi, Mrow …). Vue tries to
6
+ * resolve them as components and warns when it can't.
7
+ *
8
+ * This plugin registers stub components that render the native MathML element
9
+ * with its children, suppressing the warnings while preserving accessibility
10
+ * (screen readers can still access the MathML tree).
11
+ */
12
+ const mathMLTags = [
13
+ 'math', 'mi', 'mrow', 'msub', 'msup', 'msubsup', 'mo', 'mn', 'mtext',
14
+ 'mfrac', 'mspace', 'mstyle', 'mpadded', 'mphantom', 'menclose', 'munder',
15
+ 'mover', 'munderover', 'mmultiscripts', 'mtable', 'mtr', 'mtd', 'maction',
16
+ 'annotation', 'semantics', 'annotation-xml', 'msqrt', 'mroot', 'mfenced',
17
+ 'ms', 'mprescripts'
18
+ ]
19
+
20
+ export default defineNuxtPlugin((nuxtApp) => {
21
+ for (const tag of mathMLTags) {
22
+ const pascal = tag.charAt(0).toUpperCase() + tag.slice(1)
23
+ // PascalCase component renders native lowercase element
24
+ const component = defineComponent({
25
+ name: pascal,
26
+ inheritAttrs: true,
27
+ setup(_, { attrs, slots }) {
28
+ return () => h(tag, attrs, slots.default?.())
29
+ }
30
+ })
31
+ nuxtApp.vueApp.component(pascal, component)
32
+ }
33
+ })
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Suppress known benign Vue / Nuxt / DevTools warnings in the browser console.
3
+ *
4
+ * Covers:
5
+ * – Suspense experimental: Vue 3 core uses console.warn directly (not
6
+ * vueApp.config.warnHandler), so we patch it.
7
+ * – Extraneous non-props attrs: Nuxt DevTools internal VueElement component.
8
+ * – Hydration mismatch: Common Nuxt dev-mode SSR hydration noise
9
+ * (page transitions, HMR, etc.).
10
+ */
11
+ const SUPPRESSED = [
12
+ 'Suspense is an experimental feature',
13
+ 'Extraneous non-props attributes',
14
+ 'Hydration',
15
+ 'hydration',
16
+ ]
17
+
18
+ const originalWarn = console.warn
19
+
20
+ function shouldSuppress(msg: string) {
21
+ return SUPPRESSED.some(p => msg.includes(p))
22
+ }
23
+
24
+ // Patch console.warn – Vue core logs the Suspense warning through console.warn
25
+ // directly (not via the warnHandler), so we must intercept it at the console level.
26
+ console.warn = (...args: any[]) => {
27
+ const text = args.map(a => typeof a === 'string' ? a : '').join(' ')
28
+ if (shouldSuppress(text)) return
29
+ originalWarn.apply(console, args)
30
+ }
31
+
32
+ export default defineNuxtPlugin((nuxtApp) => {
33
+ // Also hook into Vue's warning system for framework-generated warnings
34
+ // (e.g. hydration mismatches go through warnHandler, not console.warn).
35
+ nuxtApp.vueApp.config.warnHandler = (msg) => {
36
+ const text = typeof msg === 'string' ? msg : ''
37
+ if (shouldSuppress(text)) return
38
+ originalWarn.apply(console, [msg])
39
+ }
40
+ })