forked from
npmx.dev/npmx.dev
[READ-ONLY]
a fast, modern browser for the npm registry
1<script setup lang="ts">
2import type { PackageFileTree } from '#shared/types'
3import type { RouteLocationRaw } from 'vue-router'
4import type { RouteNamedMap } from 'vue-router/auto-routes'
5import { ADDITIONAL_ICONS, getFileIcon } from '~/utils/file-icons'
6
7const props = defineProps<{
8 tree: PackageFileTree[]
9 currentPath: string
10 baseUrl: string
11 baseRoute: Pick<RouteNamedMap['code'], 'params'>
12 depth?: number
13}>()
14
15const depth = computed(() => props.depth ?? 0)
16
17// Check if a node or any of its children is currently selected
18function isNodeActive(node: PackageFileTree): boolean {
19 if (props.currentPath === node.path) return true
20 if (props.currentPath.startsWith(node.path + '/')) return true
21 return false
22}
23
24// Build route object for a file path
25function getFileRoute(nodePath: string): RouteLocationRaw {
26 return {
27 name: 'code',
28 params: {
29 org: props.baseRoute.params.org,
30 packageName: props.baseRoute.params.packageName,
31 version: props.baseRoute.params.version,
32 filePath: nodePath ?? '',
33 },
34 }
35}
36
37const { toggleDir, isExpanded, autoExpandAncestors } = useFileTreeState(props.baseUrl)
38
39// Auto-expand directories in the current path
40watch(
41 () => props.currentPath,
42 path => {
43 if (path) {
44 autoExpandAncestors(path)
45 }
46 },
47 { immediate: true },
48)
49</script>
50
51<template>
52 <ul class="list-none m-0 p-0" :class="depth === 0 ? 'py-2' : ''">
53 <li v-for="node in tree" :key="node.path">
54 <!-- Directory -->
55 <template v-if="node.type === 'directory'">
56 <ButtonBase
57 class="w-full justify-start! rounded-none! border-none!"
58 block
59 :aria-pressed="isNodeActive(node)"
60 :style="{ paddingLeft: `${depth * 12 + 12}px` }"
61 @click="toggleDir(node.path)"
62 :classicon="isExpanded(node.path) ? 'i-lucide:chevron-down' : 'i-lucide:chevron-right'"
63 >
64 <svg
65 class="size-[1em] me-1 shrink-0"
66 :class="isExpanded(node.path) ? 'text-yellow-500' : 'text-yellow-600'"
67 viewBox="0 0 16 16"
68 fill="currentColor"
69 aria-hidden="true"
70 >
71 <use
72 :href="`/file-tree-sprite.svg#${isExpanded(node.path) ? ADDITIONAL_ICONS['folder-open'] : ADDITIONAL_ICONS['folder']}`"
73 />
74 </svg>
75 <span class="truncate">{{ node.name }}</span>
76 </ButtonBase>
77 <CodeFileTree
78 v-if="isExpanded(node.path) && node.children"
79 :tree="node.children"
80 :current-path="currentPath"
81 :base-url="baseUrl"
82 :base-route="baseRoute"
83 :depth="depth + 1"
84 />
85 </template>
86
87 <!-- File -->
88 <template v-else>
89 <LinkBase
90 variant="button-secondary"
91 :to="getFileRoute(node.path)"
92 :aria-current="currentPath === node.path"
93 class="w-full justify-start! rounded-none! border-none!"
94 block
95 :style="{ paddingLeft: `${depth * 12 + 32}px` }"
96 >
97 <svg
98 class="size-[1em] me-1 shrink-0"
99 viewBox="0 0 16 16"
100 fill="currentColor"
101 aria-hidden="true"
102 >
103 <use :href="`/file-tree-sprite.svg#${getFileIcon(node.name)}`" />
104 </svg>
105 <span class="truncate">{{ node.name }}</span>
106 </LinkBase>
107 </template>
108 </li>
109 </ul>
110</template>