HextaUI

Attachment

Yüklemeler için dosya ve görsel kartları; ilerleme, durumlar, eylemler, tüm kartı kaplayan bir tetikleyici ve uzantısını koruyan adlar içerir.

sales-dashboard.pdfPDF · 2.4 MB
import { IconFileText, IconX } from "@tabler/icons-react"

import {
  Attachment,
  AttachmentAction,
  AttachmentActions,
  AttachmentContent,
  AttachmentDescription,
  AttachmentMedia,
  AttachmentTitle,
} from "@/components/ui/attachment"

export function AttachmentDemo() {
  return (
    <Attachment>
      <AttachmentMedia>
        <IconFileText />
      </AttachmentMedia>
      <AttachmentContent>
        <AttachmentTitle>sales-dashboard.pdf</AttachmentTitle>
        <AttachmentDescription>PDF · 2.4 MB</AttachmentDescription>
      </AttachmentContent>
      <AttachmentActions>
        <AttachmentAction aria-label="Remove sales-dashboard.pdf">
          <IconX />
        </AttachmentAction>
      </AttachmentActions>
    </Attachment>
  )
}
pnpm dlx shadcn@latest add https://hextaui.com/r/attachment.json

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

import {
  Attachment,
  AttachmentAction,
  AttachmentActions,
  AttachmentContent,
  AttachmentDescription,
  AttachmentMedia,
  AttachmentTitle,
} from "@/components/ui/attachment"
<Attachment>
  <AttachmentMedia>
    <IconFileText />
  </AttachmentMedia>
  <AttachmentContent>
    <AttachmentTitle>report.pdf</AttachmentTitle>
    <AttachmentDescription>PDF · 2.4 MB</AttachmentDescription>
  </AttachmentContent>
  <AttachmentActions>
    <AttachmentAction aria-label="Remove report.pdf">
      <IconX />
    </AttachmentAction>
  </AttachmentActions>
</Attachment>
Attachment
├── AttachmentMedia
├── AttachmentContent
│   ├── AttachmentTitle
│   └── AttachmentDescription
├── AttachmentActions
│   └── AttachmentAction
└── AttachmentTrigger

AttachmentGroup
└── Attachment

Image

<AttachmentMedia /> üzerinde variant="image", yüklenirken parlayan kare bir küçük resim gösterir. Önizlemenin üstte olduğu bir kutucuk için orientation="vertical" ayarlayın.

import { IconX } from "@tabler/icons-react"

import {
  Attachment,
  AttachmentAction,
  AttachmentActions,
  AttachmentContent,
  AttachmentDescription,
  AttachmentMedia,
  AttachmentTitle,
} from "@/components/ui/attachment"

export function AttachmentImage() {
  return (
    <div className="flex flex-wrap items-start justify-center gap-3">
      <Attachment>
        <AttachmentMedia variant="image">
          <img src="/preview/landscape.svg" alt="" />
        </AttachmentMedia>
        <AttachmentContent>
          <AttachmentTitle>dolomites.png</AttachmentTitle>
          <AttachmentDescription>PNG · 1.1 MB</AttachmentDescription>
        </AttachmentContent>
      </Attachment>
      <Attachment orientation="vertical">
        <AttachmentMedia variant="image">
          <img src="/preview/landscape.svg" alt="" />
        </AttachmentMedia>
        <AttachmentContent>
          <AttachmentTitle>dolomites.png</AttachmentTitle>
          <AttachmentDescription>PNG · 1.1 MB</AttachmentDescription>
        </AttachmentContent>
        <AttachmentActions>
          <AttachmentAction aria-label="Remove dolomites.png">
            <IconX />
          </AttachmentAction>
        </AttachmentActions>
      </Attachment>
      <Attachment orientation="vertical" state="uploading" progress={64}>
        <AttachmentMedia variant="image">
          <img src="/preview/landscape.svg" alt="" />
        </AttachmentMedia>
        <AttachmentContent>
          <AttachmentTitle>summit.png</AttachmentTitle>
          <AttachmentDescription>Uploading… 64%</AttachmentDescription>
        </AttachmentContent>
      </Attachment>
    </div>
  )
}

Durumlar

