Skip to content

Commit bfc524b

Browse files
committed
Merge remote-tracking branch 'origin/main' into serhalp/cmdk-palette
2 parents b515456 + 5aff68f commit bfc524b

File tree

98 files changed

+4884
-3147
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

98 files changed

+4884
-3147
lines changed
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name: welcome
1+
name: Claim Contributor Message
22

33
on:
44
pull_request_target:

.github/workflows/welcome-open.yml

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
name: Welcome Message
2+
3+
on:
4+
pull_request_target:
5+
branches: [main]
6+
types: [opened]
7+
8+
permissions:
9+
pull-requests: write
10+
11+
jobs:
12+
greeting:
13+
name: Greet First-Time Contributors
14+
if: github.repository == 'npmx-dev/npmx.dev'
15+
runs-on: ubuntu-latest
16+
steps:
17+
- uses: zephyrproject-rtos/action-first-interaction@58853996b1ac504b8e0f6964301f369d2bb22e5c
18+
with:
19+
repo-token: ${{ secrets.GITHUB_TOKEN }}
20+
pr-opened-message: |
21+
Hello! Thank you for opening your **first PR** to npmx, @${{ github.event.pull_request.user.login }}! 🚀
22+
23+
Here’s what will happen next:
24+
25+
1. Our GitHub bots will run to check your changes.
26+
If they spot any issues you will see some error messages on this PR.
27+
Don’t hesitate to ask any questions if you’re not sure what these mean!
28+
29+
2. In a few minutes, you’ll be able to see a preview of your changes on Vercel
30+
31+
3. One or more of our maintainers will take a look and may ask you to make changes.
32+
We try to be responsive, but don’t worry if this takes a few days.

app/components/AppFooter.vue

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ const closeModal = () => modalRef.value?.close?.()
3939
<LinkBase :to="{ name: 'translation-status' }">
4040
{{ $t('translation_status.title') }}
4141
</LinkBase>
42+
<LinkBase :to="{ name: 'brand' }">
43+
{{ $t('footer.brand') }}
44+
</LinkBase>
4245
<button
4346
type="button"
4447
class="cursor-pointer group inline-flex gap-x-1 items-center justify-center underline-offset-[0.2rem] underline decoration-1 decoration-fg/30 font-mono text-fg hover:(decoration-accent text-accent) focus-visible:(decoration-accent text-accent) transition-colors duration-200"

