feat(ui): Arc kayan vurgulu seçim hapları ve sayı rulosu
- SecimHaplari: tema seçici (kapsül) ve puan türü çipleri (hero + CTA formu); seçili vurgu layoutId ile seçenekten seçeneğe kayar. - SayiRulo: değişen sayılar (listem rozeti, çekmece ve durum satırı sayacı, kredi hapları, dilim tablosu sayacı, giriş yeniden gönder sayacı, kaydet bandı, üst şerit geri sayımı) rulo gibi döner; @number-flow/react kaldırıldı. - DESIGN.md §4.21–4.22, components.json @uiarc kaydı. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -23,6 +23,7 @@
|
|||||||
},
|
},
|
||||||
"registries": {
|
"registries": {
|
||||||
"@skiper-ui": "https://skiper-ui.com/registry/{name}.json",
|
"@skiper-ui": "https://skiper-ui.com/registry/{name}.json",
|
||||||
"@magicui": "https://magicui.design/r/{name}"
|
"@magicui": "https://magicui.design/r/{name}",
|
||||||
|
"@uiarc": "https://uiarc.dev/r/{name}.json"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -686,6 +686,26 @@ genişletilebilir olmalı; UI için prompt-kit kullan."
|
|||||||
zaman yazılıdır (§2.2). Sohbet ilerlese de kalır. Yeni bir liste uyarısı gerekince
|
zaman yazılıdır (§2.2). Sohbet ilerlese de kalır. Yeni bir liste uyarısı gerekince
|
||||||
`listeUyarilari`'na eklenir, sayfaya yeni kutu açılmaz.
|
`listeUyarilari`'na eklenir, sayfaya yeni kutu açılmaz.
|
||||||
|
|
||||||
|
### 4.21 Sayı rulosu — `SayiRulo`, `src/components/ui/sayi-rulo.tsx`
|
||||||
|
Ekranda **değişen** sayı (üst çubuk rozeti ve kredi hapı, "Listende 5/24", çekmece sayacı,
|
||||||
|
danışman kredi rozeti, "40 / 312 program", üst şeridin sezon geri sayımı, giriş formunun
|
||||||
|
"Yeniden gönder (59)" sayacı, kaydet bildirimindeki seçim sayısı) rulo gibi döner:
|
||||||
|
eklemede yukarı, çıkarmada aşağı; basamak artınca yeni sütun yayla açılır. Yazı boyu/rengi
|
||||||
|
kapsayıcıdan gelir. Yay §6 varsayılanı; hareketi azalt'ta düz metin; ekran okuyucu düz metni
|
||||||
|
okur. Kaynak Arc UI "Number field" (uiarc.dev, 6 Eki 2026); oradaki −/+ adım butonları ve
|
||||||
|
sürükleme alınmadı — sıralama girişi `Input` hapı olarak kalır (§4.6), rulo yalnız gösterime
|
||||||
|
takılır. Durağan sayıya (taban sıralaması, kontenjan) takılmaz. Geri sayım gibi basamak
|
||||||
|
dolgusu isteyen sayaçta `enAzBasamak={2}` ("05") ve sabit `yon={-1}` (00 → 59 sarması da aşağı
|
||||||
|
döner) verilir; sitede ikinci bir rakam kütüphanesi yoktur (`@number-flow/react` 6 Eki 2026'da
|
||||||
|
kaldırıldı, üst şerit de bu ruloyu kullanır).
|
||||||
|
|
||||||
|
### 4.22 Kayan vurgulu seçim hapları — `SecimHaplari`, `src/components/ui/secim-haplari.tsx`
|
||||||
|
Tek seçimli hap grubunda seçili vurgu yeniden boyanmaz, tek yüzey olarak seçenekten seçeneğe
|
||||||
|
**kayar** (`layoutId`). `ayrik`: kenarlıklı ayrı haplar + mavi dolu vurgu (puan türü çipleri —
|
||||||
|
hero ve CTA formu). `kapsul`: tek kapsülde koyu hap, yazı `text-slate-50` (tema seçici). Yay §6;
|
||||||
|
hareketi azalt'ta vurgu yerinde belirir. Kaynak Arc UI "Chip group" (6 Eki 2026). Çok seçimli
|
||||||
|
çipler (sihirbaz alan/il) bu desene girmez — orada her çip kendi başına yanar.
|
||||||
|
|
||||||
## 5. Yerleşim
|
## 5. Yerleşim
|
||||||
|
|
||||||
### 5.1 Genel
|
### 5.1 Genel
|
||||||
|
|||||||
@@ -31,7 +31,6 @@
|
|||||||
"@ai-sdk/react": "^4.0.130",
|
"@ai-sdk/react": "^4.0.130",
|
||||||
"@cds/city": "^1.1.0",
|
"@cds/city": "^1.1.0",
|
||||||
"@libsql/client": "^0.17.4",
|
"@libsql/client": "^0.17.4",
|
||||||
"@number-flow/react": "^0.6.2",
|
|
||||||
"@shadcn/helpers": "^0.2.0",
|
"@shadcn/helpers": "^0.2.0",
|
||||||
"ai": "^7.0.127",
|
"ai": "^7.0.127",
|
||||||
"better-auth": "^1.6.23",
|
"better-auth": "^1.6.23",
|
||||||
|
|||||||
28
pnpm-lock.yaml
generated
28
pnpm-lock.yaml
generated
@@ -20,9 +20,6 @@ importers:
|
|||||||
'@libsql/client':
|
'@libsql/client':
|
||||||
specifier: ^0.17.4
|
specifier: ^0.17.4
|
||||||
version: 0.17.4
|
version: 0.17.4
|
||||||
'@number-flow/react':
|
|
||||||
specifier: ^0.6.2
|
|
||||||
version: 0.6.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
|
||||||
'@shadcn/helpers':
|
'@shadcn/helpers':
|
||||||
specifier: ^0.2.0
|
specifier: ^0.2.0
|
||||||
version: 0.2.0(ai@7.0.127(zod@4.4.3))
|
version: 0.2.0(ai@7.0.127(zod@4.4.3))
|
||||||
@@ -1289,12 +1286,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==}
|
resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==}
|
||||||
engines: {node: '>=12.4.0'}
|
engines: {node: '>=12.4.0'}
|
||||||
|
|
||||||
'@number-flow/react@0.6.2':
|
|
||||||
resolution: {integrity: sha512-WjZuV4aA+vhRCgCF+adGLgFVVAJ8vdvq6EchRT2FiBIgElTXtDOA3YgkcMr9UHpvpS9v4H70AB5wZv0D9jc3QA==}
|
|
||||||
peerDependencies:
|
|
||||||
react: ^18 || ^19
|
|
||||||
react-dom: ^18 || ^19
|
|
||||||
|
|
||||||
'@opentelemetry/semantic-conventions@1.43.0':
|
'@opentelemetry/semantic-conventions@1.43.0':
|
||||||
resolution: {integrity: sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg==}
|
resolution: {integrity: sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg==}
|
||||||
engines: {node: '>=14'}
|
engines: {node: '>=14'}
|
||||||
@@ -3547,9 +3538,6 @@ packages:
|
|||||||
jiti:
|
jiti:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
esm-env@1.2.2:
|
|
||||||
resolution: {integrity: sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==}
|
|
||||||
|
|
||||||
espree@10.4.0:
|
espree@10.4.0:
|
||||||
resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==}
|
resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
@@ -4692,9 +4680,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==}
|
resolution: {integrity: sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
number-flow@0.6.2:
|
|
||||||
resolution: {integrity: sha512-MCnImG4Q5vPwhSXnov56nOuyyKn6LC+Qd7II1UiKc+ACRtug5iAtn0+CwXNxM38AC5lSowEY+oYEtZX2qMnUyw==}
|
|
||||||
|
|
||||||
oauth-sign@0.9.0:
|
oauth-sign@0.9.0:
|
||||||
resolution: {integrity: sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==}
|
resolution: {integrity: sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==}
|
||||||
|
|
||||||
@@ -6693,13 +6678,6 @@ snapshots:
|
|||||||
|
|
||||||
'@nolyfill/is-core-module@1.0.39': {}
|
'@nolyfill/is-core-module@1.0.39': {}
|
||||||
|
|
||||||
'@number-flow/react@0.6.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
|
|
||||||
dependencies:
|
|
||||||
esm-env: 1.2.2
|
|
||||||
number-flow: 0.6.2
|
|
||||||
react: 19.2.4
|
|
||||||
react-dom: 19.2.4(react@19.2.4)
|
|
||||||
|
|
||||||
'@opentelemetry/semantic-conventions@1.43.0': {}
|
'@opentelemetry/semantic-conventions@1.43.0': {}
|
||||||
|
|
||||||
'@postman/form-data@3.1.1':
|
'@postman/form-data@3.1.1':
|
||||||
@@ -9109,8 +9087,6 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
esm-env@1.2.2: {}
|
|
||||||
|
|
||||||
espree@10.4.0:
|
espree@10.4.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
acorn: 8.17.0
|
acorn: 8.17.0
|
||||||
@@ -10435,10 +10411,6 @@ snapshots:
|
|||||||
path-key: 4.0.0
|
path-key: 4.0.0
|
||||||
unicorn-magic: 0.3.0
|
unicorn-magic: 0.3.0
|
||||||
|
|
||||||
number-flow@0.6.2:
|
|
||||||
dependencies:
|
|
||||||
esm-env: 1.2.2
|
|
||||||
|
|
||||||
oauth-sign@0.9.0: {}
|
oauth-sign@0.9.0: {}
|
||||||
|
|
||||||
object-assign@4.1.1: {}
|
object-assign@4.1.1: {}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { SayiRulo } from "@/components/ui/sayi-rulo";
|
||||||
import { startTransition, useEffect, useState } from "react";
|
import { startTransition, useEffect, useState } from "react";
|
||||||
import NumberFlow, { NumberFlowGroup } from "@number-flow/react";
|
|
||||||
import { CalendarDays, Clock3 } from "lucide-react";
|
import { CalendarDays, Clock3 } from "lucide-react";
|
||||||
import {
|
import {
|
||||||
BANT_ARIA,
|
BANT_ARIA,
|
||||||
@@ -131,34 +131,18 @@ export function SiteTopBanner() {
|
|||||||
: "Geri sayım yükleniyor"
|
: "Geri sayım yükleniyor"
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<NumberFlowGroup>
|
{/* Rulo rakam (DESIGN.md §4.21): her birim kendi tekerleğinde
|
||||||
|
aşağı döner; 00 → 59 sarması da aşağı (yon=-1). */}
|
||||||
{BIRIMLER.map(({ anahtar, etiket }) => (
|
{BIRIMLER.map(({ anahtar, etiket }) => (
|
||||||
<div className="flex items-baseline gap-0.5" key={anahtar}>
|
<div className="flex items-baseline gap-0.5" key={anahtar}>
|
||||||
<dt className="sr-only">{etiket}</dt>
|
<dt className="sr-only">{etiket}</dt>
|
||||||
<dd className="font-semibold">
|
<dd className="font-semibold">
|
||||||
{geriSayim ? (
|
{geriSayim ? (
|
||||||
<NumberFlow
|
<SayiRulo
|
||||||
value={geriSayim[anahtar]}
|
deger={geriSayim[anahtar]}
|
||||||
trend={-1}
|
enAzBasamak={2}
|
||||||
format={{
|
yon={-1}
|
||||||
minimumIntegerDigits: 2,
|
className="min-w-[2ch] justify-end"
|
||||||
useGrouping: false,
|
|
||||||
}}
|
|
||||||
transformTiming={{
|
|
||||||
duration: 280,
|
|
||||||
easing: "cubic-bezier(0.23, 1, 0.32, 1)",
|
|
||||||
}}
|
|
||||||
spinTiming={{
|
|
||||||
duration: 280,
|
|
||||||
easing: "cubic-bezier(0.23, 1, 0.32, 1)",
|
|
||||||
}}
|
|
||||||
opacityTiming={{
|
|
||||||
duration: 180,
|
|
||||||
easing: "ease-out",
|
|
||||||
}}
|
|
||||||
willChange={anahtar === "saniye"}
|
|
||||||
className="inline-block min-w-[2ch] text-right"
|
|
||||||
aria-hidden
|
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
"--"
|
"--"
|
||||||
@@ -169,7 +153,6 @@ export function SiteTopBanner() {
|
|||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</NumberFlowGroup>
|
|
||||||
</dl>
|
</dl>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,20 +3,22 @@
|
|||||||
import { useSyncExternalStore } from "react";
|
import { useSyncExternalStore } from "react";
|
||||||
import { useTheme } from "next-themes";
|
import { useTheme } from "next-themes";
|
||||||
import { Monitor, Moon, Sun } from "lucide-react";
|
import { Monitor, Moon, Sun } from "lucide-react";
|
||||||
import { cn } from "@/lib/utils";
|
import { SecimHaplari } from "@/components/ui/secim-haplari";
|
||||||
|
|
||||||
const SECENEKLER = [
|
const SECENEKLER = [
|
||||||
{ deger: "light", etiket: "Açık", ikon: Sun },
|
{ deger: "light", etiket: "Açık", ikon: Sun },
|
||||||
{ deger: "dark", etiket: "Koyu", ikon: Moon },
|
{ deger: "dark", etiket: "Koyu", ikon: Moon },
|
||||||
{ deger: "system", etiket: "Sistem", ikon: Monitor },
|
{ deger: "system", etiket: "Sistem", ikon: Monitor },
|
||||||
] as const;
|
] as const;
|
||||||
|
type Tema = (typeof SECENEKLER)[number]["deger"];
|
||||||
|
|
||||||
const aboneOlma = () => () => {};
|
const aboneOlma = () => () => {};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tema seçici: açık / koyu / sistem (DESIGN.md §2.3). Seçili tema yalnızca
|
* Tema seçici: açık / koyu / sistem (DESIGN.md §2.3). Seçili tema yalnızca
|
||||||
* tarayıcıda bilinir; sunucu HTML'inde hiçbir seçenek işaretli gelmez,
|
* tarayıcıda bilinir; sunucu HTML'inde hiçbir seçenek işaretli gelmez,
|
||||||
* hydration sonrası işaretlenir (yerleşim oynamaz).
|
* hydration sonrası işaretlenir (yerleşim oynamaz). Koyu vurgu seçenekten
|
||||||
|
* seçeneğe kayar (secim-haplari.tsx, kapsül varyantı).
|
||||||
*/
|
*/
|
||||||
export function TemaSecici({ className }: { className?: string }) {
|
export function TemaSecici({ className }: { className?: string }) {
|
||||||
const { theme, setTheme } = useTheme();
|
const { theme, setTheme } = useTheme();
|
||||||
@@ -25,37 +27,19 @@ export function TemaSecici({ className }: { className?: string }) {
|
|||||||
() => true,
|
() => true,
|
||||||
() => false,
|
() => false,
|
||||||
);
|
);
|
||||||
|
const secili = tarayicida
|
||||||
|
? (SECENEKLER.find((s) => s.deger === theme)?.deger ?? null)
|
||||||
|
: null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<SecimHaplari<Tema>
|
||||||
role="radiogroup"
|
ariaLabel="Tema"
|
||||||
aria-label="Tema"
|
secenekler={SECENEKLER}
|
||||||
className={cn(
|
deger={secili}
|
||||||
"inline-flex items-center gap-1 rounded-full border border-slate-200 bg-card p-1",
|
onChange={setTheme}
|
||||||
className,
|
varyant="kapsul"
|
||||||
)}
|
boyut="md"
|
||||||
>
|
className={className}
|
||||||
{SECENEKLER.map(({ deger, etiket, ikon: Ikon }) => {
|
/>
|
||||||
const secili = tarayicida && theme === deger;
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
key={deger}
|
|
||||||
type="button"
|
|
||||||
role="radio"
|
|
||||||
aria-checked={secili}
|
|
||||||
onClick={() => setTheme(deger)}
|
|
||||||
className={cn(
|
|
||||||
"inline-flex h-9 cursor-pointer items-center gap-1.5 rounded-full px-3 text-sm font-semibold transition-[background-color,color,transform] duration-150 outline-none focus-visible:ring-3 focus-visible:ring-ring/50 active:scale-[0.97]",
|
|
||||||
secili
|
|
||||||
? "bg-slate-900 text-slate-50"
|
|
||||||
: "text-slate-600 hover:text-orange-500",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Ikon className="size-4" aria-hidden />
|
|
||||||
{etiket}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
172
src/components/ui/sayi-rulo.tsx
Normal file
172
src/components/ui/sayi-rulo.tsx
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
// Rulo rakam (odometer): sayı değişince her basamak kendi tekerleğinde döner —
|
||||||
|
// artarken yukarı, azalırken aşağı; 9 → 0 sarar, 10 → 9 geri döner. Basamak
|
||||||
|
// sayısı değişince (9 → 10) yeni sütun genişliğini yayla açar, kalkan sütun
|
||||||
|
// kapanır; sütunlar basamak değerine göre anahtarlı olduğundan birler hanesi
|
||||||
|
// hep birler hanesi kalır.
|
||||||
|
//
|
||||||
|
// Kaynak: Arc UI "Number field" (uiarc.dev) rakam rulosu. Oradaki adım
|
||||||
|
// butonları, sürükleyerek değiştirme ve sınır notu alınmadı — 6 haneli
|
||||||
|
// sıralama girişine uymuyor; rulo yalnız EKRANDA DEĞİŞEN sayıya takılır
|
||||||
|
// (liste sayacı gibi). Yay DESIGN.md §6 varsayılanı (sekmesiz, 0,35 s);
|
||||||
|
// hareketi azalt'ta düz metin. Ekran okuyucu düz metni okur, rulo aria-hidden.
|
||||||
|
|
||||||
|
import { useLayoutEffect, useRef, useState } from "react";
|
||||||
|
import {
|
||||||
|
AnimatePresence,
|
||||||
|
animate,
|
||||||
|
motion,
|
||||||
|
useMotionValue,
|
||||||
|
useReducedMotion,
|
||||||
|
useTransform,
|
||||||
|
type MotionValue,
|
||||||
|
type Variants,
|
||||||
|
} from "motion/react";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const YAY = { type: "spring", bounce: 0, duration: 0.35 } as const;
|
||||||
|
const RAKAMLAR = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
|
||||||
|
const BICIMLER = new Map<number, Intl.NumberFormat>();
|
||||||
|
/** Basamak dolgusu isteyen sayaç (geri sayım "05") binlik ayracı kullanmaz. */
|
||||||
|
function bicimAl(enAzBasamak: number): Intl.NumberFormat {
|
||||||
|
let b = BICIMLER.get(enAzBasamak);
|
||||||
|
if (!b) {
|
||||||
|
b = new Intl.NumberFormat("tr-TR", {
|
||||||
|
numberingSystem: "latn",
|
||||||
|
minimumIntegerDigits: enAzBasamak,
|
||||||
|
useGrouping: enAzBasamak <= 1,
|
||||||
|
});
|
||||||
|
BICIMLER.set(enAzBasamak, b);
|
||||||
|
}
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
|
||||||
|
type Parca =
|
||||||
|
| { anahtar: string; rakam: number }
|
||||||
|
| { anahtar: string; metin: string };
|
||||||
|
|
||||||
|
/** Biçimlenmiş sayıyı basamak değerine göre anahtarlı sütunlara böler. */
|
||||||
|
function parcala(deger: number, bicim: Intl.NumberFormat): Parca[] {
|
||||||
|
const parcalar = bicim.formatToParts(deger);
|
||||||
|
let basamak = parcalar.reduce(
|
||||||
|
(n, p) => n + (p.type === "integer" ? p.value.length : 0),
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
return parcalar.flatMap((p, i): Parca[] => {
|
||||||
|
if (p.type === "integer")
|
||||||
|
return [...p.value].map((c) => ({ anahtar: `b${--basamak}`, rakam: Number(c) }));
|
||||||
|
return [
|
||||||
|
{ anahtar: p.type === "group" ? `g${basamak}` : `${p.type}${i}`, metin: p.value },
|
||||||
|
];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sütun açılırken değişim yönünde yükselir, kapanırken aynı yoldan gider (§6:
|
||||||
|
// giriş ve çıkış aynı yoldan).
|
||||||
|
const yuva: Variants = {
|
||||||
|
giris: (yon: number) => ({ width: 0, opacity: 0, y: `${yon * 0.3}em` }),
|
||||||
|
merkez: { width: "auto", opacity: 1, y: 0, transition: YAY },
|
||||||
|
cikis: (yon: number) => ({
|
||||||
|
width: 0,
|
||||||
|
opacity: 0,
|
||||||
|
y: `${yon * -0.3}em`,
|
||||||
|
transition: YAY,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Tekerlekteki bir rakam: tekerleğin konumuna uzaklığı yerini ve görünürlüğünü belirler. */
|
||||||
|
function Glif({ konum, rakam }: { konum: MotionValue<number>; rakam: number }) {
|
||||||
|
const uzaklik = (simdiki: number) =>
|
||||||
|
((((rakam - simdiki) % 10) + 15) % 10) - 5;
|
||||||
|
const y = useTransform(konum, (c) => `${uzaklik(c) * 1.05}em`);
|
||||||
|
const opacity = useTransform(konum, (c) =>
|
||||||
|
Math.max(0, 1 - Math.abs(uzaklik(c)) ** 1.5 * 1.1),
|
||||||
|
);
|
||||||
|
const visibility = useTransform(konum, (c) =>
|
||||||
|
Math.abs(uzaklik(c)) >= 1 ? "hidden" : "visible",
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<motion.span
|
||||||
|
className="absolute inset-0 flex items-center justify-center"
|
||||||
|
style={{ y, opacity, visibility }}
|
||||||
|
>
|
||||||
|
{rakam}
|
||||||
|
</motion.span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Bir basamağın tekerleği: sayının gittiği yönde döner, yarım turdan uzunsa kısa yolu seçer. */
|
||||||
|
function Tekerlek({ rakam, yon }: { rakam: number; yon: number }) {
|
||||||
|
const konum = useMotionValue(rakam);
|
||||||
|
const durum = useRef({ rakam, hedef: rakam });
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
const d = durum.current;
|
||||||
|
if (d.rakam === rakam) return;
|
||||||
|
let fark =
|
||||||
|
yon > 0 ? (rakam - d.rakam + 10) % 10 : -((d.rakam - rakam + 10) % 10);
|
||||||
|
if (Math.abs(fark) > 5) fark -= Math.sign(fark) * 10;
|
||||||
|
d.hedef += fark;
|
||||||
|
d.rakam = rakam;
|
||||||
|
animate(konum, d.hedef, YAY);
|
||||||
|
}, [rakam, yon, konum]);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<span className="invisible">0</span>
|
||||||
|
{RAKAMLAR.map((r) => (
|
||||||
|
<Glif key={r} konum={konum} rakam={r} />
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ekranda değişen bir sayı (liste sayacı gibi). Yazı boyu/rengi/ağırlığı
|
||||||
|
* kapsayıcıdan gelir; `className` yalnız yerleşim için.
|
||||||
|
*/
|
||||||
|
export function SayiRulo({
|
||||||
|
deger,
|
||||||
|
className,
|
||||||
|
enAzBasamak = 1,
|
||||||
|
yon: sabitYon,
|
||||||
|
}: {
|
||||||
|
deger: number;
|
||||||
|
className?: string;
|
||||||
|
/** Basamak dolgusu: 2 → "05". Üst çubuk geri sayımı; binlik ayracı kapanır. */
|
||||||
|
enAzBasamak?: number;
|
||||||
|
/** Dönüş yönünü sabitler: geri sayımda 00 → 59 yukarı değil aşağı dönsün. */
|
||||||
|
yon?: 1 | -1;
|
||||||
|
}) {
|
||||||
|
const azalt = useReducedMotion();
|
||||||
|
const [iz, setIz] = useState({ deger, yon: 1 });
|
||||||
|
if (iz.deger !== deger) setIz({ deger, yon: deger > iz.deger ? 1 : -1 });
|
||||||
|
const yon =
|
||||||
|
sabitYon ?? (iz.deger === deger ? iz.yon : deger > iz.deger ? 1 : -1);
|
||||||
|
const bicim = bicimAl(enAzBasamak);
|
||||||
|
const metin = bicim.format(deger);
|
||||||
|
|
||||||
|
if (azalt) return <span className={className}>{metin}</span>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className={cn("inline-flex", className)}>
|
||||||
|
<span className="sr-only">{metin}</span>
|
||||||
|
<span aria-hidden className="inline-flex">
|
||||||
|
<AnimatePresence initial={false} custom={yon}>
|
||||||
|
{parcala(deger, bicim).map((p) => (
|
||||||
|
<motion.span
|
||||||
|
key={p.anahtar}
|
||||||
|
custom={yon}
|
||||||
|
variants={yuva}
|
||||||
|
initial="giris"
|
||||||
|
animate="merkez"
|
||||||
|
exit="cikis"
|
||||||
|
className="relative inline-block overflow-hidden"
|
||||||
|
>
|
||||||
|
{"rakam" in p ? <Tekerlek rakam={p.rakam} yon={yon} /> : p.metin}
|
||||||
|
</motion.span>
|
||||||
|
))}
|
||||||
|
</AnimatePresence>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
104
src/components/ui/secim-haplari.tsx
Normal file
104
src/components/ui/secim-haplari.tsx
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
// Tek seçimli hap grubu (puan türü, tema): seçili vurgu ayrı ayrı yeniden
|
||||||
|
// boyanmak yerine TEK bir yüzey olarak seçenekten seçeneğe kayar
|
||||||
|
// (`layoutId`). Göz "hangisi seçili" sorusunu hareketin bittiği yerde okur.
|
||||||
|
//
|
||||||
|
// Kaynak: Arc UI "Chip group" / "Floating button group" (uiarc.dev) kayan
|
||||||
|
// vurgu deseni; sınıf dizisi DESIGN.md hap dili. İki varyant:
|
||||||
|
// - `ayrik`: her seçenek kendi kenarlıklı hapı, vurgu mavi dolu hap (hero ve
|
||||||
|
// CTA formundaki puan türü çipleri).
|
||||||
|
// - `kapsul`: tek kapsül içinde koyu hap (üst çubuk "Giriş yap" diliyle aynı,
|
||||||
|
// tema seçici). Koyu hap yazısı `text-slate-50` (§2.3).
|
||||||
|
// Yay §6 varsayılanı; hareketi azalt'ta vurgu kaymaz, yerinde belirir.
|
||||||
|
|
||||||
|
import { useId, type ComponentType } from "react";
|
||||||
|
import { motion, useReducedMotion } from "motion/react";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const YAY = { type: "spring", bounce: 0, duration: 0.35 } as const;
|
||||||
|
|
||||||
|
export interface SecimHapiSecenek<T extends string> {
|
||||||
|
deger: T;
|
||||||
|
etiket: string;
|
||||||
|
ikon?: ComponentType<{ className?: string; "aria-hidden"?: boolean }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SecimHaplari<T extends string>({
|
||||||
|
secenekler,
|
||||||
|
deger,
|
||||||
|
onChange,
|
||||||
|
ariaLabel,
|
||||||
|
varyant = "ayrik",
|
||||||
|
boyut = "sm",
|
||||||
|
className,
|
||||||
|
}: {
|
||||||
|
secenekler: readonly SecimHapiSecenek<T>[];
|
||||||
|
/** `null`: henüz seçim yok (ör. tema hidrasyondan önce) — vurgu çizilmez. */
|
||||||
|
deger: T | null;
|
||||||
|
onChange: (deger: T) => void;
|
||||||
|
ariaLabel: string;
|
||||||
|
varyant?: "ayrik" | "kapsul";
|
||||||
|
boyut?: "sm" | "md";
|
||||||
|
className?: string;
|
||||||
|
}) {
|
||||||
|
const uid = useId();
|
||||||
|
const azalt = useReducedMotion();
|
||||||
|
const kapsul = varyant === "kapsul";
|
||||||
|
const yukseklik = boyut === "md" ? "h-9 px-3 text-sm" : "h-8 px-3 text-xs";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
role="radiogroup"
|
||||||
|
aria-label={ariaLabel}
|
||||||
|
className={cn(
|
||||||
|
"flex flex-wrap",
|
||||||
|
kapsul
|
||||||
|
? "inline-flex items-center gap-1 rounded-full border border-slate-200 bg-card p-1"
|
||||||
|
: "gap-1.5",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{secenekler.map(({ deger: d, etiket, ikon: Ikon }) => {
|
||||||
|
const secili = deger === d;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={d}
|
||||||
|
type="button"
|
||||||
|
role="radio"
|
||||||
|
aria-checked={secili}
|
||||||
|
onClick={() => onChange(d)}
|
||||||
|
className={cn(
|
||||||
|
"relative inline-flex cursor-pointer items-center gap-1.5 rounded-full font-semibold outline-none transition-[color,border-color,background-color,transform] duration-200 focus-visible:ring-3 focus-visible:ring-ring/50 active:scale-[0.97]",
|
||||||
|
yukseklik,
|
||||||
|
kapsul
|
||||||
|
? secili
|
||||||
|
? "text-slate-50"
|
||||||
|
: "text-slate-600 hover:text-orange-500"
|
||||||
|
: secili
|
||||||
|
? "border border-transparent text-white"
|
||||||
|
: "border border-slate-200 bg-card text-slate-600 hover:border-slate-300 hover:bg-slate-50",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{secili ? (
|
||||||
|
<motion.span
|
||||||
|
layoutId={`${uid}-vurgu`}
|
||||||
|
initial={false}
|
||||||
|
transition={azalt ? { duration: 0 } : YAY}
|
||||||
|
className={cn(
|
||||||
|
"absolute inset-0 rounded-full",
|
||||||
|
kapsul ? "bg-slate-900" : "bg-primary",
|
||||||
|
)}
|
||||||
|
aria-hidden
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
{Ikon ? (
|
||||||
|
<Ikon className="relative size-4 shrink-0" aria-hidden />
|
||||||
|
) : null}
|
||||||
|
<span className="relative">{etiket}</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { SayiRulo } from "@/components/ui/sayi-rulo";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { ArrowRight, MailCheck } from "lucide-react";
|
import { ArrowRight, MailCheck } from "lucide-react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@@ -129,11 +130,15 @@ export function GirisForm({ callbackURL }: { callbackURL: string }) {
|
|||||||
disabled={busy !== null || bekleme > 0}
|
disabled={busy !== null || bekleme > 0}
|
||||||
className="cursor-pointer font-medium text-primary underline-offset-2 hover:underline disabled:cursor-default disabled:text-slate-400 disabled:no-underline"
|
className="cursor-pointer font-medium text-primary underline-offset-2 hover:underline disabled:cursor-default disabled:text-slate-400 disabled:no-underline"
|
||||||
>
|
>
|
||||||
{busy === "email"
|
{busy === "email" ? (
|
||||||
? "Gönderiliyor…"
|
"Gönderiliyor…"
|
||||||
: bekleme > 0
|
) : bekleme > 0 ? (
|
||||||
? `Yeniden gönder (${bekleme})`
|
<>
|
||||||
: "Yeniden gönder"}
|
Yeniden gönder (<SayiRulo deger={bekleme} />)
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
"Yeniden gönder"
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import { SayiRulo } from "@/components/ui/sayi-rulo";
|
||||||
import { ClipboardList, Coins } from "lucide-react";
|
import { ClipboardList, Coins } from "lucide-react";
|
||||||
import { getCurrentUser } from "@/lib/session";
|
import { getCurrentUser } from "@/lib/session";
|
||||||
import { ListemButonu } from "@/features/liste/components/listem-butonu";
|
import { ListemButonu } from "@/features/liste/components/listem-butonu";
|
||||||
@@ -69,7 +70,9 @@ export async function UserNav() {
|
|||||||
title="Kredilerin — yüklemek için tıkla"
|
title="Kredilerin — yüklemek için tıkla"
|
||||||
>
|
>
|
||||||
<Coins className="size-3.5 text-amber-500 sm:size-4" aria-hidden />
|
<Coins className="size-3.5 text-amber-500 sm:size-4" aria-hidden />
|
||||||
<span className="tabular-nums">{u.creditBalance}</span>
|
<span className="tabular-nums">
|
||||||
|
<SayiRulo deger={u.creditBalance} />
|
||||||
|
</span>
|
||||||
{/* Etiket yalnızca md–lg arası: daha darda logo kesiliyor, lg üstünde
|
{/* Etiket yalnızca md–lg arası: daha darda logo kesiliyor, lg üstünde
|
||||||
ortadaki nav ile yer dar (konteyner 1152'de sabit). İkon+sayı ve
|
ortadaki nav ile yer dar (konteyner 1152'de sabit). İkon+sayı ve
|
||||||
title her genişlikte anlamı taşıyor. */}
|
title her genişlikte anlamı taşıyor. */}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
// hariç her yerde görünür; kapatma oturum boyunca kalıcıdır (sessionStorage).
|
// hariç her yerde görünür; kapatma oturum boyunca kalıcıdır (sessionStorage).
|
||||||
// Exit-intent/agresif popup YOK (PRD kuralı).
|
// Exit-intent/agresif popup YOK (PRD kuralı).
|
||||||
|
|
||||||
|
import { SayiRulo } from "@/components/ui/sayi-rulo";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
@@ -72,7 +73,7 @@ export function KaydetBanner() {
|
|||||||
Listen yalnızca bu tarayıcıda
|
Listen yalnızca bu tarayıcıda
|
||||||
</p>
|
</p>
|
||||||
<p className="text-sm leading-snug text-slate-500">
|
<p className="text-sm leading-snug text-slate-500">
|
||||||
Giriş yap; {liste.length} seçimin hesabında saklansın, her
|
Giriş yap; <SayiRulo deger={liste.length} /> seçimin hesabında saklansın, her
|
||||||
cihazdan aç{" "}
|
cihazdan aç{" "}
|
||||||
<CihazCifti className="ml-0.5 align-[-3px] text-slate-400" />
|
<CihazCifti className="ml-0.5 align-[-3px] text-slate-400" />
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { useState } from "react";
|
|||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
|
import { SayiRulo } from "@/components/ui/sayi-rulo";
|
||||||
import { CtaBandi } from "@/components/cta-bandi";
|
import { CtaBandi } from "@/components/cta-bandi";
|
||||||
import { UniLogo } from "@/components/uni-logo";
|
import { UniLogo } from "@/components/uni-logo";
|
||||||
import { RiskEtiketi } from "@/components/risk-etiketi";
|
import { RiskEtiketi } from "@/components/risk-etiketi";
|
||||||
@@ -198,7 +199,7 @@ export function ListeCekmecesi() {
|
|||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{liste.length} / {MANUEL_LISTE_MAX} tercih
|
<SayiRulo deger={liste.length} /> / {MANUEL_LISTE_MAX} tercih
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { ArrowRight, Plus } from "lucide-react";
|
import { ArrowRight, Plus } from "lucide-react";
|
||||||
|
import { SayiRulo } from "@/components/ui/sayi-rulo";
|
||||||
import {
|
import {
|
||||||
MANUEL_LISTE_MAX,
|
MANUEL_LISTE_MAX,
|
||||||
cekmeceAc,
|
cekmeceAc,
|
||||||
@@ -41,9 +42,17 @@ export function ListeDurumuSatiri() {
|
|||||||
onClick={cekmeceAc}
|
onClick={cekmeceAc}
|
||||||
className="group inline-flex h-11 cursor-pointer items-center gap-2 rounded-full border border-slate-200 bg-card px-4 text-sm font-semibold text-slate-800 transition-[background-color,border-color,color,transform] duration-150 hover:border-orange-500 hover:bg-orange-50/60 hover:text-orange-500 active:scale-[0.97]"
|
className="group inline-flex h-11 cursor-pointer items-center gap-2 rounded-full border border-slate-200 bg-card px-4 text-sm font-semibold text-slate-800 transition-[background-color,border-color,color,transform] duration-150 hover:border-orange-500 hover:bg-orange-50/60 hover:text-orange-500 active:scale-[0.97]"
|
||||||
>
|
>
|
||||||
Listende {liste.length}/{MANUEL_LISTE_MAX} program
|
<span>
|
||||||
|
Listende <SayiRulo deger={liste.length} />/{MANUEL_LISTE_MAX} program
|
||||||
|
</span>
|
||||||
<span className="font-medium text-slate-500 group-hover:text-orange-500">
|
<span className="font-medium text-slate-500 group-hover:text-orange-500">
|
||||||
{kalan > 0 ? `· ${kalan} yer kaldı` : "· liste tamam"}
|
{kalan > 0 ? (
|
||||||
|
<>
|
||||||
|
· <SayiRulo deger={kalan} /> yer kaldı
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
"· liste tamam"
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
<ArrowRight
|
<ArrowRight
|
||||||
className="size-4 transition-transform duration-150 group-hover:translate-x-0.5"
|
className="size-4 transition-transform duration-150 group-hover:translate-x-0.5"
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
// yaşadığı /listem sayfasına aittir — isim çakışmasın.)
|
// yaşadığı /listem sayfasına aittir — isim çakışmasın.)
|
||||||
|
|
||||||
import { ListChecks } from "lucide-react";
|
import { ListChecks } from "lucide-react";
|
||||||
import { motion, useReducedMotion } from "motion/react";
|
import { SayiRulo } from "@/components/ui/sayi-rulo";
|
||||||
import {
|
import {
|
||||||
MANUEL_LISTE_MAX,
|
MANUEL_LISTE_MAX,
|
||||||
cekmeceDegistir,
|
cekmeceDegistir,
|
||||||
@@ -17,7 +17,6 @@ import { preloadListeCekmecesi } from "./liste-cekmecesi-lazy";
|
|||||||
export function ListemButonu() {
|
export function ListemButonu() {
|
||||||
const liste = useManuelListe();
|
const liste = useManuelListe();
|
||||||
const acik = useCekmeceAcik();
|
const acik = useCekmeceAcik();
|
||||||
const azMotion = useReducedMotion();
|
|
||||||
|
|
||||||
// Liste boşken buton gürültü: tabloya (+) ulaşmamış ziyaretçiye "0/24"
|
// Liste boşken buton gürültü: tabloya (+) ulaşmamış ziyaretçiye "0/24"
|
||||||
// bir şey ifade etmez. İlk program eklenince belirir, liste boşalınca gider.
|
// bir şey ifade etmez. İlk program eklenince belirir, liste boşalınca gider.
|
||||||
@@ -40,16 +39,11 @@ export function ListemButonu() {
|
|||||||
aria-label anlamı taşıyor. UserNav'daki pill'le aynı desen. */}
|
aria-label anlamı taşıyor. UserNav'daki pill'le aynı desen. */}
|
||||||
<span className="hidden">Kendi Listem</span>
|
<span className="hidden">Kendi Listem</span>
|
||||||
{liste.length > 0 ? (
|
{liste.length > 0 ? (
|
||||||
// key=length: her eklemede rozet yeniden doğar ve spring ile oturur
|
// Rozet yerinde durur, rakam rulo gibi döner (eklemede yukarı,
|
||||||
<motion.span
|
// çıkarmada aşağı) — Arc "Number field" rulosu, bkz. sayi-rulo.tsx.
|
||||||
key={liste.length}
|
<span className="flex h-6 min-w-6 items-center justify-center rounded-full bg-orange-500 px-1.5 font-heading text-xs font-bold text-white">
|
||||||
initial={azMotion ? false : { scale: 0.6 }}
|
<SayiRulo deger={liste.length} />
|
||||||
animate={{ scale: 1 }}
|
</span>
|
||||||
transition={{ type: "spring", duration: 0.4, bounce: 0.45 }}
|
|
||||||
className="flex h-6 min-w-6 items-center justify-center rounded-full bg-orange-500 px-1.5 font-heading text-xs font-bold text-white"
|
|
||||||
>
|
|
||||||
{liste.length}
|
|
||||||
</motion.span>
|
|
||||||
) : null}
|
) : null}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import type {
|
|||||||
} from "@/types/yokatlas";
|
} from "@/types/yokatlas";
|
||||||
import { DILIM_ETIKET, riskHesapla } from "@/lib/risk";
|
import { DILIM_ETIKET, riskHesapla } from "@/lib/risk";
|
||||||
import { bolumSayfaSlug, ilSayfaHref, trBaslikDuzeni, uniSayfaSlug } from "@/lib/slug";
|
import { bolumSayfaSlug, ilSayfaHref, trBaslikDuzeni, uniSayfaSlug } from "@/lib/slug";
|
||||||
|
import { SayiRulo } from "@/components/ui/sayi-rulo";
|
||||||
import { TurCipi } from "@/components/tur-cipi";
|
import { TurCipi } from "@/components/tur-cipi";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { VERI_YIL_SAYISI } from "@/lib/veri-yillari";
|
import { VERI_YIL_SAYISI } from "@/lib/veri-yillari";
|
||||||
@@ -306,7 +307,7 @@ export function ProgramTablosu({
|
|||||||
<ChevronDown className="size-4" aria-hidden />
|
<ChevronDown className="size-4" aria-hidden />
|
||||||
</Button>
|
</Button>
|
||||||
<span className="text-sm tabular-nums text-slate-500">
|
<span className="text-sm tabular-nums text-slate-500">
|
||||||
{satirlar.length.toLocaleString("tr-TR")} /{" "}
|
<SayiRulo deger={satirlar.length} /> /{" "}
|
||||||
{toplam.toLocaleString("tr-TR")} program
|
{toplam.toLocaleString("tr-TR")} program
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
import { SayiRulo } from "@/components/ui/sayi-rulo";
|
||||||
import { AnimasyonluMark } from "@/components/animasyonlu-mark";
|
import { AnimasyonluMark } from "@/components/animasyonlu-mark";
|
||||||
import { AracPilleri, aracPilleri } from "./arac-pilleri";
|
import { AracPilleri, aracPilleri } from "./arac-pilleri";
|
||||||
import { SOHBET_MARKDOWN_BILESENLERI, baglantilariIsaretle } from "./sohbet-baglanti";
|
import { SOHBET_MARKDOWN_BILESENLERI, baglantilariIsaretle } from "./sohbet-baglanti";
|
||||||
@@ -643,7 +644,7 @@ export function SohbetClient({
|
|||||||
title="Kredilerin — yüklemek için tıkla"
|
title="Kredilerin — yüklemek için tıkla"
|
||||||
>
|
>
|
||||||
<Coins className="size-3.5" aria-hidden />
|
<Coins className="size-3.5" aria-hidden />
|
||||||
{kredi} kredi
|
<SayiRulo deger={kredi} /> kredi
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { useRouter } from "next/navigation";
|
|||||||
import { ArrowRight, Trophy, X } from "lucide-react";
|
import { ArrowRight, Trophy, X } from "lucide-react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { SecimHaplari } from "@/components/ui/secim-haplari";
|
||||||
import {
|
import {
|
||||||
Drawer,
|
Drawer,
|
||||||
DrawerClose,
|
DrawerClose,
|
||||||
@@ -37,6 +38,7 @@ const PUAN_TURLERI_SECENEK = [
|
|||||||
{ deger: "dil", etiket: "Dil" },
|
{ deger: "dil", etiket: "Dil" },
|
||||||
{ deger: "tyt", etiket: "TYT" },
|
{ deger: "tyt", etiket: "TYT" },
|
||||||
] as const;
|
] as const;
|
||||||
|
type PuanTuru = (typeof PUAN_TURLERI_SECENEK)[number]["deger"];
|
||||||
|
|
||||||
// Kayıtlı profilli CTA'lar /listem boş durumunun dilini taşır (bkz.
|
// Kayıtlı profilli CTA'lar /listem boş durumunun dilini taşır (bkz.
|
||||||
// listem-bos-cta.tsx): büyük turuncu buton. Sınıf seti tek yerde.
|
// listem-bos-cta.tsx): büyük turuncu buton. Sınıf seti tek yerde.
|
||||||
@@ -138,7 +140,7 @@ export function CtaSiraForm({
|
|||||||
const [acik, setAcik] = useState(false);
|
const [acik, setAcik] = useState(false);
|
||||||
const [adim, setAdim] = useState<"sira" | "sihirbaz">("sira");
|
const [adim, setAdim] = useState<"sira" | "sihirbaz">("sira");
|
||||||
const [siralama, setSiralama] = useState("");
|
const [siralama, setSiralama] = useState("");
|
||||||
const [tur, setTur] = useState<string>("say");
|
const [tur, setTur] = useState<PuanTuru>("say");
|
||||||
const [sira, setSira] = useState<number | null>(null);
|
const [sira, setSira] = useState<number | null>(null);
|
||||||
const [facetler, setFacetler] = useState<SihirbazFacetleri | null>(null);
|
const [facetler, setFacetler] = useState<SihirbazFacetleri | null>(null);
|
||||||
const [yukleniyor, setYukleniyor] = useState(false);
|
const [yukleniyor, setYukleniyor] = useState(false);
|
||||||
@@ -236,28 +238,14 @@ export function CtaSiraForm({
|
|||||||
: "flex flex-col gap-3"
|
: "flex flex-col gap-3"
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div
|
{/* Seçili vurgu çipten çipe kayar (Arc kayan vurgu, secim-haplari.tsx). */}
|
||||||
role="radiogroup"
|
<SecimHaplari
|
||||||
aria-label="Puan türü"
|
ariaLabel="Puan türü"
|
||||||
className={cn("flex flex-wrap gap-1.5", kompakt && "lg:shrink-0")}
|
secenekler={PUAN_TURLERI_SECENEK}
|
||||||
>
|
deger={tur}
|
||||||
{PUAN_TURLERI_SECENEK.map((t) => (
|
onChange={setTur}
|
||||||
<button
|
className={cn(kompakt && "lg:shrink-0")}
|
||||||
key={t.deger}
|
/>
|
||||||
type="button"
|
|
||||||
role="radio"
|
|
||||||
aria-checked={tur === t.deger}
|
|
||||||
onClick={() => setTur(t.deger)}
|
|
||||||
className={
|
|
||||||
tur === t.deger
|
|
||||||
? "cursor-pointer rounded-full border border-primary bg-primary px-3 py-1.5 text-xs font-medium text-white transition-colors duration-200"
|
|
||||||
: "cursor-pointer rounded-full border border-slate-200 bg-card px-3 py-1.5 text-xs text-slate-600 transition-colors duration-200 hover:border-slate-300 hover:bg-slate-50"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{t.etiket}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<label htmlFor={alanId} className="sr-only">
|
<label htmlFor={alanId} className="sr-only">
|
||||||
YKS başarı sıralaması
|
YKS başarı sıralaması
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { useRouter } from "next/navigation";
|
|||||||
import { ArrowRight } from "lucide-react";
|
import { ArrowRight } from "lucide-react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { SecimHaplari } from "@/components/ui/secim-haplari";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@@ -35,10 +36,11 @@ const PUAN_TURLERI_SECENEK = [
|
|||||||
{ deger: "dil", etiket: "Dil" },
|
{ deger: "dil", etiket: "Dil" },
|
||||||
{ deger: "tyt", etiket: "TYT" },
|
{ deger: "tyt", etiket: "TYT" },
|
||||||
] as const;
|
] as const;
|
||||||
|
type PuanTuru = (typeof PUAN_TURLERI_SECENEK)[number]["deger"];
|
||||||
|
|
||||||
export function HeroForm() {
|
export function HeroForm() {
|
||||||
const [siralama, setSiralama] = useState("");
|
const [siralama, setSiralama] = useState("");
|
||||||
const [tur, setTur] = useState<string>("say");
|
const [tur, setTur] = useState<PuanTuru>("say");
|
||||||
const [sira, setSira] = useState<number | null>(null);
|
const [sira, setSira] = useState<number | null>(null);
|
||||||
const [facetler, setFacetler] = useState<SihirbazFacetleri | null>(null);
|
const [facetler, setFacetler] = useState<SihirbazFacetleri | null>(null);
|
||||||
const [yukleniyor, setYukleniyor] = useState(false);
|
const [yukleniyor, setYukleniyor] = useState(false);
|
||||||
@@ -140,28 +142,14 @@ export function HeroForm() {
|
|||||||
// max-w-xl: yatay dizilimde placeholder'ın tamamı inputa sığsın diye
|
// max-w-xl: yatay dizilimde placeholder'ın tamamı inputa sığsın diye
|
||||||
// (md'de buton geniş olduğundan inputa ~200px kalıyor, metin kesiliyordu).
|
// (md'de buton geniş olduğundan inputa ~200px kalıyor, metin kesiliyordu).
|
||||||
<div className="flex w-full max-w-xl flex-col items-center gap-3">
|
<div className="flex w-full max-w-xl flex-col items-center gap-3">
|
||||||
<div
|
{/* Seçili vurgu çipten çipe kayar (Arc kayan vurgu, secim-haplari.tsx). */}
|
||||||
role="radiogroup"
|
<SecimHaplari
|
||||||
aria-label="Puan türü"
|
ariaLabel="Puan türü"
|
||||||
className="flex flex-wrap justify-center gap-1.5"
|
secenekler={PUAN_TURLERI_SECENEK}
|
||||||
>
|
deger={tur}
|
||||||
{PUAN_TURLERI_SECENEK.map((t) => (
|
onChange={setTur}
|
||||||
<button
|
className="justify-center"
|
||||||
key={t.deger}
|
/>
|
||||||
type="button"
|
|
||||||
role="radio"
|
|
||||||
aria-checked={tur === t.deger}
|
|
||||||
onClick={() => setTur(t.deger)}
|
|
||||||
className={
|
|
||||||
tur === t.deger
|
|
||||||
? "cursor-pointer rounded-full border border-primary bg-primary px-3 py-1.5 text-xs font-medium text-white transition-colors duration-200"
|
|
||||||
: "cursor-pointer rounded-full border border-slate-200 bg-card px-3 py-1.5 text-xs text-slate-600 transition-colors duration-200 hover:border-slate-300 hover:bg-slate-50"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{t.etiket}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form
|
<form
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
|
|||||||
Reference in New Issue
Block a user