# Alanlar

> Tecof Studio alan başvurusu — yerleşik tipler, çok dilli metin, zengin metin, medya, bağlantı, renk, kod, ikon, tekrar eden satırlar, harici/API kaynakları, CMS ve e-ticaret seçicileri; saklanan değer biçimleri.
>
> Kaynak: https://tecof-app-dev.vercel.app/editor-fields · Bölüm: Tema geliştirme · Son güncelleme: 2018-10-20

Alanlar Inspector’da düzenlenen prop’ları tanımlar. Yerleşik tipler doğrudan `{ type: "…" }` olarak yazılır; gelişmiş alanlar `create*Field()` fabrikalarıyla üretilir. Fabrikalar `{ type: "custom", _fieldType, label, render }` döndürür, yani Puck uyumlu config’e doğrudan konur.

## Yerleşik tipler

| `type` | Değer | Önemli seçenekler |
| :--- | :--- | :--- |
| `text`, `textarea` | `string` | `bindable` (CMS bağlama düğmesi, varsayılan `true`), `readOnly` |
| `select`, `radio` | seçilen `value` | `options: [{ label, value }]` |
| `number` | `number` | — |
| `boolean`, `toggle` | `boolean` | `onLabel`, `offLabel` (varsayılan `Açık` / `Kapalı`) |
| `range` | `number` | `min` (0), `max` (100), `step` (1), `unit` (ör. `"px"`) |
| `array` | `object[]` | `arrayFields` |
| `object` | `object` | `objectFields` |
| `slot` | (zone) | `orientation`, `repeatSource`, `itemSchema` — bkz. [Slotlar](/editor-slots) |

Her alan `label`, `defaultValue` ve `readOnly` alabilir. Dinamik salt okunurluk `resolveData().readOnly` ile gelir; ikisi VEYA’lanır.

## Ortak seçenekler

Tüm fabrikalar şunları destekler:

| Seçenek | Tip | Açıklama |
| :--- | :--- | :--- |
| `label` | `string` | Inspector etiketi |
| `labelIcon` | `ReactElement` | Etiketin yanındaki ikon (ör. `<Globe size={16} />`) |
| `visible` | `boolean` | Alanın panelde görünüp görünmediği |

## LanguageField — çok dilli metin

Merchant dillerini `merchant-info`’dan alır ve dil başına değer tutar. Studio içinde üst çubuktaki **global dil** kullanılır ve alan içindeki dil sekmeleri gizlenir; Studio dışında sekmeli moda döner.

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

fields: {
  title: createLanguageField({ label: "Başlık" }),
  description: createLanguageField({ label: "Açıklama", isTextarea: true, textareaRows: 4 }),
  html: createLanguageField({ label: "HTML içerik", isHtml: true }),
}
```

| Seçenek | Varsayılan | Açıklama |
| :--- | :--- | :--- |
| `isTextarea` | `false` | Çok satırlı giriş |
| `textareaRows` | `3` | Satır sayısı |
| `placeholder` | `''` | Yer tutucu |
| `isHtml` | `false` | Çeviride HTML etiketlerini korur |

**Değer:** `[{ code: "tr", value: "Merhaba" }, { code: "en", value: "Hello" }]`. Temada aktif dilin değerini seçin; değer yoksa varsayılan dile düşmek temanın sorumluluğundadır:

```ts
const pick = (value: Array<{ code: string; value: string }> | string | undefined, locale: string, fallback = "tr") =>
  typeof value === "string" ? value : (value?.find((v) => v.code === locale)?.value || value?.find((v) => v.code === fallback)?.value || "");
```

**Dil araçları:** Birden fazla dil varken alan üstünde **Hızlı Doldur** (aktif sekmedeki metni tüm dillere kopyalar) ve **Çevir** (API üzerinden diğer dillere çevirir; `isHtml` ile etiketler korunur) düğmeleri görünür. Aynı çubuk `EditorField`, `LinkField` (yalnız Hızlı Doldur, yalnız boş diller) ve `LocalizedUploadField`’da da vardır.

Kendi çok dilli alanınızda aynı araçları kullanabilirsiniz:

```tsx
import {
  LanguageToolsBar, useLanguageToolsStatus, normalizeLocalizedValues, fillLanguages,
  translateLanguages, isEmptyText, LANGUAGE_TOOL_MESSAGES,
} from "@tecof/theme-editor";

const { status, translating, setTranslating, flash } = useLanguageToolsStatus();
const values = normalizeLocalizedValues<string>(value, languages, () => "");