state tüm yükleme yaşam döngüsünü kapsar. Boştaki dosyalar kesikli kenarlık alır, yüklenen ve işlenen başlıklar parlar, hatalar ise medyayı ve açıklamayı kırmızıya çevirir.

import { IconFileText, IconX } from "@tabler/icons-react"

import {
  Attachment,
  AttachmentAction,
  AttachmentActions,
  AttachmentContent,
  AttachmentDescription,
  AttachmentMedia,
  AttachmentTitle,
  type AttachmentState,
} from "@/components/ui/attachment"

const files: { state: AttachmentState; description: string }[] = [
  { state: "idle", description: "Waiting to upload" },
  { state: "uploading", description: "Uploading… 42%" },
  { state: "processing", description: "Processing" },
  { state: "error", description: "Upload failed · file too large" },
  { state: "done", description: "PDF · 2.4 MB" },
]

export function AttachmentStates() {
  return (
    <div className="flex flex-col gap-2">
      {files.map(({ state, description }) => (
        <Attachment
          key={state}
          state={state}
          progress={state === "uploading" ? 42 : undefined}
        >
          <AttachmentMedia>
            <IconFileText />
          </AttachmentMedia>
          <AttachmentContent>
            <AttachmentTitle>{`${state}-report.pdf`}</AttachmentTitle>
            <AttachmentDescription>{description}</AttachmentDescription>
          </AttachmentContent>
          <AttachmentActions>
            <AttachmentAction aria-label={`Remove ${state}-report.pdf`}>
              <IconX />
            </AttachmentAction>
          </AttachmentActions>
        </Attachment>
      ))}
    </div>
  )
}

Yükleme akışı

İnce bir ilerleme çubuğunu alt kenara çizmek için state="uploading" iken progress geçin. Görsel önizlemeleri yükleme bitene kadar soluk kalır.

"use client"

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

import {
  Attachment,
  AttachmentContent,
  AttachmentDescription,
  AttachmentMedia,
  AttachmentTitle,
  type AttachmentState,
} from "@/components/ui/attachment"
import { Button } from "@/components/ui/button"

const descriptions: Record<AttachmentState, string> = {
  idle: "Ready to upload",
  uploading: "Uploading…",
  processing: "Processing",
  error: "Upload failed · connection lost",
  done: "CSV · 840 KB",
}

export function AttachmentUploadFlow() {
  const [state, setState] = React.useState<AttachmentState>("idle")
  const [progress, setProgress] = React.useState(0)
  const timer = React.useRef<ReturnType<typeof setInterval>>(undefined)

  React.useEffect(() => () => clearInterval(timer.current), [])

  const start = (fail: boolean) => {
    clearInterval(timer.current)
    setState("uploading")
    setProgress(0)
    let value = 0
    timer.current = setInterval(() => {
      value += 12
      if (fail && value >= 60) {
        clearInterval(timer.current)
        setState("error")
        return
      }
      if (value >= 100) {
        clearInterval(timer.current)
        setProgress(100)
        setState("processing")
        setTimeout(() => setState("done"), 900)
        return
      }
      setProgress(value)
    }, 250)
  }

  return (
    <div className="flex flex-col items-start gap-3">
      <Attachment state={state} progress={progress}>
        <AttachmentMedia>
          <IconTable />
        </AttachmentMedia>
        <AttachmentContent>
          <AttachmentTitle>customers-2026.csv</AttachmentTitle>
          <AttachmentDescription>
            {state === "uploading"
              ? `Uploading… ${progress}%`
              : descriptions[state]}
          </AttachmentDescription>
        </AttachmentContent>
      </Attachment>
      <div className="flex gap-2">
        <Button size="sm" onClick={() => start(false)}>
          Start upload
        </Button>
        <Button size="sm" variant="outline" onClick={() => start(true)}>
          Upload that fails
        </Button>
      </div>
    </div>
  )
}

Boyutlar

size dolguyu, medyayı ve metni birlikte ölçekler; iç köşeleri kartla eş merkezli tutar.

import { IconFileText, IconX } from "@tabler/icons-react"

import {
  Attachment,
  AttachmentAction,
  AttachmentActions,
  AttachmentContent,
  AttachmentDescription,
  AttachmentMedia,
  AttachmentTitle,
} from "@/components/ui/attachment"

const sizes = ["default", "sm", "xs"] as const

