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
16 changes: 11 additions & 5 deletions app/components/AppHeader.vue
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ withDefaults(
<span v-else class="w-1" />

<ul class="flex items-center gap-4 sm:gap-6 list-none m-0 p-0">
<li class="flex">
<li class="flex items-center">
<NuxtLink
to="/search"
class="link-subtle font-mono text-sm inline-flex items-center gap-2"
Expand All @@ -41,17 +41,23 @@ withDefaults(
</kbd>
</NuxtLink>
</li>
<li v-if="showConnector" class="flex">
<li class="flex items-center">
<ClientOnly>
<SettingsMenu />
</ClientOnly>
</li>
<li v-if="showConnector" class="flex items-center">
<ConnectorStatus />
</li>
<li v-else class="flex">
<li v-else class="flex items-center">
<a
href="https://github.com/npmx-dev/npmx.dev"
rel="noopener noreferrer"
class="link-subtle font-mono text-sm inline-flex items-center gap-1.5"
aria-label="GitHub"
>
<span class="i-carbon-logo-github w-4 h-4" />
<span class="hidden sm:inline">github</span>
<span class="i-carbon-logo-github w-4 h-4" aria-hidden="true" />
<span class="hidden sm:inline" aria-hidden="true">github</span>
</a>
</li>
</ul>
Expand Down
65 changes: 65 additions & 0 deletions app/components/DateTime.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<script setup lang="ts">
/**
* DateTime component that wraps NuxtTime with settings-aware relative date support.
* Uses the global settings to determine whether to show relative or absolute dates.
*
* Note: When relativeDates setting is enabled, the component switches between
* relative and absolute display based on user preference. The title attribute
* always shows the full date for accessibility.
*/
const props = withDefaults(
defineProps<{
/** The datetime value (ISO string or Date) */
datetime: string | Date
/** Override title (defaults to datetime) */
title?: string
/** Date style for absolute display */
dateStyle?: 'full' | 'long' | 'medium' | 'short'
/** Individual date parts for absolute display (alternative to dateStyle) */
year?: 'numeric' | '2-digit'
month?: 'numeric' | '2-digit' | 'long' | 'short' | 'narrow'
day?: 'numeric' | '2-digit'
}>(),
{
title: undefined,
dateStyle: undefined,
year: undefined,
month: undefined,
day: undefined,
},
)

const relativeDates = useRelativeDates()

// Compute the title - always show full date for accessibility
const titleValue = computed(() => {
if (props.title) return props.title
if (typeof props.datetime === 'string') return props.datetime
return props.datetime.toISOString()
})
</script>

<template>
<ClientOnly>
<NuxtTime v-if="relativeDates" :datetime="datetime" :title="titleValue" relative />
<NuxtTime
v-else
:datetime="datetime"
:title="titleValue"
:date-style="dateStyle"
:year="year"
:month="month"
:day="day"
/>
<template #fallback>
<NuxtTime
:datetime="datetime"
:title="titleValue"
:date-style="dateStyle"
:year="year"
:month="month"
:day="day"
/>
</template>
</ClientOnly>
</template>
2 changes: 1 addition & 1 deletion app/components/PackageCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ const emit = defineEmits<{
<div v-if="result.package.date" class="flex items-center gap-1.5">
<dt class="sr-only">Updated</dt>
<dd>
<NuxtTime
<DateTime
:datetime="result.package.date"
year="numeric"
month="short"
Expand Down
12 changes: 4 additions & 8 deletions app/components/PackageVersions.vue
Original file line number Diff line number Diff line change
Expand Up @@ -343,10 +343,9 @@ function getTagVersions(tag: string): VersionDisplay[] {
{{ row.primaryVersion.version }}
</NuxtLink>
<div class="flex items-center gap-2 shrink-0">
<NuxtTime
<DateTime
v-if="row.primaryVersion.time"
:datetime="row.primaryVersion.time"
:title="row.primaryVersion.time"
year="numeric"
month="short"
day="numeric"
Expand Down Expand Up @@ -393,10 +392,9 @@ function getTagVersions(tag: string): VersionDisplay[] {
{{ v.version }}
</NuxtLink>
<div class="flex items-center gap-2 shrink-0">
<NuxtTime
<DateTime
v-if="v.time"
:datetime="v.time"
:title="v.time"
class="text-[10px] text-fg-subtle"
year="numeric"
month="short"
Expand Down Expand Up @@ -475,10 +473,9 @@ function getTagVersions(tag: string): VersionDisplay[] {
{{ row.primaryVersion.version }}
</NuxtLink>
<div class="flex items-center gap-2 shrink-0">
<NuxtTime
<DateTime
v-if="row.primaryVersion.time"
:datetime="row.primaryVersion.time"
:title="row.primaryVersion.time"
class="text-[10px] text-fg-subtle"
year="numeric"
month="short"
Expand Down Expand Up @@ -586,10 +583,9 @@ function getTagVersions(tag: string): VersionDisplay[] {
{{ v.version }}
</NuxtLink>
<div class="flex items-center gap-2 shrink-0">
<NuxtTime
<DateTime
v-if="v.time"
:datetime="v.time"
:title="v.time"
class="text-[10px] text-fg-subtle"
year="numeric"
month="short"
Expand Down
116 changes: 116 additions & 0 deletions app/components/SettingsMenu.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
<script setup lang="ts">
import { onKeyStroke, onClickOutside } from '@vueuse/core'

const { settings } = useSettings()

const isOpen = ref(false)
const menuRef = useTemplateRef('menuRef')
const triggerRef = useTemplateRef('triggerRef')

function toggle() {
isOpen.value = !isOpen.value
}

function close() {
isOpen.value = false
}

// Close on click outside
onClickOutside(menuRef, close, { ignore: [triggerRef] })

// Close on Escape
onKeyStroke('Escape', () => {
if (isOpen.value) {
close()
triggerRef.value?.focus()
}
})

// Open with comma key (global shortcut)
onKeyStroke(',', e => {
// Don't trigger if user is typing in an input
const target = e.target as HTMLElement
if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable) {
return
}
e.preventDefault()
toggle()
})
</script>

<template>
<div class="relative flex items-center">
<button
ref="triggerRef"
type="button"
class="link-subtle font-mono text-sm inline-flex items-center justify-center gap-2"
:aria-expanded="isOpen"
aria-haspopup="menu"
aria-label="Settings"
aria-keyshortcuts=","
@click="toggle"
>
<span class="i-carbon-settings w-4 h-4 sm:hidden" aria-hidden="true" />
<span class="hidden sm:inline">settings</span>
<kbd
class="hidden sm:inline-flex items-center justify-center w-5 h-5 text-xs bg-bg-muted border border-border rounded"
aria-hidden="true"
>
,
</kbd>
</button>

<!-- Settings popover -->
<Transition
enter-active-class="transition-opacity transition-transform duration-100 ease-out motion-reduce:transition-none"
enter-from-class="opacity-0 scale-95"
enter-to-class="opacity-100 scale-100"
leave-active-class="transition-opacity transition-transform duration-75 ease-in motion-reduce:transition-none"
leave-from-class="opacity-100 scale-100"
leave-to-class="opacity-0 scale-95"
>
<div
v-if="isOpen"
ref="menuRef"
role="menu"
class="absolute right-0 top-full mt-2 w-64 bg-bg-elevated border border-border rounded-lg shadow-lg z-50 overflow-hidden"
>
<div class="px-3 py-2 border-b border-border">
<h2 class="text-xs text-fg-subtle uppercase tracking-wider">Settings</h2>
</div>

<div class="p-2 space-y-1">
<!-- Relative dates toggle -->
<div
class="flex items-center justify-between gap-3 px-2 py-2 rounded-md hover:bg-bg-muted transition-[background-color] duration-150 cursor-pointer"
@click="settings.relativeDates = !settings.relativeDates"
>
<label
:id="`settings-relative-dates-label`"
class="text-sm text-fg cursor-pointer select-none"
>
Relative dates
</label>
<button
type="button"
role="switch"
:aria-checked="settings.relativeDates"
aria-labelledby="settings-relative-dates-label"
class="relative inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-[background-color] duration-200 ease-in-out motion-reduce:transition-none focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50 focus-visible:ring-offset-2 focus-visible:ring-offset-bg"
:class="settings.relativeDates ? 'bg-fg' : 'bg-bg-subtle'"
@click.stop="settings.relativeDates = !settings.relativeDates"
>
<span
aria-hidden="true"
class="pointer-events-none inline-block h-4 w-4 rounded-full shadow-sm ring-0 transition-transform duration-200 ease-in-out motion-reduce:transition-none"
:class="
settings.relativeDates ? 'translate-x-4 bg-bg' : 'translate-x-0 bg-fg-muted'
"
/>
</button>
</div>
</div>
</div>
</Transition>
</div>
</template>
44 changes: 44 additions & 0 deletions app/composables/useSettings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import type { RemovableRef } from '@vueuse/core'
import { useLocalStorage } from '@vueuse/core'

/**
* Application settings stored in localStorage
*/
export interface AppSettings {
/** Display dates as relative (e.g., "3 days ago") instead of absolute */
relativeDates: boolean
}

const DEFAULT_SETTINGS: AppSettings = {
relativeDates: false,
}

const STORAGE_KEY = 'npmx-settings'

// Shared settings instance (singleton per app)
let settingsRef: RemovableRef<AppSettings> | null = null

/**
* Composable for managing application settings with localStorage persistence.
* Settings are shared across all components that use this composable.
*/
export function useSettings() {
if (!settingsRef) {
settingsRef = useLocalStorage<AppSettings>(STORAGE_KEY, DEFAULT_SETTINGS, {
mergeDefaults: true,
})
}

return {
settings: settingsRef,
}
}

/**
* Composable for accessing just the relative dates setting.
* Useful for components that only need to read this specific setting.
*/
export function useRelativeDates() {
const { settings } = useSettings()
return computed(() => settings.value.relativeDates)
}
6 changes: 1 addition & 5 deletions app/pages/[...package].vue
Original file line number Diff line number Diff line change
Expand Up @@ -577,11 +577,7 @@ defineOgImageComponent('Package', {
<div v-if="pkg.time?.modified" class="space-y-1">
<dt class="text-xs text-fg-subtle uppercase tracking-wider sm:text-right">Updated</dt>
<dd class="font-mono text-sm text-fg sm:text-right">
<NuxtTime
:datetime="pkg.time.modified"
:title="pkg.time.modified"
date-style="medium"
/>
<DateTime :datetime="pkg.time.modified" date-style="medium" />
</dd>
</div>
</dl>
Expand Down
Loading