const fill = () => {
  const res = fillLanguages(values, languages, activeTab, { isEmpty: isEmptyText });
  if (!res) return; // kaynak boş ya da doldurulacak dil yok
  onChange(res.values);
  flash({ text: LANGUAGE_TOOL_MESSAGES.filledAll, type: "success" }, 2000);
};

const translate = async () => {
  setTranslating(true);
  try {
    const out = await translateLanguages({
      values: () => valuesRef.current, // getter: yanıt gelene kadar yazılanlar korunur
      languages,
      sourceCode: activeTab,
      translate: apiClient.translate.bind(apiClient),
    });
    if (out.ok) onChange(out.values);
    flash(out.ok ? { text: LANGUAGE_TOOL_MESSAGES.translated, type: "success" } : { text: out.message, type: "error" }, 3000);
  } finally {
    setTranslating(false);
  }
};

<LanguageToolsBar onFill={fill} onTranslate={translate} translating={translating} status={status} />;
```

`fillLanguages(values, languages, sourceCode, { onlyEmpty, isEmpty, clone })` → `{ values, filled } | null`. `onlyEmpty: true` dolu dilleri ezmez ve listede olmayan dil kodlarını korur. Boşluk tanımları: `isEmptyText`, `isEmptyHtml` (medya etiketi dolu sayılır), `isEmptyLink`.

## EditorField — zengin metin

TipTap tabanlı, çok dilli WYSIWYG editör (kalın, italik, bağlantı, liste, başlık…). Hızlı Doldur boş `<p></p>` içerikte pasiftir; Çevir yalnız `TecofProvider` varken görünür. Araçlar aktif dilin içeriğini değiştirmediği için imleç korunur.

```tsx
fields: { content: createEditorField({ label: "İçerik" }) }
```

## UploadField — medya

Seçili medya kare önizleme kutuları olarak görünür; “ekle” kutusu sekmeli bir medya penceresi açar: **Kütüphane** (sunucudaki dosyalar), **Yükle** (FilePond), **Referans** (CMS değişkeni, `{{ data.alan }}`). Doka görsel düzenleyici (kırp, döndür, ayarlar), istemci tarafı WebP sıkıştırma ve **odak noktası** seçimi içerir.

```tsx
fields: {
  gallery: createUploadField({
    label: "Ürün galerisi",
    allowMultiple: true,
    maxFiles: 10,
    maxFileSize: "50MB",
    acceptedTypes: ["image/*"],
    folder: "/products",
  }),
  document: createUploadField({ label: "Katalog", allowMultiple: false, acceptedTypes: ["application/pdf"] }),
}
```

| Seçenek | Varsayılan | Açıklama |
| :--- | :--- | :--- |
| `allowMultiple` | `true` | Çoklu dosya |
| `maxFiles` | `100` | En çok dosya |
| `maxFileSize` | `"100MB"` | Tek dosya sınırı |
| `maxTotalFileSize` | `"200MB"` | Toplam sınır |
| `acceptedTypes` | tümü | MIME türleri (`["image/*"]`, `["application/pdf"]`) |
| `folder` | `"/"` | Yükleme klasörü |
| `allowReorder` | `true` | Sürükleyerek sıralama |
| `imageCompressionEnabled` | `true` | Yüklemeden önce sıkıştırma |
| `imageCompressionOptions` | `{ maxSizeMB: 1, maxWidthOrHeight: 1920 }` | Sıkıştırma ayarları (`fileType` de verilebilir) |
| `imagePreviewHeight` | — | FilePond önizleme yüksekliği |
| `showUploadedFiles` | `false` | Kullanımdan kalktı; yeni arayüzde etkisiz |

**Değer:** `UploadedFile[]` — `{ _id, name, size, type, mimeType?, url?, folder?, provider?, focalPoint?, meta? }`. Görseli temada `TecofPicture` ile basın ([Yayın render’ı](/editor-render#tecofpicture)).

**Odak noktası:** görsel kutusundaki nişangâh düğmesi odak penceresini açar (tıkla/sürükle, ok tuşlarıyla 1’er, `Shift` ile 10’ar adım; 16:9, 1:1, 9:16 kırpma önizlemeleri). Seçim dosyanın içine `focalPoint: { x, y }` (yüzde, 50/50 merkez) olarak yazılır; `TecofPicture` bunu `object-position` olarak uygular. Backend’de şema değişikliği gerekmez.

## LocalizedUploadField — dile göre medya

`UploadField`’ın dil sekmeli sürümü; seçenekleri aynıdır. Her dil için ayrı dosya listesi tutar.

```tsx
fields: {
  banner: createLocalizedUploadField({ label: "Kampanya görseli", allowMultiple: false }),
}
defaultProps: { banner: [] }
```

**Değer:** `[{ code: "tr", value: UploadedFile[] }, { code: "en", value: [] }]`. Yayında istenen dil boşsa varsayılan dile, o da boşsa ilk dolu dile düşülür:

```tsx
import { resolveLocalizedUploadFile, TecofPicture } from "@tecof/theme-editor";