export function AttachmentSizes() {
  return (
    <div className="flex flex-wrap items-center justify-center gap-3">
      {sizes.map((size) => (
        <Attachment key={size} size={size}>
          <AttachmentMedia>
            <IconFileText />
          </AttachmentMedia>
          <AttachmentContent>
            <AttachmentTitle>invoice.pdf</AttachmentTitle>
            <AttachmentDescription>{size}</AttachmentDescription>
          </AttachmentContent>
          <AttachmentActions>
            <AttachmentAction aria-label="Remove invoice.pdf">
              <IconX />
            </AttachmentAction>
          </AttachmentActions>
        </Attachment>
      ))}
    </div>
  )
}

Uzun adlar

Uzun dosya adları uzantıdan önce kısaltılır; böylece kişiler bir .pdf ile .zip'i yine ayırt edebilir. Tamamını okumak için adın üzerine gelin.

import { IconFileText } from "@tabler/icons-react"

import {
  Attachment,
  AttachmentContent,
  AttachmentDescription,
  AttachmentMedia,
  AttachmentTitle,
} from "@/components/ui/attachment"

export function AttachmentLongNames() {
  return (
    <div className="flex w-full max-w-xs flex-col gap-2">
      <Attachment>
        <AttachmentMedia>
          <IconFileText />
        </AttachmentMedia>
        <AttachmentContent>
          <AttachmentTitle>
            quarterly-sales-report-final-v3-approved-by-finance.pdf
          </AttachmentTitle>
          <AttachmentDescription>PDF · 5.2 MB</AttachmentDescription>
        </AttachmentContent>
      </Attachment>
      <Attachment>
        <AttachmentMedia>
          <IconFileText />
        </AttachmentMedia>
        <AttachmentContent>
          <AttachmentTitle>
            a-file-without-any-extension-at-all-just-a-long-name
          </AttachmentTitle>
          <AttachmentDescription>File · 12 KB</AttachmentDescription>
        </AttachmentContent>
      </Attachment>
    </div>
  )
}

Group

<AttachmentGroup /> eklentileri yana kayan bir satırda dizer; kenarlar solar ve öğeler yerine oturur.

import { IconFileZip } from "@tabler/icons-react"

import {
  Attachment,
  AttachmentContent,
  AttachmentDescription,
  AttachmentGroup,
  AttachmentMedia,
  AttachmentTitle,
} from "@/components/ui/attachment"

const files = Array.from(
  { length: 10 },
  (_, index) => `export-${index + 1}.zip`
)

export function AttachmentGroupDemo() {
  return (
    <AttachmentGroup className="w-full rounded-lg border">
      {files.map((name) => (
        <Attachment key={name}>
          <AttachmentMedia>
            <IconFileZip />
          </AttachmentMedia>
          <AttachmentContent>
            <AttachmentTitle>{name}</AttachmentTitle>
            <AttachmentDescription>ZIP · 18 MB</AttachmentDescription>
          </AttachmentContent>
        </Attachment>
      ))}
    </AttachmentGroup>
  )
}

Trigger

<AttachmentTrigger /> tüm kartı tıklanabilir yapar, örneğin bir önizleme veya bağlantı açmak için; karttaki eylemler ise bağımsız kalır. Ona bir aria-label verin.

import { IconDownload, IconFileZip, IconX } from "@tabler/icons-react"

import {
  AlertDialog,
  AlertDialogCancel,
  AlertDialogContent,
  AlertDialogDescription,
  AlertDialogFooter,
  AlertDialogHeader,
  AlertDialogTitle,
  AlertDialogTrigger,
} from "@/components/ui/alert-dialog"
import {
  Attachment,
  AttachmentAction,
  AttachmentActions,
  AttachmentContent,
  AttachmentDescription,
  AttachmentMedia,
  AttachmentTitle,
  AttachmentTrigger,
} from "@/components/ui/attachment"

