HextaUI

BetaVercel UI SDK 0.1.0-beta.1 üzerine kurulu. API’si değişebilir.

Tree

Vercel UI SDK üzerinde dosyalar ve iç içe sayfalar için; dosya yolları, onay kutuları, bağlantılar ve isteğe bağlı yüklenen alt öğeler sunan bir ağaç görünümü.

Bağımlılıklar
  • @tabler/icons-react
  • @ui-sdk/core
  • @ui-sdk/react
  • class-variance-authority
components
ui
button.tsx
tree.tsx
theme.tsx
package.json
README.md
import {
  IconBrandTypescript,
  IconFileText,
  IconFolder,
  IconFolderOpen,
} from "@tabler/icons-react"

import {
  Tree,
  TreeGroup,
  TreeItem,
  TreeItemIndicator,
  TreeItemLabel,
  type TreeItemData,
} from "@/components/ui/ui-sdk-tree"

const files: TreeItemData[] = [
  {
    value: "app",
    label: "app",
    children: [
      { value: "app/layout.tsx", label: "layout.tsx" },
      { value: "app/page.tsx", label: "page.tsx" },
      { value: "app/globals.css", label: "globals.css" },
    ],
  },
  {
    value: "components",
    label: "components",
    children: [
      {
        value: "components/ui",
        label: "ui",
        children: [
          { value: "components/ui/button.tsx", label: "button.tsx" },
          { value: "components/ui/tree.tsx", label: "tree.tsx" },
        ],
      },
      { value: "components/theme.tsx", label: "theme.tsx" },
    ],
  },
  { value: "package.json", label: "package.json" },
  { value: "README.md", label: "README.md" },
]

function Items({ items }: { items: TreeItemData[] }) {
  return items.map((item) => (
    <TreeItem key={item.value} value={item.value}>
      <TreeItemLabel>
        {item.children ? (
          <>
            <TreeItemIndicator />
            <IconFolder className="group-data-expanded/tree-label:hidden" />
            <IconFolderOpen className="not-group-data-expanded/tree-label:hidden" />
          </>
        ) : item.label.endsWith(".tsx") ? (
          <IconBrandTypescript />
        ) : (
          <IconFileText />
        )}
        <span>{item.label}</span>
      </TreeItemLabel>
      {item.children && (
        <TreeGroup>
          <Items items={item.children} />
        </TreeGroup>
      )}
    </TreeItem>
  ))
}

function UiSdkTreeDemo() {
  return (
    <Tree
      aria-label="Project files"
      items={files}
      defaultExpanded={["components", "components/ui"]}
      defaultValue="components/ui/tree.tsx"
      className="max-w-xs"
    >
      <Items items={files} />
    </Tree>
  )
}

export { UiSdkTreeDemo }
pnpm dlx shadcn@latest add https://hextaui.com/r/ui-sdk-tree.json

Bileşeni, HextaUI tema token'larını ve bileşenin bağımlı olduğu tüm HextaUI bileşenlerini ekler.

UI SDK, ui-sdk adlı bir cascade katmanında kendi CSS’ini biraz ekler. Sınıflarınızın kazanması için bu katmanı genel stil dosyanızın en üstünde, Tailwind import’unun üzerinde tanımlayın.

@layer ui-sdk;

@import "tailwindcss";
import {
  Tree,
  TreeGroup,
  TreeItem,
  TreeItemIndicator,
  TreeItemLabel,
} from "@/components/ui/ui-sdk-tree"
<Tree aria-label="Project files">
  <TreeItem value="src">
    <TreeItemLabel>
      <TreeItemIndicator />
      <span>src</span>
    </TreeItemLabel>
    <TreeGroup>
      <TreeItem value="src/index.ts">
        <TreeItemLabel>
          <span>index.ts</span>
        </TreeItemLabel>
      </TreeItem>
    </TreeGroup>
  </TreeItem>
</Tree>

İçinde TreeGroup olan bir öğe daldır. Her öğeye benzersiz bir value, ağaca da erişilebilir bir ad verin. Odak öğede kalır ve label gördüğünüz satırdır; bu yüzden seçili, geçerli ve odaklı stillerin hepsi label’a uygulanır.