const file = resolveLocalizedUploadFile(props.banner, locale, "tr"); // UploadedFile | null
return file ? <TecofPicture data={file} alt="" /> : null;
```

Çoklu dosya için `resolveLocalizedUpload(value, locale, defaultLanguage)` → `UploadedFile[]`. Eski düz `UploadedFile[]` kayıtlar varsayılan dil altında açılır ve ilk düzenlemede yeni biçimde kaydedilir. Hızlı Doldur yalnız boş dillere kopyalar; Çevir yoktur. Ters yön (yerelleştirilmiş alanı tekrar `createUploadField`’a çevirmek) desteklenmez.

## LinkField — sayfa veya URL

Mağaza sayfalarından seçim (aranabilir pencere, yayın durumu göstergesi) ya da elle URL girişi. Dil sekmelidir; değer dil başına saklanır.

```tsx
fields: {
  cta: createLinkField({ label: "CTA bağlantısı", showTarget: true, placeholder: "https://…" }),
}
```

| Seçenek | Varsayılan | Açıklama |
| :--- | :--- | :--- |
| `showTarget` | `true` | Aynı / yeni sekme seçimi |
| `placeholder` | `https://...` | URL yer tutucusu |

**Değer:** `[{ code: "tr", value: LinkFieldValue }, …]`; `LinkFieldValue = { url, label?, target?: "_self" | "_blank", type?: "page" | "custom" | "category" | "brand" | "product" | "cms" }`. Render yalnız `url` ve `target` okur. Eski düz `LinkFieldValue` kayıtlar mağaza dillerine göre normalize edilir.

## ColorField — renk

Popover renk seçici: doygunluk/parlaklık alanı, ton ve opaklık kaydırıcıları, HEX/RGB/HSL girdileri, ekrandan renk seçme (EyeDropper), canlı tema paleti, 22 tonluk Tailwind paleti, son kullanılanlar ve WCAG kontrast önizlemesi. Tam klavye erişimi vardır.

<!-- preview:color-field -->

```tsx
fields: {
  bgColor: createColorField({ label: "Arka plan" }),
  overlay: createColorField({ label: "Kaplama", showOpacity: true, defaultColor: "#00000080" }),
  accent: createColorField({ label: "Vurgu", themeVars: true }),
  brand: createColorField({ label: "Marka", swatches: ["#18181b", "#74b500", "#ffffff"], palette: false }),
}
```

| Seçenek | Varsayılan | Açıklama |
| :--- | :--- | :--- |
| `showOpacity` | `false` | Opaklık; değer `#rrggbbaa` |
| `defaultColor` | `''` | Sıfırlama hedefi |
| `placeholder` | `'#000000'` | HEX girdisi yer tutucusu |
| `showReset` | `true` | Değer varsayılandan farklıyken sıfırla düğmesi |
| `swatches` | `[]` | “Hızlı seçim” satırı |
| `themeColors` | `true` | Canlı tema renkleri (yalnız Studio içinde) |
| `themeVars` | `false` | Tema rengini `var(--theme-color-<anahtar>)` olarak bağlar; kapalıyken açık paletteki hex kopyalanır |
| `palette` | `true` | Tailwind paleti |
| `contrast` | `true` | Kontrast çipleri (WCAG AA/AAA) |
| `contrastAgainst` | — | Kontrast zemini (hex) |

**Değer sözleşmesi:** `''`, `#rrggbb`, `#rrggbbaa` (yalnız `showOpacity`) ya da `var(--theme-color-*)` (yalnız `themeVars`). `rgb()/hsl()/transparent` girdileri hex’e çevrilir; tanınmayan değerler korunur. Hex matematiği yapan tema kodu `var()` beklememelidir. Saf yardımcılar da dışa aktarılır: `parseColor`, `formatHex`, `normalizeColorValue`, `classifyColorValue`, `contrastRatio`, `wcagLevel`, `themeColorVar`, `parseThemeColorVar`, `THEME_COLOR_KEYS`, `toThemeCssKey`.

## CodeEditorField — kod

Monaco tabanlı kod editörü (ilk kullanımda tembel yüklenir).