export function AttachmentTriggerDemo() {
  return (
    <div className="flex flex-wrap justify-center gap-3">
      <AlertDialog>
        <Attachment>
          <AttachmentMedia>
            <IconFileZip />
          </AttachmentMedia>
          <AttachmentContent>
            <AttachmentTitle>research-summary.zip</AttachmentTitle>
            <AttachmentDescription>ZIP · 4.8 MB</AttachmentDescription>
          </AttachmentContent>
          <AttachmentActions>
            <AttachmentAction aria-label="Remove research-summary.zip">
              <IconX />
            </AttachmentAction>
          </AttachmentActions>
          <AlertDialogTrigger
            render={
              <AttachmentTrigger aria-label="Preview research-summary.zip" />
            }
          />
        </Attachment>
        <AlertDialogContent>
          <AlertDialogHeader>
            <AlertDialogTitle>research-summary.zip</AlertDialogTitle>
            <AlertDialogDescription>
              12 files · 4.8 MB · Uploaded today
            </AlertDialogDescription>
          </AlertDialogHeader>
          <AlertDialogFooter>
            <AlertDialogCancel>Close</AlertDialogCancel>
          </AlertDialogFooter>
        </AlertDialogContent>
      </AlertDialog>
      <Attachment>
        <AttachmentMedia variant="image">
          <img src="/preview/landscape.svg" alt="" />
        </AttachmentMedia>
        <AttachmentContent>
          <AttachmentTitle>workspace.png</AttachmentTitle>
          <AttachmentDescription>Opens in a new tab</AttachmentDescription>
        </AttachmentContent>
        <AttachmentActions>
          <AttachmentAction aria-label="Download workspace.png">
            <IconDownload />
          </AttachmentAction>
        </AttachmentActions>
        <AttachmentTrigger
          render={
            <a
              href="/preview/landscape.svg"
              target="_blank"
              rel="noreferrer"
              aria-label="Open workspace.png"
            />
          }
        />
      </Attachment>
    </div>
  )
}

Composer

Sayfa yüklendikten sonra eklenen dosyalar pop ederek belirir; biri kaldırıldığında satırın geri kalanı düzgünce kayar.

"use client"

import * as React from "react"
import { IconFileText, IconPlus, IconX } from "@tabler/icons-react"

import {
  Attachment,
  AttachmentAction,
  AttachmentActions,
  AttachmentContent,
  AttachmentDescription,
  AttachmentGroup,
  AttachmentMedia,
  AttachmentTitle,
} from "@/components/ui/attachment"
import { Button } from "@/components/ui/button"

export function AttachmentComposer() {
  const [files, setFiles] = React.useState([
    { id: 1, name: "brief.pdf" },
    { id: 2, name: "moodboard.fig" },
    { id: 3, name: "timeline.xlsx" },
  ])

  return (
    <div className="flex w-full flex-col gap-3">
      <AttachmentGroup className="rounded-lg border">
        {files.map((file) => (
          <Attachment key={file.id} size="sm">
            <AttachmentMedia>
              <IconFileText />
            </AttachmentMedia>
            <AttachmentContent>
              <AttachmentTitle>{file.name}</AttachmentTitle>
              <AttachmentDescription>Attached</AttachmentDescription>
            </AttachmentContent>
            <AttachmentActions>
              <AttachmentAction
                aria-label={`Remove ${file.name}`}
                onClick={() =>
                  setFiles((current) =>
                    current.filter((item) => item.id !== file.id)
                  )
                }
              >
                <IconX />
              </AttachmentAction>
            </AttachmentActions>
          </Attachment>
        ))}
      </AttachmentGroup>
      <Button
        size="sm"
        variant="outline"
        className="self-start"
        onClick={() =>
          setFiles((current) => [
            ...current,
            { id: Date.now(), name: `notes-${current.length + 1}.txt` },
          ])
        }
      >
        <IconPlus data-icon="inline-start" />
        Add file
      </Button>
    </div>
  )
}

Bozuk görsel

Bir görsel önizlemesi yüklenemediğinde bir dosya simgesine geri döner.

import {
  Attachment,
  AttachmentContent,
  AttachmentDescription,
  AttachmentMedia,
  AttachmentTitle,
} from "@/components/ui/attachment"

export function AttachmentBrokenImage() {
  return (
    <Attachment>
      <AttachmentMedia variant="image">
        <img src="/preview/missing.png" alt="" />
      </AttachmentMedia>
      <AttachmentContent>
        <AttachmentTitle>missing.png</AttachmentTitle>
        <AttachmentDescription>Preview unavailable</AttachmentDescription>
      </AttachmentContent>
    </Attachment>
  )
}

Stres testi

