feat(arayüz): Tailwind + shadcn/ui tasarım sistemi, tema ve biçimlendirme

- Tailwind v4 + shadcn/ui (Radix), Inter yazı tipi (yerel)
- Tasarım token'ları: 4 ana grup (renk körlüğüne uygun) ve durum renkleri, WCAG AA
- Aydınlık / Karanlık / Sistem teması (ilk açılışta parlama yok)
- lib/format.ts: para (1.250,00 ₺), miktar, yüzde, tarih (GG.AA.YYYY), tutar ayrıştırma
- Vitest: 22 biçimlendirme testi; npm test artık frontend'i de çalıştırır
- Butonlar mobil dokunma alanına (≥44 px) göre büyütüldü

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-12 00:06:27 +03:00
parent 6eb5440d4d
commit 440523b176
22 changed files with 6051 additions and 115 deletions
+117
View File
@@ -0,0 +1,117 @@
/**
* Tüm sayı / para / tarih biçimlendirmesi buradan yapılır (KAKEIBO_PROMPT §9).
*
* - Para: backend'den **kuruş cinsinden tamsayı** gelir → `1.250,00 ₺`
* (para birimi rakamın sağında, arada bir boşluk)
* - Miktar (altın, döviz): milyonda bir birim (`_e6`) → `12,50 gr`
* - Tarih: `GG.AA.YYYY`
*/
export type CurrencyCode = 'TRY' | 'USD' | 'EUR' | 'XAU_GRAM'
const CURRENCY_SUFFIX: Record<CurrencyCode, string> = {
TRY: '₺',
USD: '$',
EUR: '€',
XAU_GRAM: 'gr',
}
const numberFormats = new Map<number, Intl.NumberFormat>()
function numberFormat(decimals: number): Intl.NumberFormat {
let f = numberFormats.get(decimals)
if (!f) {
f = new Intl.NumberFormat('tr-TR', {
minimumFractionDigits: decimals,
maximumFractionDigits: decimals,
})
numberFormats.set(decimals, f)
}
return f
}
/** Ondalıklı sayıyı Türkçe biçimler: `1250.5` → `1.250,50` */
export function formatNumber(value: number, decimals = 2): string {
return numberFormat(decimals).format(value)
}
/** Kuruş → `1.250,00 ₺`. `signed: true` ise pozitiflere `+` eklenir. */
export function formatMoney(
kurus: number,
currency: CurrencyCode = 'TRY',
options: { signed?: boolean } = {},
): string {
const text = `${formatNumber(kurus / 100)} ${CURRENCY_SUFFIX[currency]}`
return options.signed && kurus > 0 ? `+${text}` : text
}
/** Milyonda bir birim → `12,50 gr` (altın, döviz miktarı) */
export function formatQuantity(e6: number, unit: string, decimals = 2): string {
return `${formatNumber(e6 / 1_000_000, decimals)} ${unit}`
}
/** Yüzde: `0.123` → `%12,3` (Türkçede % işareti başta) */
export function formatPercent(ratio: number, decimals = 1): string {
return `%${formatNumber(ratio * 100, decimals)}`
}
/**
* Kullanıcının yazdığı tutarı kuruşa çevirir. Geçersizse `null`.
*
* Kabul edilenler: `1250` · `1.250` · `1250,5` · `1.250,50` · `1250.50` (telefon klavyesi)
* Kural: virgül varsa ondalık ayırıcıdır, noktalar binliktir.
* Virgül yoksa ve son noktadan sonra 1–2 rakam varsa o nokta ondalıktır.
* Float hatası olmasın diye hesap metin üzerinden yapılır.
*/
export function parseMoney(input: string): number | null {
let s = input.replace(/[\s₺]/g, '')
if (!s) return null
let negative = false
if (s.startsWith('-')) {
negative = true
s = s.slice(1)
}
let intPart: string
let fracPart = ''
if (s.includes(',')) {
const parts = s.split(',')
if (parts.length !== 2) return null
intPart = parts[0].replace(/\./g, '')
fracPart = parts[1]
} else {
const m = /^(.*)\.(\d{1,2})$/.exec(s)
if (m && !m[1].includes('.')) {
intPart = m[1]
fracPart = m[2]
} else {
intPart = s.replace(/\./g, '')
}
}
if (intPart === '') intPart = '0'
if (!/^\d+$/.test(intPart) || !/^\d{0,2}$/.test(fracPart)) return null
const kurus = Number(intPart) * 100 + Number(fracPart.padEnd(2, '0'))
if (!Number.isSafeInteger(kurus)) return null
return negative ? -kurus : kurus
}
/** `"2026-12-09"` veya ISO zaman damgası → `09.12.2026` */
export function formatDate(value: string | Date): string {
if (typeof value === 'string') {
const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value)
if (m) return `${m[3]}.${m[2]}.${m[1]}`
value = new Date(value)
}
const dd = String(value.getDate()).padStart(2, '0')
const mm = String(value.getMonth() + 1).padStart(2, '0')
return `${dd}.${mm}.${value.getFullYear()}`
}
/** Yerel tarih → `"2026-12-09"` (backend'in takvim tarihi biçimi) */
export function toIsoDate(date: Date): string {
const mm = String(date.getMonth() + 1).padStart(2, '0')
const dd = String(date.getDate()).padStart(2, '0')
return `${date.getFullYear()}-${mm}-${dd}`
}