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
5 changes: 4 additions & 1 deletion app/components/AppHeader.vue
Original file line number Diff line number Diff line change
Expand Up @@ -72,11 +72,14 @@ onKeyStroke(',', e => {
</ul>
</div>

<!-- End: User status + GitHub -->
<!-- End: Bookmarks + Settings + Connector -->
<div
:class="{ 'hidden sm:flex': showFullSearch }"
class="flex-shrink-0 flex items-center gap-4 sm:gap-6 ms-auto sm:ms-0"
>
<ClientOnly>
<HeaderBookmarksDropdown />
</ClientOnly>
<NuxtLink
to="/about"
class="sm:hidden link-subtle font-mono text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/50 rounded"
Expand Down
37 changes: 37 additions & 0 deletions app/components/BookmarkButton.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<script setup lang="ts">
const props = defineProps<{
packageName: string
}>()

const { useIsBookmarked, toggleBookmark } = useBookmarks()

const isBookmarked = useIsBookmarked(() => props.packageName)

function handleClick() {
toggleBookmark(props.packageName)
}
</script>

<template>
<button
type="button"
class="p-1.5 rounded transition-colors duration-150 border border-transparent hover:bg-bg hover:border-border focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50"
:aria-label="
isBookmarked
? $t('bookmarks.remove', { name: packageName })
: $t('bookmarks.add', { name: packageName })
"
:aria-pressed="isBookmarked"
@click="handleClick"
>
<span
class="block w-4 h-4 transition-colors duration-150"
:class="
isBookmarked
? 'i-carbon:bookmark-filled text-accent'
: 'i-carbon:bookmark text-fg-subtle hover:text-fg'
"
aria-hidden="true"
/>
</button>
</template>
112 changes: 112 additions & 0 deletions app/components/HeaderBookmarksDropdown.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
<script setup lang="ts">
const { bookmarks, hasBookmarks, removeBookmark, clearBookmarks } = useBookmarks()

const isOpen = ref(false)

function handleMouseEnter() {
isOpen.value = true
}

function handleMouseLeave() {
isOpen.value = false
}

function handleKeydown(event: KeyboardEvent) {
if (event.key === 'Escape' && isOpen.value) {
isOpen.value = false
}
}

function handleRemove(packageName: string, event: Event) {
event.preventDefault()
event.stopPropagation()
removeBookmark(packageName)
}

function handleClearAll(event: Event) {
event.preventDefault()
clearBookmarks()
}
</script>

<template>
<div
class="relative flex items-center"
@mouseenter="handleMouseEnter"
@mouseleave="handleMouseLeave"
@keydown="handleKeydown"
>
<button
type="button"
class="link-subtle font-mono text-sm inline-flex items-center gap-1 leading-tight"
:aria-expanded="isOpen"
aria-haspopup="true"
>
<span
class="w-[1em] h-[1em] shrink-0"
:class="hasBookmarks ? 'i-carbon:bookmark-filled' : 'i-carbon:bookmark'"
aria-hidden="true"
/>
<span class="hidden sm:inline">{{ $t('header.bookmarks') }}</span>
<span
class="hidden sm:inline i-carbon-chevron-down w-3 h-3 transition-transform duration-200"
Copy link
Contributor

Choose a reason for hiding this comment

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

and here

:class="{ 'rotate-180': isOpen }"
aria-hidden="true"
/>
</button>

<Transition
enter-active-class="transition-all duration-150"
leave-active-class="transition-all duration-100"
enter-from-class="opacity-0 translate-y-1"
leave-to-class="opacity-0 translate-y-1"
>
<div v-if="isOpen" class="absolute inset-ie-0 top-full pt-2 w-72 z-50">
<div class="bg-bg-elevated border border-border rounded-lg shadow-lg overflow-hidden">
<div class="px-3 py-2 border-b border-border flex items-center justify-between">
<span class="font-mono text-xs text-fg-subtle">
{{ $t('header.bookmarks_dropdown.title') }}
</span>
<button
v-if="hasBookmarks"
type="button"
class="font-mono text-xs text-fg-subtle hover:text-fg transition-colors"
@click="handleClearAll"
>
{{ $t('header.bookmarks_dropdown.clear_all') }}
</button>
</div>

<ul v-if="hasBookmarks" class="py-1 max-h-80 overflow-y-auto">
<li v-for="bookmark in bookmarks" :key="bookmark.packageName">
<div class="flex items-center gap-1 px-3 hover:bg-bg-subtle transition-colors">
<NuxtLink
:to="`/${bookmark.packageName}`"
class="flex-1 py-2 font-mono text-sm text-fg truncate"
>
{{ bookmark.packageName }}
</NuxtLink>
<button
type="button"
class="p-1 text-fg-subtle hover:text-fg transition-colors shrink-0"
:aria-label="
$t('header.bookmarks_dropdown.remove', { name: bookmark.packageName })
"
@click="handleRemove(bookmark.packageName, $event)"
>
<span class="i-carbon-close w-3 h-3 block" aria-hidden="true" />
</button>
</div>
</li>
</ul>

<div v-else class="px-3 py-4 text-center">
<span class="text-fg-muted text-sm">
{{ $t('header.bookmarks_dropdown.empty') }}
</span>
</div>
</div>
</div>
</Transition>
</div>
</template>
92 changes: 92 additions & 0 deletions app/composables/useBookmarks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import type { RemovableRef } from '@vueuse/core'
import { useLocalStorage } from '@vueuse/core'

/**
* Bookmark entry with package name and timestamp
*/
export interface Bookmark {
packageName: string
addedAt: number
}

const STORAGE_KEY = 'npmx-bookmarks'

// Shared bookmarks instance (singleton per app)
let bookmarksRef: RemovableRef<Bookmark[]> | null = null

/**
* Composable for managing package bookmarks with localStorage persistence.
* Bookmarks are shared across all components that use this composable.
*/
export function useBookmarks() {
if (!bookmarksRef) {
bookmarksRef = useLocalStorage<Bookmark[]>(STORAGE_KEY, [], {
mergeDefaults: true,
})
}

const bookmarks = bookmarksRef

/**
* Check if a package is bookmarked
*/
function isBookmarked(packageName: string): boolean {
return bookmarks.value.some(b => b.packageName === packageName)
}

/**
* Reactive computed to check if a specific package is bookmarked
*/
function useIsBookmarked(packageName: MaybeRefOrGetter<string>) {
return computed(() => isBookmarked(toValue(packageName)))
}

/**
* Add a package to bookmarks
*/
function addBookmark(packageName: string): void {
if (!isBookmarked(packageName)) {
bookmarks.value = [{ packageName, addedAt: Date.now() }, ...bookmarks.value]
}
}

/**
* Remove a package from bookmarks
*/
function removeBookmark(packageName: string): void {
bookmarks.value = bookmarks.value.filter(b => b.packageName !== packageName)
}

/**
* Toggle bookmark status for a package
*/
function toggleBookmark(packageName: string): void {
if (isBookmarked(packageName)) {
removeBookmark(packageName)
} else {
addBookmark(packageName)
}
}

/**
* Clear all bookmarks
*/
function clearBookmarks(): void {
bookmarks.value = []
}

const bookmarkCount = computed(() => bookmarks.value.length)
const hasBookmarks = computed(() => bookmarks.value.length > 0)

return {
bookmarks,
bookmarkCount,
hasBookmarks,
isBookmarked,
useIsBookmarked,
addBookmark,
removeBookmark,
toggleBookmark,
clearBookmarks,
}
}
5 changes: 4 additions & 1 deletion app/pages/[...package].vue
Original file line number Diff line number Diff line change
Expand Up @@ -574,12 +574,15 @@ function handleClick(event: MouseEvent) {
</template>
</ClientOnly>

<!-- Internal navigation: Docs + Code (hidden on mobile, shown in external links instead) -->
<!-- Internal navigation: Bookmark + Docs + Code (hidden on mobile, shown in external links instead) -->
<nav
v-if="displayVersion"
:aria-label="$t('package.navigation')"
class="hidden sm:flex items-center gap-1 p-0.5 bg-bg-subtle border border-border-subtle rounded-md shrink-0 ms-auto self-center"
>
<ClientOnly>
<BookmarkButton :package-name="pkg.name" />
</ClientOnly>
<NuxtLink
v-if="docsLink"
:to="docsLink"
Expand Down
11 changes: 11 additions & 0 deletions i18n/locales/de-DE.json
Original file line number Diff line number Diff line change
Expand Up @@ -599,9 +599,20 @@
}
}
},
"bookmarks": {
"add": "{name} als Lesezeichen speichern",
"remove": "{name} aus Lesezeichen entfernen"
},
"header": {
"home": "npmx Startseite",
"github": "GitHub",
"bookmarks": "Lesezeichen",
"bookmarks_dropdown": {
"title": "Gespeicherte Pakete",
"empty": "Noch keine Lesezeichen",
"clear_all": "alle löschen",
"remove": "{name} aus Lesezeichen entfernen"
},
"packages": "Pakete",
"packages_dropdown": {
"title": "Deine Pakete",
Expand Down
11 changes: 11 additions & 0 deletions i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -719,9 +719,20 @@
}
}
},
"bookmarks": {
"add": "Bookmark {name}",
"remove": "Remove {name} from bookmarks"
},
"header": {
"home": "npmx home",
"github": "GitHub",
"bookmarks": "bookmarks",
"bookmarks_dropdown": {
"title": "Bookmarked Packages",
"empty": "No bookmarks yet",
"clear_all": "clear all",
"remove": "Remove {name} from bookmarks"
},
"packages": "packages",
"packages_dropdown": {
"title": "Your Packages",
Expand Down
11 changes: 11 additions & 0 deletions i18n/locales/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -708,9 +708,20 @@
}
}
},
"bookmarks": {
"add": "Guardar {name} en marcadores",
"remove": "Eliminar {name} de marcadores"
},
"header": {
"home": "inicio npmx",
"github": "GitHub",
"bookmarks": "marcadores",
"bookmarks_dropdown": {
"title": "Paquetes guardados",
"empty": "Sin marcadores",
"clear_all": "eliminar todo",
"remove": "Eliminar {name} de marcadores"
},
"packages": "paquetes",
"packages_dropdown": {
"title": "Tus Paquetes",
Expand Down
11 changes: 11 additions & 0 deletions i18n/locales/fr-FR.json
Original file line number Diff line number Diff line change
Expand Up @@ -710,9 +710,20 @@
}
}
},
"bookmarks": {
"add": "Ajouter {name} aux favoris",
"remove": "Retirer {name} des favoris"
},
"header": {
"home": "accueil npmx",
"github": "GitHub",
"bookmarks": "favoris",
"bookmarks_dropdown": {
"title": "Paquets favoris",
"empty": "Aucun favori",
"clear_all": "tout supprimer",
"remove": "Retirer {name} des favoris"
},
"packages": "paquets",
"packages_dropdown": {
"title": "Vos paquets",
Expand Down
11 changes: 11 additions & 0 deletions i18n/locales/it-IT.json
Original file line number Diff line number Diff line change
Expand Up @@ -710,9 +710,20 @@
}
}
},
"bookmarks": {
"add": "Aggiungi {name} ai preferiti",
"remove": "Rimuovi {name} dai preferiti"
},
"header": {
"home": "npmx home",
"github": "GitHub",
"bookmarks": "preferiti",
"bookmarks_dropdown": {
"title": "Pacchetti preferiti",
"empty": "Nessun preferito",
"clear_all": "elimina tutti",
"remove": "Rimuovi {name} dai preferiti"
},
"packages": "pacchetti",
"packages_dropdown": {
"title": "I tuoi pacchetti",
Expand Down
Loading
Loading