Bir arayüz kütüphanesini projeye eklemek kolaydır; onu tasarım sistemine uydurmak zordur. Hazır bir kütüphane düğmeyi, kartı ve modalı kendi stiliyle gelir. Ama ekibinizin renklerini, boşluklarını ve erişilebilirlik kurallarını taşımıyorsa, her yeni bileşen biraz farklı davranır ve sonunda kimse hangisinin standart olduğunu bilmez.

Blade bileşenleri bu ikinci işi görünmez biçimde yapar. Bir bileşen, hangi parametreleri aldığını, hangi görünümlerde kullanılabildiğini ve içinde neyin sabit kalacağını yazılı olarak taşır. Bu metinde Laravel Blade bileşenleriyle, Alpine.js ile küçük bir etkileşim katmanı birleştirerek Pines UI'a yakın bir estetik kurmanın yolunu anlatacağım.

Bileşen sözleşmesi

Bir Blade bileşeni, bir fonksiyona benzer. Dışarıdan bazı girdiler alır, içinde bir görünüm üretir. Girdilerin adı, tipi ve varsayılan değeri bir sözleşme oluşturur. Bu sözleşme yazıldığında, ekipte kullanım da kendiliğinden standartlaşır.

Laravel'de bir bileşenin girdileri @props direktifiyle tanımlanır. Tanımlanmayan her öznitelik ise $attributes değişkenine düşer ve kök öğeye aktarılır.

{{-- resources/views/components/ui/button.blade.php --}}
@props([
    'variant' => 'primary',
    'size' => 'md',
    'type' => 'button',
])

@php
    $classes = match ($variant) {
        'primary' => 'bg-brand-600 text-white hover:bg-brand-700',
        'outline' => 'border border-zinc-200 bg-white text-zinc-800 hover:border-zinc-300',
        'ghost' => 'text-zinc-600 hover:bg-zinc-100',
        default => 'bg-zinc-900 text-white',
    };
@endphp

<button type="{{ $type }}" {{ $attributes->merge(['class' => "{$classes} rounded-xl px-4 py-2 text-sm font-medium"]) }}>
    {{ $slot }}
</button>

Bu bileşenin kullanımı, bir HTML öğesi kadar okunaklıdır: <x-ui.button variant="outline">Yorum yaz</x-ui.button>. Stilin ne olduğunu bilmek için bileşenin içine bakmak yeterli; sayfadaki her düğme aynı kuralları taşır.

Bileşen, ekibin ortak dilidir; herkes aynı kelimeyi aynı anlamda kullanıyorsa hız artar.

Varyantlar: sabit bir liste

Bir bileşenin varyantları sabit bir listeden gelmelidir. Serbest bir sınıf dizisi kabul eden bir düğme, bir süre sonra on farklı mavi ve yedi farklı yuvarlaklıkla dolar. Bunun yerine tanımlı varyantlar seçilir ve her biri bir kez tanımlanır.

Yukarıdaki match ifadesi bu liste işini yapar. Ekip yeni bir varyant istediğinde, önce bu listeye bakılır, sonra eklenir. Bu, her yeni sayfada aynı düğmenin yeniden tasarlanmasını engeller.

VaryantKullanımKural
primarySayfadaki tek ana eylemBir ekranda en fazla bir tane
outlineİkincil eylemYanına primary konabilir
ghostAraç çubuğu, sekme, kapatmaDekor değil, eylem olmalı

Slot ve adlandırılmış slot

Slotlar, bileşenin içine dışarıdan içerik koymanızı sağlar. Varsayılan slot, düğmenin metnini taşır. Adlandırılmış slotlar ise bir kartın başlığını, simgesini ya da alt bilgisini ayırmak için kullanılır.

<x-ui.card>
    <x-slot:header>
        Son yazılar
    </x-slot:header>

    Bu hafta okunan yazılar burada listelenir.

    <x-slot:footer>
        <x-ui.button variant="ghost" size="sm">Tümünü gör</x-ui.button>
    </x-slot:footer>
</x-ui.card>

