Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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/app.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import { useEventListener } from '@vueuse/core'
const route = useRoute()
const router = useRouter()

// Initialize accent color before hydration to prevent flash
initAccentOnPrehydrate()

const isHomepage = computed(() => route.path === '/')

useHead({
Expand Down
29 changes: 29 additions & 0 deletions app/components/AccentColorPicker.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<script setup lang="ts">
import { useAccentColor } from '~/composables/useSettings'

const { accentColors, selectedAccentColor, setAccentColor } = useAccentColor()
</script>

<template>
<div role="listbox" aria-label="Accent colors" class="flex items-center justify-between">
<button
v-for="color in accentColors"
:key="color.id"
type="button"
role="option"
:aria-selected="selectedAccentColor === color.id"
:aria-label="color.name"
class="size-6 rounded-full transition-transform duration-150 motion-safe:hover:scale-110 focus-ring aria-selected:(ring-2 ring-fg ring-offset-2 ring-offset-bg-subtle)"
:style="{ backgroundColor: color.value }"
@click="setAccentColor(color.id)"
/>
<button
type="button"
aria-label="Clear accent color"
class="size-6 rounded-full transition-transform duration-150 motion-safe:hover:scale-110 focus-ring flex items-center justify-center bg-accent-fallback"
@click="setAccentColor(null)"
>
<span class="i-carbon-error size-4 text-bg" aria-hidden="true" />
</button>
</div>
</template>
2 changes: 1 addition & 1 deletion app/components/AppHeader.vue
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const { isConnected, npmUser } = useConnector()
:aria-label="$t('header.home')"
class="header-logo font-mono text-lg font-medium text-fg hover:text-fg transition-colors duration-200 focus-ring rounded"
>
<span class="text-fg-subtle"><span style="letter-spacing: -0.2em">.</span>/</span>npmx
<span class="text-accent"><span class="-tracking-0.2em">.</span>/</span>npmx
</NuxtLink>
<!-- Spacer when logo is hidden -->
<span v-else class="w-1" />
Expand Down
4 changes: 4 additions & 0 deletions app/components/SettingsMenu.vue
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,10 @@ onKeyStroke(',', e => {
</a>
</div>
</div>

<div class="p-3 border-t border-border">
<AccentColorPicker />
</div>
</div>
</Transition>
</div>
Expand Down
59 changes: 59 additions & 0 deletions app/composables/useSettings.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import type { RemovableRef } from '@vueuse/core'
import { useLocalStorage } from '@vueuse/core'
import { ACCENT_COLORS } from '#shared/utils/constants'

type AccentColorId = keyof typeof ACCENT_COLORS

/**
* Application settings stored in localStorage
Expand All @@ -9,11 +12,14 @@ export interface AppSettings {
relativeDates: boolean
/** Include @types/* package in install command for packages without built-in types */
includeTypesInInstall: boolean
/** Accent color theme */
accentColorId: AccentColorId | null
}

const DEFAULT_SETTINGS: AppSettings = {
relativeDates: false,
includeTypesInInstall: true,
accentColorId: null,
}

const STORAGE_KEY = 'npmx-settings'
Expand Down Expand Up @@ -45,3 +51,56 @@ export function useRelativeDates() {
const { settings } = useSettings()
return computed(() => settings.value.relativeDates)
}

/**
* Composable for managing accent color.
*/
export function useAccentColor() {
const { settings } = useSettings()

const accentColors = Object.entries(ACCENT_COLORS).map(([id, value]) => ({
id: id as AccentColorId,
name: id,
value,
}))

function setAccentColor(id: AccentColorId | null) {
const color = id ? ACCENT_COLORS[id] : null
if (color) {
document.documentElement.style.setProperty('--accent-color', color)
} else {
document.documentElement.style.removeProperty('--accent-color')
}
settings.value.accentColorId = id
}

return {
accentColors,
selectedAccentColor: computed(() => settings.value.accentColorId),
setAccentColor,
}
}

/**
* Applies accent color before hydration to prevent flash of default color.
* Call this from app.vue to ensure accent color is applied on every page.
*/
export function initAccentOnPrehydrate() {
// Callback is stringified by Nuxt - external variables won't be available.
// Colors must be hardcoded since ACCENT_COLORS can't be referenced.
onPrehydrate(() => {
const colors: Record<AccentColorId, string> = {
rose: '#e9aeba',
amber: '#fbbf24',
emerald: '#34d399',
sky: '#38bdf8',
violet: '#a78bfa',
coral: '#fb7185',
}
const settings = JSON.parse(localStorage.getItem('npmx-settings') || '{}')
const color = settings.accentColorId ? colors[settings.accentColorId as AccentColorId] : null
if (color) {
document.documentElement.style.setProperty('--accent-color', color)
}
})
}
8 changes: 4 additions & 4 deletions app/pages/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ defineOgImageComponent('Default')
<h1
class="font-mono text-5xl sm:text-7xl md:text-8xl font-medium tracking-tight mb-4 animate-fade-in animate-fill-both"
>
<span class="text-fg-subtle"><span style="letter-spacing: -0.2em">.</span>/</span>npmx
<span class="text-accent"><span class="-tracking-0.2em">.</span>/</span>npmx
</h1>

<p
Expand Down Expand Up @@ -56,7 +56,7 @@ defineOgImageComponent('Default')

<div class="search-box relative flex items-center">
<span
class="absolute left-4 text-fg-subtle font-mono text-sm pointer-events-none transition-colors duration-200 group-focus-within:text-fg-muted z-1"
class="absolute left-4 text-fg-subtle font-mono text-sm pointer-events-none transition-colors duration-200 group-focus-within:text-accent z-1"
>
/
</span>
Expand All @@ -69,7 +69,7 @@ defineOgImageComponent('Default')
:placeholder="$t('search.placeholder')"
autocomplete="off"
autofocus
class="w-full bg-bg-subtle border border-border rounded-lg pl-8 pr-24 py-4 font-mono text-base text-fg placeholder:text-fg-subtle transition-all duration-300 focus:(border-border-hover outline-none)"
class="w-full bg-bg-subtle border border-border rounded-lg pl-8 pr-24 py-4 font-mono text-base text-fg placeholder:text-fg-subtle transition-all duration-300 focus:(border-accent outline-none)"
@input="handleSearch"
@focus="isSearchFocused = true"
@blur="isSearchFocused = false"
Expand Down Expand Up @@ -103,7 +103,7 @@ defineOgImageComponent('Default')
class="link-subtle font-mono text-sm inline-flex items-center gap-2 group"
>
<span
class="w-1 h-1 rounded-full bg-fg-subtle group-hover:bg-fg transition-colors duration-200"
class="w-1 h-1 rounded-full bg-accent group-hover:bg-fg transition-colors duration-200"
/>
{{ pkg }}
</NuxtLink>
Expand Down
4 changes: 2 additions & 2 deletions app/pages/search.vue
Original file line number Diff line number Diff line change
Expand Up @@ -356,7 +356,7 @@ defineOgImageComponent('Default', {

<div class="search-box relative flex items-center">
<span
class="absolute left-4 text-fg-subtle font-mono text-base pointer-events-none transition-colors duration-200 group-focus-within:text-fg-muted"
class="absolute left-4 text-fg-subtle font-mono text-base pointer-events-none transition-colors duration-200 group-focus-within:text-accent"
aria-hidden="true"
>
/
Expand All @@ -372,7 +372,7 @@ defineOgImageComponent('Default', {
autocomplete="off"
autocorrect="off"
spellcheck="false"
class="w-full max-w-full bg-bg-subtle border border-border rounded-lg pl-8 pr-10 py-3 font-mono text-base text-fg placeholder:text-fg-subtle transition-colors duration-300 focus:border-border-hover focus-visible:outline-none appearance-none"
class="w-full max-w-full bg-bg-subtle border border-border rounded-lg pl-8 pr-10 py-3 font-mono text-base text-fg placeholder:text-fg-subtle transition-colors duration-300 focus:border-accent focus-visible:outline-none appearance-none"
@focus="isSearchFocused = true"
@blur="isSearchFocused = false"
@keydown="handleResultsKeydown"
Expand Down
10 changes: 10 additions & 0 deletions shared/utils/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,13 @@ export const NPM_MISSING_README_SENTINEL = 'ERROR: No README data found!'
export const ERROR_JSR_FETCH_FAILED = 'Failed to fetch package from JSR registry.'
export const ERROR_NPM_FETCH_FAILED = 'Failed to fetch package from npm registry.'
export const ERROR_SUGGESTIONS_FETCH_FAILED = 'Failed to fetch suggestions.'

// Theming
export const ACCENT_COLORS = {
rose: '#e9aeba',
amber: '#fbbf24',
emerald: '#34d399',
sky: '#38bdf8',
violet: '#a78bfa',
coral: '#fb7185',
} as const
2 changes: 2 additions & 0 deletions test/nuxt/components/DateTime.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ vi.mock('~/composables/useSettings', () => ({
useSettings: () => ({
settings: ref({ relativeDates: mockRelativeDates.value }),
}),
useAccentColor: () => ({}),
initAccentOnPrehydrate: () => {},
}))

describe('DateTime', () => {
Expand Down
4 changes: 2 additions & 2 deletions uno.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,8 @@ export default defineConfig({
hover: '#404040',
},
accent: {
DEFAULT: '#ffffff',
muted: '#e5e5e5',
DEFAULT: 'var(--accent-color, #666666)',
fallback: '#666666',
},
// Syntax highlighting colors (inspired by GitHub Dark)
syntax: {
Expand Down