Skip to content

Commit 91b12df

Browse files
authored
feat: add accent color picker to settings (npmx-dev#173)
1 parent 2610c7e commit 91b12df

10 files changed

Lines changed: 116 additions & 9 deletions

File tree

app/app.vue

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ import { useEventListener } from '@vueuse/core'
44
const route = useRoute()
55
const router = useRouter()
66
7+
// Initialize accent color before hydration to prevent flash
8+
initAccentOnPrehydrate()
9+
710
const isHomepage = computed(() => route.path === '/')
811
912
useHead({
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<script setup lang="ts">
2+
import { useAccentColor } from '~/composables/useSettings'
3+
4+
const { accentColors, selectedAccentColor, setAccentColor } = useAccentColor()
5+
</script>
6+
7+
<template>
8+
<div role="listbox" aria-label="Accent colors" class="flex items-center justify-between">
9+
<button
10+
v-for="color in accentColors"
11+
:key="color.id"
12+
type="button"
13+
role="option"
14+
:aria-selected="selectedAccentColor === color.id"
15+
:aria-label="color.name"
16+
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)"
17+
:style="{ backgroundColor: color.value }"
18+
@click="setAccentColor(color.id)"
19+
/>
20+
<button
21+
type="button"
22+
aria-label="Clear accent color"
23+
class="size-6 rounded-full transition-transform duration-150 motion-safe:hover:scale-110 focus-ring flex items-center justify-center bg-accent-fallback"
24+
@click="setAccentColor(null)"
25+
>
26+
<span class="i-carbon-error size-4 text-bg" aria-hidden="true" />
27+
</button>
28+
</div>
29+
</template>

app/components/AppHeader.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ const { isConnected, npmUser } = useConnector()
2424
:aria-label="$t('header.home')"
2525
class="header-logo font-mono text-lg font-medium text-fg hover:text-fg transition-colors duration-200 focus-ring rounded"
2626
>
27-
<span class="text-fg-subtle"><span style="letter-spacing: -0.2em">.</span>/</span>npmx
27+
<span class="text-accent"><span class="-tracking-0.2em">.</span>/</span>npmx
2828
</NuxtLink>
2929
<!-- Spacer when logo is hidden -->
3030
<span v-else class="w-1" />

app/components/SettingsMenu.vue

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,10 @@ onKeyStroke(',', e => {
163163
</a>
164164
</div>
165165
</div>
166+
167+
<div class="p-3 border-t border-border">
168+
<AccentColorPicker />
169+
</div>
166170
</div>
167171
</Transition>
168172
</div>

app/composables/useSettings.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import type { RemovableRef } from '@vueuse/core'
22
import { useLocalStorage } from '@vueuse/core'
3+
import { ACCENT_COLORS } from '#shared/utils/constants'
4+
5+
type AccentColorId = keyof typeof ACCENT_COLORS
36

47
/**
58
* Application settings stored in localStorage
@@ -9,11 +12,14 @@ export interface AppSettings {
912
relativeDates: boolean
1013
/** Include @types/* package in install command for packages without built-in types */
1114
includeTypesInInstall: boolean
15+
/** Accent color theme */
16+
accentColorId: AccentColorId | null
1217
}
1318

1419
const DEFAULT_SETTINGS: AppSettings = {
1520
relativeDates: false,
1621
includeTypesInInstall: true,
22+
accentColorId: null,
1723
}
1824

1925
const STORAGE_KEY = 'npmx-settings'
@@ -45,3 +51,56 @@ export function useRelativeDates() {
4551
const { settings } = useSettings()
4652
return computed(() => settings.value.relativeDates)
4753
}
54+
55+
/**
56+
* Composable for managing accent color.
57+
*/
58+
export function useAccentColor() {
59+
const { settings } = useSettings()
60+
61+
const accentColors = Object.entries(ACCENT_COLORS).map(([id, value]) => ({
62+
id: id as AccentColorId,
63+
name: id,
64+
value,
65+
}))
66+
67+
function setAccentColor(id: AccentColorId | null) {
68+
const color = id ? ACCENT_COLORS[id] : null
69+
if (color) {
70+
document.documentElement.style.setProperty('--accent-color', color)
71+
} else {
72+
document.documentElement.style.removeProperty('--accent-color')
73+
}
74+
settings.value.accentColorId = id
75+
}
76+
77+
return {
78+
accentColors,
79+
selectedAccentColor: computed(() => settings.value.accentColorId),
80+
setAccentColor,
81+
}
82+
}
83+
84+
/**
85+
* Applies accent color before hydration to prevent flash of default color.
86+
* Call this from app.vue to ensure accent color is applied on every page.
87+
*/
88+
export function initAccentOnPrehydrate() {
89+
// Callback is stringified by Nuxt - external variables won't be available.
90+
// Colors must be hardcoded since ACCENT_COLORS can't be referenced.
91+
onPrehydrate(() => {
92+
const colors: Record<AccentColorId, string> = {
93+
rose: '#e9aeba',
94+
amber: '#fbbf24',
95+
emerald: '#34d399',
96+
sky: '#38bdf8',
97+
violet: '#a78bfa',
98+
coral: '#fb7185',
99+
}
100+
const settings = JSON.parse(localStorage.getItem('npmx-settings') || '{}')
101+
const color = settings.accentColorId ? colors[settings.accentColorId as AccentColorId] : null
102+
if (color) {
103+
document.documentElement.style.setProperty('--accent-color', color)
104+
}
105+
})
106+
}

app/pages/index.vue

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ defineOgImageComponent('Default')
2727
<h1
2828
class="font-mono text-5xl sm:text-7xl md:text-8xl font-medium tracking-tight mb-4 animate-fade-in animate-fill-both"
2929
>
30-
<span class="text-fg-subtle"><span style="letter-spacing: -0.2em">.</span>/</span>npmx
30+
<span class="text-accent"><span class="-tracking-0.2em">.</span>/</span>npmx
3131
</h1>
3232

3333
<p
@@ -56,7 +56,7 @@ defineOgImageComponent('Default')
5656

5757
<div class="search-box relative flex items-center">
5858
<span
59-
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"
59+
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"
6060
>
6161
/
6262
</span>
@@ -69,7 +69,7 @@ defineOgImageComponent('Default')
6969
:placeholder="$t('search.placeholder')"
7070
autocomplete="off"
7171
autofocus
72-
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)"
72+
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)"
7373
@input="handleSearch"
7474
@focus="isSearchFocused = true"
7575
@blur="isSearchFocused = false"
@@ -103,7 +103,7 @@ defineOgImageComponent('Default')
103103
class="link-subtle font-mono text-sm inline-flex items-center gap-2 group"
104104
>
105105
<span
106-
class="w-1 h-1 rounded-full bg-fg-subtle group-hover:bg-fg transition-colors duration-200"
106+
class="w-1 h-1 rounded-full bg-accent group-hover:bg-fg transition-colors duration-200"
107107
/>
108108
{{ pkg }}
109109
</NuxtLink>

app/pages/search.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -356,7 +356,7 @@ defineOgImageComponent('Default', {
356356

357357
<div class="search-box relative flex items-center">
358358
<span
359-
class="absolute left-4 text-fg-subtle font-mono text-base pointer-events-none transition-colors duration-200 group-focus-within:text-fg-muted"
359+
class="absolute left-4 text-fg-subtle font-mono text-base pointer-events-none transition-colors duration-200 group-focus-within:text-accent"
360360
aria-hidden="true"
361361
>
362362
/
@@ -372,7 +372,7 @@ defineOgImageComponent('Default', {
372372
autocomplete="off"
373373
autocorrect="off"
374374
spellcheck="false"
375-
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"
375+
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"
376376
@focus="isSearchFocused = true"
377377
@blur="isSearchFocused = false"
378378
@keydown="handleResultsKeydown"

shared/utils/constants.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,13 @@ export const NPM_MISSING_README_SENTINEL = 'ERROR: No README data found!'
1616
export const ERROR_JSR_FETCH_FAILED = 'Failed to fetch package from JSR registry.'
1717
export const ERROR_NPM_FETCH_FAILED = 'Failed to fetch package from npm registry.'
1818
export const ERROR_SUGGESTIONS_FETCH_FAILED = 'Failed to fetch suggestions.'
19+
20+
// Theming
21+
export const ACCENT_COLORS = {
22+
rose: '#e9aeba',
23+
amber: '#fbbf24',
24+
emerald: '#34d399',
25+
sky: '#38bdf8',
26+
violet: '#a78bfa',
27+
coral: '#fb7185',
28+
} as const

test/nuxt/components/DateTime.spec.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ vi.mock('~/composables/useSettings', () => ({
99
useSettings: () => ({
1010
settings: ref({ relativeDates: mockRelativeDates.value }),
1111
}),
12+
useAccentColor: () => ({}),
13+
initAccentOnPrehydrate: () => {},
1214
}))
1315

1416
describe('DateTime', () => {

uno.config.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,8 @@ export default defineConfig({
4747
hover: '#404040',
4848
},
4949
accent: {
50-
DEFAULT: '#ffffff',
51-
muted: '#e5e5e5',
50+
DEFAULT: 'var(--accent-color, #666666)',
51+
fallback: '#666666',
5252
},
5353
// Syntax highlighting colors (inspired by GitHub Dark)
5454
syntax: {

0 commit comments

Comments
 (0)