@voidzero-dev/vitepress-theme 2.0.0 → 2.1.0

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 (79) hide show
  1. package/package.json +7 -6
  2. package/src/aliases.js +14 -0
  3. package/src/vitepress/assets/clients/clickup.svg +5 -0
  4. package/src/vitepress/assets/clients/stripe.svg +3 -0
  5. package/src/vitepress/components/oss/Footer.vue +4 -21
  6. package/src/vitepress/components/oss/Header.vue +82 -180
  7. package/src/vitepress/components/oss/Sponsors.vue +3 -3
  8. package/src/vitepress/components/oss/TopBanner.vue +20 -79
  9. package/src/vitepress/components/oss/TrustedBy.vue +1 -1
  10. package/src/vitepress/components/vite/Community.vue +3 -3
  11. package/src/vitepress/components/vite/FeatureGrid1.vue +63 -0
  12. package/src/vitepress/components/vite/{FeatureGrid.vue → FeatureGrid2.vue} +8 -10
  13. package/src/vitepress/components/vite/Hero.vue +6 -15
  14. package/src/vitepress/components/vitepress-default/VPDocOutlineItem.vue +2 -2
  15. package/src/vitepress/components/vitepress-default/VPFlyout.vue +1 -1
  16. package/src/vitepress/components/vitepress-default/VPMenuLink.vue +1 -1
  17. package/src/vitepress/components/vitepress-default/VPNavBarMenuLink.vue +1 -1
  18. package/src/vitepress/components/vitepress-default/VPSidebarItem.vue +1 -1
  19. package/src/vitepress/components/vitepress-default/VPSocialLink.vue +1 -2
  20. package/src/vitepress/fonts/APK-Protocol-Semi-Bold.woff2 +0 -0
  21. package/src/vitepress/fonts/inter-italic-cyrillic-ext.woff2 +0 -0
  22. package/src/vitepress/fonts/inter-italic-cyrillic.woff2 +0 -0
  23. package/src/vitepress/fonts/inter-italic-greek-ext.woff2 +0 -0
  24. package/src/vitepress/fonts/inter-italic-greek.woff2 +0 -0
  25. package/src/vitepress/fonts/inter-italic-latin-ext.woff2 +0 -0
  26. package/src/vitepress/fonts/inter-italic-latin.woff2 +0 -0
  27. package/src/vitepress/fonts/inter-italic-vietnamese.woff2 +0 -0
  28. package/src/vitepress/fonts/inter-roman-cyrillic-ext.woff2 +0 -0
  29. package/src/vitepress/fonts/inter-roman-cyrillic.woff2 +0 -0
  30. package/src/vitepress/fonts/inter-roman-greek-ext.woff2 +0 -0
  31. package/src/vitepress/fonts/inter-roman-greek.woff2 +0 -0
  32. package/src/vitepress/fonts/inter-roman-latin-ext.woff2 +0 -0
  33. package/src/vitepress/fonts/inter-roman-latin.woff2 +0 -0
  34. package/src/vitepress/fonts/inter-roman-vietnamese.woff2 +0 -0
  35. package/src/vitepress/index.ts +64 -230
  36. package/src/vitepress/layouts/VPLayout.vue +2 -17
  37. package/src/vitepress/styles/tokens.css +194 -10
  38. package/src/vitepress/types/theme-context.ts +33 -0
  39. package/src/vitepress/assets/clients/beehiiv.svg +0 -30
  40. package/src/vitepress/assets/clients/excalidraw.svg +0 -82
  41. package/src/vitepress/assets/clients/get-your-guide.svg +0 -1
  42. package/src/vitepress/assets/clients/posthog.svg +0 -1
  43. package/src/vitepress/assets/clients/ramp.svg +0 -1
  44. package/src/vitepress/assets/clients/shopee.svg +0 -55
  45. package/src/vitepress/components/vite/FeaturePanel1.vue +0 -41
  46. package/src/vitepress/components/vite/FeaturePanel2.vue +0 -37
  47. package/src/vitepress/components/vite/FeaturePanel3.vue +0 -43
  48. package/src/vitepress/components/vite/FeaturePanel4.vue +0 -46
  49. package/src/vitepress/components/voidzero/Footer.vue +0 -65
  50. package/src/vitepress/components/voidzero/Header.vue +0 -560
  51. package/src/vitepress/components/voidzero/Megamenu.vue +0 -190
  52. package/src/vitepress/components/voidzero/about/CareerCTA.vue +0 -56
  53. package/src/vitepress/components/voidzero/about/Hero.vue +0 -206
  54. package/src/vitepress/components/voidzero/about/Investors.vue +0 -112
  55. package/src/vitepress/components/voidzero/about/TeamGrid.vue +0 -161
  56. package/src/vitepress/components/voidzero/about/TeamSectionHeading.vue +0 -13
  57. package/src/vitepress/components/voidzero/blog/BlogArchive.vue +0 -223
  58. package/src/vitepress/components/voidzero/blog/BlogSingleContent.vue +0 -364
  59. package/src/vitepress/components/voidzero/blog/BlogSingleHero.vue +0 -113
  60. package/src/vitepress/components/voidzero/blog/BlogSingleRelated.vue +0 -92
  61. package/src/vitepress/components/voidzero/blog/FeaturedArticles.vue +0 -146
  62. package/src/vitepress/components/voidzero/blog/types.ts +0 -56
  63. package/src/vitepress/components/voidzero/home/CaseStudySlider.vue +0 -235
  64. package/src/vitepress/components/voidzero/home/CustomersSectionHeading.vue +0 -5
  65. package/src/vitepress/components/voidzero/home/GitHubStats.vue +0 -27
  66. package/src/vitepress/components/voidzero/home/Hero.vue +0 -69
  67. package/src/vitepress/components/voidzero/home/Investors.vue +0 -30
  68. package/src/vitepress/components/voidzero/home/NewsletterCTA.vue +0 -23
  69. package/src/vitepress/components/voidzero/home/OpenSourceSectionHeading.vue +0 -6
  70. package/src/vitepress/components/voidzero/home/OpenSourceSectionProjects.vue +0 -419
  71. package/src/vitepress/components/voidzero/home/Resources.vue +0 -144
  72. package/src/vitepress/components/voidzero/home/Statistics.vue +0 -507
  73. package/src/vitepress/components/voidzero/home/StatisticsSectionHeading.vue +0 -5
  74. package/src/vitepress/components/voidzero/home/TeamCTA.vue +0 -17
  75. package/src/vitepress/components/voidzero/home/TrustedBy.vue +0 -248
  76. package/src/vitepress/components/voidzero/home/VitePlusSectionFeatures.vue +0 -55
  77. package/src/vitepress/components/voidzero/home/VitePlusSectionHeading.vue +0 -17
  78. package/src/vitepress/fonts/KHTeka-Medium.woff2 +0 -0
  79. package/src/vitepress/fonts/KHTeka-Regular.woff2 +0 -0
