@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.
- package/LICENSE +201 -0
- package/README.md +168 -0
- package/app.config.d.ts +33 -0
- package/nuxt.config.ts +170 -0
- package/package.json +43 -0
- package/src/components/AppFooter.vue +225 -0
- package/src/components/AppHeader.vue +342 -0
- package/src/components/Hero.vue +438 -0
- package/src/components/Icon.vue +131 -0
- package/src/components/LanguageSwitcher.vue +71 -0
- package/src/components/MemberCard.vue +198 -0
- package/src/components/MembersGrid.vue +129 -0
- package/src/components/MermaidDiagram.vue +99 -0
- package/src/components/NewsCard.vue +119 -0
- package/src/components/PublicationCard.vue +245 -0
- package/src/components/SectionTitle.vue +75 -0
- package/src/components/content/ProseImg.vue +29 -0
- package/src/components/content/ProsePre.vue +58 -0
- package/src/components/content/ProseVideo.vue +45 -0
- package/src/composables/resolveContentImage.ts +35 -0
- package/src/content-transformers/binary-assets.ts +20 -0
- package/src/error.vue +58 -0
- package/src/layouts/default.vue +37 -0
- package/src/middleware/navigation-guard.ts +22 -0
- package/src/pages/index.vue +232 -0
- package/src/pages/members/[...slug].vue +542 -0
- package/src/pages/members/index.vue +147 -0
- package/src/pages/news/[...slug].vue +280 -0
- package/src/pages/news/index.vue +102 -0
- package/src/pages/positions/[...slug].vue +425 -0
- package/src/pages/positions/index.vue +266 -0
- package/src/pages/projects/[...slug].vue +441 -0
- package/src/pages/projects/index.vue +499 -0
- package/src/pages/publications/[...slug].vue +435 -0
- package/src/pages/publications/index.vue +145 -0
- package/src/plugins/mathml-components.ts +33 -0
- package/src/plugins/suppress-warnings.ts +40 -0
- package/src/public/_markuxt/components/AppFooter.vue +225 -0
- package/src/public/_markuxt/components/AppHeader.vue +342 -0
- package/src/public/_markuxt/components/Hero.vue +430 -0
- package/src/public/_markuxt/components/Icon.vue +131 -0
- package/src/public/_markuxt/components/LanguageSwitcher.vue +71 -0
- package/src/public/_markuxt/components/MemberCard.vue +198 -0
- package/src/public/_markuxt/components/MembersGrid.vue +129 -0
- package/src/public/_markuxt/components/MermaidDiagram.vue +99 -0
- package/src/public/_markuxt/components/NewsCard.vue +119 -0
- package/src/public/_markuxt/components/PublicationCard.vue +245 -0
- package/src/public/_markuxt/components/SectionTitle.vue +75 -0
- package/src/public/_markuxt/components/content/ProseImg.vue +29 -0
- package/src/public/_markuxt/components/content/ProsePre.vue +58 -0
- package/src/public/_markuxt/components/content/ProseVideo.vue +45 -0
- package/src/public/_markuxt/composables/resolveContentImage.ts +35 -0
- package/src/public/_markuxt/content-transformers/binary-assets.ts +20 -0
- package/src/public/_markuxt/error.vue +58 -0
- package/src/public/_markuxt/layouts/default.vue +37 -0
- package/src/public/_markuxt/middleware/navigation-guard.ts +22 -0
- package/src/public/_markuxt/pages/index.vue +232 -0
- package/src/public/_markuxt/pages/members/[...slug].vue +542 -0
- package/src/public/_markuxt/pages/members/index.vue +147 -0
- package/src/public/_markuxt/pages/news/[...slug].vue +280 -0
- package/src/public/_markuxt/pages/news/index.vue +102 -0
- package/src/public/_markuxt/pages/positions/[...slug].vue +425 -0
- package/src/public/_markuxt/pages/positions/index.vue +266 -0
- package/src/public/_markuxt/pages/projects/[...slug].vue +441 -0
- package/src/public/_markuxt/pages/projects/index.vue +499 -0
- package/src/public/_markuxt/pages/publications/[...slug].vue +435 -0
- package/src/public/_markuxt/pages/publications/index.vue +145 -0
- package/src/public/_markuxt/plugins/mathml-components.ts +33 -0
- package/src/public/_markuxt/plugins/suppress-warnings.ts +40 -0
- package/src/public/_markuxt/server/plugins/content-locale.ts +47 -0
- package/src/public/_markuxt/server/plugins/fix-content-anchors.ts +63 -0
- package/src/public/_markuxt/styles/_animations.css +99 -0
- package/src/public/_markuxt/styles/_base.css +31 -0
- package/src/public/_markuxt/styles/_code.css +109 -0
- package/src/public/_markuxt/styles/_components.css +109 -0
- package/src/public/_markuxt/styles/_layout.css +220 -0
- package/src/public/_markuxt/styles/_markdown.css +52 -0
- package/src/public/_markuxt/styles/_tokens.css +62 -0
- package/src/public/_markuxt/styles/_typography.css +144 -0
- package/src/public/_markuxt/styles/_utilities.css +110 -0
- package/src/public/_markuxt/styles/main.css +784 -0
- package/src/public/images/logo.png +0 -0
- package/src/server/plugins/content-locale.ts +47 -0
- package/src/server/plugins/fix-content-anchors.ts +63 -0
- package/src/styles/_animations.css +99 -0
- package/src/styles/_base.css +31 -0
- package/src/styles/_code.css +109 -0
- package/src/styles/_components.css +109 -0
- package/src/styles/_layout.css +220 -0
- package/src/styles/_markdown.css +52 -0
- package/src/styles/_tokens.css +62 -0
- package/src/styles/_typography.css +144 -0
- package/src/styles/_utilities.css +110 -0
- 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
|
+
})
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// Content locale detection from filename suffixes.
|
|
2
|
+
//
|
|
3
|
+
// Recognizes locale from Markdown filenames:
|
|
4
|
+
// - `page.md` → locale 'en' (default)
|
|
5
|
+
// - `page.en.md` → locale 'en'
|
|
6
|
+
// - `page.en-US.md` → locale 'en'
|
|
7
|
+
// - `page.zh.md` → locale 'zh-CN'
|
|
8
|
+
// - `page.zh-CN.md` → locale 'zh-CN'
|
|
9
|
+
//
|
|
10
|
+
// Sets `file.locale` and normalizes `_path` so locale variants
|
|
11
|
+
// share the same path (e.g. `salman-ijaz.zh-CN.md` → `/members/staff/salman-ijaz`).
|
|
12
|
+
|
|
13
|
+
const LOCALE_PATTERN = /\.(en|en-US|zh|zh-CN)\.md$/
|
|
14
|
+
|
|
15
|
+
// Normalize locale codes to the canonical form used by @nuxtjs/i18n
|
|
16
|
+
function normalizeLocale(raw: string): string {
|
|
17
|
+
if (raw === 'en-US' || raw === 'en') return 'en'
|
|
18
|
+
if (raw === 'zh' || raw === 'zh-CN') return 'zh-CN'
|
|
19
|
+
return raw
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export default defineNitroPlugin((nitroApp) => {
|
|
23
|
+
nitroApp.hooks.hook('content:file:afterParse', (file: any) => {
|
|
24
|
+
if (!file?._id || typeof file._id !== 'string') return
|
|
25
|
+
|
|
26
|
+
// Only process Markdown files
|
|
27
|
+
if (!file._id.endsWith('.md')) return
|
|
28
|
+
|
|
29
|
+
const match = file._id.match(LOCALE_PATTERN)
|
|
30
|
+
|
|
31
|
+
if (match) {
|
|
32
|
+
file.locale = normalizeLocale(match[1])
|
|
33
|
+
|
|
34
|
+
// Strip locale suffix from _path so variants share the same route
|
|
35
|
+
// e.g. `/members/staff/salman-ijaz-zh-cn` → `/members/staff/salman-ijaz`
|
|
36
|
+
if (file._path) {
|
|
37
|
+
file._path = file._path.replace(
|
|
38
|
+
new RegExp(`-(?:en|en-US|zh|zh-CN)$`),
|
|
39
|
+
''
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
} else {
|
|
43
|
+
// No locale suffix = default locale
|
|
44
|
+
file.locale = 'en'
|
|
45
|
+
}
|
|
46
|
+
})
|
|
47
|
+
})
|