# Bileşen Yapılandırması

> StudioConfig ve ComponentConfig başvurusu, defineSection/defineElement, <Slot> ve <TecofRoot>, kök className sözleşmesi, dinamik alanlar, sürükle-bırak kuralları, varyantlar ve varsayılan çocuklar.
>
> Kaynak: https://tecof-app-dev.vercel.app/editor-components · Bölüm: Tema geliştirme · Son güncelleme: 2018-10-20

Studio’nun hangi blokları göstereceğini, blokların hangi alanlarla düzenleneceğini ve nasıl render edileceğini `StudioConfig` belirler. Editör ve yayın renderer’ı **aynı** nesneyi kullanmalıdır; kayıtlı bir `type` yayındaki config’te yoksa o düğüm sessizce render edilmez.

## StudioConfig

| Alan | Tip | Açıklama |
| :--- | :--- | :--- |
| `components` | `Record<string, ComponentConfig>` | Zorunlu. Anahtar, dokümanda `type` olarak saklanır |
| `categories` | `Record<string, { title?, components? }>` | Bileşen kütüphanesindeki kategoriler ve sırası |
| `root` | `{ fields?, render? }` | Sayfa geneli alanlar (Inspector → Sayfa) ve sayfa sarmalayıcısı (`render`, `children` alır) |
| `templates` | `SectionTemplate[]` | Tek bölüm şablonları ([Slotlar ve şablonlar](/editor-slots#bolum-sablonlari)) |
| `pageTemplates` | `PageTemplate[]` | Çok bölümlü sayfa şablonları |
| `theme` | `DeepPartialThemeConfig` | Temanın varsayılan renk/tipografi/boşluk değerleri ([Canlı tema](/editor-theme)) |
| `themePresets` | `ThemePreset[]` | Tema panelinde hazır renk paletleri |
| `darkMode` | `boolean \| { defaultMode?, storageKey? }` | Karanlık modu açar ([Canlı tema](/editor-theme#karanlik-mod)) |
| `permissions` | `Partial<Permissions>` | Genel izinler ([İzinler](/editor-permissions)) |
| `migrations` | `MigrationConfig` | Eski kayıtları yükseltme ([İzinler ve migration](/editor-permissions#veri-migrationi)) |
| `ai` | `{ complete({ system, prompt }) }` | AI ile bölüm üretme ([Studio](/editor-studio#ai-ile-bolum-uretme)) |
| `nodeInfoButton` | `boolean` | Seçim araç çubuğunda ⓘ bileşen bilgisi düğmesi; varsayılan kapalı |

## ComponentConfig

Yalnız `render` zorunludur.

| Alan | Tip | Açıklama |
| :--- | :--- | :--- |
| `render` | `(props) => ReactNode` | Editörde ve yayında kullanılan render |
| `label` | `string?` | Kütüphanede görünen ad |
| `category` | `string?` | Kategori anahtarı |
| `fields` | `Record<string, FieldConfig>` | Inspector alanları ([Alanlar](/editor-fields)) |
| `fieldsGroups` | `{ name, fields[] }[]` | Alanları akordeon gruplarına ayırır; listelenmeyenler grupların ardından düz gösterilir |
| `defaultProps` | `object?` | Yeni düğümün başlangıç değerleri |
| `defaultChildren` | `Record<slot, Array<type \| { type, props? }>>` | Eklenince slotlara otomatik konan çocuklar (özyinelemeli, döngü korumalı) |
| `variants` | `Record<string, { label, props }>` | İsimli prop ön ayarları: Bölüm Ekle’de ayrı kart, Inspector’da çip |
| `inline` | `boolean?` | Editörün sarmalayıcı `div`’ini kaldırır; bileşen `puck.dragRef`’i köke bağlamalıdır |
| `acceptsChildren` | `boolean \| string[] \| Record<slot, boolean \| string[]>` | Hangi çocuk tiplerini kabul ettiği (bileşen ya da slot bazında) |
| `maxItems` | `number \| Record<slot, number>` | Doğrudan çocuk üst sınırı (bileşen ya da slot bazında) |
| `allowedParents` | `string[]` | Bu bileşenin bırakılabileceği üst tipler; doluysa kök içeriğe bırakılamaz |
| `resizable` | `boolean?` | Boyutlandırma modunda genişlik/yükseklik tutamaçları (varsayılan `true`) |
| `permissions` / `resolvePermissions` | — | Bileşen ve düğüm bazlı izinler |
| `resolveFields` | `(props, ctx) => fields` | Prop’lara göre dinamik alan seti (sync/async) |
| `resolveData` | `(props, ctx) => { props?, readOnly? }` | Türetilmiş prop’lar ve salt okunur alanlar (sync/async) |
| `metadata` | `object?` | Host’a özel veri; render’da `puck.metadata` içinde |

Render fonksiyonuna gelen prop’lar: düğüm prop’ları, editörün birleştirdiği `className`, slot alanları için hazır slot elemanları, `editMode` ve `puck` (`renderDropZone`, `isEditing`, `metadata`, düzenlemede `registerOverlayPortal` / `dragRef`).

## Kök className sözleşmesi

Editör seçim, hover ve stil sınıflarını bileşenin **kök DOM elemanına** `className` üzerinden ulaştırır. Bir bileşen `props.className`’i kökte basmazsa kanvasta **seçilemez** ve Stil sekmesinde verilen stiller görünmez.

```tsx
// Yanlış: className kaybolur, düğüm seçilemez
render: (p) => <section className="py-16">{p.title}</section>

// Doğru
render: (p) => <section className={cn("py-16", p.className)}>{p.title}</section>
```

Bileşenin her dalı (ör. erken `return` ile boş durum) aynı sözleşmeye uymalıdır. `defineSection` / `defineElement` bunu otomatik yapar; elle yazarken `TecofRoot` kullanabilirsiniz:

```tsx
import { TecofRoot } from "@tecof/theme-editor";

render: (p) => (
  <TecofRoot as="section" className={cn("py-16", p.className)}>
    {p.title}
  </TecofRoot>
)
```

## defineSection ve defineElement

Bildirimsel bir tanımdan düz bir `ComponentConfig` üretir:

- `slots` tek yerde tanımlanır; hem `type: "slot"` alanları hem `defaultChildren` buradan türetilir,
- `render`’a ikinci argüman olarak hazır `<Slot>` elemanları (`slots.<ad>`) verilir,
- render’ın döndürdüğü kök elemanın `className`’ine `p.className` eklenir.

```tsx
import { createLanguageField, defineSection } from "@tecof/theme-editor";

export const FAQSection = defineSection({
  label: "Sıkça Sorulan Sorular",
  category: "sections",
  slots: {
    content: { label: "Başlık ve açıklama", layout: "col", gap: "md", default: ["Title", "Paragraph"] },
    items: {
      label: "Sorular",
      layout: "col",
      gap: "sm",
      hideIfEmpty: true,
      className: (p) => p.compact && "max-w-2xl mx-auto",
      default: [{ type: "FAQItem" }, { type: "FAQItem" }],
    },
  },
  fields: {
    compact: { type: "toggle", label: "Dar düzen" },
    eyebrow: createLanguageField({ label: "Üst başlık" }),
  },
  defaultProps: { compact: false },
  render: (p, slots) => (
    <section className="py-16 lg:py-24">
      <div className="mx-auto max-w-4xl px-4">
        {slots.content}
        <div className="mt-10">{slots.items}</div>
      </div>
    </section>
  ),
});
```

| `slots.<ad>` seçeneği | Açıklama |
| :--- | :--- |
| `label` | Inspector’daki slot adı |
| `layout` | `row` · `row-wrap` · `col` · `contents` · `none`; `row`/`row-wrap` slot alanını yatay (`orientation: "horizontal"`) yapar |
| `gap` | `none` · `xs` .25 · `sm` .5 · `md` .75 · `lg` 1 · `xl` 1.5 rem ya da ham değer (`"1.25rem"`) |
| `className` | Kapsayıcıya ek sınıf; fonksiyon verilirse her render’da prop’larla çağrılır |
| `hideIfEmpty` | Yayında slot boşsa kapsayıcıyı hiç basma (editörde boş slot “Bileşen ekle” ipucuyla görünür kalır) |
| `default` | Eklenince otomatik doğan çocuklar |
| `fieldExtra` | Slot alanına eklenecek ek ayarlar (ör. `{ repeatSource: "items" }`) |
| `allow`, `maxItems` | Slot alanına yazılır — aşağıdaki nota bakın |

> [!WARNING] Slot alanındaki `allow` ve `maxItems` motor tarafından uygulanmaz
> 0.0.96 sürümünde sürükle-bırak kuralları **bileşen düzeyindeki** `acceptsChildren`, `maxItems` ve çocuğun `allowedParents` alanlarından okunur. Slot alanına yazılan `allow` ya da `maxItems` editörde kısıtlama yapmaz. Kısıtlama gerekiyorsa `defineSection` çıktısına bileşen düzeyinde ekleyin:
>
> ```tsx
> export const FAQSection = {
>   ...defineSection({ /* … */ }),
>   acceptsChildren: { items: ["FAQItem"] },
>   maxItems: { items: 12 },
> };
> ```

> [!CAUTION] Bileşen elemanı döndüren render
> `render` bir DOM elemanı yerine bileşen elemanı döndürürse (`<HeroBody />`), `className` o bileşene **prop** olarak geçer. Bileşen bunu kendi kök DOM’una basmazsa düğüm yine seçilemez. Hook gerektiren gövdeyi ayrı bileşene alırken `className` prop’unu kabul edip köke `cn(...)` ile geçirin.

## Slot bileşeni

`<Slot>` bir slot değerini tek satırda çizer ve yerleşimi tek kaynaktan (`layout` + `gap`) yönetir. `defineSection` kullanmayan bileşenlerde doğrudan kullanılır:

```tsx
import { Slot } from "@tecof/theme-editor";

render: (p) => (
  <section className={cn("py-16", p.className)}>
    <Slot value={p.itemsSlot} layout="row-wrap" gap="xl" className="justify-center" data-reveal-group />
  </section>
)
```

| Prop | Açıklama |
| :--- | :--- |
| `value` | Motorun enjekte ettiği slot değeri (`p.<slotAdı>`) |
| `layout`, `gap`, `className`, `style`, `hideIfEmpty` | Yukarıdaki tabloyla aynı |
| diğerleri | `data-*`, `aria-*`, `id` gibi prop’lar slot kapsayıcısına aynen geçer |

`<Slot>` motorun varsayılan yatay yerleşimini (inline `flex`/`gap`) basmasını kapatır; böylece temanın Tailwind sınıfları ezilmez. Yerleşim sınıfları (`.tecof-slot-row`, `.tecof-slot-row-wrap`, `.tecof-slot-col`, `.tecof-slot-contents`) `@layer base` içindedir: editörde paketin `styles.css`’inden gelir, **yayında ise temanın global CSS’inde bulunmalıdır** (yayın sayfası paket CSS’ini yüklemez).

## Dinamik alanlar ve türetilmiş veri

`resolveFields` prop’lara göre alan setini değiştirir; `resolveData` türetilmiş prop yazar ve alanları salt okunur yapar. İkisi de senkron ya da asenkron olabilir; geç gelen eski sonuçlar elenir, geri yazım yalnız gerçek farkı commit eder (idempotent olmalı).

```tsx
ProductCard: {
  fields: {
    source: { type: "select", options: [{ label: "Elle", value: "manual" }, { label: "CMS", value: "cms" }] },
    title: { type: "text" },
    slug: { type: "text" },
  },
  resolveFields: (props, { fields }) => ({
    ...fields,
    ...(props.source === "cms" ? { collection: createCmsCollectionField({ label: "Koleksiyon" }) } : {}),
  }),
  resolveData: (props) => ({
    props: { slug: slugify(props.title ?? "") },
    readOnly: { slug: true },
  }),
  render: (props) => <article className={props.className}>{props.title}</article>,
}
```

`resolveFields` hata fırlatırsa statik `fields` kullanılır.

## Sürükle-bırak kuralları

Kurallar hem kanvasta hem katman panelinde, dokunmatik sürüklemede de aynı şekilde uygulanır. Hiçbiri tanımlı değilse her bırakma serbesttir.

```tsx
const config: StudioConfig = {
  components: {
    Slider: {
      acceptsChildren: { slides: ["Slide"] },   // slides slotu yalnız Slide alır
      maxItems: { slides: 8 },
      fields: { slides: { type: "slot", orientation: "horizontal" } },
      render: ({ slides, className }) => <section className={className}>{slides}</section>,
    },
    Slide: {
      allowedParents: ["Slider"],                // yalnız Slider içine bırakılabilir
      render: ({ className }) => <figure className={className} />,
    },
  },
};
```

## Inline bileşenler

Sarmalayıcı `div` düzeni bozuyorsa `inline: true` verin ve `puck.dragRef`’i kök elemana bağlayın:

```tsx
InlineButton: {
  inline: true,
  fields: { text: { type: "text" } },
  render: ({ puck, text, className }) => (
    <button ref={puck?.dragRef} className={className}>{text}</button>
  ),
}
```

## Varyantlar

```tsx
Button: {
  fields: { text: { type: "text" }, tone: { type: "radio", options: [{ label: "Ana", value: "primary" }, { label: "Çizgili", value: "outline" }] } },
  variants: {
    primary: { label: "Ana buton", props: { tone: "primary" } },
    outline: { label: "Çizgili buton", props: { tone: "outline" } },
  },
  render: (p) => <a className={cn(p.tone === "primary" ? "bg-black text-white" : "border", p.className)}>{p.text}</a>,
}
```

Her varyant Bölüm Ekle penceresinde kendi önizlemeli kartı olarak görünür; Inspector’daki çip varyantı mevcut düğüme uygular (geri alınabilir, `_variant` işaretiyle).

## Kategoriler ve sayfa kökü

```tsx
export const editorConfig: StudioConfig = {
  components,
  categories: {
    sections: { title: "Sayfa bölümleri", components: ["Header", "Hero", "Footer"] },
    elements: { title: "Elementler", components: ["Title", "Paragraph", "Button"] },
  },
  root: {
    fields: {
      enableEcommerce: { type: "radio", label: "E-ticaret özellikleri", options: [{ label: "Aktif", value: "yes" }, { label: "Pasif", value: "no" }] },
    },
  },
};
```

Sayfa geneli değerler dokümanın `root.props` alanında saklanır; tema yayın tarafında bunları `data.root.props` üzerinden okur. Editör 0.0.96 `root.defaultProps` alanını okumaz: değer hiç kaydedilmemiş sayfalar için varsayılanı temada verin (ör. `data.root.props.enableEcommerce !== "no"`).
