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)