Skip to content
Open
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
4 changes: 4 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ The `.cache/` directory is a separate storage mount used for fetch-cache and atp
app/ # Nuxt 4 app directory
├── components/ # Vue components (PascalCase.vue)
├── composables/ # Vue composables (useFeature.ts)
│ └── userPreferences/ # User preference composables (synced to server)
├── pages/ # File-based routing
├── plugins/ # Nuxt plugins
├── app.vue # Root component
Expand All @@ -189,6 +190,9 @@ test/ # Vitest tests
> [!TIP]
> For more about the meaning of these directories, check out the docs on the [Nuxt directory structure](https://nuxt.com/docs/4.x/directory-structure).

> [!TIP]
> For guidance on working with user preferences and local settings, see the [User Preferences README](./app/composables/userPreferences/README.md).

### Local connector CLI

The `cli/` workspace contains a local connector that enables authenticated npm operations from the web UI. It runs on your machine and uses your existing npm credentials.
Expand Down
6 changes: 3 additions & 3 deletions app/app.vue
Original file line number Diff line number Diff line change
Expand Up @@ -47,15 +47,15 @@ if (import.meta.server) {
setJsonLd(createWebSiteSchema())
}

const keyboardShortcuts = useKeyboardShortcuts()
const { settings } = useSettings()
const keyboardShortcuts = useKeyboardShortcutsPreference()
const instantSearch = useInstantSearchPreference()

onKeyDown(
'/',
e => {
if (e.ctrlKey) {
e.preventDefault()
settings.value.instantSearch = !settings.value.instantSearch
instantSearch.value = !instantSearch.value
return
}

Expand Down
2 changes: 1 addition & 1 deletion app/components/AppHeader.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { NavigationConfig, NavigationConfigWithGroups } from '~/types'
import { isEditableElement } from '~/utils/input'
import { NPMX_DOCS_SITE } from '#shared/utils/constants'

const keyboardShortcuts = useKeyboardShortcuts()
const keyboardShortcuts = useKeyboardShortcutsPreference()
const discord = useDiscordLink()

withDefaults(
Expand Down
2 changes: 1 addition & 1 deletion app/components/Button/Base.vue
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ const props = withDefaults(

const el = useTemplateRef('el')

const keyboardShortcutsEnabled = useKeyboardShortcuts()
const keyboardShortcutsEnabled = useKeyboardShortcutsPreference()

defineExpose({
focus: () => el.value?.focus(),
Expand Down
11 changes: 5 additions & 6 deletions app/components/CollapsibleSection.vue
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const props = withDefaults(defineProps<Props>(), {
headingLevel: 'h2',
})

const appSettings = useSettings()
const { localSettings } = useUserLocalSettings()

const buttonId = `${props.id}-collapsible-button`
const contentId = `${props.id}-collapsible-content`
Expand Down Expand Up @@ -48,17 +48,16 @@ onMounted(() => {
function toggle() {
isOpen.value = !isOpen.value

const removed = appSettings.settings.value.sidebar.collapsed.filter(c => c !== props.id)
const removed = localSettings.value.sidebar.collapsed.filter(c => c !== props.id)

if (isOpen.value) {
appSettings.settings.value.sidebar.collapsed = removed
localSettings.value.sidebar.collapsed = removed
} else {
removed.push(props.id)
appSettings.settings.value.sidebar.collapsed = removed
localSettings.value.sidebar.collapsed = removed
}

document.documentElement.dataset.collapsed =
appSettings.settings.value.sidebar.collapsed.join(' ')
document.documentElement.dataset.collapsed = localSettings.value.sidebar.collapsed.join(' ')
}

const ariaLabel = computed(() => {
Expand Down
2 changes: 1 addition & 1 deletion app/components/Compare/PackageSelector.vue
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ const resultIndexOffset = computed(() => (showNoDependencyOption.value ? 1 : 0))

const numberFormatter = useNumberFormatter()

const keyboardShortcuts = useKeyboardShortcuts()
const keyboardShortcuts = useKeyboardShortcutsPreference()

function addPackage(name: string) {
if (packages.value.length >= maxPackages.value) return
Expand Down
2 changes: 1 addition & 1 deletion app/components/DateTime.vue
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ const props = withDefaults(

const { locale } = useI18n()

const relativeDates = useRelativeDates()
const relativeDates = useRelativeDatesPreference()

const dateFormatter = new Intl.DateTimeFormat(locale.value, {
month: 'short',
Expand Down
6 changes: 3 additions & 3 deletions app/components/Header/ConnectorModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
const { isConnected, isConnecting, npmUser, error, hasOperations, connect, disconnect } =
useConnector()

const { settings } = useSettings()
const { localSettings } = useUserLocalSettings()

const tokenInput = shallowRef('')
const portInput = shallowRef('31415')
Expand Down Expand Up @@ -67,7 +67,7 @@ const executeNpmxConnectorCommand = computed(() => {
<div class="flex flex-col gap-2">
<SettingsToggle
:label="$t('connector.modal.auto_open_url')"
v-model="settings.connector.autoOpenURL"
v-model="localSettings.connector.autoOpenURL"
/>
</div>

Expand Down Expand Up @@ -158,7 +158,7 @@ const executeNpmxConnectorCommand = computed(() => {
<div class="flex flex-col gap-2">
<SettingsToggle
:label="$t('connector.modal.auto_open_url')"
v-model="settings.connector.autoOpenURL"
v-model="localSettings.connector.autoOpenURL"
/>
</div>
</div>
Expand Down
2 changes: 1 addition & 1 deletion app/components/Input/Base.vue
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ const emit = defineEmits<{

const el = useTemplateRef('el')

const keyboardShortcutsEnabled = useKeyboardShortcuts()
const keyboardShortcutsEnabled = useKeyboardShortcutsPreference()

defineExpose({
focus: () => el.value?.focus(),
Expand Down
21 changes: 12 additions & 9 deletions app/components/InstantSearch.vue
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
<script setup lang="ts">
import { useSettings } from '~/composables/useSettings'

const { settings } = useSettings()
const instantSearch = useInstantSearchPreference()

onPrehydrate(el => {
const settingsSaved = JSON.parse(localStorage.getItem('npmx-settings') || '{}')
const enabled = settingsSaved.instantSearch
let userPreferences: Record<string, unknown> = {}

try {
userPreferences = JSON.parse(localStorage.getItem('npmx-user-preferences') || '{}')
} catch {}

const enabled = userPreferences.instantSearch
if (enabled === false) {
el.querySelector('[data-instant-search-on]')!.className = 'hidden'
el.querySelector('[data-instant-search-off]')!.className = ''
Expand All @@ -20,7 +23,7 @@ onPrehydrate(el => {
style="font-size: 0.8em"
aria-hidden="true"
/>
<span data-instant-search-on :class="settings.instantSearch ? '' : 'hidden'">
<span data-instant-search-on :class="instantSearch ? '' : 'hidden'">
<i18n-t keypath="search.instant_search_advisory">
<template #label>
{{ $t('search.instant_search') }}
Expand All @@ -29,13 +32,13 @@ onPrehydrate(el => {
<strong>{{ $t('search.instant_search_on') }}</strong>
</template>
<template #action>
<button type="button" class="underline" @click="settings.instantSearch = false">
<button type="button" class="underline" @click="instantSearch = false">
{{ $t('search.instant_search_turn_off') }}
</button>
</template>
</i18n-t>
</span>
<span data-instant-search-off :class="settings.instantSearch ? 'hidden' : ''">
<span data-instant-search-off :class="instantSearch ? 'hidden' : ''">
<i18n-t keypath="search.instant_search_advisory">
<template #label>
{{ $t('search.instant_search') }}
Expand All @@ -44,7 +47,7 @@ onPrehydrate(el => {
<strong>{{ $t('search.instant_search_off') }}</strong>
</template>
<template #action>
<button type="button" class="underline" @click="settings.instantSearch = true">
<button type="button" class="underline" @click="instantSearch = true">
{{ $t('search.instant_search_turn_on') }}
</button>
</template>
Expand Down
2 changes: 1 addition & 1 deletion app/components/Link/Base.vue
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ const isLink = computed(() => props.variant === 'link')
const isButton = computed(() => !isLink.value)
const isButtonSmall = computed(() => props.size === 'small' && !isLink.value)
const isButtonMedium = computed(() => props.size === 'medium' && !isLink.value)
const keyboardShortcutsEnabled = useKeyboardShortcuts()
const keyboardShortcutsEnabled = useKeyboardShortcutsPreference()
</script>

<template>
Expand Down
31 changes: 17 additions & 14 deletions app/components/Package/TrendsChart.vue
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ const props = withDefaults(

const { locale } = useI18n()
const { accentColors, selectedAccentColor } = useAccentColor()
const { settings } = useSettings()
const { localSettings } = useUserLocalSettings()
const { copy, copied } = useClipboard()

const colorMode = useColorMode()
Expand Down Expand Up @@ -977,7 +977,7 @@ const effectiveDataSingle = computed<EvolutionData>(() => {

if (isDownloadsMetric.value && data.length) {
const pkg = effectivePackageNames.value[0] ?? props.packageName ?? ''
if (settings.value.chartFilter.anomaliesFixed) {
if (localSettings.value.chartFilter.anomaliesFixed) {
data = applyBlocklistCorrection({
data,
packageName: pkg,
Expand Down Expand Up @@ -1025,7 +1025,7 @@ const chartData = computed<{
for (const pkg of names) {
let data = state.evolutionsByPackage[pkg] ?? []
if (isDownloadsMetric.value && data.length) {
if (settings.value.chartFilter.anomaliesFixed) {
if (localSettings.value.chartFilter.anomaliesFixed) {
data = applyBlocklistCorrection({ data, packageName: pkg, granularity })
}
}
Expand Down Expand Up @@ -1079,9 +1079,10 @@ const normalisedDataset = computed(() => {
const series = applyDataPipeline(
d.series.map(v => v ?? 0),
{
averageWindow: settings.value.chartFilter.averageWindow,
smoothingTau: settings.value.chartFilter.smoothingTau,
predictionPoints: settings.value.chartFilter.predictionPoints ?? DEFAULT_PREDICTION_POINTS,
averageWindow: localSettings.value.chartFilter.averageWindow,
smoothingTau: localSettings.value.chartFilter.smoothingTau,
predictionPoints:
localSettings.value.chartFilter.predictionPoints ?? DEFAULT_PREDICTION_POINTS,
},
{ granularity, lastDateMs, referenceMs, isAbsoluteMetric },
)
Expand Down Expand Up @@ -1678,10 +1679,10 @@ watch(selectedMetric, value => {
<label class="flex flex-col gap-1 flex-1">
<span class="text-2xs font-mono text-fg-subtle tracking-wide uppercase">
{{ $t('package.trends.average_window') }}
<span class="text-fg-muted">({{ settings.chartFilter.averageWindow }})</span>
<span class="text-fg-muted">({{ localSettings.chartFilter.averageWindow }})</span>
</span>
<input
v-model.number="settings.chartFilter.averageWindow"
v-model.number="localSettings.chartFilter.averageWindow"
type="range"
min="0"
max="20"
Expand All @@ -1692,10 +1693,10 @@ watch(selectedMetric, value => {
<label class="flex flex-col gap-1 flex-1">
<span class="text-2xs font-mono text-fg-subtle tracking-wide uppercase">
{{ $t('package.trends.smoothing') }}
<span class="text-fg-muted">({{ settings.chartFilter.smoothingTau }})</span>
<span class="text-fg-muted">({{ localSettings.chartFilter.smoothingTau }})</span>
</span>
<input
v-model.number="settings.chartFilter.smoothingTau"
v-model.number="localSettings.chartFilter.smoothingTau"
type="range"
min="0"
max="20"
Expand All @@ -1706,10 +1707,10 @@ watch(selectedMetric, value => {
<label class="flex flex-col gap-1 flex-1">
<span class="text-2xs font-mono text-fg-subtle tracking-wide uppercase">
{{ $t('package.trends.prediction') }}
<span class="text-fg-muted">({{ settings.chartFilter.predictionPoints }})</span>
<span class="text-fg-muted">({{ localSettings.chartFilter.predictionPoints }})</span>
</span>
<input
v-model.number="settings.chartFilter.predictionPoints"
v-model.number="localSettings.chartFilter.predictionPoints"
type="range"
min="0"
max="30"
Expand Down Expand Up @@ -1774,9 +1775,11 @@ watch(selectedMetric, value => {
:class="{ 'opacity-50 pointer-events-none': !hasAnomalies }"
>
<input
:checked="settings.chartFilter.anomaliesFixed && hasAnomalies"
:checked="localSettings.chartFilter.anomaliesFixed && hasAnomalies"
@change="
settings.chartFilter.anomaliesFixed = ($event.target as HTMLInputElement).checked
localSettings.chartFilter.anomaliesFixed = (
$event.target as HTMLInputElement
).checked
"
type="checkbox"
:disabled="!hasAnomalies"
Expand Down
8 changes: 4 additions & 4 deletions app/components/Package/WeeklyDownloadStats.vue
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const props = defineProps<{

const router = useRouter()
const route = useRoute()
const { settings } = useSettings()
const { localSettings } = useUserLocalSettings()

const chartModal = useModal('chart-modal')
const hasChartModalTransitioned = shallowRef(false)
Expand Down Expand Up @@ -185,14 +185,14 @@ watch(
const correctedDownloads = computed<WeeklyDataPoint[]>(() => {
let data = weeklyDownloads.value as WeeklyDataPoint[]
if (!data.length) return data
if (settings.value.chartFilter.anomaliesFixed) {
if (localSettings.value.chartFilter.anomaliesFixed) {
data = applyBlocklistCorrection({
data,
packageName: props.packageName,
granularity: 'weekly',
}) as WeeklyDataPoint[]
}
data = applyDataCorrection(data, settings.value.chartFilter) as WeeklyDataPoint[]
data = applyDataCorrection(data, localSettings.value.chartFilter) as WeeklyDataPoint[]
return data
})

Expand All @@ -210,7 +210,7 @@ const lastDatapoint = computed(() => dataset.value.at(-1)?.period ?? '')

const isLoop = shallowRef(false)
const showPulse = shallowRef(true)
const keyboardShortcuts = useKeyboardShortcuts()
const keyboardShortcuts = useKeyboardShortcutsPreference()

const cheatCode = [
'arrowup',
Expand Down
9 changes: 6 additions & 3 deletions app/components/Settings/AccentColorPicker.vue
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
<script setup lang="ts">
import { useAccentColor } from '~/composables/useSettings'
import type { UserPreferences } from '#shared/schemas/userPreferences'

const { accentColors, selectedAccentColor, setAccentColor } = useAccentColor()

onPrehydrate(el => {
const settings = JSON.parse(localStorage.getItem('npmx-settings') || '{}')
let preferences: UserPreferences = {}
const defaultId = 'sky'
const id = settings.accentColorId
try {
preferences = JSON.parse(localStorage.getItem('npmx-user-preferences') || '{}')
} catch {}
const id = preferences.accentColorId
if (id) {
const input = el.querySelector<HTMLInputElement>(`input[value="${id}"]`)
if (input) {
Expand Down
9 changes: 7 additions & 2 deletions app/components/Settings/BgThemePicker.vue
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
<script setup lang="ts">
import type { UserPreferences } from '#shared/schemas/userPreferences'

const { backgroundThemes, selectedBackgroundTheme, setBackgroundTheme } = useBackgroundTheme()

onPrehydrate(el => {
const settings = JSON.parse(localStorage.getItem('npmx-settings') || '{}')
let preferences: UserPreferences = {}
const defaultId = 'neutral'
const id = settings.preferredBackgroundTheme
try {
preferences = JSON.parse(localStorage.getItem('npmx-user-preferences') || '{}')
} catch {}
const id = preferences.preferredBackgroundTheme
if (id) {
const input = el.querySelector<HTMLInputElement>(`input[value="${id}"]`)
if (input) {
Expand Down
2 changes: 1 addition & 1 deletion app/composables/npm/useSearch.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { NpmSearchResponse, NpmSearchResult } from '#shared/types'
import type { SearchProvider } from '~/composables/useSettings'
import type { SearchProvider } from '#shared/schemas/userPreferences'
import type { AlgoliaMultiSearchChecks } from './useAlgoliaSearch'
import { type SearchSuggestion, emptySearchResponse, parseSuggestionIntent } from './search-utils'
import { isValidNewPackageName, checkPackageExists } from '~/utils/package-name'
Expand Down
4 changes: 2 additions & 2 deletions app/composables/useConnector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ const STORAGE_KEY = 'npmx-connector'
const DEFAULT_PORT = 31415

export const useConnector = createSharedComposable(function useConnector() {
const { settings } = useSettings()
const { localSettings } = useUserLocalSettings()

// Persisted connection config
const config = useState<{ token: string; port: number } | null>('connector-config', () => null)
Expand Down Expand Up @@ -308,7 +308,7 @@ export const useConnector = createSharedComposable(function useConnector() {
body: {
otp,
interactive: !otp,
openUrls: settings.value.connector.autoOpenURL,
openUrls: localSettings.value.connector.autoOpenURL,
},
})
if (response?.success) {
Expand Down
Loading
Loading