Skip to content

feat: add brand page#2197

Open
Adebesin-Cell wants to merge 31 commits intonpmx-dev:mainfrom
Adebesin-Cell:feat/brand-page
Open

feat: add brand page#2197
Adebesin-Cell wants to merge 31 commits intonpmx-dev:mainfrom
Adebesin-Cell:feat/brand-page

Conversation

@Adebesin-Cell
Copy link
Contributor

@Adebesin-Cell Adebesin-Cell commented Mar 22, 2026

This PR adds a dedicated /brand page for press, media, and community use, taking inspiration from Nuxt’s design kit and IQ Wiki’s branding page.

What’s included

  • Logo showcase
    A full set of logo variants (wordmark, mark), displayed on both light and dark backgrounds, with quick SVG and PNG downloads.

  • Customize your logo
    An interactive preview where you can adjust the accent color and toggle between light/dark backgrounds. You can download the customized logo as SVG or PNG, with all colors baked in (no CSS variables).

  • Color palette
    Core brand colors (Background, Foreground, Accent) with one-click copy for both HEX and OKLch values, plus screen reader-friendly aria-live feedback.

  • Typography specimens
    Geist Sans and Geist Mono are shown across multiple sizes, including pangrams and number samples.

  • Usage guidelines
    Clear do’s and don’ts to help people use the logo correctly.

  • Header logo context menu
    Right-click the header logo anywhere in the app to quickly “Copy logo as SVG” or jump to the brand kit, mirroring the pattern from nuxt.com.

Media

Screen.Recording.2026-03-22.at.17.52.14.mov

…ines

Adds a /brand page for press and media use, featuring:
- Logo section with dark/light previews and SVG/PNG downloads
- Customizable logo preview with accent color picker and background toggle
- Core brand color palette with click-to-copy hex and OKLch values
- Typography specimens for Geist Sans and Geist Mono
- Usage guidelines with do's and don'ts
- Right-click context menu on header logo (copy SVG, browse brand kit)
- Full i18n support
- Navigation links in footer and mobile menu
@vercel
Copy link

vercel bot commented Mar 22, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
npmx.dev Ready Ready Preview, Comment Mar 23, 2026 6:08am
2 Skipped Deployments
Project Deployment Actions Updated (UTC)
docs.npmx.dev Ignored Ignored Preview Mar 23, 2026 6:08am
npmx-lunaria Ignored Ignored Mar 23, 2026 6:08am

Request Review

@github-actions
Copy link

github-actions bot commented Mar 22, 2026

Lunaria Status Overview

🌕 This pull request will trigger status changes.

Learn more

By default, every PR changing files present in the Lunaria configuration's files property will be considered and trigger status changes accordingly.

You can change this by adding one of the keywords present in the ignoreKeywords property in your Lunaria configuration file in the PR's title (ignoring all files) or by including a tracker directive in the merged commit's description.

Tracked Files

File Note
i18n/locales/en.json Source changed, localizations will be marked as outdated.
Warnings reference
Icon Description
🔄️ The source for this localization has been updated since the creation of this pull request, make sure all changes in the source have been applied.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 22, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a new Brand documentation area: a Nuxt page at app/pages/brand.vue, a Brand customization component (app/components/Brand/Customize.vue), a context menu wrapper for logos (app/components/LogoContextMenu.vue), and an SVG→PNG composable (app/composables/useSvgToPng.ts). Registers /brand for prerendering and exempts it from canonical redirects. Surfaces the Brand route in header and footer and wraps header logos with LogoContextMenu. Adds i18n keys and schema entries for brand-related strings and skips two client-only components in a11y component-coverage tests.

Possibly related PRs

Suggested reviewers

  • alexdln
  • danielroe
🚥 Pre-merge checks | ✅ 1
✅ Passed checks (1 passed)
Check name Status Explanation
Description check ✅ Passed The PR description directly relates to the changeset, outlining all major features included: logo showcase, customizable logo preview, typography specimens, usage guidelines, and header context menu.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov
Copy link

codecov bot commented Mar 22, 2026

Codecov Report

❌ Patch coverage is 25.00000% with 36 lines in your changes missing coverage. Please review.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
app/components/LogoContextMenu.vue 30.55% 20 Missing and 5 partials ⚠️
app/utils/download.ts 0.00% 9 Missing ⚠️
app/components/AppHeader.vue 50.00% 1 Missing ⚠️
app/components/Package/DownloadButton.vue 0.00% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