Tree
└── TreeItem
    ├── TreeItemLabel
    │   ├── TreeItemIndicator
    │   └── TreeItemCheckbox
    └── TreeGroup
        └── TreeItem

items verip alt öğe vermezseniz ağaç sizin için oklu, stilli satırlar çizer. Her öğenin bir value, bir label ve isteğe bağlı children değeri vardır.

import { Tree, type TreeItemData } from "@/components/ui/ui-sdk-tree"

const items: TreeItemData[] = [
  {
    value: "fruit",
    label: "Fruit",
    children: [
      { value: "apple", label: "Apple" },
      { value: "banana", label: "Banana" },
      { value: "cherry", label: "Cherry" },
    ],
  },
  {
    value: "vegetables",
    label: "Vegetables",
    children: [
      { value: "carrot", label: "Carrot" },
      { value: "leek", label: "Leek" },
    ],
  },
  { value: "bread", label: "Bread" },
]

function UiSdkTreeData() {
  return (
    <Tree
      aria-label="Groceries"
      items={items}
      defaultExpanded={["fruit"]}
      className="max-w-xs"
    />
  )
}

export { UiSdkTreeData }

Dosya gezgini için klasörler önce gelecek şekilde paths verin. activation="automatic" ile ok tuşları seçer; promise döndüren bir onAction, satırı sonuçlanana kadar beklemede gösterir.

"use client"

import * as React from "react"
import { IconLoader2 } from "@tabler/icons-react"

import {
  Tree,
  TreeGroup,
  TreeItem,
  TreeItemIndicator,
  TreeItemLabel,
  treeFromPaths,
  type TreeItemData,
} from "@/components/ui/ui-sdk-tree"

const paths = [
  "src/index.ts",
  "src/lib/format.ts",
  "src/lib/parse.ts",
  "src/routes/home.tsx",
  "src/routes/about.tsx",
  "tests/",
  "package.json",
  "tsconfig.json",
]

const items = treeFromPaths(paths)
const files = new Set(paths.filter((path) => !path.endsWith("/")))

function Items({ items }: { items: TreeItemData[] }) {
  return items.map((item) => (
    <TreeItem key={item.value} value={item.value}>
      <TreeItemLabel>
        {(state) => (
          <>
            {item.children && <TreeItemIndicator />}
            <span>{item.label}</span>
            {state.pending && (
              <IconLoader2 className="ms-auto motion-safe:animate-spin" />
            )}
          </>
        )}
      </TreeItemLabel>
      {item.children && item.children.length > 0 && (
        <TreeGroup>
          <Items items={item.children} />
        </TreeGroup>
      )}
    </TreeItem>
  ))
}

function UiSdkTreePaths() {
  const [opened, setOpened] = React.useState<string | null>(null)

  return (
    <div className="flex w-full max-w-xs flex-col gap-3">
      <Tree
        aria-label="Project"
        paths={paths}
        defaultExpanded={["src"]}
        defaultValue="src/index.ts"
        activation="automatic"
        onAction={async (value) => {
          if (!files.has(value)) {
            return
          }
          await new Promise((resolve) => setTimeout(resolve, 600))
          setOpened(value)
        }}
      >
        <Items items={items} />
      </Tree>
      <p className="text-sm text-muted-foreground" aria-live="polite">
        {opened ? `Opened ${opened}` : "Press Enter to open a file."}
      </p>
    </div>
  )
}

export { UiSdkTreePaths }

mode="multiple" ile Cmd veya Ctrl tıklama bir öğeyi açıp kapatır, Shift bir aralığı genişletir. expandOnPress={false} genişletmeyi oka ve ok tuşlarına bırakır.

"use client"

import * as React from "react"

import { Tree, type TreeItemData } from "@/components/ui/ui-sdk-tree"

const items: TreeItemData[] = [
  {
    value: "inbox",
    label: "Inbox",
    children: [
      { value: "updates", label: "Updates" },
      { value: "forums", label: "Forums" },
      { value: "promotions", label: "Promotions" },
    ],
  },
  {
    value: "projects",
    label: "Projects",
    children: [
      { value: "website", label: "Website" },
      { value: "mobile", label: "Mobile app" },
    ],
  },
  { value: "archive", label: "Archive" },
  { value: "trash", label: "Trash" },
]