Slot kullanırken dikkat edilmesi gereken şey, bileşenin kendi yapısını bozmamaktır. Kartın başlık alanı her zaman aynı boşluk ve yazı boyutuna sahip olmalı; slotun içine bırakılan içerik bu kuralları yeniden tanımlamamalı. Bu yüzden başlık ve alt bilgi için yalnızca bileşen içinde sabitlenmiş sınıflar kullanılır.

Öznitelik birleştirme ve geçersiz kılma

$attributes->merge(), bileşenin varsayılan sınıflarını dışarıdan gelen sınıflarla birleştirir. Bu, bir düğmeye class="w-full" eklediğinizde varsayılan stilin kaybolmadığı anlamına gelir. Yalnızca eklenen sınıf birikir.

Ancak bu birleştirmenin bir sınırı var: çakışan sınıflar aynı anda bulunabilir ve hangisinin kazanacağı CSS'in sırasına bağlıdır. Bu nedenle bileşen içinde yalnızca temel yapıyı tanımlayın; renk ve boyut gibi karar değerleri varyant ve boyut parametreleriyle yönetin.

  • merge ile eklenen sınıfların bileşenin temel düzenini bozmadığını kontrol edin.
  • Dışarıdan bg- ya da text- ile renk değiştirme ihtiyacı doğuyorsa, bir varyant eksik demektir.
  • Dışarıdan gelen aria- öznitelikleri öğeye olduğu gibi geçmelidir; bunu bileşen yutmamalıdır.

Üst bileşenden miras alan değerler

Bazen bir grup bileşen aynı boyutu ya da varyantı paylaşmalıdır. Her birine tek tek vermek yerine, üst bileşen bu değeri bir kez tanımlar ve alt bileşenler @aware ile onu alır.

{{-- resources/views/components/ui/button-group.blade.php --}}
@props(['size' => 'md'])

<div class="inline-flex gap-2" role="group">
    {{ $slot }}
</div>

{{-- resources/views/components/ui/group-button.blade.php --}}
@aware(['size'])
@props(['size' => 'md'])
...

Bu desen güçlüdür, ama görünmezdir. Bir bileşen değerini nereden aldığını bilmeden değiştirmek zorlaşır. Bu yüzden miras alan değerleri belgelemek ve kullanımı yalnızca bir bileşen ailesiyle sınırlamak gerekir.

Koyu tema disiplini

Her sınıfın dark: karşılığını yazmak zahmetli görünür. Ama sonradan eklenen koyu tema, çoğu zaman görsel hataların kaynağıdır. Bir bileşen kendi koyu temasını baştan taşıdığında, sayfa her kullanımda aynı kontrastı verir.

Koyu tema için en büyük tuzak, yalnızca arka plan rengine bakmaktır. Kenarlıklar, odak halkaları, devre dışı durumdaki metinler ve simge renkleri de ayrıca eşlenmelidir. Bu ayrıntılar bir bileşenin içinde toplanırsa, her yeni kullanım bunları kendiliğinden miras alır.

Alpine ile küçük etkileşimler

Blade bileşenleri sunucuda çizilir; ama bir açılır menü ya da sekme gibi anlık etkileşimler için tarayıcıda küçük bir durum gerekir. Bu noktada Alpine.js bileşenin içine yerleşir. Bileşen, kendi durumunu x-data ile tanımlar ve dışarıdan yalnızca olaylar aracılığıyla konuşur.

{{-- resources/views/components/ui/dropdown.blade.php --}}
<div x-data="{ acik: false }" @click.outside="acik = false" class="relative">
    <button type="button" @click="acik = !acik" :aria-expanded="acik">
        {{ $trigger }}
    </button>

    <div x-show="acik" x-transition class="absolute mt-2">
        {{ $slot }}
    </div>
</div>

Bu bileşenin dışarıdan kullanımı yalnızca iki slot gerektirir. İçinde ne olduğu, hangi durumun nasıl yönetildiği bileşenin dosyasında kalır. Bu ayrım, sayfa şablonlarını sade tutar ve davranışı tek bir yerde toplar.

Belgelemek: özellik tablosu

