tree
File explorer tree with type icons
File explorer tree with type icons
Загрузка превью…
src/tree/c-tree-5.vue
<!--
IconPlaceholder заменена инлайновым `<svg>` (lucide "folder-open"/"folder"/
"file-code"/"palette"/"braces"/"file-text"/"file"), тем же приёмом, что и в
apps/proof/src/cases/tree/vue.vue и c-tree-3/4.
-->
<script setup lang="ts">
import { hotkeysCoreFeature, syncDataLoaderFeature } from "@headless-tree/core"
import { Tree, TreeItem, TreeItemLabel, useTree } from "@/components/reui/tree"
interface FileItem {
name: string
children?: string[]
type?: "folder" | "ts" | "tsx" | "css" | "json" | "md" | "config"
}
const items: Record<string, FileItem> = {
root: {
name: "my-project",
children: ["src", "public", "package-json", "readme", "tsconfig"],
},
src: {
name: "src",
children: ["app", "components", "lib", "globals-css"],
type: "folder",
},
app: {
name: "app",
children: ["page-tsx", "layout-tsx", "loading-tsx"],
type: "folder",
},
"page-tsx": { name: "page.tsx", type: "tsx" },
"layout-tsx": { name: "layout.tsx", type: "tsx" },
"loading-tsx": { name: "loading.tsx", type: "tsx" },
components: {
name: "components",
children: ["button-tsx", "card-tsx", "dialog-tsx"],
type: "folder",
},
"button-tsx": { name: "button.tsx", type: "tsx" },
"card-tsx": { name: "card.tsx", type: "tsx" },
"dialog-tsx": { name: "dialog.tsx", type: "tsx" },
lib: { name: "lib", children: ["utils-ts", "api-ts"], type: "folder" },
"utils-ts": { name: "utils.ts", type: "ts" },
"api-ts": { name: "api.ts", type: "ts" },
"globals-css": { name: "globals.css", type: "css" },
public: { name: "public", children: ["favicon"], type: "folder" },
favicon: { name: "favicon.ico", type: "config" },
"package-json": { name: "package.json", type: "json" },
readme: { name: "README.md", type: "md" },
tsconfig: { name: "tsconfig.json", type: "json" },
}
type FileIconKind =
| "folder-open"
| "folder"
| "file-code"
| "palette"
| "braces"
| "file-text"
| "file"
function getFileIconKind(type?: string, isExpanded?: boolean): FileIconKind {
if (!type || type === "folder") {
return isExpanded ? "folder-open" : "folder"
}
if (type === "tsx" || type === "ts") return "file-code"
if (type === "css") return "palette"
if (type === "json") return "braces"
if (type === "md") return "file-text"
return "file"
}
function getFileIconClass(type?: string): string {
if (!type || type === "folder") return "pointer-events-none size-4 text-amber-500"
if (type === "tsx" || type === "ts") return "pointer-events-none size-4 text-blue-500"
if (type === "css") return "pointer-events-none size-4 text-purple-500"
if (type === "json") return "pointer-events-none size-4 text-yellow-500"
return "text-muted-foreground pointer-events-none size-4"
}
const indent = 20
const tree = useTree<FileItem>({
initialState: {
expandedItems: ["src", "app", "components"],
},
indent,
rootItemId: "root",
getItemName: (item) => item.getItemData().name,
isItemFolder: (item) => (item.getItemData()?.children?.length ?? 0) > 0,
dataLoader: {
getItem: (itemId) => items[itemId]!,
getChildren: (itemId) => items[itemId]!.children ?? [],
},
features: [syncDataLoaderFeature, hotkeysCoreFeature],
})
</script>
<template>
<div class="mx-auto w-full grow place-self-start lg:w-xs">
<Tree :indent="indent" :tree="tree">
<TreeItem v-for="item in tree.getItems()" :key="item.getId()" :item="item">
<TreeItemLabel class="before:bg-background relative before:absolute before:inset-x-0 before:-inset-y-0.5 before:-z-10">
<span class="flex items-center gap-2">
<template v-if="getFileIconKind(item.getItemData().type, item.isExpanded()) === 'folder-open'">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" :class="getFileIconClass(item.getItemData().type)"><path d="m6 14 1.5-2.9A2 2 0 0 1 9.24 10H20a2 2 0 0 1 1.94 2.5l-1.54 6a2 2 0 0 1-1.95 1.5H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H18a2 2 0 0 1 2 2v2" /></svg>
</template>
<template v-else-if="getFileIconKind(item.getItemData().type, item.isExpanded()) === 'folder'">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" :class="getFileIconClass(item.getItemData().type)"><path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z" /></svg>
</template>
<template v-else-if="getFileIconKind(item.getItemData().type, item.isExpanded()) === 'file-code'">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" :class="getFileIconClass(item.getItemData().type)"><path d="M10 12.5 8 15l2 2.5" /><path d="m14 12.5 2 2.5-2 2.5" /><path d="M14 2v4a2 2 0 0 0 2 2h4" /><path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7z" /></svg>
</template>
<template v-else-if="getFileIconKind(item.getItemData().type, item.isExpanded()) === 'palette'">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" :class="getFileIconClass(item.getItemData().type)"><path d="M12 22a1 1 0 0 1 0-20 10 9 0 0 1 10 9 5 5 0 0 1-5 5h-2.25a1.75 1.75 0 0 0-1.4 2.8l.3.4a1.75 1.75 0 0 1-1.4 2.8z" /><circle cx="13.5" cy="6.5" r=".5" fill="currentColor" /><circle cx="17.5" cy="10.5" r=".5" fill="currentColor" /><circle cx="6.5" cy="12.5" r=".5" fill="currentColor" /><circle cx="8.5" cy="7.5" r=".5" fill="currentColor" /></svg>
</template>
<template v-else-if="getFileIconKind(item.getItemData().type, item.isExpanded()) === 'braces'">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" :class="getFileIconClass(item.getItemData().type)"><path d="M8 3H7a2 2 0 0 0-2 2v5a2 2 0 0 1-2 2 2 2 0 0 1 2 2v5c0 1.1.9 2 2 2h1" /><path d="M16 21h1a2 2 0 0 0 2-2v-5c0-1.1.9-2 2-2a2 2 0 0 1-2-2V5a2 2 0 0 0-2-2h-1" /></svg>
</template>
<template v-else-if="getFileIconKind(item.getItemData().type, item.isExpanded()) === 'file-text'">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" :class="getFileIconClass(item.getItemData().type)"><path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z" /><path d="M14 2v4a2 2 0 0 0 2 2h4" /><path d="M10 9H8" /><path d="M16 13H8" /><path d="M16 17H8" /></svg>
</template>
<template v-else>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" :class="getFileIconClass(item.getItemData().type)"><path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z" /><path d="M14 2v4a2 2 0 0 0 2 2h4" /></svg>
</template>
{{ item.getItemName() }}
</span>
</TreeItemLabel>
</TreeItem>
</Tree>
</div>
</template>
Установка
npx shadcn-vue@latest add https://revueui.rootapi.dev/r/c-tree-5.jsonЗависимости реестра
npm-зависимости
- @headless-tree/core
Источник: порт из ReUI (Keenthemes, MIT)