function UiSdkTreeMultiple() {
  const [value, setValue] = React.useState(["updates", "website"])

  return (
    <div className="flex w-full max-w-xs flex-col gap-3">
      <Tree
        aria-label="Folders"
        items={items}
        mode="multiple"
        value={value}
        onValueChange={setValue}
        expandOnPress={false}
        defaultExpanded={["inbox", "projects"]}
      />
      <p className="text-sm text-muted-foreground">{value.length} selected</p>
    </div>
  )
}

export { UiSdkTreeMultiple }

checkable ekleyin ve her label içine bir TreeItemCheckbox koyun. Bir dalı işaretlemek içindeki her şeyi işaretler; kısmen işaretli bir dal tire gösterir.

"use client"

import * as React from "react"

import {
  Tree,
  TreeGroup,
  TreeItem,
  TreeItemCheckbox,
  TreeItemIndicator,
  TreeItemLabel,
  type TreeItemData,
} from "@/components/ui/ui-sdk-tree"

const items: TreeItemData[] = [
  {
    value: "frontend",
    label: "Frontend",
    children: [
      { value: "react", label: "React" },
      { value: "vue", label: "Vue" },
      { value: "svelte", label: "Svelte" },
    ],
  },
  {
    value: "backend",
    label: "Backend",
    children: [
      { value: "node", label: "Node" },
      {
        value: "databases",
        label: "Databases",
        children: [
          { value: "postgres", label: "Postgres" },
          { value: "sqlite", label: "SQLite" },
        ],
      },
    ],
  },
]

function Items({ items }: { items: TreeItemData[] }) {
  return items.map((item) => (
    <TreeItem key={item.value} value={item.value}>
      <TreeItemLabel>
        {item.children && <TreeItemIndicator />}
        <TreeItemCheckbox />
        <span>{item.label}</span>
      </TreeItemLabel>
      {item.children && (
        <TreeGroup>
          <Items items={item.children} />
        </TreeGroup>
      )}
    </TreeItem>
  ))
}

function UiSdkTreeCheckboxes() {
  const [checked, setChecked] = React.useState(["react", "postgres"])

  return (
    <div className="flex w-full max-w-xs flex-col gap-3">
      <Tree
        aria-label="Stack"
        items={items}
        mode="none"
        checkable
        checked={checked}
        onCheckedChange={setChecked}
        defaultExpanded={["frontend", "backend", "databases"]}
      >
        <Items items={items} />
      </Tree>
      <p className="text-sm text-muted-foreground">
        {checked.length > 0 ? checked.join(", ") : "Nothing checked"}
      </p>
    </div>
  )
}

export { UiSdkTreeCheckboxes }

href içeren öğeler bağlantı olarak çizilir. mode="none" ve current ile geçerli sayfa aria-current alır, üst öğeleri açılır ve sekme durağını alır.

"use client"

import * as React from "react"

import { Tree, type TreeItemData } from "@/components/ui/ui-sdk-tree"

const pages: TreeItemData[] = [
  {
    value: "getting-started",
    label: "Getting started",
    children: [
      { value: "introduction", label: "Introduction", href: "#introduction" },
      { value: "installation", label: "Installation", href: "#installation" },
    ],
  },
  {
    value: "components",
    label: "Components",
    children: [
      { value: "accordion", label: "Accordion", href: "#accordion" },
      { value: "dialog", label: "Dialog", href: "#dialog" },
      { value: "tree", label: "Tree", href: "#tree" },
    ],
  },
  { value: "changelog", label: "Changelog", href: "#changelog" },
]

function UiSdkTreeLinks() {
  const [current, setCurrent] = React.useState("tree")

  return (
    <Tree
      aria-label="Docs"
      items={pages}
      mode="none"
      current={current}
      onAction={(value, { event }) => {
        if (value === "getting-started" || value === "components") {
          return
        }
        event?.preventDefault()
        setCurrent(value)
      }}
      className="max-w-xs"
    />
  )
}

export { UiSdkTreeLinks }

Alt öğeleri oluşmadan bir öğeyi branch olarak işaretleyin; loadChildren ilk açılışta çalışır. Ok yüklenirken dönen simgeye, başarısız olursa yeniden deneme simgesine dönüşür. Archive bir kez başarısız olur.

"use client"

import * as React from "react"

