[READ-ONLY] a fast, modern browser for the npm registry
at main 125 lines 3.4 kB view raw
1<script setup lang="ts"> 2import { shallowRef, computed } from 'vue' 3import { LinkBase } from '#components' 4 5interface Props { 6 title: string 7 isLoading?: boolean 8 headingLevel?: `h${number}` 9 id: string 10 icon?: string 11} 12 13const props = withDefaults(defineProps<Props>(), { 14 isLoading: false, 15 headingLevel: 'h2', 16}) 17 18const appSettings = useSettings() 19 20const buttonId = `${props.id}-collapsible-button` 21const contentId = `${props.id}-collapsible-content` 22 23const isOpen = shallowRef(true) 24 25onPrehydrate(() => { 26 const settings = JSON.parse(localStorage.getItem('npmx-settings') || '{}') 27 const collapsed: string[] = settings?.sidebar?.collapsed || [] 28 for (const id of collapsed) { 29 if (!document.documentElement.dataset.collapsed?.split(' ').includes(id)) { 30 document.documentElement.dataset.collapsed = ( 31 document.documentElement.dataset.collapsed + 32 ' ' + 33 id 34 ).trim() 35 } 36 } 37}) 38 39onMounted(() => { 40 if (document?.documentElement) { 41 isOpen.value = !( 42 document.documentElement.dataset.collapsed?.split(' ').includes(props.id) ?? false 43 ) 44 } 45}) 46 47function toggle() { 48 isOpen.value = !isOpen.value 49 50 const removed = appSettings.settings.value.sidebar.collapsed.filter(c => c !== props.id) 51 52 if (isOpen.value) { 53 appSettings.settings.value.sidebar.collapsed = removed 54 } else { 55 removed.push(props.id) 56 appSettings.settings.value.sidebar.collapsed = removed 57 } 58 59 document.documentElement.dataset.collapsed = 60 appSettings.settings.value.sidebar.collapsed.join(' ') 61} 62 63const ariaLabel = computed(() => { 64 const action = isOpen.value ? 'Collapse' : 'Expand' 65 return props.title ? `${action} ${props.title}` : action 66}) 67useHead({ 68 style: [ 69 { 70 innerHTML: ` 71:root[data-collapsed~='${props.id}'] section[data-anchor-id='${props.id}'] .collapsible-content { 72 grid-template-rows: 0fr; 73}`, 74 }, 75 ], 76}) 77</script> 78 79<template> 80 <section :id="id" :data-anchor-id="id" class="scroll-mt-20 xl:scroll-mt-0"> 81 <div class="flex items-center justify-between mb-3 px-1"> 82 <component 83 :is="headingLevel" 84 class="group text-xs text-fg-subtle uppercase tracking-wider flex items-center gap-2" 85 > 86 <button 87 :id="buttonId" 88 type="button" 89 class="w-4 h-4 flex items-center justify-center text-fg-subtle hover:text-fg-muted transition-colors duration-200 shrink-0 focus-visible:outline-accent/70 rounded" 90 :aria-expanded="isOpen" 91 :aria-controls="contentId" 92 :aria-label="ariaLabel" 93 @click="toggle" 94 > 95 <span v-if="isLoading" class="i-svg-spinners:ring-resize w-3 h-3" aria-hidden="true" /> 96 <span 97 v-else 98 class="w-3 h-3 transition-transform duration-200" 99 :class="isOpen ? 'i-lucide:chevron-down' : 'i-lucide:chevron-right'" 100 aria-hidden="true" 101 /> 102 </button> 103 104 <LinkBase :to="`#${id}`"> 105 {{ title }} 106 </LinkBase> 107 </component> 108 109 <!-- Actions slot for buttons or other elements --> 110 <div class="pe-1"> 111 <slot name="actions" /> 112 </div> 113 </div> 114 115 <div 116 :id="contentId" 117 class="grid ms-6 grid-rows-[1fr] transition-[grid-template-rows] duration-200 ease-in-out collapsible-content overflow-hidden" 118 :inert="!isOpen" 119 > 120 <div class="min-h-0 min-w-0"> 121 <slot /> 122 </div> 123 </div> 124 </section> 125</template>