Dar bir sütunda zorlu adlar, her 60 ms'de rastgele bir durum ve tek bir grupta 200 öğe. Hiçbir şey taşmaz ve düzen bozulmaz.

"use client"

import * as React from "react"
import { IconFileText, IconX } from "@tabler/icons-react"

import {
  Attachment,
  AttachmentAction,
  AttachmentActions,
  AttachmentContent,
  AttachmentDescription,
  AttachmentGroup,
  AttachmentMedia,
  AttachmentTitle,
  type AttachmentState,
} from "@/components/ui/attachment"
import { Button } from "@/components/ui/button"

const names = [
  "x".repeat(300) + ".pdf",
  "archive.tar.gz",
  "🎉🎉🎉 party photo final FINAL (2).png",
  "<script>alert(1)</script>.html",
  "a".repeat(400),
  ".gitignore",
  "تقرير-المبيعات-الربعي-النهائي-للإدارة.pdf",
  "名前がとても長いファイルの例です.docx",
  "",
  "file.with.many.dots.in.its.name.json",
]

const states: AttachmentState[] = [
  "idle",
  "uploading",
  "processing",
  "error",
  "done",
]

const bulk = Array.from(
  { length: 200 },
  (_, index) => `bulk-export-${index + 1}.csv`
)

export function AttachmentStress() {
  const [tick, setTick] = React.useState(0)
  const [running, setRunning] = React.useState(false)

  React.useEffect(() => {
    if (!running) {
      return
    }
    const timer = setInterval(() => setTick((value) => value + 1), 60)
    return () => clearInterval(timer)
  }, [running])

  return (
    <div className="flex w-full flex-col items-start gap-3">
      <div className="flex w-56 flex-col gap-2 rounded-lg border border-dashed p-2">
        {names.map((name, index) => {
          const state = running
            ? states[(tick + index) % states.length]
            : "done"
          return (
            <Attachment
              key={index}
              state={state}
              progress={((tick * 37 + index * 13) % 160) - 30}
            >
              <AttachmentMedia>
                <IconFileText />
              </AttachmentMedia>
              <AttachmentContent>
                <AttachmentTitle>{name}</AttachmentTitle>
                <AttachmentDescription>
                  {state === "error"
                    ? "Upload failed because the server rejected this extremely long file name"
                    : state}
                </AttachmentDescription>
              </AttachmentContent>
              <AttachmentActions>
                <AttachmentAction aria-label={`Remove ${name || "file"}`}>
                  <IconX />
                </AttachmentAction>
              </AttachmentActions>
            </Attachment>
          )
        })}
      </div>
      <Button size="sm" variant="outline" onClick={() => setRunning(!running)}>
        {running ? "Stop chaos" : "Start chaos"}
      </Button>
      <AttachmentGroup className="w-full rounded-lg border">
        {bulk.map((name) => (
          <Attachment key={name} size="xs">
            <AttachmentMedia>
              <IconFileText />
            </AttachmentMedia>
            <AttachmentContent>
              <AttachmentTitle>{name}</AttachmentTitle>
            </AttachmentContent>
          </Attachment>
        ))}
      </AttachmentGroup>
    </div>
  )
}

Sağdan sola

Media, content ve actions yansıtılır; ilerleme çubuğu başlangıç kenarından dolar.

import { IconFileText, IconX } from "@tabler/icons-react"

import {
  Attachment,
  AttachmentAction,
  AttachmentActions,
  AttachmentContent,
  AttachmentDescription,
  AttachmentMedia,
  AttachmentTitle,
} from "@/components/ui/attachment"

