Skip to content

feat: add brand page#2197

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

feat: add brand page#2197
Adebesin-Cell wants to merge 37 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 4:53pm
2 Skipped Deployments
Project Deployment Actions Updated (UTC)
docs.npmx.dev Ignored Ignored Preview Mar 23, 2026 4:53pm
npmx-lunaria Ignored Ignored Mar 23, 2026 4:53pm

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 18.46154% with 53 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 20.75% 37 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
@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 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. 😄

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!

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hmm, the word mark or the logo mark?

Copy link
Contributor

Choose a reason for hiding this comment

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

All of them - I saw you pushed a change but for me the slashes are still blue?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Looks like this atm, isn't the default color blue, or are you referring to something else?

Dark

Screenshot 1

Light

Screenshot 2

Copy link
Contributor

Choose a reason for hiding this comment

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

It's not the default theme colour - but I also was thinking of these colours

image

Copy link
Contributor Author

@Adebesin-Cell Adebesin-Cell Mar 23, 2026

Choose a reason for hiding this comment

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

Oh!

I think in first iteration we removed app logo https://discord.com/channels/1464542801676206113/1485225112461643786/1485278281077882952. Do we want to create for these or we allow a customizer for any type of color? 🤔

Adebesin-Cell and others added 2 commits March 23, 2026 13:11
- Fix Safari clipboard by using ClipboardItem with promise blob
- Add loading spinner to PNG download button in customize section
- Fix logo height mismatch between dark/light variants
- Force canonical sky accent color on light logo previews
Adebesin-Cell and others added 2 commits March 23, 2026 13:30
- Add logo-mark-light.svg with dark accent (#006fc2) and black square
- Use srcLight variant instead of filter: invert(1) for light logo mark
- Add loading spinners to PNG download buttons in logo grid
@Adebesin-Cell
Copy link
Contributor Author

Just a thought @ghostdevv,

The SVG download buttons currently use <a> tags (direct file links with the download attribute), while the PNG buttons use <ButtonBase> (since they require JavaScript for SVG → canvas conversion).

Should we convert the SVG downloads to <ButtonBase> as well for visual and semantic consistency, or is using <a> fine given that it's a straightforward file download?

@ghostdevv
Copy link
Contributor

Just a thought @ghostdevv,

The SVG download buttons currently use <a> tags (direct file links with the download attribute), while the PNG buttons use <ButtonBase> (since they require JavaScript for SVG → canvas conversion).

Should we convert the SVG downloads to <ButtonBase> as well for visual and semantic consistency, or is using <a> fine given that it's a straightforward file download?

Hmm, how much work is it to make the link here look like the button? It seems pretty close already - otherwise we should probably just use buttonbase yea

…ading spinners

Use ButtonBase consistently for all download buttons, add spinner loading
states to SVG download buttons, create light-mode wordmark SVG, and ensure
light variant downloads use the correct srcLight file.

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

Just a thought @ghostdevv,
The SVG download buttons currently use <a> tags (direct file links with the download attribute), while the PNG buttons use <ButtonBase> (since they require JavaScript for SVG → canvas conversion).
Should we convert the SVG downloads to <ButtonBase> as well for visual and semantic consistency, or is using <a> fine given that it's a straightforward file download?

Hmm, how much work is it to make the link here look like the button? It seems pretty close already - otherwise we should probably just use buttonbase yea

Made the change 👍

@trueberryless
Copy link
Contributor

trueberryless commented Mar 23, 2026

Awesome work @Adebesin-Cell. Love how the idea was randomly thrown around I think yesterday, and now we already have an amazing implementation 🙌

Not sure if it has been mentioned already (sorry I didn't read through all the threads), but I think it would be nice if the big logo at the landing page would also support the same options as the small logo in the header on all other pages. IFF it is possible in an accessible way!

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.

4 participants