@@ -1,364 +0,0 @@
1
- <script setup lang="ts">
2
- import {ref, onMounted, onUnmounted, nextTick, computed} from 'vue'
3
- import {Content, onContentUpdated} from 'vitepress'
4
-
5
- interface OutlineItem {
6
- title: string
7
- link: string
8
- level: number
9
- children?: OutlineItem[]
10
- }
11
-
12
- const headers = ref<OutlineItem[]>([])
13
- const activeId = ref<string | null>(null)
14
- const activeIndex = ref<number>(0)
15
- const contentRef = ref<HTMLElement | null>(null)
16
- const markerRef = ref<HTMLElement | null>(null)
17
- const navRef = ref<HTMLElement | null>(null)
18
-
19
- function getHeaders() {
20
- // Query headers from within the blog content
21
- const headings = document.querySelectorAll('.blog-content h1, .blog-content h2, .blog-content h3, .blog-content h4')
22
-
23
- const items: OutlineItem[] = []
24
- headings.forEach((el) => {
25
- if (el.id && el.textContent) {
26
- items.push({
27
- title: el.textContent.replace(/^#\s*/, '').trim(),
28
- link: `#${el.id}`,
29
- level: parseInt(el.tagName[1])
30
- })
31
- }
32
- })
33
-
34
- return items
35
- }
36
-
37
- function updateActiveHeader() {
38
- const headings = document.querySelectorAll('.blog-content h1, .blog-content h2, .blog-content h3, .blog-content h4')
39
- if (headings.length === 0) return
40
-
41
- const viewportCenter = window.innerHeight / 2
42
- let closestHeading: Element | null = null
43
- let closestDistance = Infinity
44
- let closestIndex = 0
45
-
46
- headings.forEach((heading, index) => {
47
- if (heading.id) {
48
- const rect = heading.getBoundingClientRect()
49
- const headingCenter = rect.top + rect.height / 2
50
- const distance = Math.abs(headingCenter - viewportCenter)
51
-
52
- // Prefer headings that are above or at the center
53
- const adjustedDistance = rect.top <= viewportCenter ? distance : distance + 1000
54
-
55
- if (adjustedDistance < closestDistance) {
56
- closestDistance = adjustedDistance
57
- closestHeading = heading
58
- closestIndex = index
59
- }
60
- }
61
- })
62
-
63
- if (closestHeading) {
64
- activeId.value = closestHeading.id
65
- activeIndex.value = closestIndex
66
- updateMarkerPosition()
67
- }
68
- }
69
-
70
- function scrollToHeader(link: string) {
71
- const id = link.slice(1)
72
- const element = document.getElementById(id)
73
- if (element) {
74
- const top = element.getBoundingClientRect().top + window.scrollY - (window.innerHeight / 2)
75
- window.scrollTo({ top, behavior: 'smooth' })
76
- }
77
- }
78
-
79
- function updateMarkerPosition() {
80
- if (!navRef.value || !markerRef.value) return
81
-
82
- const activeLink = navRef.value.querySelector('.outline-link.active') as HTMLElement
83
- if (activeLink) {
84
- const navRect = navRef.value.getBoundingClientRect()
85
- const linkRect = activeLink.getBoundingClientRect()
86
- const reducedHeight = linkRect.height * 0.6
87
- const topOffset = (linkRect.height - reducedHeight) / 2
88
- markerRef.value.style.top = `${linkRect.top - navRect.top + topOffset}px`
89
- markerRef.value.style.height = `${reducedHeight}px`
90
- markerRef.value.style.opacity = '1'
91
- } else {
92
- markerRef.value.style.opacity = '0'
93
- }
94
- }
95
-
96
- onContentUpdated(() => {
97
- nextTick(() => {
98
- headers.value = getHeaders()
99
- // Set first header as active initially
100
- if (headers.value.length > 0 && !activeId.value) {
101
- activeId.value = headers.value[0].link.slice(1)
102
- activeIndex.value = 0
103
- }
104
- nextTick(updateMarkerPosition)
105
- })
106
- })
107
-
108
- onMounted(() => {
109
- nextTick(() => {
110
- headers.value = getHeaders()
111
- // Set first header as active initially
112
- if (headers.value.length > 0) {
113
- activeId.value = headers.value[0].link.slice(1)
114
- activeIndex.value = 0
115
- }
116
- nextTick(updateMarkerPosition)
117
- })
118
- window.addEventListener('scroll', updateActiveHeader, {passive: true})
119
- })
120
-
121
- onUnmounted(() => {
122
- window.removeEventListener('scroll', updateActiveHeader)
123
- })
124
- </script>
125
-
126
- <template>
127
- <section class="wrapper wrapper--ticks border-t">
128
- <div class="flex flex-col lg:flex-row">
129
- <!-- Left: Table of Contents -->
130
- <div class="hidden lg:block px-6 lg:pl-10 lg:pr-10 py-20 w-full max-w-md flex-shrink-0">
131
- <div class="sticky top-24">
132
- <nav v-if="headers.length > 0" ref="navRef" class="outline-nav">
133
- <div ref="markerRef" class="outline-marker"></div>
134
- <ul class="outline-list">
135
- <li v-for="header in headers" :key="header.link">
136
- <a
137
- href="javascript:void(0)"
138
- class="outline-link"
139
- :class="{
140
- active: activeId === header.link.slice(1),
141
- 'pl-4': header.level === 3,
142
- 'pl-8': header.level === 4
143
- }"
144
- @click="scrollToHeader(header.link)"
145
- >
146
- {{ header.title }}
147
- </a>
148
- </li>
149
- </ul>
150
- </nav>
151
- </div>
152
- </div>
153
-
154
- <!-- Right: Blog Content (matches image width from hero) -->
155
- <div ref="contentRef" class="blog-content relative w-full lg:w-120 xl:w-200 shrink-0 px-6 lg:px-0 lg:pr-30 py-10 lg:py-16" data-theme="light">
156
- <div class="prose prose-lg vp-doc max-w-none lg:-ml-10 lg:-mr-20 lg:pr-10 prose-headings:text-primary prose-headings:font-medium prose-p:text-nickel prose-strong:text-nickel prose-li:text-nickel prose-li:marker:text-ruby prose-th:text-primary prose-td:text-primary">
157
- <Content />
158
- </div>
159
- </div>
160
- </div>
161
- </section>
162
- </template>
163
-
164
- <style scoped>
165
- /* Outline styles */
166
- .outline-nav {
167
- position: relative;
168
- }
169
-
170
- .outline-marker {
171
- position: absolute;
172
- left: -2.5rem;
173
- width: 1px;
174
- background-color: var(--color-primary);
175
- opacity: 0;
176
- transition: top 0.2s ease, height 0.2s ease, opacity 0.2s ease;
177
- }
178
-
179
- .outline-list {
180
- list-style: none;
181
- padding: 0;
182
- margin: 0;
183
- }
184
-
185
- .outline-link {
186
- display: block;
187
- padding: 0.375rem 0;
188
- font-size: 1rem;
189
- font-weight: 400;
190
- color: var(--color-grey);
191
- transition: color 0.2s;
192
- font-family: var(--font-sans);
193
- letter-spacing: -0.02em;
194
- white-space: nowrap;
195
- overflow: hidden;
196
- text-overflow: ellipsis;
197
- max-width: calc(100% - 5rem);
198
- }
199
-
200
- .outline-link:hover {
201
- color: var(--color-primary);
202
- }
203
-
204
- .outline-link.active {
205
- color: var(--color-primary);
206
- font-weight: 500;
207
- }
208
-
209
- /* Content border - offset to the left */
210
- @media (min-width: 1024px) {
211
- .blog-content::before {
212
- content: '';
213
- position: absolute;
214
- left: -7.5rem;
215
- top: 0;
216
- bottom: 0;
217
- width: 1px;
218
- background-color: var(--color-stroke);
219
- }
220
-
221
- /* Images stretch to the border edges */
222
- .blog-content :deep(img) {
223
- margin-left: -5rem;
224
- margin-right: -5rem;
225
- max-width: calc(100% + 10rem);
226
- width: calc(100% + 10rem);
227
- }
228
-
229
- /* Dividers stretch to the border edges */
230
- .blog-content :deep(hr) {
231
- margin-left: -5rem;
232
- margin-right: -5rem;
233
- max-width: calc(100% + 10rem);
234
- width: calc(100% + 10rem);
235
- border-color: var(--color-stroke);
236
- }
237
-
238
- /* Blockquotes - stretch to edges with top/bottom borders */
239
- .blog-content :deep(blockquote) {
240
- margin-left: -5rem;
241
- margin-right: -5rem;
242
- padding: 5rem 5rem;
243
- max-width: calc(100% + 10rem);
244
- width: calc(100% + 10rem);
245
- border-left: none;
246
- border-top: 1px solid var(--color-stroke);
247
- border-bottom: 1px solid var(--color-stroke);
248
- font-style: normal;
249
- quotes: none;
250
- position: relative;
251
- }
252
-
253
- .blog-content :deep(blockquote)::before {
254
- content: '/\A**\A*\A*\A*\A*\A*/';
255
- white-space: pre;
256
- position: absolute;
257
- left: 2rem;
258
- top: 50%;
259
- transform: translateY(-50%);
260
- font-family: var(--font-mono);
261
- font-size: 1.25rem;
262
- line-height: 1.4;
263
- color: var(--color-stroke);
264
- opacity: 0.5;
265
- }
266
-
267
- .blog-content :deep(blockquote p) {
268
- font-size: 1.5rem;
269
- line-height: 1.5;
270
- color: var(--color-primary);
271
- font-weight: 400;
272
- }
273
- }
274
-
275
- /* Prose styles - base variables for colors */
276
- .prose {
277
- --tw-prose-body: var(--vp-c-text-1);
278
- --tw-prose-bold: var(--vp-c-text-1);
279
- --tw-prose-counters: var(--vp-c-text-2);
280
- --tw-prose-bullets: var(--vp-c-text-2);
281
- --tw-prose-hr: var(--vp-c-divider);
282
- --tw-prose-quotes: var(--vp-c-text-2);
283
- --tw-prose-quote-borders: var(--vp-c-divider);
284
- --tw-prose-captions: var(--vp-c-text-2);
285
- --tw-prose-th-borders: var(--vp-c-divider);
286
- --tw-prose-td-borders: var(--vp-c-divider);
287
- }
288
-
289
- /* Reset prose styles for code blocks and code groups - let VitePress handle them */
290
- /* Inline code (e.g., `backticks`) keeps prose styling */
291
- .prose :deep(div[class*='language-']),
292
- .prose :deep(.vp-code-group) {
293
- margin: 16px -1.5rem;
294
- padding: 0;
295
- border-radius: 0;
296
- color: inherit;
297
- font-size: inherit;
298
- }
299
-
300
- /* Code blocks inside code groups - reset their negative margins since parent is already full-bleed */
301
- .prose :deep(.vp-code-group div[class*='language-']) {
302
- margin-left: 0 !important;
303
- margin-right: 0 !important;
304
- }
305
-
306
- /* Add internal padding for full-bleed code blocks on mobile */
307
- .prose :deep(div[class*='language-'] code),
308
- .prose :deep(.vp-code-group div[class*='language-'] code) {
309
- padding-left: 1.5rem !important;
310
- padding-right: 1.5rem !important;
311
- }
312
-
313
- .prose :deep(.vp-code-group .tabs) {
314
- padding-left: 1.5rem;
315
- padding-right: 1.5rem;
316
- margin-left: 0;
317
- margin-right: 0;
318
- }
319
-
320
- @media (min-width: 1024px) {
321
- .prose :deep(div[class*='language-']),
322
- .prose :deep(.vp-code-group) {
323
- margin: 16px 0;
324
- border-radius: 8px;
325
- }
326
-
327
- .prose :deep(div[class*='language-'] code),
328
- .prose :deep(.vp-code-group div[class*='language-'] code) {
329
- padding-left: 24px !important;
330
- padding-right: 24px !important;
331
- }
332
-
333
- .prose :deep(.vp-code-group .tabs) {
334
- padding-left: 12px;
335
- padding-right: 12px;
336
- }
337
- }
338
-
339
- .prose pre {
340
- margin: 0;
341
- padding: 0;
342
- border-radius: 0;
343
- overflow: visible;
344
- color: inherit;
345
- font-size: inherit;
346
- line-height: inherit;
347
- }
348
-
349
- .prose pre code {
350
- padding: 0;
351
- border-radius: 0;
352
- color: inherit;
353
- font-size: inherit;
354
- font-weight: inherit;
355
- line-height: inherit;
356
- border: none;
357
- }
358
-
359
- /* Remove Tailwind Typography backticks around inline code */
360
- .prose :deep(code)::before,
361
- .prose :deep(code)::after {
362
- content: none;
363
- }
364
- </style>
@@ -1,113 +0,0 @@
1
- <script setup lang="ts">
2
- import {computed} from 'vue'
3
- import {useData, withBase} from 'vitepress'
4
- import {type Author, getAuthorImage, formatDateShort} from './types'
5
-
6
- const {frontmatter} = useData()
7
-
8
- const coverUrl = computed(() => {
9
- if (!frontmatter.value.cover) return null
10
- const cover = frontmatter.value.cover
11
- // Ensure path is absolute from root
12
- const path = cover.startsWith('/') ? cover : `/${cover}`
13
- return withBase(path)
14
- })
15
-
16
- const pageUrl = computed(() => {
17
- if (typeof window !== 'undefined') {
18
- return window.location.href
19
- }
20
- return ''
21
- })
22
-
23
- function copyLink() {
24
- if (typeof navigator !== 'undefined') {
25
- navigator.clipboard.writeText(window.location.href)
26
- }
27
- }
28
- </script>
29
-
30
- <template>
31
- <section class="wrapper wrapper--ticks border-t">
32
- <!-- Main hero: two columns -->
33
- <div class="flex flex-col lg:flex-row py-10 pt-20 pb-16">
34
- <!-- Left: Content -->
35
- <div class="flex flex-col justify-between px-6 lg:pl-10 lg:pr-20 flex-1 min-w-0">
36
- <div class="flex flex-col gap-4">
37
- <span class="text-grey text-xs font-medium font-mono uppercase tracking-wide">// {{
38
- frontmatter.category
39
- }}</span>
40
- <h1 class="text-3xl md:text-4xl xl:text-5xl text-pretty text-primary font-normal leading-tight">
41
- {{ frontmatter.title }}
42
- </h1>
43
- </div>
44
- <span class="text-grey text-xs font-medium font-mono uppercase tracking-wide mt-8">{{
45
- formatDateShort(frontmatter.date)
46
- }}</span>
47
- </div>
48
-
49
- <!-- Right: Cover image -->
50
- <div class="w-full lg:w-[30rem] xl:w-[50rem] flex-shrink-0 bg-nickel overflow-hidden lg:mr-10 mt-8 lg:mt-0">
51
- <img
52
- v-if="coverUrl"
53
- :src="coverUrl"
54
- :alt="frontmatter.title"
55
- class="w-full h-full object-cover"
56
- />
57
- </div>
58
- </div>
59
-
60
- <!-- Author bar -->
61
- <div class="border-t border-stroke">
62
- <div class="flex flex-col lg:flex-row py-5">
63
- <!-- Left: Author -->
64
- <div class="flex items-center gap-4 px-6 lg:pl-10 lg:pr-20 flex-1 min-w-0">
65
- <img
66
- v-if="frontmatter.authors?.[0] && getAuthorImage(frontmatter.authors[0] as Author)"
67
- :src="getAuthorImage(frontmatter.authors[0] as Author)!"
68
- :alt="frontmatter.authors[0].name"
69
- class="w-10 h-10 rounded object-cover"
70
- />
71
- <div v-else class="w-10 h-10 rounded bg-grey/20"></div>
72
- <span class="text-sm text-primary font-mono">{{
73
- frontmatter.authors?.[0]?.name || 'Unknown'
74
- }}</span>
75
- </div>
76
-
77
- <!-- Right: Read time + Share (matches image width) -->
78
- <div class="w-full lg:w-[30rem] xl:w-[50rem] flex-shrink-0 lg:mr-10 px-6 lg:px-0 mt-4 lg:mt-0 flex items-center justify-between">
79
- <!-- Read time -->
80
- <div class="flex items-center gap-2 text-grey">
81
- <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
82
- <circle cx="12" cy="12" r="10" stroke-width="1.5"/>
83
- <path stroke-width="1.5" stroke-linecap="round" d="M12 6v6l4 2"/>
84
- </svg>
85
- <span class="text-xs font-medium font-mono uppercase tracking-wide">{{ frontmatter.readTime || '5 MIN READ' }}</span>
86
- </div>
87
-
88
- <!-- Share -->
89
- <div class="flex items-center gap-4">
90
- <span class="hidden lg:inline text-grey text-xs font-medium font-mono uppercase tracking-wide">Share</span>
91
- <div class="flex items-center gap-3">
92
- <a :href="`https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(pageUrl)}`" target="_blank" rel="noopener" class="text-grey hover:text-primary transition-colors">
93
- <svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24"><path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/></svg>
94
- </a>
95
- <a :href="`https://www.linkedin.com/shareArticle?mini=true&url=${encodeURIComponent(pageUrl)}`" target="_blank" rel="noopener" class="text-grey hover:text-primary transition-colors">
96
- <svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24"><path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/></svg>
97
- </a>
98
- <a :href="`https://x.com/intent/tweet?url=${encodeURIComponent(pageUrl)}`" target="_blank" rel="noopener" class="text-grey hover:text-primary transition-colors">
99
- <svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/></svg>
100
- </a>
101
- <button @click="copyLink" class="text-grey hover:text-primary transition-colors">
102
- <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"/></svg>
103
- </button>
104
- </div>
105
- </div>
106
- </div>
107
- </div>
108
- </div>
109
- </section>
110
- </template>
111
-
112
- <style scoped>
113
- </style>
@@ -1,92 +0,0 @@
1
- <script setup lang="ts">
2
- import {computed} from 'vue'
3
- import {useData} from 'vitepress'
4
- import {type Post, formatDateShort} from './types'
5
- import NewsletterForm from '@components/shared/NewsletterForm.vue'
6
-
7
- interface Props {
8
- articles: Post[]
9
- newsletterEndpoint?: string
10
- newsletterFormId?: string
11
- }
12
-
13
- const props = defineProps<Props>()
14
- const {frontmatter, page} = useData()
15
-
16
- // Get up to 2 related posts (same category, excluding current, most recent first)
17
- const relatedPosts = computed(() => {
18
- const currentCategory = frontmatter.value.category
19
- const currentUrl = page.value.relativePath.replace(/\.md$/, '')
20
-
21
- return props.articles
22
- .filter(post => {
23
- // Same category, not current post
24
- const postPath = post.url.replace(/^\//, '').replace(/\/$/, '')
25
- const currentPath = currentUrl.replace(/^\//, '').replace(/\/$/, '')
26
- return post.category === currentCategory && postPath !== currentPath
27
- })
28
- .sort((a, b) => b.date.time - a.date.time)
29
- .slice(0, 2)
30
- })
31
- </script>
32
-
33
- <template>
34
- <section v-if="relatedPosts.length > 0" class="wrapper wrapper--ticks border-t">
35
- <div class="flex flex-col lg:flex-row">
36
- <!-- Left: Newsletter Signup -->
37
- <div class="bg-beige px-6 lg:pl-10 py-10 flex-1 min-w-0 flex flex-col justify-between">
38
- <h2 class="text-2xl md:text-3xl text-primary font-normal leading-tight max-w-xs">
39
- Subscribe to our monthly newsletter
40
- </h2>
41
- <div class="mt-10">
42
- <NewsletterForm :endpoint="newsletterEndpoint" :form-id="newsletterFormId" />
43
- </div>
44
- </div>
45
-
46
- <!-- Right: Related Posts (matches image width from hero) -->
47
- <div class="related-posts relative w-full lg:w-[35rem] xl:w-[55rem] flex-shrink-0 px-6 lg:pl-30 lg:mr-10 py-10 lg:py-16">
48
- <div class="grid grid-cols-1 sm:grid-cols-2 gap-6">
49
- <a
50
- v-for="post in relatedPosts"
51
- :key="post.url"
52
- :href="post.url"
53
- class="group flex flex-col"
54
- >
55
- <!-- Cover Image -->
56
- <div class="w-full aspect-video bg-nickel overflow-hidden">
57
- <img
58
- v-if="post.cover"
59
- :src="post.cover"
60
- :alt="post.title"
61
- class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
62
- />
63
- </div>
64
-
65
- <!-- Content -->
66
- <div class="flex flex-col gap-2 mt-4">
67
- <span class="text-grey text-xs font-medium font-mono uppercase tracking-wide">// {{ post.category }}</span>
68
- <h4 class="text-lg text-pretty text-primary font-normal leading-snug group-hover:text-grey transition-colors">
69
- {{ post.title }}
70
- </h4>
71
- <span class="text-grey text-xs font-medium font-mono uppercase tracking-wide">{{ formatDateShort(post.date.string) }}</span>
72
- </div>
73
- </a>
74
- </div>
75
- </div>
76
- </div>
77
- </section>
78
- </template>
79
-
80
- <style scoped>
81
- @media (min-width: 1024px) {
82
- .related-posts::before {
83
- content: '';
84
- position: absolute;
85
- left: 0;
86
- top: 0;
87
- bottom: 0;
88
- width: 1px;
89
- background-color: var(--color-stroke);
90
- }
91
- }
92
- </style>