Use display:contents so the wrapper div doesn't participate in flex layout.
Adebesin-Cell and others added 2 commits March 22, 2026 15:37
- Add Brand/Customize and LogoContextMenu to a11y SKIPPED_COMPONENTS
- Replace dynamic i18n keys with static $t() calls for color names
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (5)
app/composables/useSvgToPng.ts (1)

20-20: Consider handling null canvas context for defensive coding.

While getContext('2d') returning null is extremely rare in modern browsers, the non-null assertion could mask issues in edge cases (e.g., resource constraints, unsupported canvas contexts in some environments).

🛡️ Optional: Add null check
     const ctx = canvas.getContext('2d')
+    if (!ctx) throw new Error('Failed to get canvas 2D context')
-    const ctx = canvas.getContext('2d')!
     ctx.scale(scale, scale)
app/components/Brand/Customize.vue (2)

3-3: Remove unused import.

_convert is imported from useSvgToPng() but never used. The PNG conversion is implemented inline in downloadCustomPng() rather than using this function.

♻️ Remove unused destructured variable
-const { convert: _convert, download: downloadBlob } = useSvgToPng()
+const { download: downloadBlob } = useSvgToPng()

45-79: Consider using the useSvgToPng composable for PNG conversion.

This function duplicates the logic from useSvgToPng().convert(): waiting for fonts, loading an Image, drawing to canvas, and calling toBlob. The only difference is the background fill and using a data URL from a Blob rather than an external SVG URL.

While the current implementation works, consolidating this logic would reduce duplication. However, since the composable's convert expects a URL and this needs an SVG string, the current approach is acceptable.

app/components/LogoContextMenu.vue (2)

36-44: Consider handling fetch errors gracefully.

If the fetch for /logo.svg fails (network error or non-200 response), the error will propagate silently and the user receives no feedback. The menu closes via finally, but the copy operation fails without indication.

🛡️ Add error handling with user feedback
 async function copySvg() {
   try {
     const res = await fetch('/logo.svg')
+    if (!res.ok) throw new Error('Failed to fetch logo')
     const svg = await res.text()
     await copy(svg)
+  } catch {
+    // Optionally: show toast or log error
+    console.error('Failed to copy logo SVG')
   } finally {
     close()
   }
 }

53-55: Minor: Redundant escape key handler.

The onKeyStroke('Escape', ...) at lines 53-55 already handles closing the menu globally when Escape is pressed. The @keydown.escape="close" on line 78 is redundant since both achieve the same result.

♻️ Remove redundant handler
         :style="{ left: `${x}px`, top: `${y}px` }"
-        `@keydown.escape`="close"
       >

Also applies to: 78-78


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ea36b128-de30-4d15-b876-69f94853e1fe

📥 Commits

Reviewing files that changed from the base of the PR and between 7f2fc1a and fb287bf.

📒 Files selected for processing (9)
  • app/components/AppFooter.vue
  • app/components/AppHeader.vue
  • app/components/Brand/Customize.vue
  • app/components/LogoContextMenu.vue
  • app/composables/useSvgToPng.ts
  • app/pages/brand.vue
  • i18n/locales/en.json
  • nuxt.config.ts
  • server/middleware/canonical-redirects.global.ts

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (2)
app/pages/brand.vue (2)

11-15: ⚠️ Potential issue | 🟡 Minor

Localise OG image title/description to match active locale.

Line 13 and Line 14 are hard-coded English, so social previews can diverge from translated page metadata.

🌍 Proposed fix
 defineOgImageComponent('Default', {
   primaryColor: '#51c8fc',
-  title: 'npmx brand',
-  description: 'logos, colors, typography, and usage guidelines',
+  title: $t('brand.title'),
+  description: $t('brand.meta_description'),
 })

Based on learnings: In this Nuxt project, page components should rely on auto-imported $t() in <script setup>, including callbacks like metadata definitions.


74-84: ⚠️ Potential issue | 🟡 Minor

Track PNG export loading per logo, not globally.

Using a single pngLoading token allows one export to clear another export’s loading state too early.

