@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,499 @@
1
+ <template>
2
+ <div class="projects-page">
3
+ <div class="section">
4
+ <div class="container">
5
+ <SectionTitle
6
+ :overline="t('home.research')"
7
+ :title="t('nav.projects')"
8
+ :description="t('projects.description')"
9
+ />
10
+
11
+ <!-- Status Filter -->
12
+ <div class="status-filter" v-if="projectList.length > 0">
13
+ <button
14
+ v-for="filter in statusFilters"
15
+ :key="filter.key"
16
+ class="status-filter__btn"
17
+ :class="{ 'status-filter__btn--active': activeFilter === filter.key }"
18
+ @click="activeFilter = filter.key"
19
+ >
20
+ <span class="status-filter__dot" :class="`status-dot--${filter.key}`"></span>
21
+ {{ filter.name }}
22
+ <span class="tooltip">{{ filter.tooltip }}</span>
23
+ </button>
24
+ </div>
25
+
26
+ <!-- Projects Grid -->
27
+ <div class="projects-grid" v-if="filteredProjects.length > 0">
28
+ <NuxtLink
29
+ v-for="project in filteredProjects"
30
+ :key="project._path"
31
+ :to="project._path || ''"
32
+ class="project-card"
33
+ >
34
+ <div class="project-card__image-wrapper" v-if="project.image">
35
+ <img :src="getProjectImage(project.image, project._id)" :alt="project.title" class="project-card__image" />
36
+ </div>
37
+ <div class="project-card__image-placeholder" v-else>
38
+ <component :is="getStatusIcon(project.status)" class="icon-inline" theme="outline" :size="48" fill="currentColor" :stroke-width="1.5" />
39
+ </div>
40
+ <div class="project-card__content">
41
+ <div class="project-card__header">
42
+ <h3 class="project-card__title">{{ project.title }}</h3>
43
+ <span v-if="project.status" class="status-badge" :class="`status-badge--${project.status}`">
44
+ <span class="status-badge__dot"></span>
45
+ {{ statusLabels[project.status as ProjectStatus] || project.status }}
46
+ <span class="tooltip tooltip--badge">{{ statusTooltips[project.status as ProjectStatus] }}</span>
47
+ </span>
48
+ </div>
49
+ <p class="project-card__description" v-if="project.description">
50
+ {{ project.description }}
51
+ </p>
52
+ <div class="project-card__meta" v-if="project.year || project.funded">
53
+ <span v-if="project.year" class="project-card__year">{{ project.year }}</span>
54
+ <span v-if="project.funded" class="project-card__funded">
55
+ <Funds class="icon-inline" theme="outline" :size="14" fill="currentColor" :stroke-width="3" />
56
+ {{ t('projects.funded') }}
57
+ <span class="tooltip tooltip--badge">{{ t('projects.fundedTooltip') }}</span>
58
+ </span>
59
+ </div>
60
+ </div>
61
+ </NuxtLink>
62
+ </div>
63
+
64
+ <div class="projects-placeholder" v-else-if="projectList.length === 0">
65
+ <div class="placeholder-icon">
66
+ <Experiment class="icon-inline" theme="outline" :size="64" fill="var(--color-accent)" :stroke-width="2" />
67
+ </div>
68
+ <h3>{{ t('projects.comingSoon') }}</h3>
69
+ <p>{{ t('projects.comingSoonDesc') }}</p>
70
+ <NuxtLink to="/publications" class="btn btn-secondary">{{ t('projects.viewPublications') }}</NuxtLink>
71
+ </div>
72
+
73
+ <div class="no-results" v-else>
74
+ <p>{{ t('projects.noResults') }}</p>
75
+ </div>
76
+ </div>
77
+ </div>
78
+ </div>
79
+ </template>
80
+
81
+ <script setup lang="ts">
82
+ import Experiment from '@icon-park/vue-next/es/icons/Experiment'
83
+ import Funds from '@icon-park/vue-next/es/icons/Funds'
84
+ import Lightning from '@icon-park/vue-next/es/icons/Lightning'
85
+ import Timer from '@icon-park/vue-next/es/icons/Timer'
86
+ import CheckOne from '@icon-park/vue-next/es/icons/CheckOne'
87
+ import Setting from '@icon-park/vue-next/es/icons/Setting'
88
+ import Help from '@icon-park/vue-next/es/icons/Help'
89
+
90
+ const { t } = useI18n()
91
+
92
+ interface Project {
93
+ title: string
94
+ description?: string
95
+ status?: string
96
+ year?: number
97
+ image?: string
98
+ funded?: boolean
99
+ _path?: string
100
+ _id?: string
101
+ }
102
+
103
+ const config = useRuntimeConfig()
104
+
105
+ type ProjectStatus = 'open' | 'ongoing' | 'completed' | 'maintained'
106
+
107
+ // Status definitions with labels and meanings
108
+ const statusLabels = computed(() => ({
109
+ 'open': t('projects.statusOpen'),
110
+ 'ongoing': t('projects.statusOngoing'),
111
+ 'completed': t('projects.statusCompleted'),
112
+ 'maintained': t('projects.statusMaintained')
113
+ }) satisfies Record<ProjectStatus, string>)
114
+
115
+ const statusTooltips = computed(() => ({
116
+ 'open': t('projects.tooltipOpen'),
117
+ 'ongoing': t('projects.tooltipOngoing'),
118
+ 'completed': t('projects.tooltipCompleted'),
119
+ 'maintained': t('projects.tooltipMaintained')
120
+ }) satisfies Record<ProjectStatus, string>)
121
+
122
+ const statusFilters = computed(() => [
123
+ { key: 'all', name: t('projects.filterAll'), tooltip: t('projects.tooltipAll') },
124
+ { key: 'open', name: t('projects.statusOpen'), tooltip: t('projects.tooltipOpen') },
125
+ { key: 'ongoing', name: t('projects.statusOngoing'), tooltip: t('projects.tooltipOngoing') },
126
+ { key: 'completed', name: t('projects.statusCompleted'), tooltip: t('projects.tooltipCompleted') },
127
+ { key: 'maintained', name: t('projects.statusMaintained'), tooltip: t('projects.tooltipMaintained') }
128
+ ])
129
+
130
+ const activeFilter = ref('all')
131
+
132
+ // Icon mapping for status
133
+ const statusIconMap: Record<string, any> = {
134
+ 'open': Lightning,
135
+ 'ongoing': Timer,
136
+ 'completed': CheckOne,
137
+ 'maintained': Setting
138
+ }
139
+
140
+ const getStatusIcon = (status?: string) => {
141
+ return statusIconMap[status || ''] || Help
142
+ }
143
+
144
+ // Fetch projects
145
+ const { data: projects } = await useAsyncData('projects', () =>
146
+ queryContent('/projects')
147
+ .where({ _hidden: { $ne: true } })
148
+ .where({ _extension: 'md' }).find()
149
+ )
150
+
151
+ const projectList = computed(() => projects.value ?? [])
152
+
153
+ const filteredProjects = computed(() => {
154
+ if (activeFilter.value === 'all') {
155
+ return projectList.value
156
+ }
157
+ return projectList.value.filter(p => p.status === activeFilter.value)
158
+ })
159
+
160
+ // Handle image paths: resolve relative content paths, then apply base URL
161
+ const getProjectImage = (imagePath?: string, contentId?: string) => {
162
+ const resolved = resolveContentImage(imagePath, contentId)
163
+ if (!resolved) return ''
164
+ const basePath = config.app.baseURL || ''
165
+ if (!basePath || basePath === '/') return resolved
166
+ return basePath + resolved
167
+ }
168
+
169
+ useHead({
170
+ title: t('projects.pageTitle'),
171
+ meta: [
172
+ { name: 'description', content: t('projects.pageDescription') }
173
+ ]
174
+ })
175
+ </script>
176
+
177
+ <style scoped>
178
+ .projects-page {
179
+ padding-top: var(--spacing-xl);
180
+ }
181
+
182
+ /* Status Filter */
183
+ .status-filter {
184
+ display: flex;
185
+ flex-wrap: wrap;
186
+ gap: var(--spacing-sm);
187
+ margin-bottom: var(--spacing-2xl);
188
+ padding: var(--spacing-sm);
189
+ background: var(--color-bg-alt);
190
+ border-radius: var(--radius-xl);
191
+ border: 1px solid var(--color-border);
192
+ }
193
+
194
+ .status-filter__btn {
195
+ display: inline-flex;
196
+ align-items: center;
197
+ gap: var(--spacing-xs);
198
+ padding: var(--spacing-xs) var(--spacing-md);
199
+ font-family: var(--font-body);
200
+ font-size: 0.875rem;
201
+ font-weight: 500;
202
+ color: var(--color-text-muted);
203
+ background: transparent;
204
+ border: none;
205
+ border-radius: var(--radius-full);
206
+ cursor: pointer;
207
+ transition: all var(--transition-fast);
208
+ position: relative;
209
+ }
210
+
211
+ .status-filter__btn:hover {
212
+ background: rgba(255, 255, 255, 0.6);
213
+ color: var(--color-text);
214
+ }
215
+
216
+ .status-filter__btn--active {
217
+ background: white;
218
+ color: var(--color-primary);
219
+ box-shadow: var(--shadow-sm);
220
+ }
221
+
222
+ .status-filter__dot {
223
+ width: 8px;
224
+ height: 8px;
225
+ border-radius: 50%;
226
+ flex-shrink: 0;
227
+ }
228
+
229
+ .status-dot--all { background: var(--color-text-muted); }
230
+ .status-dot--open { background: #f59e0b; }
231
+ .status-dot--ongoing { background: var(--color-accent); }
232
+ .status-dot--completed { background: var(--color-secondary); }
233
+ .status-dot--maintained { background: #8b5cf6; }
234
+
235
+ /* Projects Grid */
236
+ .projects-grid {
237
+ display: grid;
238
+ grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
239
+ gap: var(--spacing-xl);
240
+ }
241
+
242
+ .project-card {
243
+ background: var(--color-bg-alt);
244
+ border-radius: var(--radius-xl);
245
+ overflow: hidden;
246
+ border: 1px solid var(--color-border);
247
+ transition: all var(--transition-base);
248
+ text-decoration: none;
249
+ display: flex;
250
+ flex-direction: column;
251
+ }
252
+
253
+ .project-card:hover {
254
+ border-color: var(--color-secondary);
255
+ box-shadow: var(--shadow-xl);
256
+ transform: translateY(-4px);
257
+ }
258
+
259
+ .project-card__image-wrapper {
260
+ position: relative;
261
+ width: 100%;
262
+ aspect-ratio: 16 / 9;
263
+ overflow: hidden;
264
+ background: linear-gradient(135deg, var(--color-primary) 0%, var(--color-primary-light) 100%);
265
+ }
266
+
267
+ .project-card__image {
268
+ width: 100%;
269
+ height: 100%;
270
+ object-fit: cover;
271
+ transition: transform var(--transition-slow);
272
+ }
273
+
274
+ .project-card:hover .project-card__image {
275
+ transform: scale(1.05);
276
+ }
277
+
278
+ .project-card__image-placeholder {
279
+ width: 100%;
280
+ aspect-ratio: 16 / 9;
281
+ display: flex;
282
+ align-items: center;
283
+ justify-content: center;
284
+ background: linear-gradient(135deg, var(--color-primary) 0%, var(--color-primary-light) 100%);
285
+ color: rgba(255, 255, 255, 0.3);
286
+ }
287
+
288
+ .project-card__content {
289
+ padding: var(--spacing-lg);
290
+ display: flex;
291
+ flex-direction: column;
292
+ flex: 1;
293
+ }
294
+
295
+ .project-card__header {
296
+ display: flex;
297
+ align-items: flex-start;
298
+ justify-content: space-between;
299
+ gap: var(--spacing-sm);
300
+ margin-bottom: var(--spacing-sm);
301
+ }
302
+
303
+ .project-card__title {
304
+ font-family: var(--font-display);
305
+ font-size: 1.25rem;
306
+ font-weight: 600;
307
+ color: var(--color-primary);
308
+ line-height: 1.3;
309
+ flex: 1;
310
+ }
311
+
312
+ .project-card__description {
313
+ font-size: 0.9375rem;
314
+ line-height: 1.6;
315
+ color: var(--color-text-muted);
316
+ margin-bottom: var(--spacing-md);
317
+ flex: 1;
318
+ }
319
+
320
+ .project-card__meta {
321
+ display: flex;
322
+ align-items: center;
323
+ gap: var(--spacing-md);
324
+ margin-top: auto;
325
+ }
326
+
327
+ .project-card__year {
328
+ font-size: 0.8125rem;
329
+ color: var(--color-text-muted);
330
+ }
331
+
332
+ .project-card__funded {
333
+ display: inline-flex;
334
+ align-items: center;
335
+ gap: var(--spacing-xs);
336
+ font-size: 0.8125rem;
337
+ font-weight: 500;
338
+ color: var(--color-accent);
339
+ }
340
+
341
+ /* Status Badges */
342
+ .status-badge {
343
+ display: inline-flex;
344
+ align-items: center;
345
+ gap: var(--spacing-xs);
346
+ padding: var(--spacing-xs) var(--spacing-sm);
347
+ font-size: 0.7rem;
348
+ font-weight: 700;
349
+ letter-spacing: 0.08em;
350
+ text-transform: uppercase;
351
+ border-radius: var(--radius-full);
352
+ white-space: nowrap;
353
+ flex-shrink: 0;
354
+ }
355
+
356
+ .status-badge__dot {
357
+ width: 6px;
358
+ height: 6px;
359
+ border-radius: 50%;
360
+ animation: pulse 2s ease-in-out infinite;
361
+ }
362
+
363
+ @keyframes pulse {
364
+ 0%, 100% { opacity: 1; }
365
+ 50% { opacity: 0.5; }
366
+ }
367
+
368
+ /* Open - recruiting, no funding */
369
+ .status-badge--open {
370
+ background: #fef3c7;
371
+ color: #92400e;
372
+ }
373
+ .status-badge--open .status-badge__dot {
374
+ background: #f59e0b;
375
+ }
376
+
377
+ /* Ongoing - recruiting with funding */
378
+ .status-badge--ongoing {
379
+ background: #dbeafe;
380
+ color: #1e40af;
381
+ }
382
+ .status-badge--ongoing .status-badge__dot {
383
+ background: var(--color-accent);
384
+ animation: pulse 1.5s ease-in-out infinite;
385
+ }
386
+
387
+ /* Completed - not recruiting */
388
+ .status-badge--completed {
389
+ background: #d1fae5;
390
+ color: #065f46;
391
+ }
392
+ .status-badge--completed .status-badge__dot {
393
+ background: var(--color-secondary);
394
+ animation: none;
395
+ }
396
+
397
+ /* Maintained - still maintained */
398
+ .status-badge--maintained {
399
+ background: #ede9fe;
400
+ color: #5b21b6;
401
+ }
402
+ .status-badge--maintained .status-badge__dot {
403
+ background: #8b5cf6;
404
+ animation: none;
405
+ }
406
+
407
+ /* No results */
408
+ .no-results {
409
+ text-align: center;
410
+ padding: var(--spacing-3xl);
411
+ color: var(--color-text-muted);
412
+ }
413
+
414
+ /* Placeholder */
415
+ .projects-placeholder {
416
+ text-align: center;
417
+ padding: var(--spacing-4xl) var(--spacing-lg);
418
+ background: var(--color-bg-alt);
419
+ border-radius: var(--radius-2xl);
420
+ border: 1px dashed var(--color-border);
421
+ }
422
+
423
+ .placeholder-icon {
424
+ margin-bottom: var(--spacing-lg);
425
+ }
426
+
427
+ .projects-placeholder h3 {
428
+ font-family: var(--font-display);
429
+ font-size: 1.75rem;
430
+ font-weight: 600;
431
+ color: var(--color-primary);
432
+ margin-bottom: var(--spacing-md);
433
+ }
434
+
435
+ .projects-placeholder p {
436
+ font-size: 1.0625rem;
437
+ color: var(--color-text-muted);
438
+ max-width: 500px;
439
+ margin: 0 auto var(--spacing-xl);
440
+ }
441
+
442
+ @media (max-width: 768px) {
443
+ .projects-grid {
444
+ grid-template-columns: 1fr;
445
+ }
446
+
447
+ .status-filter {
448
+ justify-content: center;
449
+ }
450
+
451
+ .project-card__header {
452
+ flex-direction: column;
453
+ gap: var(--spacing-xs);
454
+ }
455
+ }
456
+
457
+ /* Tooltip Styles */
458
+ .tooltip {
459
+ position: absolute;
460
+ bottom: calc(100% + 8px);
461
+ left: 50%;
462
+ transform: translateX(-50%);
463
+ padding: var(--spacing-xs) var(--spacing-sm);
464
+ background: var(--color-primary);
465
+ color: white;
466
+ font-size: 0.75rem;
467
+ font-weight: 500;
468
+ white-space: nowrap;
469
+ border-radius: var(--radius-sm);
470
+ box-shadow: var(--shadow-lg);
471
+ opacity: 0;
472
+ visibility: hidden;
473
+ transition: all var(--transition-fast);
474
+ z-index: 100;
475
+ pointer-events: none;
476
+ }
477
+
478
+ .tooltip::after {
479
+ content: '';
480
+ position: absolute;
481
+ top: 100%;
482
+ left: 50%;
483
+ transform: translateX(-50%);
484
+ border: 6px solid transparent;
485
+ border-top-color: var(--color-primary);
486
+ }
487
+
488
+ .status-filter__btn:hover .tooltip,
489
+ .status-badge:hover .tooltip,
490
+ .project-card__funded:hover .tooltip {
491
+ opacity: 1;
492
+ visibility: visible;
493
+ }
494
+
495
+ /* Badge tooltip - positioned above */
496
+ .tooltip--badge {
497
+ bottom: calc(100% + 6px);
498
+ }
499
+ </style>