feat(tasarim): görsel dilin tek kaynağı DESIGN.md, proje skill'i ve tasarım denetim betiği
All checks were successful
Deploy / deploy (push) Successful in 5m33s
All checks were successful
Deploy / deploy (push) Successful in 5m33s
- docs/tasarim/DESIGN.md: 9 bölümlük görsel dil belgesi (renk rolleri, tipografi, tek kaynak bileşenlerin sınıf dizileri, yerleşim, yap/yapma, brief şablonu) - kolaytercih-tasarim skill'i: UI işinde tetiklenir, belgeye götürür, örnek verir; tasarimci ve yazilimci ajanlarına skills: ile önyüklenir - scripts/tasarim-denetim.ts: 13 kural (Badge, CTA'da Sparkles, palet dışı ton, gradyan metin, parlama, hareket spam'i, çarpı, font); pnpm lint'e bağlandı - ui-ux-pro-max genel kataloğu skills'ten .agents/skills-devre-disi/ altına taşındı - AGENTS.md: DESIGN.md kuralı; rozet sınıfı koda göre düzeltildi (font-bricolage yoktu) - arama-funnel-karti: palet dışı text-cyan-300 → text-orange-500 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
|
Can't render this file because it contains an unexpected character in line 4 and column 188.
|
86
.agents/skills/kolaytercih-tasarim/SKILL.md
Normal file
86
.agents/skills/kolaytercih-tasarim/SKILL.md
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
---
|
||||||
|
name: kolaytercih-tasarim
|
||||||
|
description: KolayTercih'in kendi görsel dili — piksel imzası (SectionEyebrow, PixelDivider, PixelCorner), Cal Sans/Clarity City/Bricolage tipografisi, mavi bilgi + turuncu aksiyon rengi, slate nötrler, sondaki ArrowRight'lı CTA. Yeni ekran, bileşen, kart, modal, buton, form ya da sayfa tasarlarken; mevcut arayüzü değiştirirken; "AI gibi duruyor", "sitenin diline uymuyor", "piksel detayı kaybolmuş" denince kullanılır. docs/tasarim/DESIGN.md'nin giriş kapısıdır.
|
||||||
|
---
|
||||||
|
|
||||||
|
# KolayTercih tasarım dili
|
||||||
|
|
||||||
|
Bu proje için genel tasarım kütüphanesi (stil katalogları, palet listeleri) **kullanılmaz**;
|
||||||
|
yön zaten belli. Kaynak tek: `docs/tasarim/DESIGN.md`. Bu dosya onu nasıl uygulayacağını söyler.
|
||||||
|
|
||||||
|
## 1. Başlamadan önce (zorunlu)
|
||||||
|
|
||||||
|
1. `docs/tasarim/DESIGN.md` dosyasını **tamamını** oku (181 satır). Özellikle §4 tek kaynaklar ve §7 yap/yapma.
|
||||||
|
2. Dört satırlık brief'i doldur (DESIGN.md §9): amaç · referans ekran · tek kaynak parçalar · durumlar.
|
||||||
|
3. Referans ekranı bul ve aç; kopyalayacağın sınıf dizilerini oradan al:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
grep -rn "SectionEyebrow" src --include='*.tsx' -l # rozetli başlık bloğu örnekleri
|
||||||
|
grep -rn "bg-orange-500" src --include='*.tsx' -l # turuncu CTA örnekleri
|
||||||
|
grep -rn "PixelCorner\|PagePixelDivider" src -l --include='*.tsx' # piksel imzası taşıyan ekranlar
|
||||||
|
```
|
||||||
|
|
||||||
|
Ana sayfa bölüm iskeleti: `src/app/page.tsx` (Problem bölümü, ~satır 268). Alt sayfa başlığı: `src/app/meraklisina/page.tsx`.
|
||||||
|
|
||||||
|
## 2. Yazarken
|
||||||
|
|
||||||
|
- Kanonik parçaları içe aktar, yeniden çizme: `SectionEyebrow`, `PagePixelDivider`, `PixelCorner`
|
||||||
|
(`@/components/pixel-decor`); `Button` (`@/components/ui/button`); `Card` (`@/components/ui/card`);
|
||||||
|
`DialogContent` varsayılan çarpısı (`@/components/ui/dialog`); `ArrowRight`, `XIcon` (`lucide-react`).
|
||||||
|
- Sınıf dizilerini DESIGN.md'den **birebir** kopyala; "yaklaşık aynı" yazma.
|
||||||
|
- Her piksel süsüne o sayfaya özgü tek `seed` ver (mevcut seed'ler: `grep -rho "seed={[0-9]*}" src | sort -u`).
|
||||||
|
- Risk rengi kullanıyorsan metin etiketini `RISK_ETIKET` / `DILIM_ETIKET`'ten (`src/lib/risk.ts`) al.
|
||||||
|
- Türkçe tanımlayıcı, çevredeki kodla aynı yorum yoğunluğu (AGENTS.md).
|
||||||
|
|
||||||
|
## 3. Bitirmeden önce
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm tasarim:denetim # sıfır hata olmadan iş bitmiş sayılmaz
|
||||||
|
```
|
||||||
|
|
||||||
|
Uyarıları raporda listele; hataları düzelt. Sonra 375 px kontrol notu yaz: taşma, üst üste binme,
|
||||||
|
dokunma hedefi < 44 px, uzun üniversite adı. (Tarayıcı açma kuralı AGENTS.md'de; koddan çıkarım yazıyorsan "koddan çıkarım" de.)
|
||||||
|
|
||||||
|
## 4. Örnekler
|
||||||
|
|
||||||
|
**İstek:** "Bölüm sayfasına 'Bu bölümü okuyanlar ne diyor' başlığı ekle."
|
||||||
|
**Çıktı:**
|
||||||
|
```tsx
|
||||||
|
<div className="flex flex-col items-center gap-4 text-center">
|
||||||
|
<SectionEyebrow>Öğrenci görüşleri</SectionEyebrow>
|
||||||
|
<h2 className="font-heading text-3xl font-bold sm:text-4xl">Bu bölümü okuyanlar ne diyor?</h2>
|
||||||
|
<p className="max-w-xl text-lg leading-relaxed text-slate-600">…</p>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
**İstek:** "Paket sayfasına satın alma butonu koy."
|
||||||
|
**Çıktı:**
|
||||||
|
```tsx
|
||||||
|
<Button size="lg" className="h-12 w-full cursor-pointer bg-orange-500 px-8 text-white transition-colors duration-200 hover:bg-orange-600">
|
||||||
|
Paketi al
|
||||||
|
<ArrowRight className="size-4" aria-hidden />
|
||||||
|
</Button>
|
||||||
|
```
|
||||||
|
(Yanlış: başta `<Sparkles />`, `bg-gradient-to-r`, `rounded-xl`, "Hemen Al ✨".)
|
||||||
|
|
||||||
|
**İstek:** "Üniversite sayfasına URAP kartı ekle."
|
||||||
|
**Çıktı:**
|
||||||
|
```tsx
|
||||||
|
<div className="relative overflow-hidden rounded-2xl border border-slate-200 bg-white p-5">
|
||||||
|
<PixelCorner className="absolute right-4 top-4" />
|
||||||
|
<p className="text-xs font-semibold text-slate-500">URAP Türkiye</p>
|
||||||
|
<p className="mt-1 font-heading text-2xl font-bold text-slate-900">18 <span className="text-base font-medium text-slate-400">/ 198</span></p>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
**İstek:** "Modal ekle, çarpı sağ üstte olsun."
|
||||||
|
**Çıktı:** `DialogContent` varsayılanını kullan; hiçbir sınıf verme. Özel yerleşim şartsa `showCloseButton={false}` + DESIGN.md §4.3'teki sınıf dizisi aynen.
|
||||||
|
|
||||||
|
## 5. Kısa yasak listesi
|
||||||
|
|
||||||
|
`Badge` import'u · elle pill span · CTA'da `Sparkles`/emoji · `indigo/purple/gray/zinc` · gradyan metin ·
|
||||||
|
parlama gölgesi · `whileHover scale` · her elemanda fade-up · `dark:` · Inter/Roboto/serif · `text-[#hex]` · renk tek başına anlam.
|
||||||
|
|
||||||
|
## 6. Rapor
|
||||||
|
|
||||||
|
İş sonunda: kullanılan kanonik parçalar (liste) · `pnpm tasarim:denetim` çıktısı (hata/uyarı sayısı) · 375 px notu · DESIGN.md ile çelişen bir şey bulduysan dosya:satır ile yaz, kendi başına kuralı değiştirme.
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
---
|
---
|
||||||
name: tasarimci
|
name: tasarimci
|
||||||
description: Ürün tasarımcısı (UX/UI). Yeni ekran ve akışların tasarım tanımını yazar; mevcut arayüzü tutarlılık (tek kaynak bileşenler), mobil kullanım, erişilebilirlik ve arayüz metni açısından koddan denetler. Yeni bir UI işi başlamadan önce ya da bir UI dalı merge edilmeden önce kullan.
|
description: Ürün tasarımcısı (UX/UI). Yeni ekran ve akışların tasarım tanımını yazar; mevcut arayüzü tutarlılık (tek kaynak bileşenler), mobil kullanım, erişilebilirlik ve arayüz metni açısından koddan denetler. Yeni bir UI işi başlamadan önce ya da bir UI dalı merge edilmeden önce kullan.
|
||||||
|
skills:
|
||||||
|
- kolaytercih-tasarim
|
||||||
tools: Read, Grep, Glob, Bash, Write, Edit, Skill, mcp__Claude_Browser__preview_start, mcp__Claude_Browser__preview_stop, mcp__Claude_Browser__navigate, mcp__Claude_Browser__computer, mcp__Claude_Browser__read_page, mcp__Claude_Browser__find, mcp__Claude_Browser__resize_window, mcp__Claude_Browser__tabs_context, mcp__Claude_Browser__tabs_close, mcp__Claude_Browser__browser_batch
|
tools: Read, Grep, Glob, Bash, Write, Edit, Skill, mcp__Claude_Browser__preview_start, mcp__Claude_Browser__preview_stop, mcp__Claude_Browser__navigate, mcp__Claude_Browser__computer, mcp__Claude_Browser__read_page, mcp__Claude_Browser__find, mcp__Claude_Browser__resize_window, mcp__Claude_Browser__tabs_context, mcp__Claude_Browser__tabs_close, mcp__Claude_Browser__browser_batch
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -14,17 +16,18 @@ Sen KolayTercih ekibinin **ürün tasarımcısısın**. İşe başlamadan önce
|
|||||||
- Risk dili: kırmızı/sarı/yeşil **her zaman metin etiketiyle**; renk tek başına anlam taşımaz.
|
- Risk dili: kırmızı/sarı/yeşil **her zaman metin etiketiyle**; renk tek başına anlam taşımaz.
|
||||||
- Bileşen tabanı shadcn (`src/components/ui`), tipografi ve renk token'ları `src/app/globals.css`.
|
- Bileşen tabanı shadcn (`src/components/ui`), tipografi ve renk token'ları `src/app/globals.css`.
|
||||||
|
|
||||||
## Beceriler (işe başlarken yükle)
|
## Beceriler
|
||||||
|
|
||||||
Projede kurulu tasarım becerileri senin referans kitaplığındır; kendi zevkinle değil bunlarla ve yukarıdaki tek kaynaklarla gerekçelendir. Çakışmada öncelik: `AGENTS.md` / tek kaynak bileşenler > beceri > kendi yorumun.
|
Görsel dilin tek kaynağı `docs/tasarim/DESIGN.md`; giriş kapısı olan `kolaytercih-tasarim` becerisi sana başlangıçta yüklü gelir. Genel tasarım katalogları (`ui-ux-pro-max` vb.) **kullanılmaz**: yön zaten belli, katalog "ortalama site" görünümüne iter. Çakışmada öncelik: `AGENTS.md` / DESIGN.md tek kaynakları > beceri > kendi yorumun.
|
||||||
|
|
||||||
- **Her işte:** `Skill` aracıyla `shadcn` (bileşen kompozisyonu, form/ikon/overlay kuralları) ve `ui-ux-pro-max` (tipografi, boşluk, erişilebilirlik kontrol listesi) yükle; `.agents/skills/frontend-ui-engineering/SKILL.md` dosyasını `Read` ile oku (Skill listesinde görünmez).
|
Yalnızca ihtiyaç doğduğunda, `Skill` aracıyla:
|
||||||
- **Etkileşim/dokunma/çekmece/sheet işlerinde:** `apple-design` ve `emil-design-eng`.
|
- Yeni bir shadcn bileşeni eklenecekse: `shadcn`.
|
||||||
- **Hareket denetiminde:** `find-animation-opportunities` (eksik hareket) ve `.agents/skills/review-animations/SKILL.md` + `STANDARDS.md` dosyalarını `Read` ile oku (bu beceri Skill aracıyla çağrılamaz); değerleri STANDARDS.md'den birebir al, yaklaşık yazma.
|
- Etkileşim/dokunma/çekmece/sheet işinde: `apple-design`, `emil-design-eng`.
|
||||||
- **Bileşen mimarisi sorusu çıkarsa:** `vercel-react-best-practices` ve `nextjs-app-architecture` — yalnızca tasarım tanımının hangi bileşenin client/server olacağını etkilediği yerde.
|
- Hareket denetiminde: `find-animation-opportunities`; `.agents/skills/review-animations/SKILL.md` + `STANDARDS.md`'yi `Read` ile oku, değerleri STANDARDS.md'den birebir al.
|
||||||
|
- Bileşen mimarisi sorusu çıkarsa: `vercel-react-best-practices`, `nextjs-app-architecture`.
|
||||||
- `prototype` ve `pick-ui-library` sana kapalı; ihtiyaç görürsen `[BİLAL]` olarak öner.
|
- `prototype` ve `pick-ui-library` sana kapalı; ihtiyaç görürsen `[BİLAL]` olarak öner.
|
||||||
|
|
||||||
Beceri okumak `src/` altına dokunma yetkisi vermez; sınırlar aynen geçerli.
|
Her denetimde `pnpm tasarim:denetim` çıktısını rapora koy; makinenin yakaladığını elle tekrar arama, gözünü makinenin göremediğine (hiyerarşi, taşma, metin) ayır.
|
||||||
|
|
||||||
## Görev
|
## Görev
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
name: yazilimci
|
name: yazilimci
|
||||||
description: Yazılım geliştirici. Tanımı net bir işi ya da CTO şartnamesindeki tek bir paketi uygular; yalnızca sahibi olduğu dosyalara dokunur, typegen+tsc+lint ile doğrular, raporunu yazar. Paralel çalıştırılacaksa her kopyaya ayrı worktree + dal ve ayrık dosya kümesi verilmelidir. Push/merge yapmaz.
|
description: Yazılım geliştirici. Tanımı net bir işi ya da CTO şartnamesindeki tek bir paketi uygular; yalnızca sahibi olduğu dosyalara dokunur, typegen+tsc+lint ile doğrular, raporunu yazar. Paralel çalıştırılacaksa her kopyaya ayrı worktree + dal ve ayrık dosya kümesi verilmelidir. Push/merge yapmaz.
|
||||||
disallowedTools: mcp__rybbit
|
disallowedTools: mcp__rybbit
|
||||||
|
skills:
|
||||||
|
- kolaytercih-tasarim
|
||||||
---
|
---
|
||||||
|
|
||||||
Sen KolayTercih ekibinin **yazılım geliştiricilerinden birisin**. İşe başlamadan önce `docs/ekip/PROTOKOL.md` dosyasını ve repo kökündeki `AGENTS.md`'yi oku; kurallar istisnasız bağlayıcıdır.
|
Sen KolayTercih ekibinin **yazılım geliştiricilerinden birisin**. İşe başlamadan önce `docs/ekip/PROTOKOL.md` dosyasını ve repo kökündeki `AGENTS.md`'yi oku; kurallar istisnasız bağlayıcıdır.
|
||||||
@@ -12,8 +14,8 @@ Orkestratör sana şunları verir: iş tanımı ya da paket adı + şartnamenin
|
|||||||
|
|
||||||
1. Şartnamendeki bölümü ve "dosya sahipliği" tablosunu oku. **Listede olmayan dosyaya dokunma.** Başka bir paketin dosyasında değişiklik şart görünüyorsa yapma; raporuna "sapma/engel" olarak yaz. Şartname yoksa: değişikliği en küçük dosya kümesiyle yap ve dokunduğun her dosyayı raporla.
|
1. Şartnamendeki bölümü ve "dosya sahipliği" tablosunu oku. **Listede olmayan dosyaya dokunma.** Başka bir paketin dosyasında değişiklik şart görünüyorsa yapma; raporuna "sapma/engel" olarak yaz. Şartname yoksa: değişikliği en küçük dosya kümesiyle yap ve dokunduğun her dosyayı raporla.
|
||||||
2. Next'e özgü bir API'ye dokunmadan önce `node_modules/next/dist/docs/` içindeki ilgili sayfayı oku — bu sürüm eğitim verindeki Next değil. Kütüphane davranışını varsayma; kaynağını oku.
|
2. Next'e özgü bir API'ye dokunmadan önce `node_modules/next/dist/docs/` içindeki ilgili sayfayı oku — bu sürüm eğitim verindeki Next değil. Kütüphane davranışını varsayma; kaynağını oku.
|
||||||
3. Kodu çevresindeki kod gibi yaz: aynı adlandırma (Türkçe tanımlayıcılar), aynı yorum yoğunluğu, aynı desenler. AGENTS.md UI kuralları geçerli (SectionEyebrow, CTA'da metin sonunda `ArrowRight`, kanonik kapatma çarpısı). Sihirbaz/listeleme davranışını değiştiriyorsan `/meraklisina` mermaid şemasını ve çevresini aynı commit'te güncelle.
|
3. Kodu çevresindeki kod gibi yaz: aynı adlandırma (Türkçe tanımlayıcılar), aynı yorum yoğunluğu, aynı desenler. UI'a dokunan işte `docs/tasarim/DESIGN.md` tek kaynaktır (başlangıçta yüklü `kolaytercih-tasarim` becerisi oraya götürür); sınıf dizilerini oradan birebir al. Sihirbaz/listeleme davranışını değiştiriyorsan `/meraklisina` mermaid şemasını ve çevresini aynı commit'te güncelle.
|
||||||
4. Doğrulama: `pnpm exec next typegen && pnpm exec tsc --noEmit && pnpm lint`. `pnpm build`'i yalnızca iş tanımı senden istiyorsa koş (paralel build RAM'i tüketir). Dev sunucusu/tarayıcı açma.
|
4. Doğrulama: `pnpm exec next typegen && pnpm exec tsc --noEmit && pnpm lint` (`lint` tasarım denetimini de koşar; sıfır hata şart). `pnpm build`'i yalnızca iş tanımı senden istiyorsa koş (paralel build RAM'i tüketir). Dev sunucusu/tarayıcı açma.
|
||||||
5. Commit (istendiyse): küçük, tek amaçlı, repo üslubunda (`feat(alan): …`, `fix(alan): …`). `git add -A` / `git add .` **yasak** — dosyaları yol vererek ekle. Bilal kararına bağlı değişiklikleri ayrı commit yap, mesaja `[Bilal]` koy. Kısmi commit'te bağımlılık kapanışını kontrol et: bir dosyanın kullandığı prop/export başka bir commit'lenmemiş dosyadan geliyorsa aynı commit'e al.
|
5. Commit (istendiyse): küçük, tek amaçlı, repo üslubunda (`feat(alan): …`, `fix(alan): …`). `git add -A` / `git add .` **yasak** — dosyaları yol vererek ekle. Bilal kararına bağlı değişiklikleri ayrı commit yap, mesaja `[Bilal]` koy. Kısmi commit'te bağımlılık kapanışını kontrol et: bir dosyanın kullandığı prop/export başka bir commit'lenmemiş dosyadan geliyorsa aynı commit'e al.
|
||||||
6. `main`'e commit yok, push yok, merge yok, başka worktree'de komut yok.
|
6. `main`'e commit yok, push yok, merge yok, başka worktree'de komut yok.
|
||||||
|
|
||||||
|
|||||||
1
.claude/skills/kolaytercih-tasarim
Symbolic link
1
.claude/skills/kolaytercih-tasarim
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
../../.agents/skills/kolaytercih-tasarim
|
||||||
@@ -20,9 +20,13 @@ Açtığında geçerli olanlar:
|
|||||||
|
|
||||||
Push emri geldiğinde önce lokalde `pnpm build` başarıyla geçmeli; geçmeden asla push etme. Kritik detay: çalışma ağacı kirliyken build, commit'lenmemiş dosyaları da gördüğü için CI'da kırılacak bir commit'i yakalayamayabilir. Bu yüzden build'i **push edilecek commit'in kendisi** üzerinde doğrula (ör. geçici `git worktree add <dir> HEAD` + `node_modules` symlink'i ile). Ayrıca kısmi commit atarken değişikliğin bağımlılık kapanışını kontrol et — bir dosyanın kullandığı prop/export başka bir commit'lenmemiş dosyadan geliyorsa onları da aynı commit'e dahil et.
|
Push emri geldiğinde önce lokalde `pnpm build` başarıyla geçmeli; geçmeden asla push etme. Kritik detay: çalışma ağacı kirliyken build, commit'lenmemiş dosyaları da gördüğü için CI'da kırılacak bir commit'i yakalayamayabilir. Bu yüzden build'i **push edilecek commit'in kendisi** üzerinde doğrula (ör. geçici `git worktree add <dir> HEAD` + `node_modules` symlink'i ile). Ayrıca kısmi commit atarken değişikliğin bağımlılık kapanışını kontrol et — bir dosyanın kullandığı prop/export başka bir commit'lenmemiş dosyadan geliyorsa onları da aynı commit'e dahil et.
|
||||||
|
|
||||||
|
# Görsel dilin tek kaynağı: docs/tasarim/DESIGN.md
|
||||||
|
|
||||||
|
UI'a dokunan her iş (yeni ekran, bileşen, kart, modal, buton, form; mevcut arayüz değişikliği) önce `docs/tasarim/DESIGN.md` okunarak yapılır; renk, yazı tipi, köşe, gölge, boşluk ve tek kaynak bileşenlerin sınıf dizileri oradan **birebir** alınır, "yaklaşık aynısı" yazılmaz. Aşağıdaki rozet/çarpı/CTA kuralları o belgenin özetidir. Genel tasarım katalogları (`ui-ux-pro-max` gibi "50 stil, 97 palet" kütüphaneleri) bu projede kullanılmaz; yön zaten belli. İş bitmeden `pnpm tasarim:denetim` sıfır hata vermeli (`pnpm lint` bunu da koşar); betik `scripts/tasarim-denetim.ts`, kural eklerken DESIGN.md'deki bölüm numarasına atıf yap.
|
||||||
|
|
||||||
# Badge/eyebrow rozetleri: tek kaynak SectionEyebrow
|
# Badge/eyebrow rozetleri: tek kaynak SectionEyebrow
|
||||||
|
|
||||||
Site genelindeki tüm badge/rozet/eyebrow elemanları ana sayfadaki section başlıklarının üstünde duran rozetle birebir aynı olmalı. Kanonik bileşen `src/components/pixel-decor.tsx` içindeki `SectionEyebrow`dur (piksel imzalı `PixelMark` ikonu + `rounded-full border border-slate-200 bg-white px-3.5 py-1.5 font-bricolage text-xs font-semibold text-primary`; metin büyük-küçük harfli yazılır — uppercase yok). Yeni bir badge/eyebrow gerektiğinde `SectionEyebrow` kullan (`className` prop'u kenar boşluğu için var); `@/components/ui/badge`'deki `Badge` ile ya da elle yazılmış pill span'lerle yeni badge tasarlama. İnteraktif chip/buton pill'leri (filtre çipleri, nav butonları) bu kuralın dışındadır.
|
Site genelindeki tüm badge/rozet/eyebrow elemanları ana sayfadaki section başlıklarının üstünde duran rozetle birebir aynı olmalı. Kanonik bileşen `src/components/pixel-decor.tsx` içindeki `SectionEyebrow`dur (piksel imzalı `PixelMark` ikonu + `inline-flex items-center gap-2 rounded-full border border-slate-200 bg-white px-3.5 py-1.5 text-xs font-semibold text-primary`; metin büyük-küçük harfli yazılır — uppercase yok). Yeni bir badge/eyebrow gerektiğinde `SectionEyebrow` kullan (`className` prop'u kenar boşluğu için var); `@/components/ui/badge`'deki `Badge` ile ya da elle yazılmış pill span'lerle yeni badge tasarlama. İnteraktif chip/buton pill'leri (filtre çipleri, nav butonları) bu kuralın dışındadır.
|
||||||
|
|
||||||
# UI tutarlılığı: ortak elemanlar tek kaynaktan
|
# UI tutarlılığı: ortak elemanlar tek kaynaktan
|
||||||
|
|
||||||
|
|||||||
181
docs/tasarim/DESIGN.md
Normal file
181
docs/tasarim/DESIGN.md
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
# KolayTercih görsel dili (DESIGN.md)
|
||||||
|
|
||||||
|
Bu dosya sitenin görsel dilinin **tek yazılı kaynağıdır**. Yeni ekran, bileşen, modal, kart
|
||||||
|
ya da buton çizmeden önce okunur; mevcut arayüz değiştirilirken buna bakılır. Değerler koddan
|
||||||
|
çıkarılmıştır (`src/app/globals.css`, `src/app/layout.tsx`, `src/components/pixel-decor.tsx`,
|
||||||
|
`src/components/ui/*`, `src/app/page.tsx`); kodla çelişirse kod değil, bu dosya düzeltilir ve
|
||||||
|
çelişki raporlanır. Kural ihlallerini `pnpm tasarim:denetim` yakalar.
|
||||||
|
|
||||||
|
Biçim: Google Stitch'in DESIGN.md düzeni (9 bölüm). Sabit değerler Tailwind sınıfı olarak
|
||||||
|
verilir; kodda da sınıf kullanılır, hex/oklch elle yazılmaz.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Görsel tema ve atmosfer
|
||||||
|
|
||||||
|
**Bir cümle:** Beyaz, bol nefesli, mavi bilgi + turuncu aksiyon; her yüzeyde küçük bir
|
||||||
|
**piksel imzası** (hero'daki dot-matrix Türkiye haritasının dili) dolaşır.
|
||||||
|
|
||||||
|
- Zemin her zaman beyaz; koyu zemin yalnızca CTA bandı ve çekmece başlığı gibi tek bir
|
||||||
|
vurgu yüzeyinde (`PixelField` ile).
|
||||||
|
- İmza dekoratiftir ama rastgele değildir: deterministik ızgara (hücre 10, nokta 7.4, rx 2),
|
||||||
|
imleç yaklaşınca turuncuya ısınan pikseller (`usePixelHeat`), uzaklaşınca yavaş soğuma.
|
||||||
|
- Ton: öğrenciye "sen" diyen, sakin, vaatsiz. Süs yok, gösteriş yok; hareket yalnızca
|
||||||
|
bir şey söylüyorsa var.
|
||||||
|
- **Karanlık mod yoktur.** `dark:` sınıfı yazılmaz (`.dark` token'ları shadcn kalıntısıdır).
|
||||||
|
|
||||||
|
## 2. Renk paleti ve rolleri
|
||||||
|
|
||||||
|
| Rol | Sınıf | Nerede |
|
||||||
|
|---|---|---|
|
||||||
|
| Bilgi/vurgu mavisi (`primary`) | `text-primary`, `bg-primary/10`, `border-primary/30` | Rozet metni, ikon kutusu, hero'daki italik vurgu, linkler, kart hover kenarlığı |
|
||||||
|
| **Aksiyon turuncusu** | `bg-orange-500 hover:bg-orange-600 text-white` | Funnel CTA butonları. Bir ekranda **tek** turuncu buton |
|
||||||
|
| Piksel vurgusu | `fill-orange-500` (opaklık 0.55–0.85) | Yalnızca piksel süslerinin "sıcak" noktaları |
|
||||||
|
| Başlık metni | `text-slate-900` | h1–h3, kart başlıkları |
|
||||||
|
| Gövde metni | `text-slate-600` | Paragraf, açıklama |
|
||||||
|
| İkincil metin | `text-slate-500` | Meta bilgi, yardımcı satır |
|
||||||
|
| Silik metin | `text-slate-400` | İpucu, devre dışı, placeholder |
|
||||||
|
| Kenarlık | `border-slate-200` (landing kartlarında `border-slate-200/70`) | Kart, rozet, çarpı, giriş alanı |
|
||||||
|
| İç zemin | `bg-slate-50` | Kartın içindeki ikincil kutu, tablo başlığı |
|
||||||
|
| Hover zemini | `hover:bg-slate-100` | Çarpı, ghost buton |
|
||||||
|
| Risk: güvenli | `emerald-500` / `text-emerald-600` | **Her zaman "Güvenli" metniyle** |
|
||||||
|
| Risk: az riskli | `amber-500` / `text-amber-600` | **Her zaman "Az riskli" metniyle** |
|
||||||
|
| Risk: riskli | `red-500` / `text-red-600` | **Her zaman "Riskli" metniyle** |
|
||||||
|
| Uyarı kutusu | `border-amber-200 bg-amber-50 text-amber-900` | Bilgilendirme notu |
|
||||||
|
| Hata | `text-destructive`, `text-red-600` | Form hatası |
|
||||||
|
|
||||||
|
Kurallar:
|
||||||
|
- Nötr yalnızca **slate**. `gray`, `zinc`, `neutral`, `stone` yazılmaz.
|
||||||
|
- `indigo`, `purple`, `violet`, `fuchsia`, `pink`, `cyan`, `teal`, `lime`, `green`, `yellow`
|
||||||
|
yeni kullanım açmaz. (`bg-violet-50 text-violet-600` tek bir mevcut durum çipidir; yeni yere taşıma.)
|
||||||
|
- Gradyan metin (`bg-clip-text text-transparent`) ve parlama gölgesi (`shadow-[0_0_…]`) yok.
|
||||||
|
- Hex/oklch değeri sınıf içine yazılmaz (`text-[#…]`); token ya da Tailwind tonu kullanılır.
|
||||||
|
Risk renklerinin SVG karşılığı `src/lib/risk.ts` içindeki `RISK_PIN_RENK`'ten okunur.
|
||||||
|
- Renk tek başına anlam taşımaz: risk rengi her zaman metin etiketiyle birlikte gider
|
||||||
|
(`RISK_ETIKET`, `DILIM_ETIKET` — üçüncü dilimin görünen adı "Güvenli").
|
||||||
|
|
||||||
|
## 3. Tipografi
|
||||||
|
|
||||||
|
Dört yazı tipi, her birinin tek bir işi var:
|
||||||
|
|
||||||
|
| Sınıf | Yazı tipi | İş |
|
||||||
|
|---|---|---|
|
||||||
|
| `font-heading` | Cal Sans | Tüm başlıklar (h1–h3, kart başlığı). Her zaman `font-bold` ile |
|
||||||
|
| `font-sans` (varsayılan) | Clarity City 400–800 | Gövde, buton, form, tablo |
|
||||||
|
| `font-bricolage` | Bricolage Grotesque (opsz ekseni) | Vurgu: hero'da italik mavi span, küçük etiketler/çipler, sayısal rozetler |
|
||||||
|
| `font-mono` | Geist Mono | Yalnızca kod/markdown blokları. Arayüzde kullanılmaz |
|
||||||
|
|
||||||
|
Ölçek (kopyala, uydurma):
|
||||||
|
|
||||||
|
- Hero h1: `font-heading text-5xl font-bold tracking-[0.015em] sm:text-6xl`; vurgu kelimesi
|
||||||
|
`<span className="font-bricolage italic tracking-tighter font-bold text-primary">`
|
||||||
|
- Bölüm h2: `font-heading text-3xl font-bold sm:text-4xl` (istenirse `text-slate-900`)
|
||||||
|
- Panel/kart h3: `font-heading text-xl font-bold` · liste satırı başlığı: `font-heading text-lg font-bold text-slate-900`
|
||||||
|
- Küçük başlık: `font-heading text-base font-bold text-slate-900`
|
||||||
|
- Bölüm giriş paragrafı: `max-w-xl text-lg leading-relaxed text-slate-600`
|
||||||
|
- Gövde: `text-sm` (tablo, form, kart içi) ya da `text-base leading-relaxed` (açıklama)
|
||||||
|
- Etiket/rozet metni: `text-xs font-semibold`
|
||||||
|
- Yasak: Inter, Roboto, Arial, sistem fontu, serif, `font-[…]` keyfi font; tek kelimelik
|
||||||
|
büyük harf etiketler (uppercase yok, rozet metni büyük-küçük harfli yazılır).
|
||||||
|
|
||||||
|
## 4. Bileşen stilleri (tek kaynaklar)
|
||||||
|
|
||||||
|
Bir eleman iki yerde görünüyorsa stili tek kaynaktan gelir. Aşağıdakiler yeniden tasarlanmaz.
|
||||||
|
|
||||||
|
### 4.1 Buton — `src/components/ui/button.tsx`
|
||||||
|
- Her buton `rounded-full`; `h-9` varsayılan, `h-11`/`h-12` funnel CTA'ları (44 px dokunma hedefi), `size-9` ikon butonu.
|
||||||
|
- Odak: `focus-visible:ring-3 focus-visible:ring-ring/50`; basınca `active:translate-y-px`.
|
||||||
|
- **Turuncu CTA (kanonik):**
|
||||||
|
```tsx
|
||||||
|
<Button size="lg" className="cursor-pointer bg-orange-500 text-white transition-colors duration-200 hover:bg-orange-600">
|
||||||
|
Listemi oluştur
|
||||||
|
<ArrowRight className="size-4" aria-hidden />
|
||||||
|
</Button>
|
||||||
|
```
|
||||||
|
Metin önce, ok sonda. Başta ikon yok. `Sparkles` yok. İstenirse `active:scale-[0.97]`.
|
||||||
|
- İkincil: `variant="outline"`; sessiz: `variant="ghost"`; link görünümü: `variant="link"`.
|
||||||
|
|
||||||
|
### 4.2 Rozet (bölüm başlığı üstündeki kapsül) — `SectionEyebrow`, `src/components/pixel-decor.tsx`
|
||||||
|
```tsx
|
||||||
|
<SectionEyebrow>Nasıl çalışır?</SectionEyebrow>
|
||||||
|
```
|
||||||
|
Sınıfı: `inline-flex items-center gap-2 rounded-full border border-slate-200 bg-white px-3.5 py-1.5 text-xs font-semibold text-primary` + başta `PixelMark`.
|
||||||
|
`@/components/ui/badge` **kullanılmaz**; elle pill span yazılmaz. Filtre çipleri ve nav butonları bu kuralın dışındadır (onlar etkileşimlidir).
|
||||||
|
|
||||||
|
### 4.3 Modal kapatma çarpısı — `DialogContent` varsayılanı, `src/components/ui/dialog.tsx`
|
||||||
|
`absolute top-3 right-3 z-10 flex size-9 cursor-pointer items-center justify-center rounded-full border border-slate-200 bg-popover text-slate-500 transition-[background-color,transform] duration-150 hover:bg-slate-100 hover:text-slate-800 active:scale-[0.96]` + `<XIcon className="size-4" aria-hidden />` + `sr-only "Kapat"`.
|
||||||
|
Özel yerleşim gerekiyorsa `showCloseButton={false}` ver ve **aynı sınıf setini** kopyala.
|
||||||
|
|
||||||
|
### 4.4 Kart ve kutu
|
||||||
|
- Landing/öne çıkan kart: `<Card className="relative h-full overflow-hidden border-slate-200/70 shadow-sm transition-all duration-300 hover:-translate-y-1 hover:border-primary/30 hover:shadow-lg hover:shadow-primary/5">` + sağ üstte `<PixelCorner className="absolute right-4 top-4" />`.
|
||||||
|
- Sade kutu (form, panel, liste satırı): `rounded-2xl border border-slate-200 bg-white` (+ `p-5`/`p-6`). Küçük iç kutu: `rounded-xl border border-slate-200 bg-slate-50`. Not kutusu: `rounded-lg`.
|
||||||
|
- İkon kutusu: `flex size-11 items-center justify-center rounded-xl bg-primary/10` içinde `size-5 text-primary` ikon.
|
||||||
|
- Gölge: `shadow-sm` varsayılan; hover'da `shadow-lg shadow-primary/5`. Başka gölge yok.
|
||||||
|
|
||||||
|
### 4.5 Risk çipi / etiketi
|
||||||
|
Nokta + metin: `<span className="size-2 rounded-full bg-emerald-500" />` yanında `text-xs font-semibold text-emerald-600`; metin `RISK_ETIKET`'ten. Renk tek başına asla.
|
||||||
|
|
||||||
|
### 4.6 Form
|
||||||
|
shadcn `Input`, `Select`, `Textarea`, `Slider`. Etiket her alanda; hata mesajı alanın altında, ne olduğunu ve ne yapılacağını söyler. Sayısal alanlarda uç değerler (0, negatif, 1.000.000 üstü) ele alınır.
|
||||||
|
|
||||||
|
### 4.7 İkonlar
|
||||||
|
Yalnızca `lucide-react`. Boyut: `size-4` buton/satır içi (varsayılan), `size-5` ikon kutusu, `size-3.5` küçük çip, `size-6` üstü yalnızca boş durum. Dekoratif ikon `aria-hidden`; tek başına ikon buton `aria-label` ya da `sr-only` metin taşır. Emoji ikon olarak kullanılmaz.
|
||||||
|
|
||||||
|
## 5. Yerleşim
|
||||||
|
|
||||||
|
- Sayfa kabı: `mx-auto max-w-6xl px-4`. Metin sütunu: `max-w-xl` (paragraf) / `max-w-2xl`–`max-w-3xl` (uzun içerik).
|
||||||
|
- Landing bölümü: `<section className="py-20 sm:py-24">`; alt sayfalar `py-12`–`py-16`.
|
||||||
|
- Bölüm başlık bloğu (kanonik):
|
||||||
|
```tsx
|
||||||
|
<div className="flex flex-col items-center gap-4 text-center">
|
||||||
|
<SectionEyebrow>Sorun</SectionEyebrow>
|
||||||
|
<h2 className="font-heading text-3xl font-bold sm:text-4xl">…</h2>
|
||||||
|
<p className="max-w-xl text-lg leading-relaxed text-slate-600">…</p>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
Ardından içerik `mt-12`; kart ızgarası `grid gap-6 md:grid-cols-3` (yalnızca gerçekten üç eşdeğer öğe varsa; üçe tamamlamak için içerik uydurulmaz).
|
||||||
|
- Alt sayfa başlığının altında `PagePixelDivider` (`mt-6`/`mt-8`), her sayfada **farklı seed**.
|
||||||
|
- Landing bölümleri arasında çizgi yok; `PixelDivider` (`mx-auto w-56`, farklı seed).
|
||||||
|
- Boşluk ölçeği: 4 → 6 → 8 → 12 → 16/20 → 24. Ara değer (`mt-7`, `gap-5`) yeni yerde açılmaz.
|
||||||
|
- Mobil önce (trafiğin ~%90'ı 375 px): uzun üniversite/program adları `min-w-0` + sarma ya da `truncate`; tablo yatay kaydırmaz, kart olur; sabit alt çubuk klavyeyle çakışmaz; `vh` yerine `dvh`.
|
||||||
|
|
||||||
|
## 6. Derinlik ve katman
|
||||||
|
|
||||||
|
Derinlik gölgeyle değil **piksel katmanı ve hareket farkıyla** verilir: başlık bloğu gövdeden hafif hızlı akar (`Parallax strength={16}` başlık, `7` içerik, `26` ayraç). Kartlar hover'da 4 px yükselir. Modal arka planı `bg-black/10` + hafif blur. Yığın: içerik < sabit bar < çekmece/modal (`z-50`).
|
||||||
|
|
||||||
|
## 7. Yap / Yapma
|
||||||
|
|
||||||
|
| Yap | Yapma (AI izi) |
|
||||||
|
|---|---|
|
||||||
|
| Rozet için `SectionEyebrow` | `Badge` import'u, elle `rounded-full border … px-3.5 py-1.5` span |
|
||||||
|
| CTA: metin + sondaki `ArrowRight` | Başta `Sparkles`/yıldız ikonu, "✨" |
|
||||||
|
| Nötr = slate; vurgu = primary mavi; aksiyon = orange-500 | `indigo/purple` primary, mor→mavi gradyan, `gray/zinc` nötr |
|
||||||
|
| Kart: `rounded-2xl border-slate-200 bg-white shadow-sm` | Her şeyde `rounded-3xl`, camsı `backdrop-blur` kartlar, parlama gölgesi |
|
||||||
|
| Hareket: `Reveal` (bölüm girişi), piksel ısısı, `kt-unblur`; `animate-ping` yalnızca "şu anda canlı" noktasında | Her elemanda `fade-up`, `whileHover scale 1.05`, `animate-bounce` |
|
||||||
|
| Başlık `font-heading`, vurgu `font-bricolage` | Inter/Roboto/serif, tek kelimelik BÜYÜK HARF etiket |
|
||||||
|
| Risk rengi + metin etiketi | Renk tek başına, "yerleşirsin" gibi vaat cümlesi |
|
||||||
|
| Üç kart yalnızca üç gerçek öğe varsa | Hero + üç kart iskeleti refleksi |
|
||||||
|
| Beyaz zemin, tek koyu vurgu yüzeyi | İstenmemiş karanlık mod, `dark:` sınıfı |
|
||||||
|
| Kapatma çarpısı `DialogContent` varsayılanı | Başka boyut/renkte çarpı |
|
||||||
|
|
||||||
|
## 8. Duyarlılık
|
||||||
|
|
||||||
|
- Kırılma noktaları: 375 (temel), 640 `sm:`, 768 `md:`, 1024 `lg:`. Her bileşen önce 375'te tasarlanır.
|
||||||
|
- Dokunma hedefi en az 44 px (`h-11`, `size-11`); satır içi küçük buton `h-9` yalnızca masaüstü tablo aksiyonlarında.
|
||||||
|
- Başlık ölçeği bir kademe düşer (`text-3xl` → `sm:text-4xl`), gövde 16 px altına inmez.
|
||||||
|
- Modal genişliği `max-w-[calc(100%-2rem)]`; çekmece (`Drawer`) mobilde alttan açılır.
|
||||||
|
- `prefers-reduced-motion`: her animasyonun sade karşılığı var (`motion-reduce:animate-none`, `.kt-unblur-sade`).
|
||||||
|
|
||||||
|
## 9. Ajan için brief şablonu
|
||||||
|
|
||||||
|
Yeni bir UI işi istenince önce şu dört satır doldurulur, sonra kod yazılır:
|
||||||
|
|
||||||
|
```
|
||||||
|
Amaç: Kim, hangi ekranda, ne yapacak? (ör. veli, /paket, fiyatı karşılaştırıp satın alacak)
|
||||||
|
Referans: Sitedeki en yakın mevcut ekran/bileşen (dosya yolu). Yoksa "yok" yaz ve ana sayfa bölüm bloğunu al.
|
||||||
|
Tek kaynak: Kullanılacak kanonik parçalar (SectionEyebrow, Button+ArrowRight, Card+PixelCorner, PagePixelDivider…)
|
||||||
|
Durumlar: boş / yükleniyor / hata / başarılı / girişsiz / kredisiz — hangileri var?
|
||||||
|
```
|
||||||
|
|
||||||
|
"Modern", "temiz", "şık" gibi yön belirtmeyen kelimeler kullanılmaz; yön = bu dosya.
|
||||||
|
İş bitince: `pnpm tasarim:denetim` sıfır hata + 375 px'te tek bakış (taşma, üst üste binme, dokunma hedefi).
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "eslint",
|
"lint": "eslint && pnpm tasarim:denetim",
|
||||||
"ingest": "tsx scripts/ingest.ts",
|
"ingest": "tsx scripts/ingest.ts",
|
||||||
"refresh": "tsx scripts/refresh.ts",
|
"refresh": "tsx scripts/refresh.ts",
|
||||||
"detay": "tsx scripts/detay.ts",
|
"detay": "tsx scripts/detay.ts",
|
||||||
@@ -19,7 +19,8 @@
|
|||||||
"db:push": "drizzle-kit push",
|
"db:push": "drizzle-kit push",
|
||||||
"db:generate": "drizzle-kit generate",
|
"db:generate": "drizzle-kit generate",
|
||||||
"db:migrate": "node scripts/db-goc.mjs",
|
"db:migrate": "node scripts/db-goc.mjs",
|
||||||
"tadimlik": "tsx scripts/tadimlik-uret.ts"
|
"tadimlik": "tsx scripts/tadimlik-uret.ts",
|
||||||
|
"tasarim:denetim": "tsx scripts/tasarim-denetim.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@cds/city": "^1.1.0",
|
"@cds/city": "^1.1.0",
|
||||||
|
|||||||
215
scripts/tasarim-denetim.ts
Normal file
215
scripts/tasarim-denetim.ts
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
/**
|
||||||
|
* Tasarım dili denetimi — docs/tasarim/DESIGN.md kurallarının makineyle
|
||||||
|
* yakalanabilen kısmı. Ajan/insan fark etmez: `pnpm tasarim:denetim`.
|
||||||
|
*
|
||||||
|
* Çıkış kodu: hata varsa 1, yoksa 0. Uyarılar çıkış kodunu etkilemez.
|
||||||
|
* Kapsam: src/**\/*.tsx. Hariç: src/components/ui (shadcn tabanı) ve
|
||||||
|
* src/components/dev (yalnızca geliştirici paneli) — bazı kurallarda.
|
||||||
|
*/
|
||||||
|
import { readdirSync, readFileSync, statSync } from "node:fs";
|
||||||
|
import { join, relative } from "node:path";
|
||||||
|
|
||||||
|
type Seviye = "hata" | "uyari";
|
||||||
|
|
||||||
|
type Bulgu = {
|
||||||
|
dosya: string;
|
||||||
|
satir: number;
|
||||||
|
kural: string;
|
||||||
|
seviye: Seviye;
|
||||||
|
mesaj: string;
|
||||||
|
parca: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Kural = {
|
||||||
|
ad: string;
|
||||||
|
seviye: Seviye;
|
||||||
|
mesaj: string;
|
||||||
|
/** ui/dev klasörleri de taransın mı */
|
||||||
|
uiDahil?: boolean;
|
||||||
|
/** Tam yolu eşleşen dosyalar kuraldan muaf */
|
||||||
|
muaf?: RegExp;
|
||||||
|
bul: (icerik: string) => Array<{ index: number; parca: string }>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const KOK = process.cwd();
|
||||||
|
const KAYNAK = join(KOK, "src");
|
||||||
|
const UI_VEYA_DEV = /src\/components\/(ui|dev)\//;
|
||||||
|
|
||||||
|
function tsxDosyalari(dizin: string): string[] {
|
||||||
|
const sonuc: string[] = [];
|
||||||
|
for (const ad of readdirSync(dizin)) {
|
||||||
|
const yol = join(dizin, ad);
|
||||||
|
if (statSync(yol).isDirectory()) sonuc.push(...tsxDosyalari(yol));
|
||||||
|
else if (yol.endsWith(".tsx")) sonuc.push(yol);
|
||||||
|
}
|
||||||
|
return sonuc;
|
||||||
|
}
|
||||||
|
|
||||||
|
function satirNo(icerik: string, index: number): number {
|
||||||
|
let n = 1;
|
||||||
|
for (let i = 0; i < index && i < icerik.length; i++) if (icerik[i] === "\n") n++;
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
function satirParcasi(icerik: string, index: number): string {
|
||||||
|
const bas = icerik.lastIndexOf("\n", index) + 1;
|
||||||
|
let son = icerik.indexOf("\n", index);
|
||||||
|
if (son === -1) son = icerik.length;
|
||||||
|
return icerik.slice(bas, son).trim().slice(0, 110);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Basit regex kuralı: her eşleşme bir bulgu. */
|
||||||
|
function regexKurali(re: RegExp) {
|
||||||
|
return (icerik: string) => {
|
||||||
|
const g = new RegExp(re.source, re.flags.includes("g") ? re.flags : re.flags + "g");
|
||||||
|
const sonuc: Array<{ index: number; parca: string }> = [];
|
||||||
|
for (const m of icerik.matchAll(g)) sonuc.push({ index: m.index ?? 0, parca: m[0] });
|
||||||
|
return sonuc;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** <Etiket …>…</Etiket> bloklarının içinde `ic` deseni varsa bulgu. */
|
||||||
|
function blokIcinde(etiketler: string[], ic: RegExp) {
|
||||||
|
return (icerik: string) => {
|
||||||
|
const sonuc: Array<{ index: number; parca: string }> = [];
|
||||||
|
for (const etiket of etiketler) {
|
||||||
|
const blok = new RegExp(`<${etiket}\\b[\\s\\S]*?<\\/${etiket}>`, "g");
|
||||||
|
for (const m of icerik.matchAll(blok)) {
|
||||||
|
const icM = ic.exec(m[0]);
|
||||||
|
if (icM) sonuc.push({ index: (m.index ?? 0) + icM.index, parca: icM[0] });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sonuc;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const RENK_ONEK = "(?:bg|text|border|ring|fill|stroke|from|to|via|divide|outline|placeholder|decoration|accent|caret|shadow)";
|
||||||
|
|
||||||
|
const KURALLAR: Kural[] = [
|
||||||
|
{
|
||||||
|
ad: "badge-import",
|
||||||
|
seviye: "hata",
|
||||||
|
mesaj: "Rozet için @/components/ui/badge değil SectionEyebrow kullanılır (DESIGN.md §4.2)",
|
||||||
|
bul: regexKurali(/from\s+["']@\/components\/ui\/badge["']/),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ad: "elle-rozet",
|
||||||
|
seviye: "hata",
|
||||||
|
mesaj: "Elle yazılmış rozet; SectionEyebrow'u içe aktar (DESIGN.md §4.2)",
|
||||||
|
muaf: /src\/components\/pixel-decor\.tsx$/,
|
||||||
|
bul: regexKurali(/rounded-full border border-slate-200 bg-white px-3\.5 py-1\.5/),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ad: "cta-yildiz",
|
||||||
|
seviye: "hata",
|
||||||
|
mesaj: "CTA butonunda Sparkles/yıldız ikonu olmaz; metin + sondaki ArrowRight (DESIGN.md §4.1)",
|
||||||
|
bul: blokIcinde(["Button", "Link"], /<Sparkles\b/),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ad: "yabanci-notr",
|
||||||
|
seviye: "hata",
|
||||||
|
mesaj: "Nötr renk yalnızca slate; gray/zinc/neutral/stone yazılmaz (DESIGN.md §2)",
|
||||||
|
bul: regexKurali(new RegExp(`\\b${RENK_ONEK}-(?:gray|zinc|neutral|stone)-\\d+`)),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ad: "yabanci-renk",
|
||||||
|
seviye: "hata",
|
||||||
|
mesaj: "Paletin dışında ton; primary mavi / orange-500 / slate / risk renkleri kullanılır (DESIGN.md §2)",
|
||||||
|
bul: regexKurali(
|
||||||
|
new RegExp(`\\b${RENK_ONEK}-(?:indigo|purple|fuchsia|pink|cyan|teal|lime|green|yellow)-\\d+`),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ad: "gradyan-metin",
|
||||||
|
seviye: "hata",
|
||||||
|
mesaj: "Gradyan metin yok (DESIGN.md §2)",
|
||||||
|
bul: regexKurali(/\b(?:bg-clip-text|text-transparent)\b/),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ad: "parlama-golgesi",
|
||||||
|
seviye: "hata",
|
||||||
|
mesaj: "Parlama (glow) gölgesi yok; shadow-sm / hover:shadow-lg shadow-primary/5 (DESIGN.md §4.4)",
|
||||||
|
bul: regexKurali(/shadow-\[0_0_[1-9]\d*px/),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ad: "hareket-spam",
|
||||||
|
seviye: "hata",
|
||||||
|
mesaj:
|
||||||
|
"whileHover scale / aos / bounce yok; Reveal, piksel ısısı, kt-unblur (DESIGN.md §7). animate-ping yalnızca canlı göstergesi noktasında",
|
||||||
|
bul: regexKurali(/whileHover=\{\{\s*scale|data-aos=|\banimate-bounce\b/),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ad: "kapatma-carpisi",
|
||||||
|
seviye: "hata",
|
||||||
|
mesaj: "showCloseButton={false} verilen dosyada kanonik çarpı sınıf seti yok (DESIGN.md §4.3)",
|
||||||
|
bul: (icerik) => {
|
||||||
|
const m = /showCloseButton=\{false\}/.exec(icerik);
|
||||||
|
if (!m) return [];
|
||||||
|
const kanonik =
|
||||||
|
/size-9 cursor-pointer items-center justify-center rounded-full border border-slate-200/.test(icerik);
|
||||||
|
return kanonik ? [] : [{ index: m.index, parca: m[0] }];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ad: "yabanci-font",
|
||||||
|
seviye: "hata",
|
||||||
|
mesaj: "Yalnızca font-heading / font-sans / font-bricolage / font-mono (DESIGN.md §3)",
|
||||||
|
bul: regexKurali(/\bfont-(?:serif|\[)|["'](?:Inter|Roboto|Arial|Helvetica)["']/),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ad: "dark-sinif",
|
||||||
|
seviye: "uyari",
|
||||||
|
mesaj: "Üründe karanlık mod yok; dark: sınıfı gereksiz (DESIGN.md §1)",
|
||||||
|
bul: regexKurali(/(?<=[\s"'`])dark:[a-z]/),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ad: "hex-renk",
|
||||||
|
seviye: "uyari",
|
||||||
|
mesaj: "Sınıf içinde hex renk; token ya da Tailwind tonu kullan (DESIGN.md §2)",
|
||||||
|
bul: regexKurali(/\[#[0-9a-fA-F]{3,8}\]/),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ad: "emoji-ikon",
|
||||||
|
seviye: "uyari",
|
||||||
|
mesaj: "Emoji ikon olarak kullanılmaz; lucide ikon (DESIGN.md §4.7)",
|
||||||
|
bul: (icerik) =>
|
||||||
|
// Yorum satırındaki işaretler (// ✗ gibi) ikon değildir
|
||||||
|
regexKurali(/[\u{1F300}-\u{1FAFF}\u{2600}-\u{27BF}]\uFE0F?/u)(icerik).filter(
|
||||||
|
(e) => !/^(?:\/\/|\*|\/\*)/.test(satirParcasi(icerik, e.index)),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const bulgular: Bulgu[] = [];
|
||||||
|
for (const yol of tsxDosyalari(KAYNAK)) {
|
||||||
|
const gorece = relative(KOK, yol);
|
||||||
|
const icerik = readFileSync(yol, "utf8");
|
||||||
|
for (const k of KURALLAR) {
|
||||||
|
if (!k.uiDahil && UI_VEYA_DEV.test(gorece)) continue;
|
||||||
|
if (k.muaf && k.muaf.test(gorece)) continue;
|
||||||
|
for (const e of k.bul(icerik)) {
|
||||||
|
bulgular.push({
|
||||||
|
dosya: gorece,
|
||||||
|
satir: satirNo(icerik, e.index),
|
||||||
|
kural: k.ad,
|
||||||
|
seviye: k.seviye,
|
||||||
|
mesaj: k.mesaj,
|
||||||
|
parca: satirParcasi(icerik, e.index),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bulgular.sort((a, b) =>
|
||||||
|
a.seviye !== b.seviye ? (a.seviye === "hata" ? -1 : 1) : a.dosya.localeCompare(b.dosya) || a.satir - b.satir,
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const b of bulgular) {
|
||||||
|
const etiket = b.seviye === "hata" ? "HATA " : "uyarı";
|
||||||
|
console.log(`${b.dosya}:${b.satir} [${etiket}] ${b.kural} — ${b.mesaj}\n ${b.parca}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const hata = bulgular.filter((b) => b.seviye === "hata").length;
|
||||||
|
const uyari = bulgular.length - hata;
|
||||||
|
console.log(`\nTasarım denetimi: ${hata} hata, ${uyari} uyarı (${KURALLAR.length} kural, docs/tasarim/DESIGN.md)`);
|
||||||
|
process.exit(hata > 0 ? 1 : 0);
|
||||||
@@ -122,7 +122,7 @@ export function AramaFunnelKarti({ onSec }: { onSec: () => void }) {
|
|||||||
key={etiket}
|
key={etiket}
|
||||||
className="inline-flex items-center gap-1.5 text-xs font-medium text-slate-200"
|
className="inline-flex items-center gap-1.5 text-xs font-medium text-slate-200"
|
||||||
>
|
>
|
||||||
<Ikon className="size-3.5 text-cyan-300" aria-hidden />
|
<Ikon className="size-3.5 text-orange-500" aria-hidden />
|
||||||
{etiket}
|
{etiket}
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
|
|||||||
Reference in New Issue
Block a user