[READ-ONLY] a fast, modern browser for the npm registry
at main 173 lines 4.0 kB view raw
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>