app/components/AppHeader.vue

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,14 @@ const mobileLinks = computed<NavigationConfigWithGroups>(() => [
9494
external: false,
9595
iconClass: 'i-lucide:languages',
9696
},
97+
{
98+
name: 'Brand',
99+
label: $t('footer.brand'),
100+
to: { name: 'brand' },
101+
type: 'link',
102+
external: false,
103+
iconClass: 'i-lucide:palette',
104+
},
97105
],
98106
},
99107
{
@@ -220,17 +228,18 @@ onKeyStroke(
220228
class="relative container min-h-14 flex items-center gap-2 z-1 justify-end"
221229
>
222230
<!-- Mobile: Logo (navigates home) -->
223-
<NuxtLink
224-
v-if="!isSearchExpanded && !isOnHomePage"
225-
to="/"
226-
:aria-label="$t('header.home')"
227-
class="sm:hidden flex-shrink-0 font-mono text-lg font-medium text-fg hover:text-fg transition-colors duration-200 focus-ring me-4"
228-
>
229-
<AppMark class="w-6 h-auto" />
230-
</NuxtLink>
231+
<LogoContextMenu v-if="!isSearchExpanded && !isOnHomePage" class="sm:hidden flex-shrink-0">
232+
<NuxtLink
233+
to="/"
234+
:aria-label="$t('header.home')"
235+
class="font-mono text-lg font-medium text-fg hover:text-fg transition-colors duration-200 focus-ring me-4"
236+
>
237+
<AppMark class="w-6 h-auto" />
238+
</NuxtLink>
239+
</LogoContextMenu>
231240

232241
<!-- Desktop: Logo (navigates home) -->
233-
<div v-if="showLogo" class="hidden sm:flex flex-shrink-0 items-center">
242+
<LogoContextMenu v-if="showLogo" class="hidden sm:flex flex-shrink-0 items-center">
234243
<NuxtLink
235244
:to="{ name: 'index' }"
236245
:aria-label="$t('header.home')"
@@ -245,7 +254,7 @@ onKeyStroke(
245254
{{ env === 'release' ? 'alpha' : env }}
246255
</span>
247256
</NuxtLink>
248-
</div>
257+
</LogoContextMenu>
249258

250259
<NuxtLink
251260
v-if="showLogo && !isSearchExpanded && prNumber"

app/components/Brand/Customize.vue

Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
<script setup lang="ts">
2+
import { ACCENT_COLORS, type AccentColorId } from '#shared/utils/constants'
3+
4+
const { selectedAccentColor } = useAccentColor()
5+
const { t } = useI18n()
6+
7+
const customAccent = ref<string | null>(null)
8+
const customBgDark = ref(true)
9+
const customLogoRef = useTemplateRef('customLogoRef')
10+
11+
const activeAccentId = computed(() => customAccent.value ?? selectedAccentColor.value ?? 'sky')
12+
13+
// Use the palette matching the preview background, not the site theme
14+
const previewPalette = computed(() =>
15+
customBgDark.value ? ACCENT_COLORS.dark : ACCENT_COLORS.light,
16+
)
17+
18+
const activeAccentColor = computed(() => {
19+
return previewPalette.value[activeAccentId.value as AccentColorId] ?? previewPalette.value.sky
20+
})
21+
22+
const accentColorLabels = computed<Record<AccentColorId, string>>(() => ({
23+
sky: t('settings.accent_colors.sky'),
24+
coral: t('settings.accent_colors.coral'),
25+
amber: t('settings.accent_colors.amber'),
26+
emerald: t('settings.accent_colors.emerald'),
27+
violet: t('settings.accent_colors.violet'),
28+
magenta: t('settings.accent_colors.magenta'),
29+
neutral: t('settings.clear_accent'),
30+
}))
31+
32+
// Color swatches match the preview background palette so the circles reflect what the logo will render
33+
const pickerColors = computed(() =>
34+
Object.entries(previewPalette.value).map(([id, value]) => ({
35+
id: id as AccentColorId,
36+
label: accentColorLabels.value[id as AccentColorId],
37+
value,
38+
})),
39+
)
40+
41+
function getCustomSvgString(): string {
42+
const el = customLogoRef.value?.$el as SVGElement | undefined
43+
if (!el) return ''
44+
const clone = el.cloneNode(true) as SVGElement
45+
clone.querySelectorAll<SVGElement>('[fill="currentColor"]').forEach(path => {
46+
path.setAttribute('fill', customBgDark.value ? '#fafafa' : '#0a0a0a')
47+
})
48+
clone.querySelectorAll<SVGElement>('[fill="var(--accent)"]').forEach(path => {
49+
const style = getComputedStyle(path as SVGElement)
50+
path.setAttribute('fill', style.fill || activeAccentColor.value)
51+
})
52+
clone.removeAttribute('aria-hidden')
53+
clone.removeAttribute('class')
54+
return new XMLSerializer().serializeToString(clone)
55+
}
56+
57+
const svgLoading = ref(false)
58+
59+
function downloadCustomSvg() {
60+
const svg = getCustomSvgString()
61+
if (!svg) return
62+
svgLoading.value = true
63+
try {
64+
const blob = new Blob([svg], { type: 'image/svg+xml' })
65+
downloadFile(blob, `npmx-logo-${activeAccentId.value}.svg`)
66+
} finally {
67+
svgLoading.value = false
68+
}
69+
}
70+
71+
const pngLoading = ref(false)
72+
73+
async function downloadCustomPng() {
74+
const svg = getCustomSvgString()
75+
if (!svg) return
76+
pngLoading.value = true
77+
78+
const blob = new Blob([svg], { type: 'image/svg+xml' })
79+
const url = URL.createObjectURL(blob)
80+
81+
try {
82+
await document.fonts.ready
83+
84+
const img = new Image()
85+
const loaded = new Promise<void>((resolve, reject) => {
86+
// oxlint-disable-next-line eslint-plugin-unicorn(prefer-add-event-listener)
87+
img.onload = () => resolve()
88+
// oxlint-disable-next-line eslint-plugin-unicorn(prefer-add-event-listener)
89+
img.onerror = () => reject(new Error('Failed to load custom SVG'))
90+
})
91+
img.src = url
92+
await loaded
93+
94+
const scale = 2
95+
const canvas = document.createElement('canvas')
96+
canvas.width = 602 * scale
97+
canvas.height = 170 * scale
98+
const ctx = canvas.getContext('2d')!
99+
ctx.fillStyle = customBgDark.value ? '#0a0a0a' : '#ffffff'
100+
ctx.fillRect(0, 0, canvas.width, canvas.height)
101+
ctx.scale(scale, scale)
102+
ctx.drawImage(img, 0, 0, 602, 170)
103+
104+
await new Promise<void>(resolve => {
105+
canvas.toBlob(pngBlob => {
106+
if (pngBlob) downloadFile(pngBlob, `npmx-logo-${activeAccentId.value}.png`)
107+
resolve()
108+
}, 'image/png')
109+
})
110+
} finally {
111+
URL.revokeObjectURL(url)
112+
pngLoading.value = false
113+
}
114+
}
115+
</script>
116+
117+
<template>
118+
<section aria-labelledby="brand-customize-heading">
119+
<h2 id="brand-customize-heading" class="text-lg text-fg uppercase tracking-wider mb-4">
120+
{{ $t('brand.customize.title') }}
121+
</h2>
122+
<p class="text-fg-muted leading-relaxed mb-8">
123+
{{ $t('brand.customize.description') }}
124+
</p>
125+
126+
<div class="border border-border rounded-lg overflow-hidden">
127+
<!-- Live preview -->
128+
<div
129+
class="flex items-center justify-center p-10 sm:p-16 transition-colors duration-300 motion-reduce:transition-none"
130+
:class="customBgDark ? 'bg-[#0a0a0a]' : 'bg-white'"
131+
>
132+
<AppLogo
133+
ref="customLogoRef"
134+
class="h-10 sm:h-14 w-auto max-w-full transition-colors duration-300 motion-reduce:transition-none"
135+
:class="customBgDark ? 'text-[#fafafa]' : 'text-[#0a0a0a]'"
136+
:style="{ '--accent': activeAccentColor }"
137+
/>
138+
</div>
139+
140+
<!-- Controls -->
141+
<div class="border-t border-border p-4 sm:p-6 flex flex-col gap-4">
142+
<!-- Row 1: Accent color picker -->
143+
<fieldset class="flex items-center gap-3 border-none p-0 m-0">
144+
<legend class="sr-only">{{ $t('brand.customize.accent_label') }}</legend>
145+
<span class="text-sm font-mono text-fg-muted shrink-0">{{
146+
$t('brand.customize.accent_label')
147+
}}</span>
148+
<div class="flex items-center gap-1.5" role="radiogroup">
149+
<ButtonBase
150+
v-for="color in pickerColors"
151+
:key="color.id"
152+
role="radio"
153+
:aria-checked="activeAccentId === color.id"
154+
:aria-label="color.label"
155+
class="!w-6 !h-6 !rounded-full !border-2 !p-0 !min-w-0 transition-all duration-150 motion-reduce:transition-none"
156+
:class="
157+
activeAccentId === color.id
158+
? '!border-fg scale-110'
159+
: color.id === 'neutral'
160+
? '!border-border hover:!border-border-hover'
161+
: '!border-transparent hover:!border-border-hover'
162+
"
163+
:style="{ backgroundColor: color.value }"
164+
@click="customAccent = color.id"
165+
/>
166+
</div>
167+
</fieldset>
168+
169+
<!-- Row 2: Background toggle + Download buttons -->
170+
<div class="flex items-center gap-4 flex-wrap">
171+
<div class="flex items-center gap-3">
172+
<span class="text-sm font-mono text-fg-muted">{{
173+
$t('brand.customize.bg_label')
174+
}}</span>
175+
<div
176+
class="flex items-center border border-border rounded-md overflow-hidden"
177+
role="radiogroup"
178+
>
179+
<ButtonBase
180+
size="md"
181+
role="radio"
182+
:aria-checked="customBgDark"
183+
:aria-label="$t('brand.logos.on_dark')"
184+
class="!border-none !rounded-none motion-reduce:transition-none"
185+
:class="
186+
customBgDark
187+
? 'bg-bg-muted text-fg'
188+
: 'bg-transparent text-fg-muted hover:text-fg'
189+
"
190+
@click="customBgDark = true"
191+
>
192+
{{ $t('brand.logos.on_dark') }}
193+
</ButtonBase>
194+
<ButtonBase
195+
size="md"
196+
role="radio"
197+
:aria-checked="!customBgDark"
198+
:aria-label="$t('brand.logos.on_light')"
199+
class="!border-none !rounded-none border-is border-is-border motion-reduce:transition-none"
200+
:class="
201+
!customBgDark
202+
? 'bg-bg-muted text-fg'
203+
: 'bg-transparent text-fg-muted hover:text-fg'
204+
"
205+
@click="customBgDark = false"
206+
>
207+
{{ $t('brand.logos.on_light') }}
208+
</ButtonBase>
209+
</div>
210+
</div>
211+
212+
<div class="flex items-center gap-2">
213+
<ButtonBase
214+
size="md"
215+
:aria-label="$t('brand.customize.download_svg_aria')"
216+
:disabled="svgLoading"
217+
@click="downloadCustomSvg"
218+
>
219+
<span
220+
class="size-[1em]"
221+
aria-hidden="true"
222+
:class="svgLoading ? 'i-lucide:loader-circle animate-spin' : 'i-lucide:download'"
223+
/>
224+
{{ $t('brand.logos.download_svg') }}
225+
</ButtonBase>
226+
<ButtonBase
227+
size="md"
228+
:aria-label="$t('brand.customize.download_png_aria')"
229+
:disabled="pngLoading"
230+
@click="downloadCustomPng"
231+
>
232+
<span
233+
class="size-[1em]"
234+
aria-hidden="true"
235+
:class="pngLoading ? 'i-lucide:loader-circle animate-spin' : 'i-lucide:download'"
236+
/>
237+
{{ $t('brand.logos.download_png') }}
238+
</ButtonBase>
239+
</div>
240+
</div>
241+
</div>
242+
</div>
243+
</section>
244+
</template>

app/components/ColumnPicker.vue

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,6 @@ const columnLabels = computed(() => ({
4949
updated: $t('filters.columns.published'),
5050
maintainers: $t('filters.columns.maintainers'),
5151
keywords: $t('filters.columns.keywords'),
52-
qualityScore: $t('filters.columns.quality_score'),
53-
popularityScore: $t('filters.columns.popularity_score'),
54-
maintenanceScore: $t('filters.columns.maintenance_score'),
55-
combinedScore: $t('filters.columns.combined_score'),
5652
security: $t('filters.columns.security'),
5753
selection: $t('filters.columns.selection'),
5854
}))

0 commit comments

Comments
 (0)