Bir bileşen kütüphanesinin en çok ihmal edilen parçası belgedir. Bir bileşenin hangi parametreleri aldığını ve hangi değerlerin kabul edildiğini yazmak, kullanıcıların tahmin yapmasını önler. Basit bir tablo çoğu zaman yeterlidir.

ParametreTipVarsayılanAçıklama
variantprimary, outline, ghostprimaryGörsel ağırlık
sizesm, md, lgmdYükseklik ve yazı boyutu
typebutton, submit, resetbuttonForm içinde yanlışlıkla gönderimi engeller

Tablonun her satırı, bileşenin @props bloğuyla eşleşmelidir. Bu eşleşmeyi bir test ya da bir inceleme kontrolü ile korumak, belgenin zamanla eskimesini önler.

Yaygın hatalar

İlk hata, bileşeni çok erken genelleştirmektir. Her yeni kullanım için bir parametre eklemek, bileşeni birkaç sayfa boyunca faydalı kılmaz; tersine, her biri kendi koşullu mantığını taşıyan bir yapıya dönüşür. Bir bileşen, en az iki gerçek kullanım görene kadar sade kalmalıdır.

İkinci hata, bileşenin içinde iş mantığı yazmaktır. Bir düğme yalnızca görünümü taşımalı; ne yapacağına dair kararlar çağıran sayfada ya da bir denetleyicide kalmalıdır.

Üçüncü hata, erişilebilirliği dışarıya bırakmaktır. Bir ikon düğmesinin aria-label değeri, o düğmeyi kullanan herkes tarafından hatırlanmaz; bu değer bileşenin içinde, sabit olarak yazılmalıdır.

Form girdisi bileşeni ve hata gösterimi

Formlar, bir tasarım sisteminin en çok tekrar eden parçalarıdır. Her girdinin etiketi, yardım metni, hata mesajı ve eski değeri vardır. Bunları her sayfada elle yazmak hem uzun hem de hataya açıktır. Bir girdi bileşeni bu dört parçayı tek bir yerde toplar ve doğru erişilebilirlik bağlantılarını kendiliğinden kurar.

{{-- resources/views/components/ui/input.blade.php --}}
@props([
    'name',
    'label',
    'type' => 'text',
    'value' => null,
    'hint' => null,
])

@php
    $hasError = $errors->has($name);
    $describedBy = collect([$hint ? "{$name}-ipucu" : null, $hasError ? "{$name}-hata" : null])
        ->filter()
        ->implode(' ');
@endphp

<div class="space-y-1.5">
    <label for="{{ $name }}" class="text-sm font-medium text-zinc-800 dark:text-zinc-200">
        {{ $label }}
    </label>

    <input
        id="{{ $name }}"
        name="{{ $name }}"
        type="{{ $type }}"
        value="{{ old($name, $value) }}"
        aria-invalid="{{ $hasError ? 'true' : 'false' }}"
        @if ($describedBy !== '') aria-describedby="{{ $describedBy }}" @endif
        {{ $attributes->class(['w-full rounded-xl border px-3.5 py-2.5 text-sm', 'border-rose-500' => $hasError, 'border-zinc-200' => ! $hasError]) }}
    >

    @if ($hint)
        <p id="{{ $name }}-ipucu" class="text-xs text-zinc-600 dark:text-zinc-400">{{ $hint }}</p>
    @endif

    @error($name)
        <p id="{{ $name }}-hata" class="text-xs text-rose-700 dark:text-rose-400">{{ $message }}</p>
    @enderror
</div>

Bu bileşenin kullanımı tek satırdır: <x-ui.input name="eposta" label="E-posta" type="email" hint="Bültenimizi bu adrese göndereceğiz." />. Bileşen, hata durumunu, eski değeri ve ilişkili metinleri kendisi halleder. Sayfayı kullanan kişi bunları düşünmek zorunda kalmaz.

Bu bileşenin en önemli kuralı, aria-invalid ve aria-describedby bağlantılarının bileşenin içinde kalmasıdır. Kullanıcı bu bağlantıları elle eklemeye başlarsa, bir gün biri bunu unutur ve hata mesajı ekran okuyucuya ulaşmaz.

İkon ve slot ile esneklik