import {
  Tree,
  TreeGroup,
  TreeItem,
  TreeItemIndicator,
  TreeItemLabel,
} from "@/components/ui/ui-sdk-tree"

type Row = { value: string; label: string; branch?: boolean }

const top: Row[] = [
  { value: "users", label: "users", branch: true },
  { value: "orders", label: "orders", branch: true },
  { value: "archive", label: "archive", branch: true },
]

async function fetchRows(value: string, signal: AbortSignal, fail: boolean) {
  await new Promise((resolve, reject) => {
    const timer = setTimeout(resolve, 800)
    signal.addEventListener("abort", () => {
      clearTimeout(timer)
      reject(signal.reason)
    })
  })
  if (fail) {
    throw new Error("Network error")
  }
  return Array.from({ length: 3 }, (_, index) => ({
    value: `${value}/${index + 1}`,
    label: `${value}_${index + 1}.json`,
  }))
}

function Items({
  items,
  loaded,
}: {
  items: Row[]
  loaded: Record<string, Row[]>
}) {
  return items.map((item) => (
    <TreeItem key={item.value} value={item.value} branch={item.branch}>
      <TreeItemLabel>
        {item.branch && <TreeItemIndicator />}
        <span>{item.label}</span>
      </TreeItemLabel>
      {loaded[item.value] && (
        <TreeGroup>
          <Items items={loaded[item.value]} loaded={loaded} />
        </TreeGroup>
      )}
    </TreeItem>
  ))
}

function UiSdkTreeLazy() {
  const [loaded, setLoaded] = React.useState<Record<string, Row[]>>({})
  const failed = React.useRef(false)

  return (
    <Tree
      aria-label="Database"
      loadChildren={async (value, { signal }) => {
        const fail = value === "archive" && !failed.current
        failed.current ||= fail
        const rows = await fetchRows(value, signal, fail)
        setLoaded((current) => ({ ...current, [value]: rows }))
      }}
      className="max-w-xs"
    >
      <Items items={top} loaded={loaded} />
    </Tree>
  )
}

export { UiSdkTreeLazy }

variant="lines" her açık grup için bir girinti çizgisi çizer, size="sm" satırları kısaltır.

import { Tree, type TreeItemData } from "@/components/ui/ui-sdk-tree"

const items: TreeItemData[] = [
  {
    value: "src",
    label: "src",
    children: [
      {
        value: "src/components",
        label: "components",
        children: [
          { value: "src/components/header.tsx", label: "header.tsx" },
          { value: "src/components/footer.tsx", label: "footer.tsx" },
        ],
      },
      { value: "src/main.tsx", label: "main.tsx" },
    ],
  },
  { value: "vite.config.ts", label: "vite.config.ts" },
]

function UiSdkTreeLines() {
  return (
    <Tree
      aria-label="Source"
      items={items}
      variant="lines"
      size="sm"
      defaultExpanded={["src", "src/components"]}
      className="max-w-xs"
    />
  )
}

export { UiSdkTreeLines }

createTreeController() ile oluşturulan bir controller, öğeleri her yerden genişletir, daraltır, seçer ve gösterir.

"use client"

import * as React from "react"

import { Button } from "@/components/ui/button"
import {
  createTreeController,
  Tree,
  type TreeItemData,
} from "@/components/ui/ui-sdk-tree"

const items: TreeItemData[] = [
  {
    value: "design",
    label: "Design",
    children: [
      { value: "tokens", label: "Tokens" },
      {
        value: "icons",
        label: "Icons",
        children: [
          { value: "outline", label: "Outline" },
          { value: "filled", label: "Filled" },
        ],
      },
    ],
  },
  {
    value: "engineering",
    label: "Engineering",
    children: [
      { value: "api", label: "API" },
      { value: "web", label: "Web" },
    ],
  },
]

function UiSdkTreeController() {
  const [tree] = React.useState(createTreeController)

  return (
    <div className="flex w-full max-w-xs flex-col gap-3">
      <div className="flex gap-2">
        <Button variant="outline" size="sm" onClick={() => tree.expandAll()}>
          Expand all
        </Button>
        <Button variant="outline" size="sm" onClick={() => tree.collapseAll()}>
          Collapse all
        </Button>
        <Button variant="ghost" size="sm" onClick={() => tree.reveal("filled")}>
          Reveal Filled
        </Button>
      </div>
      <Tree aria-label="Teams" items={items} controller={tree} />
    </div>
  )
}

