forked from
npmx.dev/npmx.dev
[READ-ONLY]
a fast, modern browser for the npm registry
1import type { RemovableRef } from '@vueuse/core'
2import { useLocalStorage } from '@vueuse/core'
3import { ACCENT_COLORS } from '#shared/utils/constants'
4import type { LocaleObject } from '@nuxtjs/i18n'
5import { BACKGROUND_THEMES } from '#shared/utils/constants'
6
7type BackgroundThemeId = keyof typeof BACKGROUND_THEMES
8
9type AccentColorId = keyof typeof ACCENT_COLORS.light
10
11/** Available search providers */
12export type SearchProvider = 'npm' | 'algolia'
13
14/**
15 * Application settings stored in localStorage
16 */
17export interface AppSettings {
18 /** Display dates as relative (e.g., "3 days ago") instead of absolute */
19 relativeDates: boolean
20 /** Include @types/* package in install command for packages without built-in types */
21 includeTypesInInstall: boolean
22 /** Accent color theme */
23 accentColorId: AccentColorId | null
24 /** Preferred background shade */
25 preferredBackgroundTheme: BackgroundThemeId | null
26 /** Hide platform-specific packages (e.g., @scope/pkg-linux-x64) from search results */
27 hidePlatformPackages: boolean
28 /** User-selected locale */
29 selectedLocale: LocaleObject['code'] | null
30 /** Search provider for package search */
31 searchProvider: SearchProvider
32 /** Connector preferences */
33 connector: {
34 /** Automatically open the web auth page in the browser */
35 autoOpenURL: boolean
36 }
37 sidebar: {
38 collapsed: string[]
39 }
40}
41
42const DEFAULT_SETTINGS: AppSettings = {
43 relativeDates: false,
44 includeTypesInInstall: true,
45 accentColorId: null,
46 hidePlatformPackages: true,
47 selectedLocale: null,
48 preferredBackgroundTheme: null,
49 searchProvider: import.meta.test ? 'npm' : 'algolia',
50 connector: {
51 autoOpenURL: false,
52 },
53 sidebar: {
54 collapsed: [],
55 },
56}
57
58const STORAGE_KEY = 'npmx-settings'
59
60// Shared settings instance (singleton per app)
61let settingsRef: RemovableRef<AppSettings> | null = null
62
63/**
64 * Composable for managing application settings with localStorage persistence.
65 * Settings are shared across all components that use this composable.
66 */
67export function useSettings() {
68 if (!settingsRef) {
69 settingsRef = useLocalStorage<AppSettings>(STORAGE_KEY, DEFAULT_SETTINGS, {
70 mergeDefaults: true,
71 })
72 }
73
74 return {
75 settings: settingsRef,
76 }
77}
78
79/**
80 * Composable for accessing just the relative dates setting.
81 * Useful for components that only need to read this specific setting.
82 */
83export function useRelativeDates() {
84 const { settings } = useSettings()
85 return computed(() => settings.value.relativeDates)
86}
87
88/**
89 * Composable for managing accent color.
90 */
91export function useAccentColor() {
92 const { settings } = useSettings()
93 const colorMode = useColorMode()
94
95 const accentColors = computed(() => {
96 const isDark = colorMode.value === 'dark'
97 const colors = isDark ? ACCENT_COLORS.dark : ACCENT_COLORS.light
98
99 return Object.entries(colors).map(([id, value]) => ({
100 id: id as AccentColorId,
101 name: id,
102 value,
103 }))
104 })
105
106 function setAccentColor(id: AccentColorId | null) {
107 if (id) {
108 document.documentElement.style.setProperty('--accent-color', `var(--swatch-${id})`)
109 } else {
110 document.documentElement.style.removeProperty('--accent-color')
111 }
112 settings.value.accentColorId = id
113 }
114
115 return {
116 accentColors,
117 selectedAccentColor: computed(() => settings.value.accentColorId),
118 setAccentColor,
119 }
120}
121
122/**
123 * Composable for managing the search provider setting.
124 */
125export function useSearchProvider() {
126 const { settings } = useSettings()
127
128 const searchProvider = computed({
129 get: () => settings.value.searchProvider,
130 set: (value: SearchProvider) => {
131 settings.value.searchProvider = value
132 },
133 })
134
135 const isAlgolia = computed(() => searchProvider.value === 'algolia')
136
137 function toggle() {
138 searchProvider.value = searchProvider.value === 'npm' ? 'algolia' : 'npm'
139 }
140
141 return {
142 searchProvider,
143 isAlgolia,
144 toggle,
145 }
146}
147
148export function useBackgroundTheme() {
149 const backgroundThemes = Object.entries(BACKGROUND_THEMES).map(([id, value]) => ({
150 id: id as BackgroundThemeId,
151 name: id,
152 value,
153 }))
154
155 const { settings } = useSettings()
156
157 function setBackgroundTheme(id: BackgroundThemeId | null) {
158 if (id) {
159 document.documentElement.dataset.bgTheme = id
160 } else {
161 document.documentElement.removeAttribute('data-bg-theme')
162 }
163 settings.value.preferredBackgroundTheme = id
164 }
165
166 return {
167 backgroundThemes,
168 selectedBackgroundTheme: computed(() => settings.value.preferredBackgroundTheme),
169 setBackgroundTheme,
170 }
171}