@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.
- package/package.json +7 -6
- package/src/aliases.js +14 -0
- package/src/vitepress/assets/clients/clickup.svg +5 -0
- package/src/vitepress/assets/clients/stripe.svg +3 -0
- package/src/vitepress/components/oss/Footer.vue +4 -21
- package/src/vitepress/components/oss/Header.vue +82 -180
- package/src/vitepress/components/oss/Sponsors.vue +3 -3
- package/src/vitepress/components/oss/TopBanner.vue +20 -79
- package/src/vitepress/components/oss/TrustedBy.vue +1 -1
- package/src/vitepress/components/vite/Community.vue +3 -3
- package/src/vitepress/components/vite/FeatureGrid1.vue +63 -0
- package/src/vitepress/components/vite/{FeatureGrid.vue → FeatureGrid2.vue} +8 -10
- package/src/vitepress/components/vite/Hero.vue +6 -15
- package/src/vitepress/components/vitepress-default/VPDocOutlineItem.vue +2 -2
- package/src/vitepress/components/vitepress-default/VPFlyout.vue +1 -1
- package/src/vitepress/components/vitepress-default/VPMenuLink.vue +1 -1
- package/src/vitepress/components/vitepress-default/VPNavBarMenuLink.vue +1 -1
- package/src/vitepress/components/vitepress-default/VPSidebarItem.vue +1 -1
- package/src/vitepress/components/vitepress-default/VPSocialLink.vue +1 -2
- package/src/vitepress/fonts/APK-Protocol-Semi-Bold.woff2 +0 -0
- package/src/vitepress/fonts/inter-italic-cyrillic-ext.woff2 +0 -0
- package/src/vitepress/fonts/inter-italic-cyrillic.woff2 +0 -0
- package/src/vitepress/fonts/inter-italic-greek-ext.woff2 +0 -0
- package/src/vitepress/fonts/inter-italic-greek.woff2 +0 -0
- package/src/vitepress/fonts/inter-italic-latin-ext.woff2 +0 -0
- package/src/vitepress/fonts/inter-italic-latin.woff2 +0 -0
- package/src/vitepress/fonts/inter-italic-vietnamese.woff2 +0 -0
- package/src/vitepress/fonts/inter-roman-cyrillic-ext.woff2 +0 -0
- package/src/vitepress/fonts/inter-roman-cyrillic.woff2 +0 -0
- package/src/vitepress/fonts/inter-roman-greek-ext.woff2 +0 -0
- package/src/vitepress/fonts/inter-roman-greek.woff2 +0 -0
- package/src/vitepress/fonts/inter-roman-latin-ext.woff2 +0 -0
- package/src/vitepress/fonts/inter-roman-latin.woff2 +0 -0
- package/src/vitepress/fonts/inter-roman-vietnamese.woff2 +0 -0
- package/src/vitepress/index.ts +64 -230
- package/src/vitepress/layouts/VPLayout.vue +2 -17
- package/src/vitepress/styles/tokens.css +194 -10
- package/src/vitepress/types/theme-context.ts +33 -0
- package/src/vitepress/assets/clients/beehiiv.svg +0 -30
- package/src/vitepress/assets/clients/excalidraw.svg +0 -82
- package/src/vitepress/assets/clients/get-your-guide.svg +0 -1
- package/src/vitepress/assets/clients/posthog.svg +0 -1
- package/src/vitepress/assets/clients/ramp.svg +0 -1
- package/src/vitepress/assets/clients/shopee.svg +0 -55
- package/src/vitepress/components/vite/FeaturePanel1.vue +0 -41
- package/src/vitepress/components/vite/FeaturePanel2.vue +0 -37
- package/src/vitepress/components/vite/FeaturePanel3.vue +0 -43
- package/src/vitepress/components/vite/FeaturePanel4.vue +0 -46
- package/src/vitepress/components/voidzero/Footer.vue +0 -65
- package/src/vitepress/components/voidzero/Header.vue +0 -560
- package/src/vitepress/components/voidzero/Megamenu.vue +0 -190
- package/src/vitepress/components/voidzero/about/CareerCTA.vue +0 -56
- package/src/vitepress/components/voidzero/about/Hero.vue +0 -206
- package/src/vitepress/components/voidzero/about/Investors.vue +0 -112
- package/src/vitepress/components/voidzero/about/TeamGrid.vue +0 -161
- package/src/vitepress/components/voidzero/about/TeamSectionHeading.vue +0 -13
- package/src/vitepress/components/voidzero/blog/BlogArchive.vue +0 -223
- package/src/vitepress/components/voidzero/blog/BlogSingleContent.vue +0 -364
- package/src/vitepress/components/voidzero/blog/BlogSingleHero.vue +0 -113
- package/src/vitepress/components/voidzero/blog/BlogSingleRelated.vue +0 -92
- package/src/vitepress/components/voidzero/blog/FeaturedArticles.vue +0 -146
- package/src/vitepress/components/voidzero/blog/types.ts +0 -56
- package/src/vitepress/components/voidzero/home/CaseStudySlider.vue +0 -235
- package/src/vitepress/components/voidzero/home/CustomersSectionHeading.vue +0 -5
- package/src/vitepress/components/voidzero/home/GitHubStats.vue +0 -27
- package/src/vitepress/components/voidzero/home/Hero.vue +0 -69
- package/src/vitepress/components/voidzero/home/Investors.vue +0 -30
- package/src/vitepress/components/voidzero/home/NewsletterCTA.vue +0 -23
- package/src/vitepress/components/voidzero/home/OpenSourceSectionHeading.vue +0 -6
- package/src/vitepress/components/voidzero/home/OpenSourceSectionProjects.vue +0 -419
- package/src/vitepress/components/voidzero/home/Resources.vue +0 -144
- package/src/vitepress/components/voidzero/home/Statistics.vue +0 -507
- package/src/vitepress/components/voidzero/home/StatisticsSectionHeading.vue +0 -5
- package/src/vitepress/components/voidzero/home/TeamCTA.vue +0 -17
- package/src/vitepress/components/voidzero/home/TrustedBy.vue +0 -248
- package/src/vitepress/components/voidzero/home/VitePlusSectionFeatures.vue +0 -55
- package/src/vitepress/components/voidzero/home/VitePlusSectionHeading.vue +0 -17
- package/src/vitepress/fonts/KHTeka-Medium.woff2 +0 -0
- package/src/vitepress/fonts/KHTeka-Regular.woff2 +0 -0
|
@@ -1,507 +0,0 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
import { onMounted, onUnmounted, ref, computed, watch, shallowRef, markRaw, Ref, Component } from 'vue'
|
|
3
|
-
import type { Chart as ChartType, ChartData } from 'chart.js'
|
|
4
|
-
import moment from 'moment'
|
|
5
|
-
import viteIcon from '@assets/icons/vite-dark.svg'
|
|
6
|
-
import vitestIcon from '@assets/icons/vitest-dark.svg'
|
|
7
|
-
import rolldownIcon from '@assets/icons/rolldown-dark.svg'
|
|
8
|
-
import oxcIcon from '@assets/icons/oxc-dark.svg'
|
|
9
|
-
|
|
10
|
-
interface GithubStat {
|
|
11
|
-
repo: string
|
|
12
|
-
stars: number
|
|
13
|
-
contributors: number
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
interface Props {
|
|
17
|
-
statsUrl?: string
|
|
18
|
-
githubStats?: GithubStat[]
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const props = withDefaults(defineProps<Props>(), {
|
|
22
|
-
statsUrl: '/stats.json',
|
|
23
|
-
githubStats: () => []
|
|
24
|
-
})
|
|
25
|
-
|
|
26
|
-
type ProjectKey = 'vite' | 'vitest' | 'rolldown' | 'oxlint'
|
|
27
|
-
|
|
28
|
-
const projects = [
|
|
29
|
-
{ key: 'vite' as ProjectKey, name: 'Vite', icon: viteIcon, color: '#6C3BFF' },
|
|
30
|
-
{ key: 'vitest' as ProjectKey, name: 'Vitest', icon: vitestIcon, color: '#22FF73' },
|
|
31
|
-
{ key: 'rolldown' as ProjectKey, name: 'Rolldown', icon: rolldownIcon, color: '#FF5500' },
|
|
32
|
-
{ key: 'oxlint' as ProjectKey, name: 'Oxc', icon: oxcIcon, color: '#32F3E9' }
|
|
33
|
-
]
|
|
34
|
-
|
|
35
|
-
// Repo name to project key mapping
|
|
36
|
-
const repoMapping: Record<string, ProjectKey> = {
|
|
37
|
-
'vitejs/vite': 'vite',
|
|
38
|
-
'vitest-dev/vitest': 'vitest',
|
|
39
|
-
'rolldown/rolldown': 'rolldown',
|
|
40
|
-
'oxc-project/oxc': 'oxlint'
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const selectedProject = ref<ProjectKey>('vite')
|
|
44
|
-
const allProjectData = ref<Record<ProjectKey, { x: number; y: number }[]>>({
|
|
45
|
-
vite: [],
|
|
46
|
-
vitest: [],
|
|
47
|
-
rolldown: [],
|
|
48
|
-
oxlint: []
|
|
49
|
-
})
|
|
50
|
-
const allTotalDownloads = ref<Record<ProjectKey, number>>({
|
|
51
|
-
vite: 0,
|
|
52
|
-
vitest: 0,
|
|
53
|
-
rolldown: 0,
|
|
54
|
-
oxlint: 0
|
|
55
|
-
})
|
|
56
|
-
const allGithubStars = ref<Record<ProjectKey, number>>({
|
|
57
|
-
vite: 0,
|
|
58
|
-
vitest: 0,
|
|
59
|
-
rolldown: 0,
|
|
60
|
-
oxlint: 0
|
|
61
|
-
})
|
|
62
|
-
const allContributors = ref<Record<ProjectKey, number>>({
|
|
63
|
-
vite: 0,
|
|
64
|
-
vitest: 0,
|
|
65
|
-
rolldown: 0,
|
|
66
|
-
oxlint: 0
|
|
67
|
-
})
|
|
68
|
-
|
|
69
|
-
const dataPoints = ref<{ x: number; y: number }[]>([])
|
|
70
|
-
const stats: Ref<ChartData<'line', { x: number; y: number }[], unknown>> = ref({
|
|
71
|
-
datasets: []
|
|
72
|
-
})
|
|
73
|
-
const statsChart = shallowRef<ChartType | null>(null)
|
|
74
|
-
|
|
75
|
-
// Dynamically loaded NumberFlow - use shallowRef for components
|
|
76
|
-
const NumberFlow = shallowRef<Component | null>(null)
|
|
77
|
-
const continuous = shallowRef<any>(null)
|
|
78
|
-
|
|
79
|
-
const totalDownloads = computed(() => {
|
|
80
|
-
return allTotalDownloads.value[selectedProject.value]
|
|
81
|
-
})
|
|
82
|
-
|
|
83
|
-
const weeklyDownloads = computed(() => {
|
|
84
|
-
const projectData = allProjectData.value[selectedProject.value]
|
|
85
|
-
return (projectData.slice().reverse().find(p => p.y > 0)?.y || 0)
|
|
86
|
-
})
|
|
87
|
-
|
|
88
|
-
const weeklyDownloadsFormatted = computed(() => {
|
|
89
|
-
const value = weeklyDownloads.value
|
|
90
|
-
if (value >= 1000000) {
|
|
91
|
-
return (value / 1000000).toFixed(1) + 'M'
|
|
92
|
-
} else if (value >= 1000) {
|
|
93
|
-
return (value / 1000).toFixed(1) + 'K'
|
|
94
|
-
}
|
|
95
|
-
return value.toString()
|
|
96
|
-
})
|
|
97
|
-
|
|
98
|
-
const githubStars = computed(() => {
|
|
99
|
-
return allGithubStars.value[selectedProject.value]
|
|
100
|
-
})
|
|
101
|
-
|
|
102
|
-
const githubStarsFormatted = computed(() => {
|
|
103
|
-
const value = githubStars.value
|
|
104
|
-
if (value >= 1000000) {
|
|
105
|
-
return (value / 1000000).toFixed(1) + 'M'
|
|
106
|
-
} else if (value >= 1000) {
|
|
107
|
-
return (value / 1000).toFixed(1) + 'K'
|
|
108
|
-
}
|
|
109
|
-
return value.toString()
|
|
110
|
-
})
|
|
111
|
-
|
|
112
|
-
const contributors = computed(() => {
|
|
113
|
-
return allContributors.value[selectedProject.value]
|
|
114
|
-
})
|
|
115
|
-
|
|
116
|
-
const contributorsFormatted = computed(() => {
|
|
117
|
-
const value = contributors.value
|
|
118
|
-
if (value >= 1000000) {
|
|
119
|
-
return (value / 1000000).toFixed(1) + 'M'
|
|
120
|
-
} else if (value >= 1000) {
|
|
121
|
-
return (value / 1000).toFixed(1) + 'K'
|
|
122
|
-
}
|
|
123
|
-
return value.toString()
|
|
124
|
-
})
|
|
125
|
-
|
|
126
|
-
const selectedProjectData = computed(() => {
|
|
127
|
-
return projects.find(p => p.key === selectedProject.value)
|
|
128
|
-
})
|
|
129
|
-
|
|
130
|
-
const dropdownOpen = ref(false)
|
|
131
|
-
const dropdownButtonRef = ref<HTMLButtonElement | null>(null)
|
|
132
|
-
const focusedIndex = ref<number>(0)
|
|
133
|
-
const announcement = ref<string>('')
|
|
134
|
-
|
|
135
|
-
// Helper function to convert hex to rgba
|
|
136
|
-
const hexToRgba = (hex: string, alpha: number) => {
|
|
137
|
-
const r = parseInt(hex.slice(1, 3), 16)
|
|
138
|
-
const g = parseInt(hex.slice(3, 5), 16)
|
|
139
|
-
const b = parseInt(hex.slice(5, 7), 16)
|
|
140
|
-
return `rgba(${r}, ${g}, ${b}, ${alpha})`
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// Close dropdown when clicking outside
|
|
144
|
-
const closeDropdown = (e: MouseEvent) => {
|
|
145
|
-
const target = e.target as HTMLElement
|
|
146
|
-
if (!target.closest('.project-dropdown')) {
|
|
147
|
-
dropdownOpen.value = false
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
const handleKeydown = (e: KeyboardEvent) => {
|
|
152
|
-
if (!dropdownOpen.value) {
|
|
153
|
-
// When closed, open with Enter or Space or ArrowDown
|
|
154
|
-
if (e.key === 'Enter' || e.key === ' ' || e.key === 'ArrowDown') {
|
|
155
|
-
e.preventDefault()
|
|
156
|
-
dropdownOpen.value = true
|
|
157
|
-
focusedIndex.value = projects.findIndex(p => p.key === selectedProject.value)
|
|
158
|
-
}
|
|
159
|
-
} else {
|
|
160
|
-
// When open, handle navigation
|
|
161
|
-
switch (e.key) {
|
|
162
|
-
case 'Escape':
|
|
163
|
-
e.preventDefault()
|
|
164
|
-
dropdownOpen.value = false
|
|
165
|
-
dropdownButtonRef.value?.focus()
|
|
166
|
-
break
|
|
167
|
-
case 'ArrowDown':
|
|
168
|
-
e.preventDefault()
|
|
169
|
-
focusedIndex.value = (focusedIndex.value + 1) % projects.length
|
|
170
|
-
break
|
|
171
|
-
case 'ArrowUp':
|
|
172
|
-
e.preventDefault()
|
|
173
|
-
focusedIndex.value = focusedIndex.value === 0 ? projects.length - 1 : focusedIndex.value - 1
|
|
174
|
-
break
|
|
175
|
-
case 'Enter':
|
|
176
|
-
case ' ':
|
|
177
|
-
e.preventDefault()
|
|
178
|
-
selectedProject.value = projects[focusedIndex.value].key
|
|
179
|
-
dropdownOpen.value = false
|
|
180
|
-
dropdownButtonRef.value?.focus()
|
|
181
|
-
break
|
|
182
|
-
case 'Home':
|
|
183
|
-
e.preventDefault()
|
|
184
|
-
focusedIndex.value = 0
|
|
185
|
-
break
|
|
186
|
-
case 'End':
|
|
187
|
-
e.preventDefault()
|
|
188
|
-
focusedIndex.value = projects.length - 1
|
|
189
|
-
break
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
const selectProject = (projectKey: ProjectKey) => {
|
|
195
|
-
const project = projects.find(p => p.key === projectKey)
|
|
196
|
-
selectedProject.value = projectKey
|
|
197
|
-
dropdownOpen.value = false
|
|
198
|
-
dropdownButtonRef.value?.focus()
|
|
199
|
-
|
|
200
|
-
// Announce change to screen readers
|
|
201
|
-
if (project) {
|
|
202
|
-
announcement.value = `${project.name} selected. Showing ${project.name} statistics.`
|
|
203
|
-
setTimeout(() => announcement.value = '', 1000)
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
onMounted(async () => {
|
|
208
|
-
// Add click listener for closing dropdown
|
|
209
|
-
document.addEventListener('click', closeDropdown)
|
|
210
|
-
|
|
211
|
-
// Dynamically import Chart.js and NumberFlow client-side only
|
|
212
|
-
const { default: Chart } = await import('chart.js/auto')
|
|
213
|
-
await import('chartjs-adapter-moment')
|
|
214
|
-
|
|
215
|
-
const numberFlowModule = await import('@number-flow/vue')
|
|
216
|
-
NumberFlow.value = markRaw(numberFlowModule.default)
|
|
217
|
-
continuous.value = markRaw(numberFlowModule.continuous)
|
|
218
|
-
|
|
219
|
-
// Get the data from the stats URL
|
|
220
|
-
const response = await fetch(props.statsUrl)
|
|
221
|
-
const rawData = await response.json()
|
|
222
|
-
|
|
223
|
-
// Extract data for all projects
|
|
224
|
-
for (const project of projects) {
|
|
225
|
-
const projectData = rawData[project.key] || []
|
|
226
|
-
let points = projectData
|
|
227
|
-
.map((point: {x: number; y: number}) => ({
|
|
228
|
-
x: moment(point['x']).valueOf(),
|
|
229
|
-
y: point['y']
|
|
230
|
-
}))
|
|
231
|
-
.filter(point => point.y > 0) // Remove points with y = 0
|
|
232
|
-
|
|
233
|
-
// Remove trailing incomplete data points (last 1-2 weeks of data)
|
|
234
|
-
// This ensures we don't show incomplete current week data
|
|
235
|
-
if (points.length > 2) {
|
|
236
|
-
points = points.slice(0, -2) // Remove last 2 points
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
allProjectData.value[project.key] = points
|
|
240
|
-
allTotalDownloads.value[project.key] = rawData.totalDownloads?.[project.key] || 0
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
// Parse GitHub stats data
|
|
244
|
-
if (props.githubStats && props.githubStats.length > 0) {
|
|
245
|
-
props.githubStats.forEach((stat) => {
|
|
246
|
-
const projectKey = repoMapping[stat.repo]
|
|
247
|
-
if (projectKey) {
|
|
248
|
-
allGithubStars.value[projectKey] = stat.stars
|
|
249
|
-
allContributors.value[projectKey] = stat.contributors
|
|
250
|
-
}
|
|
251
|
-
})
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
// Set initial data points for selected project
|
|
255
|
-
dataPoints.value = allProjectData.value[selectedProject.value]
|
|
256
|
-
|
|
257
|
-
const canvas = document.getElementById('chart-canvas') as HTMLCanvasElement
|
|
258
|
-
const ctx = canvas.getContext('2d') as CanvasRenderingContext2D
|
|
259
|
-
|
|
260
|
-
// Get initial project color
|
|
261
|
-
const initialColor = projects.find(p => p.key === selectedProject.value)?.color || '#6C3BFF'
|
|
262
|
-
const gradient = ctx.createLinearGradient(0, 0, -50, 400)
|
|
263
|
-
gradient.addColorStop(0, initialColor)
|
|
264
|
-
gradient.addColorStop(1, initialColor)
|
|
265
|
-
|
|
266
|
-
stats.value = {
|
|
267
|
-
datasets: [
|
|
268
|
-
{
|
|
269
|
-
data: dataPoints.value,
|
|
270
|
-
fill: {
|
|
271
|
-
target: 'origin',
|
|
272
|
-
above: hexToRgba(initialColor, 0.1)
|
|
273
|
-
},
|
|
274
|
-
pointStyle: false,
|
|
275
|
-
borderWidth: 3,
|
|
276
|
-
borderColor: gradient,
|
|
277
|
-
stepped: false
|
|
278
|
-
}
|
|
279
|
-
]
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
statsChart.value = new Chart(canvas as HTMLCanvasElement, {
|
|
283
|
-
type: 'line',
|
|
284
|
-
options: {
|
|
285
|
-
scales: {
|
|
286
|
-
x: {
|
|
287
|
-
type: 'time',
|
|
288
|
-
time: {
|
|
289
|
-
unit: 'week'
|
|
290
|
-
},
|
|
291
|
-
display: false,
|
|
292
|
-
alignToPixels: true,
|
|
293
|
-
ticks: { padding: 0 },
|
|
294
|
-
border: {
|
|
295
|
-
display: false
|
|
296
|
-
}
|
|
297
|
-
},
|
|
298
|
-
y: {
|
|
299
|
-
min: 0,
|
|
300
|
-
display: false,
|
|
301
|
-
ticks: { padding: 0 },
|
|
302
|
-
alignToPixels: true,
|
|
303
|
-
border: {
|
|
304
|
-
display: false
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
},
|
|
308
|
-
plugins: {
|
|
309
|
-
legend: {
|
|
310
|
-
display: false
|
|
311
|
-
},
|
|
312
|
-
tooltip: {
|
|
313
|
-
enabled: false
|
|
314
|
-
}
|
|
315
|
-
},
|
|
316
|
-
layout: {
|
|
317
|
-
autoPadding: false,
|
|
318
|
-
padding: {
|
|
319
|
-
left: 0,
|
|
320
|
-
right: 0,
|
|
321
|
-
top: 0,
|
|
322
|
-
bottom: 80
|
|
323
|
-
}
|
|
324
|
-
},
|
|
325
|
-
aspectRatio: 5/3.5
|
|
326
|
-
},
|
|
327
|
-
data: {
|
|
328
|
-
datasets: stats.value.datasets
|
|
329
|
-
}
|
|
330
|
-
})
|
|
331
|
-
})
|
|
332
|
-
|
|
333
|
-
onUnmounted(() => {
|
|
334
|
-
if (statsChart.value) {
|
|
335
|
-
statsChart.value.destroy()
|
|
336
|
-
}
|
|
337
|
-
// Remove click listener
|
|
338
|
-
document.removeEventListener('click', closeDropdown)
|
|
339
|
-
})
|
|
340
|
-
|
|
341
|
-
// Watch for project changes and update the chart
|
|
342
|
-
watch(selectedProject, (newProject) => {
|
|
343
|
-
const newData = allProjectData.value[newProject]
|
|
344
|
-
|
|
345
|
-
if (statsChart.value && statsChart.value.data && statsChart.value.data.datasets[0]) {
|
|
346
|
-
// Get the dataset
|
|
347
|
-
const dataset = statsChart.value.data.datasets[0]
|
|
348
|
-
|
|
349
|
-
// Replace the data array entirely (not mutate in place)
|
|
350
|
-
dataset.data = newData.slice() // Use slice to create a new array
|
|
351
|
-
|
|
352
|
-
// Update the color
|
|
353
|
-
const canvas = document.getElementById('chart-canvas') as HTMLCanvasElement
|
|
354
|
-
const ctx = canvas.getContext('2d') as CanvasRenderingContext2D
|
|
355
|
-
const newColor = projects.find(p => p.key === newProject)?.color || '#6C3BFF'
|
|
356
|
-
const newGradient = ctx.createLinearGradient(0, 0, -50, 400)
|
|
357
|
-
newGradient.addColorStop(0, newColor)
|
|
358
|
-
newGradient.addColorStop(1, newColor)
|
|
359
|
-
dataset.borderColor = newGradient
|
|
360
|
-
|
|
361
|
-
// Update the fill color
|
|
362
|
-
if (dataset.fill && typeof dataset.fill === 'object') {
|
|
363
|
-
dataset.fill.above = hexToRgba(newColor, 0.1)
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
// Call update to re-render the chart
|
|
367
|
-
statsChart.value.update()
|
|
368
|
-
}
|
|
369
|
-
}, { flush: 'post' })
|
|
370
|
-
</script>
|
|
371
|
-
|
|
372
|
-
<template>
|
|
373
|
-
<!-- Screen reader announcements -->
|
|
374
|
-
<div aria-live="polite" aria-atomic="true" class="sr-only">
|
|
375
|
-
{{ announcement }}
|
|
376
|
-
</div>
|
|
377
|
-
|
|
378
|
-
<section class="wrapper wrapper--ticks border-t grid grid-cols-1 md:grid-cols-2 divide-y md:divide-y-0 md:divide-x divide-ceramic">
|
|
379
|
-
<div class="p-6 md:p-10 flex flex-col justify-between">
|
|
380
|
-
<h6 class="flex gap-2 items-center">
|
|
381
|
-
<span>Total downloads</span>
|
|
382
|
-
</h6>
|
|
383
|
-
<h1>
|
|
384
|
-
<component v-if="NumberFlow" :is="NumberFlow" :plugins="[continuous]" :value="totalDownloads" />
|
|
385
|
-
<span v-else>{{ totalDownloads }}</span>
|
|
386
|
-
</h1>
|
|
387
|
-
</div>
|
|
388
|
-
<div class="relative w-full aspect-[5/3.5]">
|
|
389
|
-
<div class="absolute top-4 md:top-6 left-4 md:left-6 z-10 project-dropdown">
|
|
390
|
-
<button
|
|
391
|
-
ref="dropdownButtonRef"
|
|
392
|
-
@click.stop="dropdownOpen = !dropdownOpen"
|
|
393
|
-
@keydown="handleKeydown"
|
|
394
|
-
:aria-expanded="dropdownOpen"
|
|
395
|
-
:aria-label="`Select project: ${selectedProjectData?.name}`"
|
|
396
|
-
aria-haspopup="listbox"
|
|
397
|
-
class="button cursor-pointer"
|
|
398
|
-
>
|
|
399
|
-
<img v-if="selectedProjectData" :src="selectedProjectData.icon" class="size-6" :alt="selectedProjectData?.name">
|
|
400
|
-
<span>{{ selectedProjectData?.name }} <span class="hidden md:inline-block">Downloads</span></span>
|
|
401
|
-
<svg class="size-3 ml-1 fill-primary" :class="{ 'rotate-180': dropdownOpen }" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 7" aria-hidden="true">
|
|
402
|
-
<path d="M1.41 0L6 4.58 10.59 0 12 1.42l-6 6-6-6z"/>
|
|
403
|
-
</svg>
|
|
404
|
-
</button>
|
|
405
|
-
<ul
|
|
406
|
-
v-if="dropdownOpen"
|
|
407
|
-
role="listbox"
|
|
408
|
-
:aria-label="'Project selection'"
|
|
409
|
-
class="absolute top-full left-0 mt-1 bg-white border border-stroke rounded-lg shadow-lg overflow-hidden min-w-full select-none"
|
|
410
|
-
>
|
|
411
|
-
<li
|
|
412
|
-
v-for="(project, index) in projects"
|
|
413
|
-
:key="project.key"
|
|
414
|
-
role="option"
|
|
415
|
-
:aria-selected="selectedProject === project.key"
|
|
416
|
-
>
|
|
417
|
-
<button
|
|
418
|
-
@click.stop="selectProject(project.key)"
|
|
419
|
-
@keydown="handleKeydown"
|
|
420
|
-
:class="{
|
|
421
|
-
'bg-gray-50': selectedProject === project.key,
|
|
422
|
-
'bg-gray-100': focusedIndex === index
|
|
423
|
-
}"
|
|
424
|
-
class="select-none cursor-pointer flex items-center gap-2 px-3 py-2 w-full text-left text-base hover:bg-stroke/20 focus:outline-none focus:bg-stroke/20 hover:text-primary focus:text-primary"
|
|
425
|
-
>
|
|
426
|
-
<img :src="project.icon" class="size-5" alt="">
|
|
427
|
-
<span class="font-medium">{{ project.name }}</span>
|
|
428
|
-
<svg v-if="selectedProject === project.key" class="size-4 ml-auto" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
|
429
|
-
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd" />
|
|
430
|
-
</svg>
|
|
431
|
-
</button>
|
|
432
|
-
</li>
|
|
433
|
-
</ul>
|
|
434
|
-
</div>
|
|
435
|
-
<canvas id="chart-canvas" width="200" height="200"></canvas>
|
|
436
|
-
<div class="flex justify-between absolute bottom-6 md:bottom-10 left-6 md:left-10 right-6 md:right-10">
|
|
437
|
-
<p class="font-mono text-sm text-nickel">April 2020</p>
|
|
438
|
-
<p class="font-mono text-sm text-nickel">Today</p>
|
|
439
|
-
</div>
|
|
440
|
-
</div>
|
|
441
|
-
</section>
|
|
442
|
-
<section class="wrapper wrapper--ticks border-t grid grid-cols-1 md:grid-cols-3 divide-y md:divide-y-0 md:divide-x divide-ceramic">
|
|
443
|
-
<div class="flex flex-col gap-2 p-6 md:p-10">
|
|
444
|
-
<h2>
|
|
445
|
-
<component
|
|
446
|
-
v-if="NumberFlow"
|
|
447
|
-
:is="NumberFlow"
|
|
448
|
-
:plugins="[continuous]"
|
|
449
|
-
:value="weeklyDownloads"
|
|
450
|
-
:format="{ notation: 'compact', maximumFractionDigits: 1 }"
|
|
451
|
-
/>
|
|
452
|
-
<span v-else>{{ weeklyDownloadsFormatted }}</span>+
|
|
453
|
-
</h2>
|
|
454
|
-
<p class="lead">Weekly NPM downloads</p>
|
|
455
|
-
</div>
|
|
456
|
-
<div class="flex flex-col gap-2 p-6 md:p-10">
|
|
457
|
-
<h2>
|
|
458
|
-
<component
|
|
459
|
-
v-if="NumberFlow"
|
|
460
|
-
:is="NumberFlow"
|
|
461
|
-
:plugins="[continuous]"
|
|
462
|
-
:value="githubStars"
|
|
463
|
-
:format="{ notation: 'compact', maximumFractionDigits: 1 }"
|
|
464
|
-
/>
|
|
465
|
-
<span v-else>{{ githubStarsFormatted }}</span>+
|
|
466
|
-
</h2>
|
|
467
|
-
<p class="lead">GitHub Stars</p>
|
|
468
|
-
</div>
|
|
469
|
-
<div class="flex flex-col gap-2 p-6 md:p-10">
|
|
470
|
-
<h2>
|
|
471
|
-
<component
|
|
472
|
-
v-if="NumberFlow"
|
|
473
|
-
:is="NumberFlow"
|
|
474
|
-
:plugins="[continuous]"
|
|
475
|
-
:value="contributors"
|
|
476
|
-
:format="{ notation: 'compact', maximumFractionDigits: 1 }"
|
|
477
|
-
/>
|
|
478
|
-
<span v-else>{{ contributorsFormatted }}</span>+
|
|
479
|
-
</h2>
|
|
480
|
-
<p class="lead">Contributors</p>
|
|
481
|
-
</div>
|
|
482
|
-
</section>
|
|
483
|
-
</template>
|
|
484
|
-
|
|
485
|
-
<style scoped>
|
|
486
|
-
#chart-canvas {
|
|
487
|
-
margin: 0;
|
|
488
|
-
padding: 0;
|
|
489
|
-
position: absolute;
|
|
490
|
-
inset: 0;
|
|
491
|
-
width: 100%;
|
|
492
|
-
height: 100%;
|
|
493
|
-
pointer-events: none;
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
.sr-only {
|
|
497
|
-
position: absolute;
|
|
498
|
-
width: 1px;
|
|
499
|
-
height: 1px;
|
|
500
|
-
padding: 0;
|
|
501
|
-
margin: -1px;
|
|
502
|
-
overflow: hidden;
|
|
503
|
-
clip: rect(0, 0, 0, 0);
|
|
504
|
-
white-space: nowrap;
|
|
505
|
-
border-width: 0;
|
|
506
|
-
}
|
|
507
|
-
</style>
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
|
|
3
|
-
</script>
|
|
4
|
-
|
|
5
|
-
<template>
|
|
6
|
-
<section class="wrapper wrapper--ticks border-t px-5 md:px-10 py-10 md:py-40 flex flex-col items-center gap-10 bg-[#FBFAF7]">
|
|
7
|
-
<h3 class="max-w-[45rem] text-center text-balance mx-auto">Our mission is to make the next generation of JavaScript developers more productive than ever before.</h3>
|
|
8
|
-
<div class="flex flex-row gap-6 items-baseline">
|
|
9
|
-
<a href="/about" class="button">Learn more</a>
|
|
10
|
-
<!-- <a href="/about" class="text-primary font-medium border-b">Meet our team →</a>-->
|
|
11
|
-
</div>
|
|
12
|
-
</section>
|
|
13
|
-
</template>
|
|
14
|
-
|
|
15
|
-
<style scoped>
|
|
16
|
-
|
|
17
|
-
</style>
|