export { UiSdkTreeController }

hiddenUntilFound ile kapalı gruplar sayfada kalır; böylece tarayıcının sayfada bul özelliği Phobos’u bulur ve göstermek için Mars’ı açar.

import { Tree, type TreeItemData } from "@/components/ui/ui-sdk-tree"

const items: TreeItemData[] = [
  {
    value: "planets",
    label: "Planets",
    children: [
      { value: "mercury", label: "Mercury" },
      { value: "venus", label: "Venus" },
      {
        value: "earth",
        label: "Earth",
        children: [{ value: "moon", label: "The Moon" }],
      },
      {
        value: "mars",
        label: "Mars",
        children: [
          { value: "phobos", label: "Phobos" },
          { value: "deimos", label: "Deimos" },
        ],
      },
    ],
  },
  {
    value: "dwarf-planets",
    label: "Dwarf planets",
    children: [
      { value: "ceres", label: "Ceres" },
      {
        value: "pluto",
        label: "Pluto",
        children: [{ value: "charon", label: "Charon" }],
      },
    ],
  },
]

function UiSdkTreeFindInPage() {
  return (
    <Tree
      aria-label="Solar system"
      items={items}
      hiddenUntilFound
      className="max-w-xs"
    />
  )
}

export { UiSdkTreeFindInPage }

Devre dışı bir öğe ekran okuyucuların bulabilmesi için yine odak alır, ancak seçilemez, işaretlenemez veya açılamaz.

import { Tree, type TreeItemData } from "@/components/ui/ui-sdk-tree"

const items: TreeItemData[] = [
  {
    value: "workspace",
    label: "Workspace",
    children: [
      { value: "general", label: "General" },
      { value: "billing", label: "Billing", disabled: true },
      { value: "members", label: "Members" },
    ],
  },
  { value: "danger", label: "Danger zone", disabled: true },
]

function UiSdkTreeDisabled() {
  return (
    <Tree
      aria-label="Settings"
      items={items}
      defaultExpanded={["workspace"]}
      defaultValue="general"
      className="max-w-xs"
    />
  )
}

export { UiSdkTreeDisabled }

Satırlar sağdan girintilenir, ok sola bakar ve sol ile sağ ok tuşları yer değiştirir.

import { Tree, type TreeItemData } from "@/components/ui/ui-sdk-tree"

const items: TreeItemData[] = [
  {
    value: "documents",
    label: "المستندات",
    children: [
      { value: "reports", label: "التقارير" },
      { value: "invoices", label: "الفواتير" },
    ],
  },
  {
    value: "photos",
    label: "الصور",
    children: [{ value: "trips", label: "الرحلات" }],
  },
  { value: "notes", label: "ملاحظات" },
]

function UiSdkTreeRtl() {
  return (
    <div dir="rtl" className="w-full max-w-xs">
      <Tree
        aria-label="الملفات"
        items={items}
        defaultExpanded={["documents"]}
        defaultValue="reports"
      />
    </div>
  )
}

export { UiSdkTreeRtl }
TuşAction
↓↑Sonraki veya önceki görünür öğeye geçer.
→Bir dalı açar veya ilk alt öğesine geçer.
←Bir dalı kapatır veya üst öğesine geçer.
HomeEndİlk veya son görünür öğeye geçer.
Alt→Bir dalı ve içindeki her şeyi açar.
*Odaktaki dalın yanındaki tüm dalları açar.
EnterÖğeyi seçer ve eylemini çalıştırır ya da bağlantısını izler.
SpaceAğaç işaretlenebilirse öğeyi işaretler, değilse seçer.
Shift↓Çoklu seçimi genişletir.
⌘AÇoklu modda tüm görünür öğeleri seçer.

Ağaç, tek sekme duraklı WAI-ARIA ağaç görünümü desenini izler. Tab geçerli sayfaya, seçili öğeye veya ilk öğeye gelir. Bir harf yazmak, onunla başlayan sonraki öğeye geçer. Yavaş bir onAction çalışırken ekran okuyucular bir saniye sonra pendingAnnouncement metnini duyar; bu yüzden onu sayfanızın dilinde ayarlayın.

