@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,198 @@
1
+ <template>
2
+ <NuxtLink :to="memberLink" class="member-card">
3
+ <div class="member-card__image-wrapper">
4
+ <img
5
+ v-if="imageUrl"
6
+ :src="imageUrl"
7
+ :alt="member.name"
8
+ class="member-card__image"
9
+ loading="lazy"
10
+ />
11
+ <div class="member-card__overlay">
12
+ <a
13
+ v-if="member.email"
14
+ :href="`mailto:${member.email}`"
15
+ class="member-card__action"
16
+ :aria-label="t('members.sendEmail')"
17
+ @click.stop
18
+ >
19
+ <Mail class="icon-inline" theme="outline" :size="20" fill="currentColor" :stroke-width="3.6" />
20
+ </a>
21
+ <a
22
+ v-if="member.scholar"
23
+ :href="member.scholar"
24
+ target="_blank"
25
+ rel="noopener"
26
+ class="member-card__action"
27
+ :aria-label="t('members.googleScholar')"
28
+ @click.stop
29
+ >
30
+ <Google class="icon-inline" theme="outline" :size="20" fill="currentColor" :stroke-width="2.5" />
31
+ </a>
32
+ </div>
33
+ </div>
34
+ <div class="member-card__content">
35
+ <h3 class="member-card__name">{{ member.name }}</h3>
36
+ <p class="member-card__role">{{ member.role || member.title }}</p>
37
+ <p v-if="member.interests && member.interests.length" class="member-card__interests">
38
+ {{ formattedInterests }}
39
+ </p>
40
+ </div>
41
+ </NuxtLink>
42
+ </template>
43
+
44
+ <script setup lang="ts">
45
+ import { computed } from 'vue'
46
+ import Mail from '@icon-park/vue-next/es/icons/Mail'
47
+ import Google from '@icon-park/vue-next/es/icons/Google'
48
+
49
+ interface Member {
50
+ name: string
51
+ role?: string
52
+ title?: string
53
+ email?: string
54
+ scholar?: string
55
+ image?: string
56
+ interests?: string[]
57
+ category?: string
58
+ order?: number
59
+ _path?: string // Nuxt Content route path (e.g., "/members/staff/salman-ijaz")
60
+ _id?: string // Nuxt Content internal ID (e.g., "content:members:staff:salman-ijaz.md")
61
+ }
62
+
63
+ interface Props {
64
+ member: Member
65
+ }
66
+
67
+ const props = defineProps<Props>()
68
+
69
+ const { t } = useI18n()
70
+ const config = useRuntimeConfig()
71
+
72
+ const imageUrl = computed(() => {
73
+ const resolved = resolveContentImage(props.member.image, props.member._id)
74
+ if (!resolved) return ''
75
+
76
+ const basePath = config.app.baseURL || ''
77
+ if (!basePath || basePath === '/') return resolved
78
+ return basePath + resolved
79
+ })
80
+
81
+ const formattedInterests = computed(() => {
82
+ if (!props.member.interests) return ''
83
+ return props.member.interests.slice(0, 3).join(' · ')
84
+ })
85
+
86
+ // Generate link to member detail page
87
+ const memberLink = computed(() => {
88
+ if (props.member._path) {
89
+ // _path is like "/members/staff/salman-ijaz"
90
+ // Use it directly as route
91
+ return props.member._path
92
+ }
93
+ // Fallback: generate from name with /members/ prefix
94
+ return `/members/${props.member.name?.toLowerCase().replace(/\s+/g, '-') || ''}`
95
+ })
96
+ </script>
97
+
98
+ <style scoped>
99
+ .member-card {
100
+ background: var(--color-bg-alt);
101
+ border-radius: var(--radius-xl);
102
+ overflow: hidden;
103
+ box-shadow: var(--shadow-md);
104
+ border: 1px solid var(--color-border);
105
+ transition: all var(--transition-base);
106
+ text-decoration: none;
107
+ display: block;
108
+ }
109
+
110
+ .member-card:hover {
111
+ transform: translateY(-8px) rotate(1deg);
112
+ box-shadow: var(--shadow-xl);
113
+ border-color: var(--color-secondary);
114
+ }
115
+
116
+ .member-card__image-wrapper {
117
+ position: relative;
118
+ width: 100%;
119
+ aspect-ratio: 3 / 4;
120
+ overflow: hidden;
121
+ background: linear-gradient(135deg, var(--color-primary) 0%, var(--color-primary-light) 100%);
122
+ }
123
+
124
+ .member-card__image {
125
+ width: 100%;
126
+ height: 100%;
127
+ object-fit: cover;
128
+ object-position: center top;
129
+ transition: transform var(--transition-slow);
130
+ }
131
+
132
+ .member-card:hover .member-card__image {
133
+ transform: scale(1.05);
134
+ }
135
+
136
+ .member-card__overlay {
137
+ position: absolute;
138
+ bottom: 0;
139
+ left: 0;
140
+ right: 0;
141
+ padding: var(--spacing-md);
142
+ background: linear-gradient(transparent, rgba(10, 37, 64, 0.9));
143
+ display: flex;
144
+ gap: var(--spacing-sm);
145
+ opacity: 0;
146
+ transform: translateY(100%);
147
+ transition: all var(--transition-base);
148
+ }
149
+
150
+ .member-card:hover .member-card__overlay {
151
+ opacity: 1;
152
+ transform: translateY(0);
153
+ }
154
+
155
+ .member-card__action {
156
+ display: flex;
157
+ align-items: center;
158
+ justify-content: center;
159
+ width: 40px;
160
+ height: 40px;
161
+ background: white;
162
+ border-radius: var(--radius-full);
163
+ color: var(--color-primary);
164
+ transition: all var(--transition-fast);
165
+ }
166
+
167
+ .member-card__action:hover {
168
+ background: var(--color-accent);
169
+ color: white;
170
+ transform: scale(1.1);
171
+ }
172
+
173
+ .member-card__content {
174
+ padding: var(--spacing-lg);
175
+ }
176
+
177
+ .member-card__name {
178
+ font-family: var(--font-display);
179
+ font-size: 1.25rem;
180
+ font-weight: 600;
181
+ color: var(--color-primary);
182
+ margin-bottom: var(--spacing-xs);
183
+ line-height: 1.3;
184
+ }
185
+
186
+ .member-card__role {
187
+ font-size: 0.875rem;
188
+ font-weight: 500;
189
+ color: var(--color-secondary);
190
+ margin-bottom: var(--spacing-sm);
191
+ }
192
+
193
+ .member-card__interests {
194
+ font-size: 0.8125rem;
195
+ color: var(--color-text-muted);
196
+ line-height: 1.5;
197
+ }
198
+ </style>
@@ -0,0 +1,129 @@
1
+ <template>
2
+ <section class="members-section" v-if="members.length > 0">
3
+ <div v-for="(category, index) in categorizedMembers" :key="category.name" class="members-category">
4
+ <h3 class="members-category__title" v-if="groupBy">{{ category.name }}</h3>
5
+ <div class="members-grid">
6
+ <MemberCard
7
+ v-for="member in category.members"
8
+ :key="member.slug"
9
+ :member="member"
10
+ />
11
+ </div>
12
+ </div>
13
+ </section>
14
+ </template>
15
+
16
+ <script setup lang="ts">
17
+ interface Member {
18
+ name: string
19
+ slug: string
20
+ role?: string
21
+ title?: string
22
+ email?: string
23
+ scholar?: string
24
+ image?: string
25
+ interests?: string[]
26
+ category?: string
27
+ order?: number
28
+ _path?: string
29
+ _id?: string
30
+ [key: string]: any // Preserve all Nuxt Content fields
31
+ }
32
+
33
+ interface Props {
34
+ members: Member[]
35
+ groupBy?: boolean
36
+ }
37
+
38
+ const props = withDefaults(defineProps<Props>(), {
39
+ groupBy: true
40
+ })
41
+
42
+ const { t } = useI18n()
43
+
44
+ // Category display names
45
+ const categoryNames = computed(() => ({
46
+ staff: t('members.staff'),
47
+ 'research-students': t('members.researchStudents'),
48
+ 'research-assistants': t('members.researchAssistants'),
49
+ alumni: t('members.alumni')
50
+ }))
51
+
52
+ // Category sort order
53
+ const categoryOrder = ['staff', 'research-students', 'research-assistants', 'alumni']
54
+
55
+ // Sort members by category order, then by order field
56
+ const sortedMembers = computed(() => {
57
+ return [...props.members].sort((a, b) => {
58
+ const catOrderA = categoryOrder.indexOf(a.category || '')
59
+ const catOrderB = categoryOrder.indexOf(b.category || '')
60
+ if (catOrderA !== catOrderB) {
61
+ return catOrderA - catOrderB
62
+ }
63
+ return (a.order || 999) - (b.order || 999)
64
+ })
65
+ })
66
+
67
+ const categorizedMembers = computed(() => {
68
+ if (!props.groupBy) {
69
+ return [{
70
+ name: t('members.section'),
71
+ members: sortedMembers.value
72
+ }]
73
+ }
74
+
75
+ const categories: Record<string, Member[]> = {}
76
+
77
+ for (const member of sortedMembers.value) {
78
+ const cat = member.category || 'staff'
79
+ if (!categories[cat]) {
80
+ categories[cat] = []
81
+ }
82
+ categories[cat].push(member)
83
+ }
84
+
85
+ // Return categories in predefined order
86
+ return categoryOrder
87
+ .filter(key => categories[key] && categories[key].length > 0)
88
+ .map(key => ({
89
+ name: categoryNames.value[key] || key,
90
+ members: categories[key]
91
+ }))
92
+ })
93
+ </script>
94
+
95
+ <style scoped>
96
+ .members-section {
97
+ display: flex;
98
+ flex-direction: column;
99
+ gap: var(--spacing-3xl);
100
+ }
101
+
102
+ .members-category {
103
+ display: flex;
104
+ flex-direction: column;
105
+ gap: var(--spacing-xl);
106
+ }
107
+
108
+ .members-category__title {
109
+ font-family: var(--font-display);
110
+ font-size: 1.75rem;
111
+ font-weight: 700;
112
+ color: var(--color-primary);
113
+ padding-bottom: var(--spacing-sm);
114
+ border-bottom: 2px solid var(--color-border);
115
+ }
116
+
117
+ .members-grid {
118
+ display: grid;
119
+ grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
120
+ gap: var(--spacing-xl);
121
+ }
122
+
123
+ @media (max-width: 640px) {
124
+ .members-grid {
125
+ grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
126
+ gap: var(--spacing-lg);
127
+ }
128
+ }
129
+ </style>
@@ -0,0 +1,99 @@
1
+ <template>
2
+ <div class="mermaid-diagram">
3
+ <!-- Rendered SVG is injected here once mermaid resolves on the client -->
4
+ <div v-if="svg" class="mermaid-diagram__svg" v-html="svg" />
5
+ <!-- Fallback: show the raw source until hydration renders the diagram -->
6
+ <pre v-else class="mermaid-diagram__source"><code>{{ code }}</code></pre>
7
+ </div>
8
+ </template>
9
+
10
+ <script setup lang="ts">
11
+ const props = defineProps<{ code: string }>()
12
+
13
+ const svg = ref('')
14
+
15
+ // A unique id per diagram avoids mermaid's internal id collisions when
16
+ // multiple diagrams render on the same page.
17
+ const renderId = `mermaid-${Math.random().toString(36).slice(2, 10)}`
18
+
19
+ async function renderDiagram() {
20
+ if (!props.code) return
21
+ try {
22
+ // Wait for fonts so mermaid measures text correctly
23
+ await document.fonts.ready
24
+
25
+ const mermaid = (await import('mermaid')).default
26
+ mermaid.initialize({
27
+ startOnLoad: false,
28
+ securityLevel: 'strict',
29
+ theme: 'base',
30
+ fontFamily: 'var(--font-body), sans-serif',
31
+ htmlLabels: true,
32
+ themeVariables: {
33
+ primaryColor: '#e6f7fb',
34
+ primaryBorderColor: '#009bc1',
35
+ primaryTextColor: '#0a2540',
36
+ lineColor: '#64748b',
37
+ fontSize: '15px',
38
+ },
39
+ flowchart: {
40
+ htmlLabels: true,
41
+ useMaxWidth: false,
42
+ padding: 18,
43
+ },
44
+ })
45
+ const { svg: rendered } = await mermaid.render(renderId, props.code)
46
+ svg.value = rendered
47
+ } catch (err) {
48
+ console.error('[MermaidDiagram] render failed:', err)
49
+ }
50
+ }
51
+
52
+ // Mermaid touches the DOM, so only run in the browser after mount.
53
+ onMounted(renderDiagram)
54
+ watch(() => props.code, renderDiagram)
55
+ </script>
56
+
57
+ <style scoped>
58
+ .mermaid-diagram {
59
+ margin: var(--spacing-lg) 0;
60
+ padding: var(--spacing-lg);
61
+ background: var(--color-bg-alt);
62
+ border: 1px solid var(--color-border);
63
+ border-radius: var(--radius-lg);
64
+ overflow-x: auto;
65
+ text-align: center;
66
+ }
67
+
68
+ .mermaid-diagram__svg :deep(svg) {
69
+ height: auto;
70
+ }
71
+
72
+ /* Prevent text clipping inside mermaid nodes */
73
+ .mermaid-diagram__svg :deep(.node) {
74
+ overflow: visible;
75
+ }
76
+
77
+ .mermaid-diagram__svg :deep(.node rect),
78
+ .mermaid-diagram__svg :deep(.node polygon),
79
+ .mermaid-diagram__svg :deep(.node circle) {
80
+ overflow: visible;
81
+ }
82
+
83
+ .mermaid-diagram__svg :deep(.nodeLabel) {
84
+ overflow: visible;
85
+ white-space: nowrap;
86
+ }
87
+
88
+ .mermaid-diagram__svg :deep(foreignObject) {
89
+ overflow: visible;
90
+ }
91
+
92
+ .mermaid-diagram__source {
93
+ margin: 0;
94
+ text-align: left;
95
+ color: var(--color-text-muted);
96
+ font-size: 0.85rem;
97
+ white-space: pre-wrap;
98
+ }
99
+ </style>
@@ -0,0 +1,119 @@
1
+ <template>
2
+ <article class="news-card">
3
+ <time class="news-card__date" :datetime="news.date">
4
+ {{ formattedDate }}
5
+ </time>
6
+ <NuxtLink :to="news._path" class="news-card__link">
7
+ <h3 class="news-card__title">{{ news.title }}</h3>
8
+ </NuxtLink>
9
+ <p class="news-card__excerpt" v-if="news.description">
10
+ {{ news.description }}
11
+ </p>
12
+ <div class="news-card__tags" v-if="news.tags && news.tags.length">
13
+ <span v-for="tag in news.tags.slice(0, 3)" :key="tag" class="news-card__tag">
14
+ {{ tag }}
15
+ </span>
16
+ </div>
17
+ </article>
18
+ </template>
19
+
20
+ <script setup lang="ts">
21
+ // Accept any Nuxt content properties
22
+ interface NewsItem {
23
+ title?: string
24
+ date?: string
25
+ description?: string
26
+ tags?: string[]
27
+ _path?: string
28
+ key?: string
29
+ }
30
+
31
+ interface Props {
32
+ news: NewsItem
33
+ }
34
+
35
+ const props = defineProps<Props>()
36
+
37
+ const { locale } = useI18n()
38
+
39
+ const formattedDate = computed(() => {
40
+ const date = props.news.date ? new Date(props.news.date) : new Date()
41
+ return date.toLocaleDateString(locale.value === 'zh-CN' ? 'zh-CN' : 'en-US', {
42
+ year: 'numeric',
43
+ month: 'short',
44
+ day: 'numeric'
45
+ })
46
+ })
47
+ </script>
48
+
49
+ <style scoped>
50
+ .news-card {
51
+ background: var(--color-bg-alt);
52
+ border-radius: var(--radius-lg);
53
+ padding: var(--spacing-lg);
54
+ border: 1px solid var(--color-border);
55
+ transition: all var(--transition-base);
56
+ display: flex;
57
+ flex-direction: column;
58
+ gap: var(--spacing-sm);
59
+ }
60
+
61
+ .news-card:hover {
62
+ border-color: var(--color-secondary);
63
+ box-shadow: var(--shadow-lg);
64
+ transform: translateY(-4px);
65
+ }
66
+
67
+ .news-card__date {
68
+ font-family: var(--font-display);
69
+ font-size: 0.8125rem;
70
+ font-weight: 500;
71
+ color: var(--color-secondary);
72
+ text-transform: uppercase;
73
+ letter-spacing: 0.05em;
74
+ }
75
+
76
+ .news-card__link {
77
+ text-decoration: none;
78
+ }
79
+
80
+ .news-card__title {
81
+ font-family: var(--font-display);
82
+ font-size: 1.125rem;
83
+ font-weight: 600;
84
+ line-height: 1.4;
85
+ color: var(--color-primary);
86
+ margin: 0;
87
+ transition: color var(--transition-fast);
88
+ }
89
+
90
+ .news-card:hover .news-card__title {
91
+ color: var(--color-secondary);
92
+ }
93
+
94
+ .news-card__excerpt {
95
+ font-size: 0.9375rem;
96
+ line-height: 1.6;
97
+ color: var(--color-text-muted);
98
+ display: -webkit-box;
99
+ -webkit-line-clamp: 2;
100
+ line-clamp: 2;
101
+ -webkit-box-orient: vertical;
102
+ overflow: hidden;
103
+ }
104
+
105
+ .news-card__tags {
106
+ display: flex;
107
+ flex-wrap: wrap;
108
+ gap: var(--spacing-xs);
109
+ margin-top: auto;
110
+ }
111
+
112
+ .news-card__tag {
113
+ font-size: 0.75rem;
114
+ padding: 2px 8px;
115
+ background: var(--color-bg);
116
+ color: var(--color-text-muted);
117
+ border-radius: var(--radius-sm);
118
+ }
119
+ </style>