Conversation
…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
|
The latest updates on your projects. Learn more about Vercel for GitHub.
2 Skipped Deployments
|
Lunaria Status Overview🌕 This pull request will trigger status changes. Learn moreBy default, every PR changing files present in the Lunaria configuration's You can change this by adding one of the keywords present in the Tracked Files
Warnings reference
|
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds 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
🚥 Pre-merge checks | ✅ 1✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
Use display:contents so the wrapper div doesn't participate in flex layout.
- Add Brand/Customize and LogoContextMenu to a11y SKIPPED_COMPONENTS - Replace dynamic i18n keys with static $t() calls for color names
There was a problem hiding this comment.
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')returningnullis 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.
_convertis imported fromuseSvgToPng()but never used. The PNG conversion is implemented inline indownloadCustomPng()rather than using this function.♻️ Remove unused destructured variable
-const { convert: _convert, download: downloadBlob } = useSvgToPng() +const { download: downloadBlob } = useSvgToPng()
45-79: Consider using theuseSvgToPngcomposable for PNG conversion.This function duplicates the logic from
useSvgToPng().convert(): waiting for fonts, loading an Image, drawing to canvas, and callingtoBlob. 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
convertexpects 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.svgfails (network error or non-200 response), the error will propagate silently and the user receives no feedback. The menu closes viafinally, 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
📒 Files selected for processing (9)
app/components/AppFooter.vueapp/components/AppHeader.vueapp/components/Brand/Customize.vueapp/components/LogoContextMenu.vueapp/composables/useSvgToPng.tsapp/pages/brand.vuei18n/locales/en.jsonnuxt.config.tsserver/middleware/canonical-redirects.global.ts
There was a problem hiding this comment.
♻️ Duplicate comments (2)
app/pages/brand.vue (2)
11-15:⚠️ Potential issue | 🟡 MinorLocalise 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 | 🟡 MinorTrack PNG export loading per logo, not globally.
Using a single
pngLoadingtoken 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
📒 Files selected for processing (2)
app/pages/brand.vuetest/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
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
app/components/Brand/Customize.vue (1)
56-59: PreferaddEventListener()overon-property assignment.The linter flags these event handler assignments. Using
addEventListeneris 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
📒 Files selected for processing (3)
app/components/Brand/Customize.vueapp/pages/brand.vuei18n/schema.json
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
app/components/Brand/Customize.vue (2)
32-42: Consider using the importeddownloadBlobhelper to reduce duplication.The
downloadBlobfunction fromuseSvgToPng()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: PreferaddEventListenerover direct event handler properties.Static analysis correctly flags that
addEventListenershould be used instead of assigning toonload/onerrorproperties 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
📒 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
- 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
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>
|
nvm, went with delay |
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
| async function copySvg() { | ||
| const res = await fetch('/logo.svg') | ||
| const svg = await res.text() | ||
| await copy(svg) | ||
| setTimeout(close, 1000) | ||
| } |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
I added a fix for this here. 😄
ghostdevv
left a comment
There was a problem hiding this comment.
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 = [ |
There was a problem hiding this comment.
Hmm, the word mark or the logo mark?
There was a problem hiding this comment.
All of them - I saw you pushed a change but for me the slashes are still blue?
There was a problem hiding this comment.
I'm not following the thread super well, so I've tagged Alex to take a look - to me it makes sense that that section uses the grey/white colours and then the customiser section you have bellow allows for changing - but Alex can let us know what is actually correct 😄
There was a problem hiding this comment.
Our default color is blue right now, so this is probably a more convenient format.
But I think you could combine the "customize your logo" and "logo" sections (add accent select to each block, including to mark).
Also, the variant above with white-and-gray is our favicon. It can probably be added separately as one more option without customization
There was a problem hiding this comment.
Also, the variant above with white-and-gray is our favicon. It can probably be added separately as one more option without customization
ah, I've seen the white-and-grey logo used everywhere off-site and as such assumed that it's like the main logo with the other colours being for customization which is why I suggested that block use those colours as I think it's what people would expect?
There was a problem hiding this comment.
We should figure out what kind of logo this is 😅
But we don't use it anywhere with other colors. It could be done - the color would need to be changed differently (f.e. instead of a gray dot, make it dark blue)
But in general, we rarely use it now
P.S. Oh goddesses, there is one more colors option in og - https://npmx.dev/__og-image__/static/og.png ...
There was a problem hiding this comment.
I think we can just add it as a separate asset for now and then create an additional issue to bring all the mark colors to the same look and update it here
- 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
- 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
|
Just a thought @ghostdevv, The SVG download buttons currently use Should we convert the SVG downloads to |
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>
Made the change 👍 |
|
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! |
Wording opinion:
AdditionalAccessibility matters to us, and we would love you to follow us in this vision. When using mentioned media, ensure there is enough contrast against the background, and don’t go smaller than 24px. If you need any other resources or additional information about the project, feel free to reach us at chat.npmx.dev Other suggestions:Please increase the sizes to 14px+ (I think 16px will work good everywhere) and buttons/tabs to 28px Add "back" button to follow other pages in this category I think would be better decrease container size to 2xl (to follow other pages in this category) |




This PR adds a dedicated
/brandpage 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-livefeedback.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