Gruplar yalnızca harekete izin verildiğinde yükseklik animasyonuyla açılıp kapanır. Dokunmatik ekranlarda satırlar daha uzundur; zorunlu renkler modunda seçili ve geçerli satırlar bir çerçeve alır.

Her parça aynı adlı UI SDK parçasını sarar ve render ile parçanın durumuna bağlı bir fonksiyon olabilen className dahil tüm prop’larını kabul eder.

PropTürVarsayılan
itemsTüm hiyerarşi. Alt öğe verilmezse ağaç ondan stilli satırlar çizer.
TreeItemData[]–
pathsDosya yolları. Klasörler dal olur; / ile biten bir yol boş klasördür.
string[]–
mode
"single" | "multiple" | "none""single"
value
string | string[] | null–
defaultValue
string | string[] | null–
onValueChange
(value, details) => void–
activation
"automatic" | "manual""manual"
expanded
string[]–
defaultExpanded
string[]–
onExpandedChange
(expanded, details) => void–
expandOnPress
booleantrue
checkable
booleanfalse
checked
string[]–
defaultChecked
string[]–
onCheckedChange
(checked, details) => void–
checkStrategy
"leaf" | "trunk" | "branch" | "independent""leaf"
onActionBasınca veya Enter ile çalışır. Öğeyi beklemede göstermek için bir promise döndürün.
(value, details) => unknown–
pendingAnnouncement
string–
loadChildren
(value, { signal }) => Promise<unknown>–
current
string | null–
disabled
booleanfalse
hiddenUntilFound
booleanfalse
keepMounted
booleanfalse
variant
"default" | "lines""default"
size
"default" | "sm""default"
controllercreateTreeController() ile oluşturulur.
TreeController–
ÖznitelikAçıklama
data-slot="tree"CSS’te ağacı hedefler.
data-variantVaryant: default veya lines.
data-sizeBoyut: default veya sm.
--tree-row-heightHer satırın yüksekliği; size ile ayarlanır.
--tree-indentHer seviyenin ne kadar girintilendiği.
PropTürVarsayılan
valueAğaçta benzersiz.
string–
branchAlt öğeleri sonra yüklenen bir dal için ayarlayın.
boolean–
disabled
booleanfalse
labelLabel düz metin değilse, yazarak aramanın eşleştirdiği metin.
string–
render
ReactElement | (props, state) => ReactElement–
ÖznitelikAçıklama
data-slot="tree-item"CSS’te bir öğeyi hedefler. Odak ondadır.
data-level1’den başlayan derinlik.
data-branchBir dalda bulunur.
data-expandedBir dal açıkken bulunur.
--tree-levelDüz bir listeyi girintilemek için derinlik.
PropTürVarsayılan
render
ReactElement | (props, state) => ReactElement–
ÖznitelikAçıklama
data-slot="tree-label"CSS’te bir satırı hedefler.
data-selectedÖğe seçiliyken bulunur.
data-currentGeçerli sayfanın öğesinde bulunur.
data-expandedBir dal açıkken bulunur.
data-pendingÖğenin onAction’ı çalışırken bulunur.
data-disabledÖğe devre dışıyken bulunur.
PropTürVarsayılan
childrenVarsayılan olarak bir ok, yüklenirken dönen simge ve hatadan sonra yeniden deneme simgesi.
ReactNode | (state) => ReactNode–
ÖznitelikAçıklama
data-slot="tree-indicator"CSS'te ok işaretini hedefler.
data-loadingAlt öğeler yüklenirken bulunur.
data-failedAlt öğeler yüklenemedikten sonra bulunur.
PropTürVarsayılan
childrenVarsayılan olarak onay işareti, karışık durumda tire.
ReactNode | (state) => ReactNode–
ÖznitelikAçıklama
data-slot="tree-checkbox"CSS’te onay kutusunu hedefler.
data-checkedİşaretliyken bulunur.
data-indeterminateBir dal kısmen işaretliyken bulunur.
PropTürVarsayılan
keepMounted
boolean–
hiddenUntilFound
boolean–
ÖznitelikAçıklama
data-slot="tree-group"CSS’te bir grubu hedefler.
data-openAçıkken bulunur.
--tree-group-heightGrubun animasyon sırasındaki yüksekliği.