hasari-api / docs /INCLUSIVE_VISUALS.md
erdoganpeker's picture
v0.3.0 — multimodal vehicle damage MVP
e327f0d

Inclusive Visuals Audit — Hasarİ Web

Audit Date: 2026-05-16 Scope: apps/web + packages/ui + packages/design (READ-ONLY) Audit Type: Renk, tipografi, ikonografi, lokalizasyon, erişilebilirlik (WCAG AA + color-blind) Stance: Pratik fix odaklı. Kod değiştirilmedi.


1. Genel Tablo

# Bulgu Severity Kategori Konum
F-01 Severity rengi color-blind için sadece renge bağımlı (ikon yok) HIGH Renk / a11y packages/ui/src/components/SeverityBadge.tsx
F-02 Intl.NumberFormat/DateTimeFormat her yerde tr-TR hardcoded HIGH Lokalizasyon apps/web/app/**/*.tsx (5 dosya)
F-03 Shared UI bileşenlerinde TR string hardcoded (EN locale'de TR yazı görünür) HIGH Lokalizasyon packages/ui/src/components/*
F-04 <html lang> doğru ama dir attribute yok (LTR varsayılan implicit) LOW Yön / a11y apps/web/app/layout.tsx:50
F-05 PartCard "hasarsız parça" sadece yeşil ringle gösteriliyor (ikon var ama küçük dot, simgesel ayırt edici zayıf) MEDIUM Renk / a11y packages/ui/src/components/PartCard.tsx:13-25
F-06 CostDisplay "doğruluk" göstergesi (high/medium/low) sadece renk noktası MEDIUM Renk / a11y packages/ui/src/components/CostDisplay.tsx:18-22
F-07 severity.hafif = amber-400 (#fbbf24) — beyaz arkaplanda 3:1 contrast sınır altında küçük metin için MEDIUM Renk / WCAG packages/design/src/colors.ts:45
F-08 home/page.tsx Hero "preview card" — kırmızı dot + amber/orange badge sadece renk ile severity ima ediyor MEDIUM Renk / a11y apps/web/app/page.tsx:88-104, 175-198
F-09 "Hasarsız parça" yeşil + "ağır hasar" kırmızı → klasik protanopia çakışması HIGH Renk / a11y Genel (severity sistemi)
F-10 Profil ayarlarında telefon alanı i18n'de var ama formda yok (ölü string) LOW Form / UX apps/web/messages/{tr,en}.json + apps/web/app/(app)/settings/page.tsx
F-11 İsim alanı min(2) — Türkçe tek heceli isimler için OK; maxLength yok (DB güvenliği zayıf) LOW Form apps/web/app/(auth)/register/page.tsx:19
F-12 E-posta validation z.string().email()+ ve . özel karakterleri Zod default destekler ✓ OK Form apps/web/app/(auth)/register/page.tsx:20
F-13 Para sembolü hep sonda 1.234 ₺ — TR doğru, EN için de aynı sembol kullanılıyor (tutarlı) OK Para birimi Tüm *Display bileşenleri
F-14 EN locale'de tarih tr-TR formatında gösterilecek (16 May 2026, 14:32) yerine US format gerekirdi HIGH Lokalizasyon apps/web/app/history/page.tsx:307-320
F-15 Logo bir image değil, Lucide ShieldCheck ikon — TR/EN tutarlı, kültürel sorun yok OK Brand apps/web/components/Header.tsx:34-37
F-16 Anasayfada hero görseli yok (placeholder kart) — "lüks/ekonomik araç" temsil sorunu YOK OK Brand / Görsel apps/web/public/ (boş)
F-17 Plaka tipi UI'da hiçbir yerde gösterilmiyor — bölgesel önyargı YOK OK Brand
F-18 Brand adı "Hasarİ" — büyük dotted İ Türkçe karakter; EN locale'de aynen kalıyor (kasıtlı, doğru) OK Brand apps/web/messages/{tr,en}.json:3
F-19 Inter font subset ['latin', 'latin-ext'] — ç, ş, ğ, ı, ö, ü destekleniyor ✓ OK Tipografi apps/web/app/layout.tsx:11-15
F-20 tabular-nums kullanılıyor maliyet/sayı için — sayı hizalama doğru OK Tipografi Genel

2. Detaylı Bulgular

F-01 — Severity ikon eşleştirmesi yok (HIGH)

Konum: packages/ui/src/components/SeverityBadge.tsx

Sorun: Severity'yi sadece renk ile ayırt ediyor (hafif=amber, orta=orange, agir=red) + küçük renkli dot (aria-hidden). Yazılı etiket var (Hafif/Orta/Ağır) — bu kısmi OK. Ancak:

  • Protanopia (kırmızı görmeyen) için orange ve red ayırt edilemez → "orta" ve "ağır" birbirinden farksız görünür.
  • Renkli dot screen reader'a aria-hidden, yani sadece görsel ayraç. Eksik kalan ikon ayraç.

Fix önerisi (kod değiştirmeden, plan):

// SeverityBadge.tsx — sadece öneri, kod değişmedi
const ICONS: Record<SeverityLevel, IconComponent> = {
  hafif: Info,       // ⓘ
  orta: AlertTriangle, // ⚠
  agir: AlertOctagon,  // ⛔
};
// dot yerine <Icon className="h-3 w-3" aria-hidden /> ekle

Bu, redundant coding (renk + ikon + metin) prensibiyle WCAG 1.4.1 "Use of Color" gereksinimini karşılar.


F-02 / F-14 — Hardcoded tr-TR locale (HIGH)

Konum:

  • apps/web/app/history/page.tsx:268, 310
  • apps/web/app/(app)/settings/page.tsx:360, 363
  • apps/web/app/(admin)/users/page.tsx:116
  • apps/web/app/(app)/dashboard/page.tsx:96, 152, 159
  • packages/ui/src/components/CostDisplay.tsx:38, 47
  • packages/ui/src/components/DamageBadge.tsx:53
  • packages/ui/src/components/PartCard.tsx:87-88

Sorun: Tüm toLocaleString('tr-TR') ve Intl.DateTimeFormat('tr-TR', …) çağrıları locale parametresini hardcoded geçiyor. EN locale aktif olduğunda kullanıcı şöyle görür:

  • Beklenen (EN): 1,234.56 ₺ ve 5/16/2026, 2:32 PM
  • Gerçekleşen: 1.234,56 ₺ ve 16 May 2026 14:32

EN locale switching çalışır ama sayı/tarih formatları TR kalır — yarım lokalizasyon.

Fix önerisi:

// next-intl helper kullan
const t = useLocale(); // 'tr' | 'en'
const fmt = new Intl.NumberFormat(t === 'tr' ? 'tr-TR' : 'en-US');
fmt.format(value);

Veya next-intl'in built-in useFormatter() API'sini kullan (format.number(), format.dateTime()).


F-03 — Shared UI'da TR string sızıntısı (HIGH)

Konum:

  • packages/ui/src/components/CleanPartsBadgeRow.tsx:22, 37Hasarsız parçalar, daha
  • packages/ui/src/components/InspectionSummary.tsx:11-17Toplam parça, Hasarlı parça, Çoklu parça vb.
  • packages/ui/src/components/PartCard.tsx:62, 84Hasarsız, Parça toplam
  • packages/ui/src/components/DamageBadge.tsx:41-49, 56Çoklu parça, Düşük güven, güven
  • packages/ui/src/components/CostDisplay.tsx:13-16, 35, 38, 53, 65Yüksek doğruluk, Tahmini onarım maliyeti, Orta nokta, gün
  • packages/ui/src/components/InspectionStatusBadge.tsx:5-10Sırada, İşleniyor, Tamamlandı, Başarısız

Sorun: @arac-hasar/ui package'ı Türkçe stringleri gömülü olarak ihraç ediyor. Web app'in i18n switcher'ı bu bileşenler için EN'e geçemez → kullanıcı EN seçse de "Hasarsız parça", "Tahmini onarım maliyeti" gibi yazılar TR görür.

Fix önerisi: İki seçenek:

  1. Bileşenleri props'la beslenebilir yap: <CleanPartsBadgeRow parts={…} labels={{title: t('cleanParts'), more: t('more')}} />
  2. Bileşeni i18n-aware yap: useTranslations çağır, sözlüğü packages/ui içinde tut (ya da @arac-hasar/types'tan re-export et). Mevcut SEVERITY_TR, PART_STATUS_TR zaten types'ta var; aynı pattern'i diğer label'lara uygula.

F-04 — <html dir> hard-coded değil ama eksik (LOW)

Konum: apps/web/app/layout.tsx:50

Mevcut:

<html lang={locale} className={inter.variable}>

Sorun: dir attribute yok → tarayıcı default LTR davranır. Bu şu an TR/EN için doğru ama ileride AR/HE locale eklenirse fark edilmeden bozulur. RTL kapsamda değil, ama explicit yazmak best practice.

Fix önerisi:

<html lang={locale} dir="ltr" className={inter.variable}>

Hard-coded dir="ltr" şu an kabul edilebilir (kapsamda RTL yok); ileride RTL desteği eklenince locale-aware yapılır.


F-05 — PartCard "clean" durumu ikon eksik (MEDIUM)

Konum: packages/ui/src/components/PartCard.tsx:53, 61

Sorun: "Hasarsız" durumu yeşil dot + yeşil pill ile gösteriliyor. Hasarlı durum siyah pill + sayı (2 hasar) ile gösteriliyor. Yeşil/kırmızı ayrımı clean vs damaged için kritik ve burada ikon yok.

Mevcut: <span className="bg-emerald-500 h-2 w-2 rounded-full" aria-hidden /> + emerald pill. Fix önerisi: Status indicator olarak ikon ekle:

  • clean<Check /> (yeşil)
  • minor_damage<Info /> (amber)
  • moderate_damage<AlertTriangle /> (orange)
  • severe_damage<AlertOctagon /> (red)

F-06 — CostDisplay confidence renge bağımlı (MEDIUM)

Konum: packages/ui/src/components/CostDisplay.tsx:57-62

Sorun: high=emerald, medium=amber, low=slate. Metin etiketi var (Yüksek doğruluk vb.) → kısmen OK. Ancak amber #f59e0b ve slate #94a3b8 color-blind için ayırt edilebilir ama yeşil/sarı (high/medium) tritanopia için sorunlu.

Fix önerisi: Metin etiketi yeterli, ancak küçük bir ikon eklenebilir (<ShieldCheck /> high, <Shield /> medium, <ShieldAlert /> low).


F-07 — severity.hafif amber-400 kontrast sınırı (MEDIUM)

Konum: packages/design/src/colors.ts:45

Mevcut yorum:

amber-400 (mild) — yellow-leaning amber for contrast

Doğrulama:

  • #fbbf24 (amber-400) beyaz üstünde: contrast ratio ~1.85:1 → AA non-text 3:1 fail.
  • WCAG 1.4.11 "Non-text Contrast" → UI components 3:1 ister.
  • SeverityBadge zaten bg-amber-100 (açık zemin) + text-amber-900 (koyu yazı) kullanıyor, yazı kontrastı OK (amber-900 üstünde amber-100 ~7.5:1). Ancak severity.hafif token'ı doğrudan badge zemini olarak kullanılırsa sorun olur.

Fix önerisi: Token kullanım kuralını dokümante et:

  • severity.hafif → sadece dot/icon dolgu rengi.
  • Badge zemini için amber-100 + yazı amber-900 kombinasyonu zorunlu.
  • Veya severity.hafif değerini amber-500 (#f59e0b) yap → ~2.8:1, hâlâ sınır ama daha iyi. Önerilen: #d97706 (amber-600) → 4.5:1 AA ✓

F-08 — Anasayfa preview kartı sadece renk/dot (MEDIUM)

Konum: apps/web/app/page.tsx:88-104, 175-198

Sorun:

  • Satır 90: <span className="h-2.5 w-2.5 rounded-full bg-red-500" aria-hidden /> → "kırmızı = hasar" sadece renkle ima.
  • Badge component (satır 175-198) color prop alıyor, severity yazısı + renkli zemin var ama ikon yok.

Fix önerisi: Preview kart sadece marketing içerikli, ama "feature parity" göstermek için SeverityBadge'in ikon-iyileştirilmiş hâlini burada da kullan.


F-09 — Yeşil/kırmızı protanopia çakışması (HIGH, sistemik)

Konum: Genel — severity.clean (#22c55e) + severity.agir (#ef4444)

Sorun: Klasik problem: protanopia/deuteranopia kullanıcıları için yeşil ve kırmızı aynı kahverengi/sarımtırak ton olarak algılanır → "hasarsız" ve "ağır hasar" birbirinden ayırt edilemez. Bu, ürünün ana semantic mesajını (✓ vs ✕) görsel olarak yok eder.

Fix önerisi (zorunlu, redundancy):

  • ✅ Metin etiketi (Hasarsız / Ağır) — mevcut, OK.
  • ✅ İkon farklılaştırma (<Check /> vs <AlertOctagon />) — eksik (F-01, F-05 ile bağlantılı).
  • ✅ Şekil/border farklılaştırma — clean pill rounded-full, severe rounded-md gibi shape coding.
  • ✅ Pattern (örn. ağır hasar için diagonal stripe overlay) — opsiyonel ama power tool.

Minimum kabul: ikon + metin + renk = 3 kanal redundancy.


F-10 — Telefon alanı i18n'de var, formda yok (LOW)

Konum:

  • apps/web/messages/tr.json:172"phone": "Telefon (opsiyonel)"
  • apps/web/messages/en.json:172"phone": "Phone (optional)"
  • Hiçbir .tsx dosyasında phone kullanılmıyor (grep doğrulandı).

Fix önerisi: Ya ölü string'leri kaldır, ya da register/page.tsx ve settings/page.tsx'e telefon alanı ekle. Eklenecekse:

  • +90 prefix zorlama — uluslararası destek için libphonenumber-js ile validate.
  • inputMode="tel", autoComplete="tel", pattern="[0-9+\s()-]+".
  • TR placeholder: +90 5xx xxx xx xx, EN placeholder: +1 (555) 123-4567.

F-11 — İsim alanı maxLength yok (LOW)

Konum: apps/web/app/(auth)/register/page.tsx:19

Mevcut: full_name: z.string().min(2)

Sorun:

  • TR isimleri min(2) OK (örn. "Ay" gibi nadir kısa adlar).
  • max yok → DoS/DB satır limiti riski. i18n'de zaten fullNameTooLong: "İsim en fazla 120 karakter olabilir." mesajı var ama schema'da kullanılmıyor.

Fix önerisi: z.string().min(2).max(120).


F-12 — Email validation OK

Zod .email() regex'i + ve . karakterlerini kabul eder (user+tag@example.com, first.last@domain.com.tr). Türkçe karakter yerel adında sorun olabilir (çağrı@example.com → Zod fail). Eğer .tr IDN destekliyorsa, z.string().email() yetersiz; punycode ile pre-process gerekir. Düşük öncelik — Türkçe yerel ad e-postaları nadir.


F-13 — Para sembolü pozisyonu

TR norm: 1.234,56 ₺ (sembol sonda, locale-correct). EN ne yapmalı? Endüstri pratiği: TRY için ₺1,234.56 (sembol başta) ya da 1,234.56 TRY. Mevcut tüm component'lerde sembol sonda (TR style) — EN kullanıcısı için biraz garip ama anlaşılır. Tutarlılık için kabul edilebilir, opsiyonel iyileştirme: useFormatter().number(value, { style: 'currency', currency: 'TRY' }) → locale-correct otomatik pozisyon.


F-15 / F-16 / F-17 — Brand / görsel temsil

  • Logo: Lucide ShieldCheck ikon. Hem TR hem EN için aynı. Kültürel önyargı YOK.
  • Hero görseli: Yok (public/ boş, hero bölümü sadece text + sentetik card). Bu kapsayıcılık için iyi haber: ne lüks Mercedes ne 1995 Şahin temsil edilmemiş, sadece soyut "ön tampon" mock-up var (Ön tampon / Front bumper text).
  • Plaka tipi: Hiçbir UI'da plaka örneği yok. Bölgesel önyargı sorunu yok.

Marketing önerisi (ileride hero görseli eklenirse):

  • Çeşitli araç sınıfı (sedan, hatchback, SUV, ticari) gösteren bir görsel/illustration set.
  • TR plakası göstermek istenirse: jenerik 34 XXX 1234 (İstanbul kodu yerine XX) veya bulanık plaka.
  • Renk: tek renk araç değil — gri, beyaz, kırmızı mix (en yaygın TR pazarı).
  • AI generated görsel kullanılacaksa: clone car artefaktları, fake plaka karakterleri, lastik şekli bozuklukları için negative prompt zorunlu.

F-18 — Hasarİ brand kelimesi

Dotted capital İ Türkçeye özgü karakter. EN locale'de de Hasarİ olarak korunmuş — bu doğru bir brand kararı (brand adı çevrilmez). Inter font latin-ext subset'i bu karakteri render eder ✓.


F-19 — Türkçe karakter desteği

Inter font subsets: ['latin', 'latin-ext'] ile yükleniyor. latin-ext subset şunları içerir: ç, Ç, ş, Ş, ğ, Ğ, ı, İ, ö, Ö, ü, Ü — TR karakter seti tam destek. Form alanlarında bu karakterler kabul edilir (<input> Unicode-by-default).


3. Top 3 Öncelikli Düzeltme

🥇 P1 — Severity ikon eşleştirmesi ekle (F-01, F-05, F-09)

Etki: ~%8 erkek nüfusunda görülen kırmızı/yeşil körlüğü için ürünün ana semantic mesajını kurtarır. Efor: ~2 saat. Dokunulacak dosyalar:

  • packages/ui/src/components/SeverityBadge.tsx — Lucide ikon mapping ekle.
  • packages/ui/src/components/PartCard.tsx — status icon mapping ekle.
  • apps/web/app/page.tsx — preview kartı SeverityBadge kullanacak şekilde refactor.

Bonus: SeverityBadge'e shape="rounded" | "square" prop'u ekleyerek shape coding ile redundancy katmanı ekle.


🥈 P2 — Locale-aware sayı/tarih formatting (F-02, F-14)

Etki: EN locale şu an yarım çalışıyor — tüm sayılar ve tarihler TR formatında kalıyor. Efor: ~3 saat. Yaklaşım:

  1. apps/web/lib/format.ts adında yeni helper:
    import { useFormatter, useLocale } from 'next-intl';
    export function useNumberFmt() { … }
    export function useDateTimeFmt() { … }
    
  2. Tüm toLocaleString('tr-TR') çağrılarını helper ile değiştir.
  3. packages/ui içindeki component'ler için formatter prop ya da next-intl'i UI package'ında peer-dep yap.

🥉 P3 — Shared UI'da TR string sızıntısını temizle (F-03)

Etki: EN locale switcher gerçek anlamda çalışsın — sadece sayfa içi değil bileşen içi yazılar da çevrilsin. Efor: ~4 saat. Yaklaşım:

  1. packages/ui içindeki tüm hardcoded TR string'i labels prop'una taşı (bileşen i18n-agnostic kalsın).
  2. Çağıran sayfalarda useTranslations ile beslen.
  3. @arac-hasar/types'taki mevcut SEVERITY_TR, PART_STATUS_TR constants'lara karşı SEVERITY_EN, PART_STATUS_EN ekle (veya tek bir locale-resolver fonksiyon).

4. Kapsam Dışı / Önemsiz

  • RTL desteği: Kapsam dışı, ama dir="ltr" explicit yazılması önerilir (F-04).
  • Email IDN (çağrı@…): Düşük öncelik, kullanım frekansı düşük (F-12).
  • Para birimi locale-aware format: Mevcut hâli (sembol sonda) tutarlı ve kabul edilebilir (F-13).
  • Telefon validation: Şu an form yok, eklenmeden tasarım kararı bekler (F-10).
  • Logo / hero görseli: Mevcut hâli kapsayıcılık açısından temiz (F-15, F-16, F-17).

5. Doğrulanmış İyi Pratikler ✓

  • aria-hidden decorative ikonlarda doğru kullanılmış.
  • tabular-nums sayı hizalama için tutarlı.
  • Inter latin-ext ile Türkçe karakter destek tam.
  • Logo brand'i hem TR hem EN'de Hasarİ — kasıtlı, doğru.
  • Hero görseli yok → bölgesel/sınıfsal araç önyargısı yok.
  • Plaka kullanılmıyor → bölgesel önyargı yok.
  • Form noValidate + custom error messages + aria-invalid + aria-describedby doğru.
  • FormField label htmlFor ile input'a bağlı.
  • Focus ring (focus-visible:ring-2 ring-brand-500) keyboard navigation için belirgin.
  • CostDisplay aria-label ile screen reader için doğal dil özet sunuyor (38. satır).

Audit Ends.