Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
9fde292
feat: add brand page with logos, colors, typography, and usage guidel…
Adebesin-Cell Mar 22, 2026
eeef5e5
[autofix.ci] apply automated fixes
autofix-ci[bot] Mar 22, 2026
fb287bf
fix: prevent LogoContextMenu wrapper from breaking mobile nav layout
Adebesin-Cell Mar 22, 2026
c58e7e7
fix: resolve failing unit and i18n tests
Adebesin-Cell Mar 22, 2026
c956570
[autofix.ci] apply automated fixes
autofix-ci[bot] Mar 22, 2026
b4b5630
fix: regenerate i18n schema for brand page keys
Adebesin-Cell Mar 22, 2026
32900dc
fix: address CodeRabbit review comments
Adebesin-Cell Mar 22, 2026
284f70a
fix: replace remaining bare buttons with ButtonBase in Brand/Customize
Adebesin-Cell Mar 22, 2026
35f9f07
fix: await toBlob before clearing loading state in custom PNG download
Adebesin-Cell Mar 22, 2026
3639ee2
[autofix.ci] apply automated fixes
autofix-ci[bot] Mar 22, 2026
f22cec2
refactor: simplify brand page — remove colors, app icon, and strict g…
Adebesin-Cell Mar 22, 2026
ba79696
refactor: per-variant downloads, better spacing, friendlier guidelines
Adebesin-Cell Mar 22, 2026
33410bf
[autofix.ci] apply automated fixes
autofix-ci[bot] Mar 22, 2026
508ecca
fix: align logo cards with flex-col and consistent min-height
Adebesin-Cell Mar 22, 2026
9edd0ee
fix: increase spacing between logo cards
Adebesin-Cell Mar 22, 2026
e5ab1bb
fix: use space-y-16 for logo card spacing
Adebesin-Cell Mar 22, 2026
8c71173
fix: use mt-12 on figures instead of space-y for logo spacing
Adebesin-Cell Mar 22, 2026
8cf0210
fix: replace space-y-16 with flex col gap-16 for section spacing
Adebesin-Cell Mar 22, 2026
ab9d274
fix: reduce context menu size — remove min-w, use sm buttons
Adebesin-Cell Mar 22, 2026
a848281
fix: remove w-full from context menu buttons to shrink width
Adebesin-Cell Mar 22, 2026
22c3bb1
fix: add flex-col to context menu so items stack vertically
Adebesin-Cell Mar 22, 2026
7f07fcb
fix: use contrast-appropriate accent colors based on preview background
Adebesin-Cell Mar 22, 2026
f4c6916
fix: sync color picker swatches with preview background palette
Adebesin-Cell Mar 22, 2026
78df9f3
[autofix.ci] apply automated fixes
autofix-ci[bot] Mar 22, 2026
d1cf5be
fix: delay context menu close after copy so confirmation is readable
Adebesin-Cell Mar 22, 2026
d63a73b
style: Update setTimeout duration in copySvg function
Adebesin-Cell Mar 22, 2026
7abe2cd
Merge branch 'main' into feat/brand-page
Adebesin-Cell Mar 23, 2026
07e9928
chore: use generics
ghostdevv Mar 23, 2026
a97cf16
chore: ignore some lint issues
ghostdevv Mar 23, 2026
c3671ab
refactor: create downloadFile and downloadFileLink utils
ghostdevv Mar 23, 2026
7c04e99
refactor: remove composable
ghostdevv Mar 23, 2026
540ed93
fix: address PR review feedback for brand page
Adebesin-Cell Mar 23, 2026
7d1025a
[autofix.ci] apply automated fixes
autofix-ci[bot] Mar 23, 2026
1e0788f
fix: use correct light-mode colors for logo mark and add PNG spinners
Adebesin-Cell Mar 23, 2026
dc799b9
[autofix.ci] apply automated fixes
autofix-ci[bot] Mar 23, 2026
7dbca4b
fix: replace anchor tags with ButtonBase for SVG downloads and add lo…
Adebesin-Cell Mar 23, 2026
b015c8e
[autofix.ci] apply automated fixes
autofix-ci[bot] Mar 23, 2026
b01a764
fix: address brand page review feedback — sizing, wording, and layout
Adebesin-Cell Mar 24, 2026
15d62bd
fix: revert customize section to sm buttons to prevent overflow
Adebesin-Cell Mar 24, 2026
bba0662
fix: restructure customize controls into two rows to prevent overflow
Adebesin-Cell Mar 24, 2026
9b9f5f7
fix: increase customize section buttons to md size
Adebesin-Cell Mar 24, 2026
6ea2928
fix: increase customize download buttons to md size
Adebesin-Cell Mar 24, 2026
cb58641
[autofix.ci] apply automated fixes
autofix-ci[bot] Mar 24, 2026
decb485
fix: sync i18n locale files with schema
Adebesin-Cell Mar 24, 2026
4c65b17
Revert "fix: sync i18n locale files with schema"
Adebesin-Cell Mar 24, 2026
8dbb9da
fix: regenerate i18n schema for discord_link_text key
Adebesin-Cell Mar 24, 2026
3bff857
Merge branch 'main' into feat/brand-page
Adebesin-Cell Mar 24, 2026
fcd3211
fix: uppercase logo headings, lowercase types, remove blockquote italic
Adebesin-Cell Mar 25, 2026
4add8d5
[autofix.ci] apply automated fixes
autofix-ci[bot] Mar 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions app/components/AppFooter.vue
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ const closeModal = () => modalRef.value?.close?.()
<LinkBase :to="{ name: 'translation-status' }">
{{ $t('translation_status.title') }}
</LinkBase>
<LinkBase :to="{ name: 'brand' }">
{{ $t('footer.brand') }}
</LinkBase>
<button
type="button"
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"
Expand Down
29 changes: 19 additions & 10 deletions app/components/AppHeader.vue
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,14 @@ const mobileLinks = computed<NavigationConfigWithGroups>(() => [
external: false,
iconClass: 'i-lucide:languages',
},
{
name: 'Brand',
label: $t('footer.brand'),
to: { name: 'brand' },
type: 'link',
external: false,
iconClass: 'i-lucide:palette',
},
],
},
{
Expand Down Expand Up @@ -218,17 +226,18 @@ onKeyStroke(
class="relative container min-h-14 flex items-center gap-2 z-1 justify-end"
>
<!-- Mobile: Logo (navigates home) -->
<NuxtLink
v-if="!isSearchExpanded && !isOnHomePage"
to="/"
:aria-label="$t('header.home')"
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"
>
<AppMark class="w-6 h-auto" />
</NuxtLink>
<LogoContextMenu v-if="!isSearchExpanded && !isOnHomePage" class="sm:hidden flex-shrink-0">
<NuxtLink
to="/"
:aria-label="$t('header.home')"
class="font-mono text-lg font-medium text-fg hover:text-fg transition-colors duration-200 focus-ring me-4"
>
<AppMark class="w-6 h-auto" />
</NuxtLink>
</LogoContextMenu>

<!-- Desktop: Logo (navigates home) -->
<div v-if="showLogo" class="hidden sm:flex flex-shrink-0 items-center">
<LogoContextMenu v-if="showLogo" class="hidden sm:flex flex-shrink-0 items-center">
<NuxtLink
:to="{ name: 'index' }"
:aria-label="$t('header.home')"
Expand All @@ -243,7 +252,7 @@ onKeyStroke(
{{ env === 'release' ? 'alpha' : env }}
</span>
</NuxtLink>
</div>
</LogoContextMenu>

<NuxtLink
v-if="showLogo && !isSearchExpanded && prNumber"
Expand Down
195 changes: 195 additions & 0 deletions app/components/Brand/Customize.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
<script setup lang="ts">
const { accentColors, selectedAccentColor } = useAccentColor()
const { convert: _convert, download: downloadBlob } = useSvgToPng()

const customAccent = ref<string | null>(null)
const customBgDark = ref(true)
const customLogoRef = useTemplateRef('customLogoRef')

const activeAccentId = computed(() => customAccent.value ?? selectedAccentColor.value ?? 'sky')
const activeAccentColor = computed(() => {
const match = accentColors.value.find(c => c.id === activeAccentId.value)
const fallback = accentColors.value[0]?.value ?? 'oklch(0.787 0.128 230.318)'
return match?.value ?? fallback
})
Comment thread
coderabbitai[bot] marked this conversation as resolved.

function getCustomSvgString(): string {
const el = customLogoRef.value?.$el as SVGElement | undefined
if (!el) return ''
const clone = el.cloneNode(true) as SVGElement
clone.querySelectorAll('[fill="currentColor"]').forEach(path => {
;(path as SVGElement).setAttribute('fill', customBgDark.value ? '#fafafa' : '#0a0a0a')
})
clone.querySelectorAll('[fill="var(--accent)"]').forEach(path => {
const style = getComputedStyle(path as SVGElement)
;(path as SVGElement).setAttribute('fill', style.fill || activeAccentColor.value)
})
clone.removeAttribute('aria-hidden')
clone.removeAttribute('class')
return new XMLSerializer().serializeToString(clone)
}

function downloadCustomSvg() {
const svg = getCustomSvgString()
if (!svg) return
const blob = new Blob([svg], { type: 'image/svg+xml' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `npmx-logo-${activeAccentId.value}.svg`
a.click()
URL.revokeObjectURL(url)
}

const pngLoading = ref(false)

async function downloadCustomPng() {
const svg = getCustomSvgString()
if (!svg) return
pngLoading.value = true
try {
await document.fonts.ready
const blob = new Blob([svg], { type: 'image/svg+xml' })
const url = URL.createObjectURL(blob)

const img = new Image()
const loaded = new Promise<void>((resolve, reject) => {
img.onload = () => resolve()

Check warning on line 57 in app/components/Brand/Customize.vue

View workflow job for this annotation

GitHub Actions / 🤖 Autofix code

eslint-plugin-unicorn(prefer-add-event-listener)

Prefer `addEventListener()` over their `on`-function counterparts.
img.onerror = () => reject(new Error('Failed to load custom SVG'))

Check warning on line 58 in app/components/Brand/Customize.vue

View workflow job for this annotation

GitHub Actions / 🤖 Autofix code

eslint-plugin-unicorn(prefer-add-event-listener)

Prefer `addEventListener()` over their `on`-function counterparts.
})
img.src = url
await loaded

const scale = 2
const canvas = document.createElement('canvas')
canvas.width = 602 * scale
canvas.height = 170 * scale
const ctx = canvas.getContext('2d')!
Comment thread
ghostdevv marked this conversation as resolved.
ctx.fillStyle = customBgDark.value ? '#0a0a0a' : '#ffffff'
ctx.fillRect(0, 0, canvas.width, canvas.height)
ctx.scale(scale, scale)
ctx.drawImage(img, 0, 0, 602, 170)

canvas.toBlob(pngBlob => {
if (pngBlob) downloadBlob(pngBlob, `npmx-logo-${activeAccentId.value}.png`)
URL.revokeObjectURL(url)
}, 'image/png')
} finally {
pngLoading.value = false
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
}
</script>

<template>
<section aria-labelledby="brand-customize-heading">
<h2 id="brand-customize-heading" class="text-lg text-fg uppercase tracking-wider mb-4">
{{ $t('brand.customize.title') }}
</h2>
<p class="text-fg-muted leading-relaxed mb-8">
{{ $t('brand.customize.description') }}
</p>

<div class="border border-border rounded-lg overflow-hidden">
<!-- Live preview -->
<div
class="flex items-center justify-center p-10 sm:p-16 transition-colors duration-300 motion-reduce:transition-none"
:class="customBgDark ? 'bg-[#0a0a0a]' : 'bg-white'"
>
<AppLogo
ref="customLogoRef"
class="h-10 sm:h-14 w-auto max-w-full transition-colors duration-300 motion-reduce:transition-none"
:class="customBgDark ? 'text-[#fafafa]' : 'text-[#0a0a0a]'"
:style="{ '--accent': activeAccentColor }"
/>
</div>

<!-- Controls -->
<div
class="border-t border-border p-4 sm:p-6 flex flex-col sm:flex-row sm:items-center gap-4"
>
<!-- Accent color picker -->
<fieldset class="flex items-center gap-3 flex-1 border-none p-0 m-0">
<legend class="sr-only">{{ $t('brand.customize.accent_label') }}</legend>
<span class="text-xs font-mono text-fg-muted shrink-0">{{
$t('brand.customize.accent_label')
}}</span>
<div class="flex items-center gap-1.5" role="radiogroup">
<button
v-for="color in accentColors"
:key="color.id"
type="button"
role="radio"
:aria-checked="activeAccentId === color.id"
:aria-label="color.label"
class="w-6 h-6 rounded-full border-2 cursor-pointer transition-all duration-150 focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:ring-offset-bg motion-reduce:transition-none"
:class="
activeAccentId === color.id
? 'border-fg scale-110'
: 'border-transparent hover:border-border-hover'
"
:style="{ backgroundColor: color.value }"
@click="customAccent = color.id"
/>
</div>
</fieldset>

<!-- Background toggle -->
<div class="flex items-center gap-3">
<span class="text-xs font-mono text-fg-muted">{{ $t('brand.customize.bg_label') }}</span>
<div
class="flex items-center border border-border rounded-md overflow-hidden"
role="radiogroup"
>
<button
type="button"
role="radio"
:aria-checked="customBgDark"
:aria-label="$t('brand.logos.on_dark')"
class="px-2.5 py-1 text-xs font-mono cursor-pointer border-none transition-colors duration-150 focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-accent motion-reduce:transition-none"
:class="
customBgDark ? 'bg-bg-muted text-fg' : 'bg-transparent text-fg-muted hover:text-fg'
"
@click="customBgDark = true"
>
{{ $t('brand.logos.on_dark') }}
</button>
<button
type="button"
role="radio"
:aria-checked="!customBgDark"
:aria-label="$t('brand.logos.on_light')"
class="px-2.5 py-1 text-xs font-mono cursor-pointer border-none border-is border-is-border transition-colors duration-150 focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-accent motion-reduce:transition-none"
:class="
!customBgDark ? 'bg-bg-muted text-fg' : 'bg-transparent text-fg-muted hover:text-fg'
"
@click="customBgDark = false"
>
{{ $t('brand.logos.on_light') }}
</button>
</div>
</div>

<!-- Download buttons -->
<div class="flex items-center gap-2">
<ButtonBase
size="sm"
classicon="i-lucide:download"
:aria-label="$t('brand.customize.download_svg_aria')"
@click="downloadCustomSvg"
>
{{ $t('brand.logos.download_svg') }}
</ButtonBase>
<ButtonBase
size="sm"
classicon="i-lucide:download"
:aria-label="$t('brand.customize.download_png_aria')"
:disabled="pngLoading"
@click="downloadCustomPng"
>
{{ $t('brand.logos.download_png') }}
</ButtonBase>
</div>
</div>
</div>
</section>
</template>
100 changes: 100 additions & 0 deletions app/components/LogoContextMenu.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
<script setup lang="ts">
const show = shallowRef(false)
const x = shallowRef(0)
const y = shallowRef(0)
const menuRef = useTemplateRef('menuRef')

function onContextMenu(e: MouseEvent) {
e.preventDefault()
x.value = e.clientX
y.value = e.clientY
show.value = true
nextTick(() => {
menuRef.value?.focus()
clampPosition()
})
}

function clampPosition() {
const el = menuRef.value
if (!el) return
const rect = el.getBoundingClientRect()
if (rect.right > window.innerWidth) {
x.value -= rect.right - window.innerWidth + 8
}
if (rect.bottom > window.innerHeight) {
y.value -= rect.bottom - window.innerHeight + 8
}
}

function close() {
show.value = false
}

const { copy, copied } = useClipboard({ copiedDuring: 2000 })

async function copySvg() {
try {
const res = await fetch('/logo.svg')
const svg = await res.text()
await copy(svg)
} finally {
close()
}
}
Comment on lines +36 to +67
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this won't work on Safari but I'm not going to block this right now - we can fix this at the same time we fix #2151

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a fix for this here. 😄


function goToBrand() {
close()
navigateTo({ name: 'brand' })
}

onClickOutside(menuRef, close)

onKeyStroke('Escape', () => {
if (show.value) close()
})
</script>

<template>
<div class="contents" @contextmenu="onContextMenu">
<slot />

<Teleport to="body">
<Transition
enter-active-class="transition duration-100 ease-out"
enter-from-class="opacity-0 scale-95"
enter-to-class="opacity-100 scale-100"
leave-active-class="transition duration-75 ease-in"
leave-from-class="opacity-100 scale-100"
leave-to-class="opacity-0 scale-95"
>
<div
v-if="show"
ref="menuRef"
role="menu"
tabindex="-1"
class="fixed z-[999] min-w-48 bg-bg-elevated border border-border rounded-lg shadow-lg py-1 origin-top-left focus:outline-none motion-reduce:transition-none"
:style="{ left: `${x}px`, top: `${y}px` }"
@keydown.escape="close"
>
<ButtonBase
role="menuitem"
class="w-full text-start gap-x-3 border-none"
:classicon="copied ? 'i-lucide:check text-badge-green' : 'i-lucide:copy'"
@click="copySvg"
>
{{ copied ? $t('brand.colors.copied') : $t('logo_menu.copy_svg') }}
</ButtonBase>
<ButtonBase
role="menuitem"
class="w-full text-start gap-x-3 border-none"
classicon="i-lucide:palette"
@click="goToBrand"
>
{{ $t('logo_menu.browse_brand') }}
</ButtonBase>
</div>
</Transition>
</Teleport>
</div>
</template>
Loading
Loading