@voidzero-dev/vitepress-theme 3.0.2 → 3.2.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/config.js CHANGED
@@ -53,8 +53,17 @@ export function extendConfig(c) {
53
53
 
54
54
  // inject alias
55
55
  v.resolve ??= {};
56
- v.resolve.alias ??= {};
57
- Object.assign(v.resolve.alias, aliases);
56
+ const al = (v.resolve.alias ??= {});
57
+ if (Array.isArray(al)) {
58
+ for (const key in aliases) {
59
+ al.push({
60
+ find: key,
61
+ replacement: aliases[key]
62
+ })
63
+ }
64
+ } else {
65
+ Object.assign(v.resolve.alias, aliases);
66
+ }
58
67
 
59
68
  const pkg = "@voidzero-dev/vitepress-theme";
60
69
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voidzero-dev/vitepress-theme",
3
- "version": "3.0.2",
3
+ "version": "3.2.0",
4
4
  "description": "Shared VitePress theme for VoidZero projects",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
@@ -1,44 +1,15 @@
1
1
  <script setup lang="ts">
2
2
  import { computed } from 'vue'
3
-
4
- // Sponsor with optional explicit image path
5
- interface Sponsor {
6
- name: string
7
- url: string
8
- img?: string // If provided, use directly; otherwise auto-generate
9
- }
10
-
11
- interface SponsorWithImg extends Sponsor {
12
- img: string
13
- }
14
-
15
- // Tier configuration structure
16
- interface TierConfig {
17
- displayName: string
18
- columns: number
19
- size?: 'large' | 'medium' | 'small' | 'avatar'
20
- avatarMode?: boolean // For backers dense grid
21
- }
22
-
23
- // Processed tier for rendering
24
- interface ProcessedTier {
25
- tierKey: string
26
- displayName: string
27
- columns: number
28
- size: 'large' | 'medium' | 'small' | 'avatar'
29
- avatarMode: boolean
30
- items: SponsorWithImg[]
31
- }
3
+ import type { SponsorTier, SponsorSize } from '../../types/sponsors'
32
4
 
33
5
  interface Props {
34
- sponsors?: Record<string, Sponsor[]>
35
- tierConfig?: Record<string, Partial<TierConfig>> // Override defaults
36
- imageBasePath?: string // Default: '/sponsors/'
37
- sideBySideTiers?: [string, string] // e.g., ['bronze', 'backers'] for OXC layout
6
+ sponsors?: SponsorTier[]
38
7
  heading?: string
39
8
  description?: string
40
9
  sponsorLink?: string
41
10
  sponsorLinkText?: string
11
+ sideBySideTiers?: [string, string] // e.g., ['bronze', 'backers'] for side-by-side layout
12
+ logoStyle?: 'opencollective'
42
13
  }
43
14
 
44
15
  const props = withDefaults(defineProps<Props>(), {
@@ -48,285 +19,176 @@ const props = withDefaults(defineProps<Props>(), {
48
19
  sponsorLinkText: 'Become a Sponsor',
49
20
  })
50
21
 
51
- // Default tier configuration with display names, column counts, and sizes
52
- const defaultTierConfig: Record<string, TierConfig> = {
53
- // Vite tiers
54
- partnership: { displayName: 'IN PARTNERSHIP WITH', columns: 2, size: 'large' },
55
- platinum: { displayName: 'PLATINUM SPONSORS', columns: 3, size: 'large' },
56
-
57
- // Vitest tiers
58
- special: { displayName: 'SPECIAL SPONSORS', columns: 3, size: 'large' },
59
-
60
- // Shared tier
61
- gold: { displayName: 'GOLD SPONSORS', columns: 5, size: 'medium' },
62
-
63
- // OXC tiers
64
- bronze: { displayName: 'BRONZE SPONSORS', columns: 1, size: 'medium' },
65
- silver: { displayName: 'SILVER SPONSORS', columns: 3, size: 'medium' },
66
- backers: { displayName: 'BACKERS', columns: 15, size: 'avatar', avatarMode: true },
22
+ const sizeConfig: Record<SponsorSize, { columns: number; padding: string; logoSize: string; avatarMode?: boolean }> = {
23
+ big: { columns: 3, padding: 'py-16', logoSize: 'h-13 w-[220px]' },
24
+ medium: { columns: 5, padding: 'py-7', logoSize: 'h-8 w-[120px]' },
25
+ small: { columns: 7, padding: 'py-5', logoSize: 'h-6 w-[100px]' },
26
+ avatar: { columns: 15, padding: 'py-3', logoSize: 'w-10 h-10', avatarMode: true },
67
27
  }
68
28
 
69
- // Resolve image path - use explicit img or auto-generate from name
70
- const resolveImagePath = (sponsor: Sponsor): string => {
71
- if (sponsor.img) return sponsor.img
72
- const basePath = props.imageBasePath ?? '/sponsors/'
73
- return `${basePath}${sponsor.name.toLowerCase().replace(/\s+/g, '')}.svg`
29
+ const gridClasses: Record<number, string> = {
30
+ 1: 'grid-cols-1',
31
+ 2: 'grid-cols-1 sm:grid-cols-2',
32
+ 3: 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-3',
33
+ 4: 'grid-cols-2 sm:grid-cols-3 lg:grid-cols-4',
34
+ 5: 'grid-cols-2 sm:grid-cols-3 lg:grid-cols-5',
35
+ 6: 'grid-cols-2 sm:grid-cols-3 lg:grid-cols-6',
36
+ 7: 'grid-cols-2 sm:grid-cols-4 lg:grid-cols-7',
74
37
  }
75
38
 
76
- // Get merged tier config (defaults + overrides)
77
- const getMergedTierConfig = (tierKey: string): TierConfig => {
78
- const defaultConfig = defaultTierConfig[tierKey] || {
79
- displayName: tierKey.toUpperCase(),
80
- columns: 5,
81
- size: 'medium' as const,
82
- avatarMode: false,
83
- }
84
- const overrideConfig = props.tierConfig?.[tierKey] || {}
85
- return { ...defaultConfig, ...overrideConfig }
39
+ const getEffectiveColumns = (size: SponsorSize, itemCount: number) =>
40
+ Math.min(sizeConfig[size].columns, itemCount)
41
+
42
+ const getGridColumns = (size: SponsorSize, itemCount: number) =>
43
+ gridClasses[getEffectiveColumns(size, itemCount)] || 'grid-cols-1'
44
+
45
+ const getEmptyCells = (itemCount: number, size: SponsorSize) => {
46
+ const cols = getEffectiveColumns(size, itemCount)
47
+ const remainder = itemCount % cols
48
+ return remainder === 0 ? 0 : cols - remainder
86
49
  }
87
50
 
88
- // Process a single tier
89
- const processTier = (tierKey: string, items: Sponsor[]): ProcessedTier => {
90
- const config = getMergedTierConfig(tierKey)
91
- return {
92
- tierKey,
93
- displayName: config.displayName,
94
- columns: config.columns,
95
- size: config.size || 'medium',
96
- avatarMode: config.avatarMode || false,
97
- items: items.map(sponsor => ({
98
- ...sponsor,
99
- img: resolveImagePath(sponsor),
100
- })),
101
- }
51
+ // Calculate empty slots for backers grid (maintain visual consistency)
52
+ const getBackerEmptySlots = (itemCount: number, minSlots: number = 30) => {
53
+ const targetSlots = Math.max(itemCount, minSlots)
54
+ return Math.max(0, targetSlots - itemCount)
102
55
  }
103
56
 
104
- // Side-by-side tier data (for OXC layout)
57
+ // Side-by-side tier data (for layouts like OXC: Bronze + Backers)
105
58
  const sideBySideData = computed(() => {
106
59
  if (!props.sponsors || !props.sideBySideTiers) return null
107
60
 
108
- const [leftTierKey, rightTierKey] = props.sideBySideTiers
109
- const leftItems = props.sponsors[leftTierKey]
110
- const rightItems = props.sponsors[rightTierKey]
61
+ const [leftTierName, rightTierName] = props.sideBySideTiers
62
+ const leftTier = props.sponsors.find(t => t.tier.toLowerCase() === leftTierName.toLowerCase())
63
+ const rightTier = props.sponsors.find(t => t.tier.toLowerCase() === rightTierName.toLowerCase())
111
64
 
112
- if (!leftItems || !rightItems) return null
65
+ if (!leftTier || !rightTier) return null
113
66
 
114
- return {
115
- left: processTier(leftTierKey, leftItems),
116
- right: processTier(rightTierKey, rightItems),
117
- }
67
+ return { left: leftTier, right: rightTier }
118
68
  })
119
69
 
120
70
  // Standard stacked tier data (excludes side-by-side tiers)
121
- const stackedSponsorData = computed(() => {
71
+ const stackedSponsors = computed(() => {
122
72
  if (!props.sponsors) return []
73
+ if (!props.sideBySideTiers) return props.sponsors
123
74
 
124
- const sideBySideKeys = props.sideBySideTiers || []
125
-
126
- return Object.entries(props.sponsors)
127
- .filter(([tierKey]) => !sideBySideKeys.includes(tierKey))
128
- .map(([tierKey, items]) => processTier(tierKey, items))
75
+ const sideBySideNames = props.sideBySideTiers.map(n => n.toLowerCase())
76
+ return props.sponsors.filter(tier => !sideBySideNames.includes(tier.tier.toLowerCase()))
129
77
  })
130
-
131
- // Get grid columns class based on specified columns
132
- const getGridColumns = (columns: number) => {
133
- const gridClasses: Record<number, string> = {
134
- 1: 'grid-cols-1',
135
- 2: 'grid-cols-1 sm:grid-cols-2',
136
- 3: 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-3',
137
- 4: 'grid-cols-2 sm:grid-cols-3 lg:grid-cols-4',
138
- 5: 'grid-cols-2 sm:grid-cols-3 lg:grid-cols-5',
139
- 6: 'grid-cols-2 sm:grid-cols-3 lg:grid-cols-6',
140
- 7: 'grid-cols-2 sm:grid-cols-4 lg:grid-cols-7',
141
- }
142
- return gridClasses[columns] || 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-4'
143
- }
144
-
145
- // Size-based padding classes
146
- const getSponsorPadding = (size: string) => {
147
- const paddingMap: Record<string, string> = {
148
- large: 'py-16',
149
- medium: 'py-7',
150
- small: 'py-5',
151
- avatar: 'py-3',
152
- }
153
- return paddingMap[size] || 'py-7'
154
- }
155
-
156
- // Size-based logo dimension classes
157
- const getLogoSize = (size: string) => {
158
- const sizeMap: Record<string, string> = {
159
- large: 'max-h-12 max-w-[200px]',
160
- medium: 'max-h-6 max-w-[120px]',
161
- small: 'max-h-5 max-w-[100px]',
162
- avatar: 'max-h-8 max-w-[80px]',
163
- }
164
- return sizeMap[size] || 'max-h-6 max-w-[120px]'
165
- }
166
-
167
- // Calculate number of empty cells needed to fill the last row
168
- const getEmptyCells = (itemCount: number, columns: number) => {
169
- const remainder = itemCount % columns
170
- return remainder === 0 ? 0 : columns - remainder
171
- }
172
-
173
- // Calculate empty slots for backers grid (maintain visual consistency)
174
- const getBackerEmptySlots = (itemCount: number, minSlots: number = 30) => {
175
- const targetSlots = Math.max(itemCount, minSlots)
176
- return Math.max(0, targetSlots - itemCount)
177
- }
178
78
  </script>
179
79
 
180
80
  <template>
181
- <div class="wrapper wrapper--ticks border-t py-14 sm:py-30 px-5 sm:px-10">
182
- <div class="flex flex-col md:flex-row justify-between items-center gap-8 md:gap-20 text-center md:text-left md:pl-15">
81
+ <div class="wrapper wrapper--ticks border-t py-14 sm:py-30 px-10">
82
+ <div
83
+ class="flex flex-col md:flex-row justify-between items-center gap-8 md:gap-20 text-center md:text-left md:pl-15">
183
84
  <div class="flex flex-col gap-3">
184
85
  <h3 class="text-white max-w-xl text-balance">{{ heading }}</h3>
185
86
  <p class="max-w-lg text-white/70 text-balance">{{ description }}</p>
186
87
  <a :href="sponsorLink" target="_blank" class="button w-fit mt-8 mx-auto md:mx-0">{{ sponsorLinkText }}</a>
187
88
  </div>
188
- <div class="flex gap-8 md:gap-12 items-start justify-center md:justify-start md:pr-25">
189
- <img src="../../assets/vite/vite-by-voidzero.png" alt="Brought to you by VoidZero" class="h-44 max-w-full object-contain mt-10 md:mt-0"/>
190
- </div>
89
+ <a class="flex gap-8 md:gap-12 items-start justify-center md:justify-start md:pr-25" href="https://voidzero.dev"
90
+ target="_blank">
91
+ <img src="../../assets/vite/vite-by-voidzero.png" alt="Brought to you by VoidZero"
92
+ class="h-44 max-w-full object-contain mt-10 md:mt-0" />
93
+ </a>
191
94
  </div>
192
95
  </div>
193
- <div v-if="sponsors && Object.keys(sponsors).length > 0" class="wrapper">
194
- <!-- Side-by-Side Layout (OXC: Bronze + Backers) -->
96
+ <div v-if="sponsors && sponsors.length > 0" class="wrapper"
97
+ :class="{ 'sponsor-style-oc': logoStyle === 'opencollective' }">
98
+ <!-- Standard Stacked Sponsors Grid -->
99
+ <div v-for="(tier, tierIndex) in stackedSponsors" :key="tierIndex" class="border-t border-nickel">
100
+ <!-- Tier Heading -->
101
+ <div class="py-5 px-5 sm:px-10 border-b border-nickel">
102
+ <span class="text-white/70 text-xs font-mono uppercase tracking-wide">
103
+ {{ tier.tier }}
104
+ </span>
105
+ </div>
106
+
107
+ <!-- Avatar Mode Grid (for backers-style tiers) -->
108
+ <div v-if="sizeConfig[tier.size].avatarMode" class="backers-grid px-5 sm:px-10 py-6">
109
+ <a v-for="(sponsor, index) in tier.items" :key="index" :href="sponsor.url" target="_blank"
110
+ rel="noopener noreferrer" class="backer-avatar group relative">
111
+ <img :src="sponsor.img" :alt="sponsor.name"
112
+ class="w-10 h-10 rounded object-cover border border-nickel transition-all group-hover:border-white/50 group-hover:scale-110" />
113
+ <span class="backer-tooltip">{{ sponsor.name }}</span>
114
+ </a>
115
+ <!-- Empty placeholder slots -->
116
+ <div v-for="emptyIndex in getBackerEmptySlots(tier.items.length)" :key="`empty-${emptyIndex}`"
117
+ class="backer-avatar-placeholder" aria-hidden="true">
118
+ <div class="w-10 h-10 rounded border border-nickel/30"></div>
119
+ </div>
120
+ </div>
121
+
122
+ <!-- Standard Logo Grid -->
123
+ <div v-else class="grid [&>*]:border-r [&>*]:border-b [&>*]:border-nickel"
124
+ :class="getGridColumns(tier.size, tier.items.length)">
125
+ <a v-for="(sponsor, index) in tier.items" :key="index" :href="sponsor.url" target="_blank"
126
+ rel="noopener noreferrer"
127
+ class="sponsor-link flex items-center justify-center px-8 transition-opacity opacity-85 hover:opacity-100"
128
+ :class="sizeConfig[tier.size].padding">
129
+ <img :src="sponsor.img" :alt="sponsor.name" class="sponsor-logo object-contain"
130
+ :class="sizeConfig[tier.size].logoSize" />
131
+ <span class="sponsor-name ml-5">{{ sponsor.name }}</span>
132
+ </a>
133
+
134
+ <!-- Empty cells to fill incomplete rows -->
135
+ <div v-for="emptyIndex in getEmptyCells(tier.items.length, tier.size)" :key="`empty-${emptyIndex}`"
136
+ class="sponsor-link flex items-center justify-center px-8" :class="sizeConfig[tier.size].padding"
137
+ aria-hidden="true" />
138
+ </div>
139
+ </div>
140
+
141
+ <!-- Side-by-Side Layout (e.g., Bronze + Backers) -->
195
142
  <div v-if="sideBySideData" class="grid grid-cols-1 lg:grid-cols-2 border-t border-nickel">
196
- <!-- Left Tier (Bronze Sponsors) -->
143
+ <!-- Left Tier -->
197
144
  <div class="border-b lg:border-b-0 lg:border-r border-nickel">
198
145
  <div class="py-5 px-5 sm:px-10 border-b border-nickel">
199
146
  <span class="text-white/70 text-xs font-mono uppercase tracking-wide">
200
- {{ sideBySideData.left.displayName }}
147
+ {{ sideBySideData.left.tier }}
201
148
  </span>
202
149
  </div>
203
150
  <div class="flex flex-col">
204
- <a
205
- v-for="(sponsor, index) in sideBySideData.left.items"
206
- :key="index"
207
- :href="sponsor.url"
208
- target="_blank"
151
+ <a v-for="(sponsor, index) in sideBySideData.left.items" :key="index" :href="sponsor.url" target="_blank"
209
152
  rel="noopener noreferrer"
210
- class="sponsor-link flex items-center justify-center px-8 py-12 transition-opacity opacity-70 hover:opacity-100 border-b border-nickel last:border-b-0"
211
- >
212
- <img
213
- :src="sponsor.img"
214
- :alt="sponsor.name"
215
- class="sponsor-logo object-contain max-h-8 max-w-[160px]"
216
- />
153
+ class="sponsor-link flex items-center justify-center px-8 py-12 transition-opacity opacity-85 hover:opacity-100 border-b border-nickel last:border-b-0">
154
+ <img :src="sponsor.img" :alt="sponsor.name" class="sponsor-logo object-contain"
155
+ :class="sizeConfig[sideBySideData.left.size].logoSize" />
156
+ <span class="sponsor-name ml-5">{{ sponsor.name }}</span>
217
157
  </a>
218
158
  </div>
219
159
  </div>
220
160
 
221
- <!-- Right Tier (Backers) -->
161
+ <!-- Right Tier (Backers style) -->
222
162
  <div>
223
163
  <div class="py-5 px-5 sm:px-10 border-b border-nickel">
224
164
  <span class="text-white/70 text-xs font-mono uppercase tracking-wide">
225
- {{ sideBySideData.right.displayName }}
165
+ {{ sideBySideData.right.tier }}
226
166
  </span>
227
167
  </div>
228
- <div class="backers-grid px-5 sm:px-10 py-6">
229
- <a
230
- v-for="(sponsor, index) in sideBySideData.right.items"
231
- :key="index"
232
- :href="sponsor.url"
233
- target="_blank"
234
- rel="noopener noreferrer"
235
- class="backer-avatar group relative"
236
- >
237
- <img
238
- :src="sponsor.img"
239
- :alt="sponsor.name"
240
- class="w-10 h-10 rounded object-cover border border-nickel transition-all group-hover:border-white/50 group-hover:scale-110"
241
- />
168
+ <div v-if="sizeConfig[sideBySideData.right.size].avatarMode" class="backers-grid px-5 sm:px-10 py-6">
169
+ <a v-for="(sponsor, index) in sideBySideData.right.items" :key="index" :href="sponsor.url" target="_blank"
170
+ rel="noopener noreferrer" class="backer-avatar group relative">
171
+ <img :src="sponsor.img" :alt="sponsor.name"
172
+ class="w-10 h-10 rounded object-cover border border-nickel transition-all group-hover:border-white/50 group-hover:scale-110" />
242
173
  <span class="backer-tooltip">{{ sponsor.name }}</span>
243
174
  </a>
244
175
  <!-- Empty placeholder slots for visual consistency -->
245
- <div
246
- v-for="emptyIndex in getBackerEmptySlots(sideBySideData.right.items.length)"
247
- :key="`empty-${emptyIndex}`"
248
- class="backer-avatar-placeholder"
249
- aria-hidden="true"
250
- >
251
- <div class="w-10 h-10 rounded border border-nickel/30"></div>
252
- </div>
253
- </div>
254
- </div>
255
- </div>
256
-
257
- <!-- Standard Stacked Sponsors Grid -->
258
- <div v-if="stackedSponsorData && stackedSponsorData.length > 0">
259
- <div
260
- v-for="(tierData, tierIndex) in stackedSponsorData"
261
- :key="tierIndex"
262
- class="border-t border-nickel"
263
- >
264
- <!-- Tier Heading -->
265
- <div class="py-5 px-5 sm:px-10 border-b border-nickel">
266
- <span class="text-white/70 text-xs font-mono uppercase tracking-wide">
267
- {{ tierData.displayName }}
268
- </span>
269
- </div>
270
-
271
- <!-- Avatar Mode Grid (for backers-style tiers) -->
272
- <div v-if="tierData.avatarMode" class="backers-grid px-5 sm:px-10 py-6">
273
- <a
274
- v-for="(sponsor, index) in tierData.items"
275
- :key="index"
276
- :href="sponsor.url"
277
- target="_blank"
278
- rel="noopener noreferrer"
279
- class="backer-avatar group relative"
280
- >
281
- <img
282
- :src="sponsor.img"
283
- :alt="sponsor.name"
284
- class="w-10 h-10 rounded object-cover border border-nickel transition-all group-hover:border-white/50 group-hover:scale-110"
285
- />
286
- <span class="backer-tooltip">{{ sponsor.name }}</span>
287
- </a>
288
- <!-- Empty placeholder slots -->
289
- <div
290
- v-for="emptyIndex in getBackerEmptySlots(tierData.items.length)"
291
- :key="`empty-${emptyIndex}`"
292
- class="backer-avatar-placeholder"
293
- aria-hidden="true"
294
- >
176
+ <div v-for="emptyIndex in getBackerEmptySlots(sideBySideData.right.items.length)" :key="`empty-${emptyIndex}`"
177
+ class="backer-avatar-placeholder" aria-hidden="true">
295
178
  <div class="w-10 h-10 rounded border border-nickel/30"></div>
296
179
  </div>
297
180
  </div>
298
-
299
- <!-- Standard Logo Grid -->
300
- <div
301
- v-else
302
- class="grid [&>*]:border-r [&>*]:border-b [&>*]:border-nickel"
303
- :class="getGridColumns(tierData.columns)"
304
- >
305
- <a
306
- v-for="(sponsor, index) in tierData.items"
307
- :key="index"
308
- :href="sponsor.url"
309
- target="_blank"
181
+ <!-- Fallback to standard grid if not avatar mode -->
182
+ <div v-else class="grid [&>*]:border-r [&>*]:border-b [&>*]:border-nickel"
183
+ :class="getGridColumns(sideBySideData.right.size, sideBySideData.right.items.length)">
184
+ <a v-for="(sponsor, index) in sideBySideData.right.items" :key="index" :href="sponsor.url" target="_blank"
310
185
  rel="noopener noreferrer"
311
- class="sponsor-link flex items-center justify-center px-8 transition-opacity opacity-70 hover:opacity-100"
312
- :class="getSponsorPadding(tierData.size)"
313
- >
314
- <img
315
- :src="sponsor.img"
316
- :alt="sponsor.name"
317
- class="sponsor-logo object-contain"
318
- :class="getLogoSize(tierData.size)"
319
- />
186
+ class="sponsor-link flex items-center justify-center px-8 transition-opacity opacity-85 hover:opacity-100"
187
+ :class="sizeConfig[sideBySideData.right.size].padding">
188
+ <img :src="sponsor.img" :alt="sponsor.name" class="sponsor-logo object-contain"
189
+ :class="sizeConfig[sideBySideData.right.size].logoSize" />
190
+ <span class="sponsor-name ml-5">{{ sponsor.name }}</span>
320
191
  </a>
321
-
322
- <!-- Empty cells to fill incomplete rows -->
323
- <div
324
- v-for="emptyIndex in getEmptyCells(tierData.items.length, tierData.columns)"
325
- :key="`empty-${emptyIndex}`"
326
- class="sponsor-link flex items-center justify-center px-8"
327
- :class="getSponsorPadding(tierData.size)"
328
- aria-hidden="true"
329
- />
330
192
  </div>
331
193
  </div>
332
194
  </div>
@@ -334,6 +196,31 @@ const getBackerEmptySlots = (itemCount: number, minSlots: number = 30) => {
334
196
  </template>
335
197
 
336
198
  <style scoped>
199
+ /* Sponsor grid - clip outer borders using overflow */
200
+ .grid[class*="grid-cols"] {
201
+ overflow: hidden;
202
+ margin-right: -1px;
203
+ margin-bottom: -1px;
204
+ }
205
+
206
+ .sponsor-logo {
207
+ filter: grayscale(1) invert(1);
208
+ }
209
+
210
+ .sponsor-style-oc .sponsor-logo {
211
+ filter: none;
212
+ width: auto;
213
+ border-radius: 0.25rem;
214
+ }
215
+
216
+ .sponsor-name {
217
+ display: none;
218
+ }
219
+
220
+ .sponsor-style-oc .sponsor-name {
221
+ display: inline-block;
222
+ }
223
+
337
224
  /* Backers dense avatar grid */
338
225
  .backers-grid {
339
226
  display: flex;
@@ -389,11 +276,4 @@ const getBackerEmptySlots = (itemCount: number, minSlots: number = 30) => {
389
276
  .backer-avatar-placeholder div {
390
277
  background: transparent;
391
278
  }
392
-
393
- /* Sponsor grid - clip outer borders using overflow */
394
- .grid[class*="grid-cols"] {
395
- overflow: hidden;
396
- margin-right: -1px;
397
- margin-bottom: -1px;
398
- }
399
279
  </style>
@@ -1,40 +1,14 @@
1
1
  <script setup lang="ts">
2
- import { computed } from 'vue'
3
2
  import LogoGrid from '@components/shared/LogoGrid.vue'
4
3
 
5
- // Glob import all client logos
6
- const logoModules = import.meta.glob('../../assets/clients/*.svg', {
7
- eager: true,
8
- import: 'default'
9
- })
4
+ const props = defineProps<{
5
+ logos: string[]
6
+ }>()
10
7
 
11
- // Create logo asset map: { 'openai': 'url', 'framer': 'url', ... }
12
- const logoAssets: Record<string, string> = {}
13
- Object.entries(logoModules).forEach(([path, module]) => {
14
- const filename = path.split('/').pop()?.replace('.svg', '') || ''
15
- logoAssets[filename] = module as string
16
- })
17
-
18
- // Default logo order (matches current implementation)
19
- const DEFAULT_LOGO_ORDER = ['openai', 'framer', 'linear', 'ramp', 'shopify']
20
-
21
- // Props interface
22
- interface Props {
23
- logos?: string[] // Optional: array of logo names in desired order
24
- }
25
-
26
- const props = defineProps<Props>()
27
-
28
- // Computed logos array - resolves logo names to actual assets
29
- const logos = computed(() => {
30
- const order = props.logos || DEFAULT_LOGO_ORDER
31
- return order
32
- .filter(name => name in logoAssets) // Filter out invalid names
33
- .map(name => ({
34
- src: logoAssets[name],
35
- alt: name.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ')
36
- }))
37
- })
8
+ const logos = props.logos.map(name => ({
9
+ src: `/trusted-by/${name}.svg`,
10
+ alt: name,
11
+ }))
38
12
  </script>
39
13
 
40
14
  <template>
@@ -51,5 +25,6 @@ const logos = computed(() => {
51
25
  <style scoped>
52
26
  :deep(img) {
53
27
  filter: brightness(0) invert(1);
28
+ opacity: 0.7;
54
29
  }
55
30
  </style>
@@ -0,0 +1,30 @@
1
+ <template>
2
+ <section id="feature-formatter" class="wrapper grid border-t md:grid-cols-2">
3
+ <div class="px-5 py-6 md:p-10 flex flex-col justify-center gap-15">
4
+ <div class="flex flex-col gap-5">
5
+ <span class="text-grey text-xs font-mono uppercase tracking-wide">Formatter<span class="text-aqua"> (Alpha)</span></span>
6
+ <h4 class="text-white">Oxfmt: Prettier-compatible formatter</h4>
7
+ <p class="text-white/70 text-base max-w-[25rem] text-pretty">Enforce consistent code styles</p>
8
+ <ul class="checkmark-list">
9
+ <li>3x faster than <code class="mx-1 outline-none bg-nickel/50 text-aqua">Biome</code></li>
10
+ <li>35x faster than <code class="mx-1 outline-none bg-nickel/50 text-aqua">Prettier</code></li>
11
+ <li>Tailwind class sorting support</li>
12
+ </ul>
13
+ <a href="/docs/guide/usage/formatter" class="button w-fit mt-6">Usage Guide</a>
14
+ </div>
15
+ </div>
16
+ <div class="flex flex-col">
17
+ <div class="h-full overflow-clip flex justify-end items-center py-5 px-5 md:py-20 md:pl-10 md:pr-0">
18
+ <img loading="lazy" src="@assets/oxc/oxc-formatter-terminal.png" alt="Linter" class="[mask-image:linear-gradient(to_bottom,black_50%,transparent),linear-gradient(to_right,black_50%,transparent)] [mask-composite:intersect] [-webkit-mask-composite:source-in]">
19
+ </div>
20
+ </div>
21
+ </section>
22
+ </template>
23
+
24
+ <style scoped>
25
+ .card-bg {
26
+ background-image: url('@assets/oxc/oxc-feature-background.jpg');
27
+ background-size: cover;
28
+ background-position: center;
29
+ }
30
+ </style>
@@ -1,18 +1,15 @@
1
- <script setup lang="ts">
2
-
3
- </script>
4
-
5
1
  <template>
6
- <section id="feature-linter" class="wrapper border-t grid md:grid-cols-2">
2
+ <section id="feature-linter" class="wrapper grid md:grid-cols-2">
7
3
  <div class="px-5 py-6 md:p-10 flex flex-col justify-center gap-15">
8
4
  <div class="flex flex-col gap-5">
9
5
  <span class="text-grey text-xs font-mono uppercase tracking-wide">Linter</span>
10
- <h4 class="text-white">Oxlint: The Rust-powered linter</h4>
6
+ <h4 class="text-white">Oxlint: ESLint-compatible linter</h4>
11
7
  <p class="text-white/70 text-base max-w-[25rem] text-pretty">Catch bugs before they make it to production</p>
12
8
  <ul class="checkmark-list">
13
9
  <li>50~100x faster than <code class="mx-1 outline-none bg-nickel/50 text-aqua">ESLint</code></li>
14
10
  <li>570+ rules and growing</li>
15
- <li>Type-aware Linting</li>
11
+ <li>True type-aware Linting powered by <code class="mx-1 outline-none bg-nickel/50 text-aqua">tsgo</code></li>
12
+ <li>Support for <code class="mx-1 outline-none bg-nickel/50 text-aqua">ESLint</code> JS Plugins</li>
16
13
  </ul>
17
14
  <a href="/docs/guide/usage/linter" class="button w-fit mt-6">Usage Guide</a>
18
15
  </div>
@@ -31,4 +28,4 @@
31
28
  background-size: cover;
32
29
  background-position: center;
33
30
  }
34
- </style>
31
+ </style>