export function AttachmentRtl() {
  return (
    <div dir="rtl">
      <Attachment>
        <AttachmentMedia>
          <IconFileText />
        </AttachmentMedia>
        <AttachmentContent>
          <AttachmentTitle>تقرير-المبيعات-الربعي-النهائي.pdf</AttachmentTitle>
          <AttachmentDescription>PDF · 2.4 ميغابايت</AttachmentDescription>
        </AttachmentContent>
        <AttachmentActions>
          <AttachmentAction aria-label="إزالة الملف">
            <IconX />
          </AttachmentAction>
        </AttachmentActions>
      </Attachment>
    </div>
  )
}
TuşAction
TabOdağı her eyleme, sonra trigger'a taşır. Dikey kutucuklardaki gizli eylemler, odak karta girdiğinde görünür.
EnterSpaceOdaktaki eylemi veya trigger'ı etkinleştirir.
  • Yükleme sırasında ilerleme çubuğunda role="progressbar" bulunur ve başlıkla etiketlenir.
  • Dize başlıklar tam adı içeren bir title niteliği alır; böylece kısaltılmış adlar yine okunabilir.
  • Yalnızca simgeli eylemlerde ve trigger'da metin yoktur; bu yüzden onlara her zaman dosyayı adlandıran bir aria-label verin.
  • Azaltılmış hareket açıkken ekler pop etmeden görünür, ilerleme çubuğu yumuşamak yerine atlar ve gruplar anında yeniden düzenlenir.

Grup, eylem ve trigger dışındaki her parça düz bir öğe oluşturur ve onun niteliklerini kabul eder. Kart stilleri attachmentVariants olarak dışa aktarılır.

PropTürVarsayılan
state
"idle" | "uploading" | "processing" | "error" | "done""done"
progress0 ile 100 arası. Yalnızca yükleme sırasında gösterilir ve bu aralığa sabitlenir.
number–
size
"default" | "sm" | "xs""default"
orientation
"horizontal" | "vertical""horizontal"
ÖznitelikAçıklama
data-slot="attachment"CSS'te kartı hedefleyin.
data-stateGeçerli durum.
data-sizeGeçerli boyut.
data-orientationGeçerli yönelim.
data-slot="attachment-progress"Yükleme sırasında bulunan ilerleme çubuğu.
--attachment-radiusKartın köşe yarıçapı. İç köşeler buna göre türetilir.
PropTürVarsayılan
variantimage, alt öğesini bir dosya simgesi fallback'ine sahip kare bir AspectRatio içine sarar.
"icon" | "image""icon"
ÖznitelikAçıklama
data-slot="attachment-media"CSS'te medyayı hedefleyin.
data-variantGeçerli varyant.

Başlığı ve açıklamayı tutan, kalan genişliği kaplayan bir <div>.

ÖznitelikAçıklama
data-slot="attachment-content"CSS'te içeriği hedefleyin.
PropTürVarsayılan
childrenBir dize, uzantısı asla kırpılmayacak şekilde bölünür.
ReactNode–
titleÜzerine gelindiğinde gösterilir. Varsayılan olarak tam addır.
stringchildren
idBelirtilmezse oluşturulur. İlerleme çubuğu bununla etiketlenir.
string–
ÖznitelikAçıklama
data-slot="attachment-title"CSS'te başlığı hedefleyin.

Boyut, tür veya durum için tek satırlık bir <span>. Yer bittiğinde kısaltılır.

ÖznitelikAçıklama
data-slot="attachment-description"CSS'te açıklamayı hedefleyin.

Eylem düğmeleri için bir <div>. Dikey kutucuklarda önizlemenin üzerinde yüzer ve fare olan cihazlarda üzerine gelindiğinde veya odaklandığında görünür.

ÖznitelikAçıklama
data-slot="attachment-actions"CSS'te eylemleri hedefleyin.

Trigger'ın üstünde duran bir <Button />. Button'ın tüm prop'larını kabul eder.

PropTürVarsayılan
variant
ButtonVariant"ghost"
size
ButtonSize"icon-xs"
ÖznitelikAçıklama
data-slot="attachment-action"CSS'te eylemleri hedefleyin.

Tüm kartı kaplayan görünmez bir katman. Bir diyalog trigger'ı, bir bağlantı veya herhangi bir düğmeyle birleştirin.

PropTürVarsayılan
render
ReactElement | (props, state) => ReactElement<button type="button">
ÖznitelikAçıklama
data-slot="attachment-trigger"CSS'te tetikleyiciyi hedefleyin.

Her eke oturan ve öğeler eklendiğinde veya kaldırıldığında satırı animasyonla kaydıran yatay bir <ScrollArea />. scrollbars dışındaki tüm ScrollArea prop'larını kabul eder.

ÖznitelikAçıklama
data-slot="attachment-group"CSS'te grubu hedefleyin.
data-slot="attachment-group-list"Ekleri tutan satır.

Kullanıldığı bloklar

Attachment üzerine kurulan bloklar.