Bir düğmede ikon ve metin birlikte kullanıldığında, ikonun nasıl görüneceği bileşenin sorumluluğundadır. İkonun boyutu, rengi ve metinle arasındaki boşluk sabitlenmelidir. Dışarıdan gelen ikon ise bir adlandırılmış slot ile yerleşir.

<x-ui.button variant="primary">
    <x-slot:icon>
        <svg class="h-4 w-4" aria-hidden="true" viewBox="0 0 24 24">…</svg>
    </x-slot:icon>
    Yorumu gönder
</x-ui.button>

Dekoratif ikonlara aria-hidden="true" eklemek, ekran okuyucunun simgeyi bir kez daha okumasını engeller. Düğmenin anlamı zaten metindedir. Yalnızca metin olmayan bir ikon düğmesi kullanılacaksa, düğmeye bir aria-label verilmelidir ve bu etiket de bileşenin bir parametresi olarak zorunlu tutulmalıdır.

Bileşenleri test etmek

Blade bileşenleri, bir sayfa testinin içinde çok az kod ile test edilebilir. Laravel'in blade test yardımcısı, bileşeni bir metin gibi render eder ve çıktısında beklediğiniz öznitelikleri arar. Bu sayede bir bileşenin varyantı değiştiğinde, testler bunu hemen fark eder.

public function test_outline_button_renders_its_variant_classes(): void
{
    $this->blade('<x-ui.button variant="outline">Kaydet</x-ui.button>')
        ->assertSee('border-zinc-200', false)
        ->assertSee('Kaydet');
}

Bu test, bileşenin kendi sözleşmesini doğrular. Sayfa testlerinin yerini almaz; ama bileşen değiştiğinde, onu kullanan onlarca sayfanın tek tek incelenmesini gereksiz kılar.

Bileşen ailesini sürümlemek

Bir bileşenin parametrelerini değiştirmek, onu kullanan tüm sayfaları etkiler. Bir parametrenin adını değiştirmek ya da bir varyantı kaldırmak, sessiz hatalara yol açabilir. Bu yüzden değişiklikler, eski ve yeni değerleri bir süre birlikte destekleyerek yapılmalıdır.

  • Eski bir varyant kaldırılacaksa, önce ona yeni bir ad eşleyin ve eski adı kullanan yerlerde uyarı üretin.
  • Bir parametre yeniden adlandırılıyorsa, geçiş döneminde iki adı da kabul edin.
  • Değişikliği, belgeleme tablosunu güncelleyerek ve kısa bir not düşerek duyurun.

Bileşen mi, şablon mu?

Her tekrar eden parça bir bileşen olmak zorunda değildir. Yalnızca bir sayfada kullanılan bir bölüm, bir şablon dosyası olarak kalabilir. Bir parçayı bileşene çevirmek için iki kullanım yeterlidir; üçüncüsü ise onun gerçekten bir sözleşme olduğunu gösterir.

Bir bileşen, dışarıdan aldığı değerlerle davranışını değiştiriyorsa, ama içinde iş mantığı yoksa ideal bir adaydır. İş mantığı içeren parçalar ise denetleyicide ya da bir sınıfta daha doğru yerde durur. Bu ayrım, bileşen kütüphanesinin temiz kalmasını sağlar.

Sonuç

Blade bileşenleri, bir tasarım sistemini kod olarak yaşatmanın en sade yoludur. Sözleşme, varyant listesi, slot kuralları ve belge birlikte kullanıldığında, yeni bir sayfa eski bileşenleri yeniden kullanır ve tutarlılık kendiliğinden korunur.

Başlamak için birkaç adım:

  1. En sık kullanılan üç öğeyi seçin: düğme, kart ve girdi.
  2. Her biri için sabit bir varyant listesi ve @props sözleşmesi yazın.
  3. Koyu temayı ve odak durumunu bileşenin içinde tamamlayın.
  4. Bir özellik tablosu ekleyin ve yeni bileşenleri aynı biçimde belgelendirin.

Bir bileşen kütüphanesi bir kez kurulur; ama her sayfada yeniden kullanılır. Bu nedenle onun dili, ekibinizin dilinden biraz daha özenli olmalı.