```tsx
fields: {
  customHtml: createCodeEditorField({ label: "Özel kod", defaultLanguage: "html" }),
  json: createCodeEditorField({ label: "Yapılandırma", defaultLanguage: "json", height: "240px" }),
}
```

Seçenekler: `defaultLanguage`, `height`, `theme`.

## RepeaterField — tekrar eden satırlar

Alt alan setini (`subFields`) satırlar hâlinde düzenler: ekle, sil, çoğalt, sürükleyerek sırala, satırı katla.

```tsx
fields: {
  items: createRepeaterField({
    label: "Öğeler",
    subFields: {
      title: createLanguageField({ label: "Başlık" }),
      link: createLinkField({ label: "Bağlantı" }),
    },
    minItems: 1,
    maxItems: 6,
    defaultRow: { title: "", link: null },
  }),
}
```

**Değer:** `Record<string, unknown>[]`. Bir slot’un `repeatSource`’u olarak kullanılabilir ([Repeat zone](/editor-slots#repeat-zone-oge-sablonu)).

## IconField — ikon

`lucide-react` setinden aranabilir ikon seçici (arama yoksa ilk 120 ikon). **Değer** ikonun adıdır (`"ShoppingCart"`); bileşeni render’da siz çözersiniz:

```tsx
import { icons } from "lucide-react";

const Icon = icons[props.icon as keyof typeof icons];
return Icon ? <Icon className="size-5" /> : null;
```

## ExternalField — harici tek kayıt

Kendi API’nizden aranabilir bir pencerede **tek kayıt** seçtirir.

```tsx
fields: {
  product: createExternalField({
    label: "Ürün",
    fetchList: async ({ query }) => (await fetch(`/api/products?q=${query ?? ""}`)).json(),
    mapProp: (row) => ({ id: row.id, name: row.name }),
    mapRow: (row) => ({ Ad: row.name, SKU: row.sku }),
    getItemSummary: (value) => value?.name ?? "",
    placeholder: "Ürün seçin",
  }),
}
```

| Seçenek | Açıklama |
| :--- | :--- |
| `fetchList({ query?, filters? })` | Zorunlu. Satırları getirir |
| `mapProp(row)` | Prop’a yazılacak değer (varsayılan satırın kendisi) |
| `mapRow(row)` | Pencere tablosunun sütunları |
| `getItemSummary(value)` | Seçili değerin etiketi |
| `showSearch` | Arama kutusu (varsayılan `true`) |

## ApiListField — API’den liste

Kendi API’nizden **liste** akıtır; bir slot’un `repeatSource`’una bağlanınca öğe şablonu her satır için tekrarlanır.

```tsx
fields: {
  source: createApiListField({
    label: "Ürünler",
    fetchList: async ({ query, limit }) => (await fetch(`/api/products?q=${query ?? ""}&limit=${limit ?? 8}`)).json(),
    itemSchema: [{ key: "name", label: "Ad", type: "text" }, { key: "image", label: "Görsel", type: "image" }],
    defaultLimit: 8,
  }),
  card: { type: "slot", orientation: "horizontal", repeatSource: "source" },
}
```

**Değer:** `{ query?: string; limit?: number }` — dokümana yalnız sorgu parametreleri yazılır; satırlar render anında çözülür (oturum içi önbellek, eşzamanlı istek tekleme). Seçenekler: `showQuery`, `showLimit` (varsayılan `true`), `defaultLimit`.

## E-ticaret seçicileri

Mağazanın kendi kayıtlarından seçim yaptırır; slug ya da ObjectId elle yazdırmaz.

```tsx
import {
  createCategoryField, createCategoryListField, createProductField, createProductListField,
  createBrandField, createBrandListField, createTagField, createTagListField,
  createAttributeField, createAttributeListField, createVariantTypeField, createVariantField,
  createFlashSaleField, createCampaignField, createDiscountField,
} from "@tecof/theme-editor";

fields: {
  category: createCategoryField({ label: "Kategori" }),
  featured: createProductField({ label: "Öne çıkan ürün" }),
  tags: createTagListField({ label: "Etiketler", max: 6 }),
  coupon: createDiscountField({ label: "Kupon" }),
}
```

Saklanan değer seçim anında donan bir **anlık görüntüdür**; yayın ek istek atmadan render edebilir:

| Alan | Değer |
| :--- | :--- |
| `createCategoryField` | `{ id, slug, name, path }` |
| `createProductField` | `{ id, slug, name, image }` |
| `createBrandField` | `{ id, slug, name, image }` |
| `createTagField` | `{ id, slug, name }` |
| `createAttributeField` | `{ id, slug, name, type, isFilterable, options: [{ value, label, colorCode }] }` |
| `createVariantTypeField` | `{ id, name, selectionType, values: [{ label, colorCode }] }` |
| `createVariantField` | `{ productId, productSlug, productName, variantId, sku, title, price }` |
| `createFlashSaleField` | `{ id, name, startDate, endDate, showCountdown, bannerText, highlightColor, discountType, discountValue }` |
| `createCampaignField` | `{ id, name, status, startDate, endDate, discountCode }` |
| `createDiscountField` | `{ id, code, title, type, value, minCartAmount, maxDiscountAmount, startDate, endDate }` |

- `*ListField` varyantlarında değer bu nesnelerin dizisidir; editördeki sıra korunur. Seçenekler: `label`, `placeholder`, `visible`, listelerde `max`.
- **Neden hem `id` hem `slug`:** ürün listeleme ucu markayı ve etiketi **ObjectId**, kategoriyi **slug** ile filtreler. Temada filtreyi kimlikle gönderin: `getProducts({ brand: props.brand?.id, tag: props.tag?.id })`.
- **Veri kaynağı:** kategori/ürün/marka vitrin uçlarından (`/api/store/ecommerce/*`); etiket, özellik, varyant tipi, flaş satış, kampanya ve kupon panel uçlarından (`/api/merchant/ecommerce/*`) gelir. Panel uçları JWT’ye ek olarak `x-merchant-id` ister; `TecofApiClient` bunu `merchant-info`’dan bir kez çözer. Kimlik çözülemezse liste boş değil **hata** gösterir.
- **Anlık görüntü donar:** markanın adı ya da kampanyanın tarihi panelde sonradan değişirse sayfa eski değeri gösterir; alanı yeniden seçin.
- **Boş durum:** liste boşsa “Panelde ürün ekle” gibi bir bağlantı, doluysa başlıkta “Panelde yönet” görünür; adresler `panelUrl` üzerine kurulur ve yeni sekmede açılır. Panelde kayıt ekledikten sonra seçicideki **Yenile** listeyi tazeler.
- **Geriye uyum:** var olan bir metin alanının yerine koyarken eski prop’u okumaya devam edin: `props.brand?.id ?? props.brandIdText ?? ""`.

## CmsCollectionField — koleksiyon bağlama

Bir bileşeni CMS koleksiyonuna bağlar: koleksiyon, adet ve sıralama seçilir; bileşenin veri yuvaları koleksiyon alanlarına eşlenir.

```tsx
fields: {
  source: createCmsCollectionField({
    label: "Veri kaynağı",
    defaultLimit: 6,
    slots: {
      title: { label: "Başlık", fieldTypes: ["text"] },
      image: { label: "Görsel", fieldTypes: ["image"] },
      link: { label: "Bağlantı" },
    },
  }),
}
```

| Seçenek | Varsayılan | Açıklama |
| :--- | :--- | :--- |
| `defaultLimit` | `10` | Varsayılan öğe sayısı |
| `showLimit`, `showSort` | `true` | Adet ve sıralama (özel / yeni → eski / eski → yeni) kontrolleri |
| `slots` | — | Bileşenin ihtiyaç duyduğu veri yuvaları |

**Değer:** `{ collectionSlug, collectionName?, limit?, sort?: "newest" | "oldest" | "custom", fieldMap?: Record<yuva, cmsAlanKısaKodu> }`. Bir repeat slot’a kaynak olarak bağlanırsa satırlar koleksiyondan otomatik çekilir (ağaçta `TecofProvider` gerekir).

## CMS veri bağlama (metin alanları)

`text` ve `textarea` alanlarının yanında bir **`{ }`** düğmesi görünür; koleksiyon → alan seçilince alana `{{ data.<kısaKod> }}` token’ı eklenir. Yayında token, `TecofRender`’a verilen `cmsData` kaydıyla çözülür. Kapatmak için `bindable: false`:

```tsx
fields: {
  title: { type: "text", label: "Başlık" },                         // bağlama açık
  staticLabel: { type: "text", label: "Sabit etiket", bindable: false },
}
```

Repeat zone şablonunun içindeyken aynı düğme **Öğe alanları** bölümünü de gösterir (`{{ item.<alan> }}`).

## Hata yalıtımı

Fabrikaların ürettiği alanlar `FieldErrorBoundary` içinde render edilir; bir alanın hatası Inspector’ı düşürmez. Kendi `render` alanlarınızda da aynı bileşeni kullanabilirsiniz.
