forked from
npmx.dev/npmx.dev
[READ-ONLY]
a fast, modern browser for the npm registry
1<script setup lang="ts">
2import type { Directions } from '@nuxtjs/i18n'
3import { useEventListener, onKeyDown, onKeyUp } from '@vueuse/core'
4import { isEditableElement } from '~/utils/input'
5
6const route = useRoute()
7const router = useRouter()
8const { locale, locales } = useI18n()
9
10// Initialize user preferences (accent color, package manager) before hydration to prevent flash/CLS
11initPreferencesOnPrehydrate()
12
13const isHomepage = computed(() => route.name === 'index')
14const showKbdHints = shallowRef(false)
15
16const localeMap = locales.value.reduce(
17 (acc, l) => {
18 acc[l.code] = l.dir ?? 'ltr'
19 return acc
20 },
21 {} as Record<string, Directions>,
22)
23
24const darkMode = usePreferredDark()
25const colorMode = useColorMode()
26const colorScheme = computed(() => {
27 return {
28 system: darkMode ? 'dark light' : 'light dark',
29 light: 'only light',
30 dark: 'only dark',
31 }[colorMode.preference]
32})
33
34useHead({
35 htmlAttrs: {
36 'lang': () => locale.value,
37 'dir': () => localeMap[locale.value] ?? 'ltr',
38 'data-kbd-hints': () => showKbdHints.value,
39 },
40 titleTemplate: titleChunk => {
41 return titleChunk ? titleChunk : 'npmx - Better npm Package Browser'
42 },
43 meta: [{ name: 'color-scheme', content: colorScheme }],
44})
45
46if (import.meta.server) {
47 setJsonLd(createWebSiteSchema())
48}
49
50onKeyDown(
51 '/',
52 e => {
53 if (isEditableElement(e.target)) return
54 e.preventDefault()
55
56 const searchInput = document.querySelector<HTMLInputElement>(
57 'input[type="search"], input[name="q"]',
58 )
59
60 if (searchInput) {
61 searchInput.focus()
62 return
63 }
64
65 router.push({ name: 'search' })
66 },
67 { dedupe: true },
68)
69
70onKeyDown(
71 '?',
72 e => {
73 if (isEditableElement(e.target)) return
74 e.preventDefault()
75 showKbdHints.value = true
76 },
77 { dedupe: true },
78)
79
80onKeyUp(
81 '?',
82 e => {
83 if (isEditableElement(e.target)) return
84 e.preventDefault()
85 showKbdHints.value = false
86 },
87 { dedupe: true },
88)
89
90// Light dismiss fallback for browsers that don't support closedby="any" (Safari + old Chrome/Firefox)
91// https://codepen.io/paramagicdev/pen/gbYompq
92// see: https://github.com/npmx-dev/npmx.dev/pull/522#discussion_r2749978022
93function handleModalLightDismiss(e: MouseEvent) {
94 const target = e.target as HTMLElement
95 if (target.tagName === 'DIALOG' && target.hasAttribute('open')) {
96 const rect = target.getBoundingClientRect()
97 const isOutside =
98 e.clientX < rect.left ||
99 e.clientX > rect.right ||
100 e.clientY < rect.top ||
101 e.clientY > rect.bottom
102
103 if (!isOutside) return
104 ;(target as HTMLDialogElement).close()
105 }
106}
107
108if (import.meta.client) {
109 // Feature check for native light dismiss support via closedby="any"
110 // https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/dialog#closedby
111 const supportsClosedBy =
112 typeof HTMLDialogElement !== 'undefined' &&
113 typeof HTMLDialogElement.prototype === 'object' &&
114 'closedBy' in HTMLDialogElement.prototype
115 if (!supportsClosedBy) {
116 useEventListener(document, 'click', handleModalLightDismiss)
117 }
118}
119</script>
120
121<template>
122 <div class="min-h-screen flex flex-col bg-bg text-fg">
123 <NuxtPwaAssets />
124 <LinkBase to="#main-content" external variant="button-primary" class="skip-link">{{
125 $t('common.skip_link')
126 }}</LinkBase>
127
128 <AppHeader :show-logo="!isHomepage" />
129
130 <div id="main-content" class="flex-1 flex flex-col" tabindex="-1">
131 <NuxtPage />
132 </div>
133
134 <AppFooter />
135
136 <ScrollToTop />
137 </div>
138</template>
139
140<style scoped>
141/* Skip link */
142.skip-link {
143 position: fixed;
144 top: -100%;
145 z-index: 100;
146}
147
148.skip-link:focus {
149 top: 0;
150}
151</style>
152
153<style>
154/* Keyboard shortcut highlight on "?" key press */
155kbd {
156 position: relative;
157}
158
159kbd::before {
160 content: '';
161 position: absolute;
162 inset: 0;
163 border-radius: inherit;
164 box-shadow: 0 0 4px 2px var(--accent);
165 opacity: 0;
166 transition: opacity 200ms ease-out;
167 pointer-events: none;
168}
169
170html[data-kbd-hints='true'] kbd::before {
171 opacity: 1;
172}
173</style>