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)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure this is necessary - we don't do it for any other composables

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You mean creating a folder for composables?

I followed your suggestion from Feb 10 😅
#1189 (comment)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no i mean this literal change, the one here this comment is on.

it doesn't make sense to list only one composable sub-dir, so that means either list them all, or don't list any. but if we list them all, it'll very quickly go out of sync - so my preference would be to just not do this.

├── 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
8 changes: 4 additions & 4 deletions app/app.vue
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ const localeMap = locales.value.reduce(
)

const darkMode = usePreferredDark()
const colorMode = useColorMode()
const { colorMode } = useColorModePreference()
const colorScheme = computed(() => {
return {
system: darkMode ? 'dark light' : 'light dark',
Expand All @@ -47,8 +47,8 @@ if (import.meta.server) {
setJsonLd(createWebSiteSchema())
}

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

initKeyShortcuts()

Expand All @@ -57,7 +57,7 @@ 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/Button/Base.vue
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ const props = withDefaults(

const el = useTemplateRef('el')

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

defineExpose({
focus: () => el.value?.focus(),
Expand Down
2 changes: 1 addition & 1 deletion app/components/Chart/SplitSparkline.vue
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ const props = defineProps<{
}>()

const { locale } = useI18n()
const colorMode = useColorMode()
const { colorMode } = useColorModePreference()
const resolvedMode = shallowRef<'light' | 'dark'>('light')
const rootEl = shallowRef<HTMLElement | null>(null)
const palette = getPalette('')
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/FacetBarChart.vue
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ const props = defineProps<{
facetLoading?: boolean
}>()

const colorMode = useColorMode()
const { colorMode } = useColorModePreference()
const resolvedMode = shallowRef<'light' | 'dark'>('light')
const rootEl = shallowRef<HTMLElement | null>(null)
const { width } = useElementSize(rootEl)
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 @@ -68,7 +68,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 @@ -157,7 +157,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 === 'sm' && !isLink.value)
const isButtonMedium = computed(() => props.size === 'md' && !isLink.value)
const keyboardShortcutsEnabled = useKeyboardShortcuts()
const keyboardShortcutsEnabled = useKeyboardShortcutsPreference()
</script>

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

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

const colorMode = useColorMode()
const { colorMode } = useColorModePreference()
const resolvedMode = shallowRef<'light' | 'dark'>('light')
const rootEl = shallowRef<HTMLElement | null>(null)
const isZoomed = shallowRef(false)
Expand Down Expand Up @@ -979,7 +979,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 @@ -1027,7 +1027,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 @@ -1081,12 +1081,12 @@ const normalisedDataset = computed(() => {
const series = applyDataPipeline(
d.series.map(v => v ?? 0),
{
averageWindow: settings.value.chartFilter.averageWindow,
smoothingTau: settings.value.chartFilter.smoothingTau,
averageWindow: localSettings.value.chartFilter.averageWindow,
smoothingTau: localSettings.value.chartFilter.smoothingTau,
predictionPoints:
granularity === 'weekly'
? 0 // weekly buckets are end-aligned → always complete, no prediction needed
: (settings.value.chartFilter.predictionPoints ?? DEFAULT_PREDICTION_POINTS),
: (localSettings.value.chartFilter.predictionPoints ?? DEFAULT_PREDICTION_POINTS),
},
{ granularity, lastDateMs, referenceMs, isAbsoluteMetric },
)
Expand Down Expand Up @@ -1758,10 +1758,10 @@ const isSparklineLayout = computed({
<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"
:disabled="!showCorrectionControls"
type="range"
min="0"
Expand All @@ -1773,10 +1773,10 @@ const isSparklineLayout = computed({
<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"
:disabled="!showCorrectionControls"
type="range"
min="0"
Expand All @@ -1788,10 +1788,12 @@ const isSparklineLayout = computed({
<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"
:disabled="!showCorrectionControls"
type="range"
min="0"
Expand Down Expand Up @@ -1863,10 +1865,10 @@ const isSparklineLayout = computed({
:class="{ 'opacity-50': !hasAnomalies }"
>
<input
:checked="settings.chartFilter.anomaliesFixed"
:checked="localSettings.chartFilter.anomaliesFixed"
:disabled="!showCorrectionControls"
@change="
settings.chartFilter.anomaliesFixed = (
localSettings.chartFilter.anomaliesFixed = (
$event.target as HTMLInputElement
).checked
"
Expand Down
2 changes: 1 addition & 1 deletion app/components/Package/VersionDistribution.vue
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const props = defineProps<{
const { accentColors, selectedAccentColor } = useAccentColor()
const { copy, copied } = useClipboard()

const colorMode = useColorMode()
const { colorMode } = useColorModePreference()
const resolvedMode = shallowRef<'light' | 'dark'>('light')
const rootEl = shallowRef<HTMLElement | null>(null)

Expand Down
15 changes: 8 additions & 7 deletions app/components/Package/WeeklyDownloadStats.vue
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ const props = defineProps<{

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

const chartModal = useModal('chart-modal')
const hasChartModalTransitioned = shallowRef(false)
Expand Down Expand Up @@ -63,7 +64,7 @@ const { fetchPackageDownloadEvolution } = useCharts()

const { accentColors, selectedAccentColor } = useAccentColor()

const colorMode = useColorMode()
const { colorMode } = useColorModePreference()

const resolvedMode = shallowRef<'light' | 'dark'>('light')

Expand Down Expand Up @@ -185,14 +186,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 @@ -209,7 +210,7 @@ const dataset = computed<VueUiSparklineDatasetItem[]>(() =>
const lastDatapoint = computed(() => dataset.value.at(-1)?.period ?? '')

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

const cheatCode = [
'arrowup',
Expand Down Expand Up @@ -306,7 +307,7 @@ function layEgg() {
showPulse.value = false
nextTick(() => {
showPulse.value = true
settings.value.enableGraphPulseLooping = !settings.value.enableGraphPulseLooping
preferences.value.enableGraphPulseLooping = !preferences.value.enableGraphPulseLooping
playEggPulse()
})
}
Expand Down Expand Up @@ -364,7 +365,7 @@ const config = computed<VueUiSparklineConfig>(() => {
color: colors.value.borderHover,
pulse: {
show: showPulse.value, // the pulse will not show if prefers-reduced-motion (enforced by vue-data-ui)
loop: settings.value.enableGraphPulseLooping,
loop: preferences.value.enableGraphPulseLooping,
radius: 1.5,
color: pulseColor.value!,
easing: 'ease-in-out',
Expand Down
Loading
Loading