🛠️ Proposed fix
-const pngLoading = ref<string | null>(null)
+const pngLoading = ref(new Set<string>())

 async function handlePngDownload(logo: (typeof logos)[number]) {
-  pngLoading.value = logo.src
+  if (pngLoading.value.has(logo.src)) return
+  pngLoading.value.add(logo.src)
   try {
     const blob = await convert(logo.src, logo.width, logo.height)
     const filename = logo.src.replace(/^\//, '').replace('.svg', '.png')
     downloadPng(blob, filename)
   } finally {
-    pngLoading.value = null
+    pngLoading.value.delete(logo.src)
   }
 }
-:disabled="pngLoading === logo.src"
+:disabled="pngLoading.has(logo.src)"

Also applies to: 170-170


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1d8009eb-6e5d-4e10-b1d6-5793de384132

📥 Commits

Reviewing files that changed from the base of the PR and between fb287bf and c956570.

📒 Files selected for processing (2)
  • app/pages/brand.vue
  • test/unit/a11y-component-coverage.spec.ts

- Localise OG image metadata with $t() instead of hardcoded strings
- Track PNG loading per-logo with a Set to prevent race conditions
- Add safe fallback for empty accentColors array
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
app/components/Brand/Customize.vue (1)

56-59: Prefer addEventListener() over on-property assignment.

The linter flags these event handler assignments. Using addEventListener is the recommended pattern.

♻️ Suggested refactor
     const loaded = new Promise<void>((resolve, reject) => {
-      img.onload = () => resolve()
-      img.onerror = () => reject(new Error('Failed to load custom SVG'))
+      img.addEventListener('load', () => resolve())
+      img.addEventListener('error', () => reject(new Error('Failed to load custom SVG')))
     })

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 655116fe-f57d-4712-bf76-565cfa75ac1a

📥 Commits

Reviewing files that changed from the base of the PR and between c956570 and 32900dc.

📒 Files selected for processing (3)
  • app/components/Brand/Customize.vue
  • app/pages/brand.vue
  • i18n/schema.json

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
app/components/Brand/Customize.vue (2)

32-42: Consider using the imported downloadBlob helper to reduce duplication.

The downloadBlob function from useSvgToPng() is already imported but unused here. The manual download logic duplicates what that helper provides.

♻️ Proposed refactor
 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)
+  downloadBlob(blob, `npmx-logo-${activeAccentId.value}.svg`)
 }

55-61: Prefer addEventListener over direct event handler properties.

Static analysis correctly flags that addEventListener should be used instead of assigning to onload/onerror properties directly.

♻️ Proposed refactor
     const img = new Image()
     const loaded = new Promise<void>((resolve, reject) => {
-      img.onload = () => resolve()
-      img.onerror = () => reject(new Error('Failed to load custom SVG'))
+      img.addEventListener('load', () => resolve())
+      img.addEventListener('error', () => reject(new Error('Failed to load custom SVG')))
     })
     img.src = url
     await loaded

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b45a838a-515d-4fc2-92f9-3cddf9aa662c

📥 Commits

Reviewing files that changed from the base of the PR and between 32900dc and 3639ee2.

📒 Files selected for processing (1)
  • app/components/Brand/Customize.vue

…uidelines

- Remove app icon (irrelevant to branding)
- Remove colors section (not needed for asset page)
- Replace do's/don'ts with a single accessibility-focused blockquote
- Move "copied" key to logo_menu namespace
Adebesin-Cell and others added 2 commits March 22, 2026 17:36
- Each dark/light logo preview now has its own SVG/PNG download buttons
- Increased spacing between logo cards
- Guidelines reworded to a friendly blockquote ("just a note")
- Removed app icon from logos (not relevant to branding)
- Removed colors section
When the customizer is set to light background, use the light-mode accent
palette so colors like neutral render as dark instead of white-on-white.
@Adebesin-Cell
Copy link
Contributor Author

This looks awesome !

I wonder if it is possible to avoid this specific outcome (light with white):

image

Thanks! Perhaps we can keep a note of the accent so we can swap based off bg theme?

@Adebesin-Cell
Copy link
Contributor Author

Screen.Recording.2026-03-22.at.17.57.43.mov

What do you think of this? @graphieros

@graphieros
Copy link
Contributor

Thanks! Perhaps we can keep a note of the accent so we can swap based off bg theme?

Or have the white option become black in light mode ?

@graphieros
Copy link
Contributor

What do you think of this? @graphieros

This fixes the issue, I just find it weird to have the white option colour circle in light mode. i guess a black one would require a white border.

@Adebesin-Cell
Copy link
Contributor Author

What do you think of this? @graphieros

This fixes the issue, I just find it weird to have the white option colour circle in light mode. i guess a black one would require a white border.

Screen.Recording.2026-03-22.at.18.03.00.mov

I think our accent color useAccentColor sort of took care of that. Or am I missing sth? 🤔

@graphieros
Copy link
Contributor

I think our accent color useAccentColor sort of took care of that. Or am I missing sth? 🤔

You have a slightly different situation here, a special case where you can simulate light mode in dark mode and vice-versa.

For example, in dark mode, when simulating light mode rendering, clicking the white circle renders a black colour in the logo. It's probably a minor detail.

image

Adebesin-Cell and others added 2 commits March 22, 2026 20:05
The accent color picker was using the site's theme palette while the
logo preview used the customizer's own dark/light toggle. This caused
the neutral swatch to show white while the logo rendered black (or
vice-versa). Now the picker derives its colors from the same palette
as the preview, and the neutral swatch gets a visible border so it
doesn't disappear against contrasting backgrounds.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@Adebesin-Cell
Copy link
Contributor Author

I think our accent color useAccentColor sort of took care of that. Or am I missing sth? 🤔

You have a slightly different situation here, a special case where you can simulate light mode in dark mode and vice-versa.

For example, in dark mode, when simulating light mode rendering, clicking the white circle renders a black colour in the logo. It's probably a minor detail.

image

Oh! Now I get it. I was a bit lost before on what you meant.

Fixed it! When you toggle to light preview, the neutral circle shows dark; toggle to dark preview, it shows white.

Screen.Recording.2026-03-22.at.20.08.41.mov

@graphieros
Copy link
Contributor

Fixed it! When you toggle to light preview, the neutral circle shows dark; toggle to dark preview, it shows white.

Awesome :)

One other detail, when copying the svg from the logo in the header, it closes the popover immediately. Perhaps a small delay would allow the confirmation label to be readable ?

@Adebesin-Cell
Copy link
Contributor Author

Fixed it! When you toggle to light preview, the neutral circle shows dark; toggle to dark preview, it shows white.

Awesome :)

One other detail, when copying the svg from the logo in the header, it closes the popover immediately. Perhaps a small delay would allow the confirmation label to be readable ?

Sounds good, or perhaps we go full Nuxt design-kit style? Display a toast on successful copy, wonder if that's overkill or not

Screen.Recording.2026-03-22.at.20.28.07.mov

Adebesin-Cell and others added 2 commits March 22, 2026 20:34
Keep the menu open for 800ms after copying the SVG so the user can
see the "Copied!" label before it disappears.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@Adebesin-Cell
Copy link
Contributor Author

nvm, went with delay

Copy link
Contributor

@graphieros graphieros left a comment

Choose a reason for hiding this comment

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

LGTM 🌿

@graphieros graphieros requested a review from serhalp March 22, 2026 19:50
I've seen this code used all over the place and so this is a good chance
to make a util for it proper - I'll leave the chart stuff though for
this PR as it needs more involved testing that I don't have time for
tonight
Since it doesn't hold any state, it seems like it's better as a utility
fn - we can always add it back if needed later
Comment on lines +36 to +41
async function copySvg() {
const res = await fetch('/logo.svg')
const svg = await res.text()
await copy(svg)
setTimeout(close, 1000)
}
Copy link
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
Contributor

@ghostdevv ghostdevv left a comment

Choose a reason for hiding this comment

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

this is awesome!!

I pushed a couple commits that fixed some lint errors, and removed the composable since it didn't have any state and seems better suited as a util function - just a couple more things then we can merge!

Comment on lines +27 to +35
{
name: () => $t('brand.logos.mark'),
src: '/logo-mark.svg',
altDark: () => $t('brand.logos.mark_alt'),
altLight: () => $t('brand.logos.mark_light_alt'),
width: 153,
height: 153,
span: true,
},
Copy link
Contributor

Choose a reason for hiding this comment

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

these two seem to be different heights? not sure if this is just a weird trick of the brain

Image

description: $t('brand.meta_description'),
})

const logos = [
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we have these be the normal colours? Currently it looks like they respect the users theme

Image

Comment on lines +172 to +197
<ButtonBase
size="sm"
role="radio"
:aria-checked="customBgDark"
:aria-label="$t('brand.logos.on_dark')"
class="!border-none !rounded-none 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') }}
</ButtonBase>
<ButtonBase
size="sm"
role="radio"
:aria-checked="!customBgDark"
:aria-label="$t('brand.logos.on_light')"
class="!border-none !rounded-none border-is border-is-border 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') }}
</ButtonBase>
Copy link
Contributor

Choose a reason for hiding this comment

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

It'd be cool if it had a little spinner while it's downloading (e.g.), but it's kinda extra so don't do it if you don't want to xD

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants