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:
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { SayiRulo } from "@/components/ui/sayi-rulo";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { ArrowRight, MailCheck } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
@@ -129,11 +130,15 @@ export function GirisForm({ callbackURL }: { callbackURL: string }) {
|
||||
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"
|
||||
>
|
||||
{busy === "email"
|
||||
? "Gönderiliyor…"
|
||||
: bekleme > 0
|
||||
? `Yeniden gönder (${bekleme})`
|
||||
: "Yeniden gönder"}
|
||||
{busy === "email" ? (
|
||||
"Gönderiliyor…"
|
||||
) : bekleme > 0 ? (
|
||||
<>
|
||||
Yeniden gönder (<SayiRulo deger={bekleme} />)
|
||||
</>
|
||||
) : (
|
||||
"Yeniden gönder"
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import Link from "next/link";
|
||||
import { SayiRulo } from "@/components/ui/sayi-rulo";
|
||||
import { ClipboardList, Coins } from "lucide-react";
|
||||
import { getCurrentUser } from "@/lib/session";
|
||||
import { ListemButonu } from "@/features/liste/components/listem-butonu";
|
||||
@@ -69,7 +70,9 @@ export async function UserNav() {
|
||||
title="Kredilerin — yüklemek için tıkla"
|
||||
>
|
||||
<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
|
||||
ortadaki nav ile yer dar (konteyner 1152'de sabit). İkon+sayı ve
|
||||
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).
|
||||
// Exit-intent/agresif popup YOK (PRD kuralı).
|
||||
|
||||
import { SayiRulo } from "@/components/ui/sayi-rulo";
|
||||
import { useEffect, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
@@ -72,7 +73,7 @@ export function KaydetBanner() {
|
||||
Listen yalnızca bu tarayıcıda
|
||||
</p>
|
||||
<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ç{" "}
|
||||
<CihazCifti className="ml-0.5 align-[-3px] text-slate-400" />
|
||||
</p>
|
||||
|
||||
@@ -15,6 +15,7 @@ import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { SayiRulo } from "@/components/ui/sayi-rulo";
|
||||
import { CtaBandi } from "@/components/cta-bandi";
|
||||
import { UniLogo } from "@/components/uni-logo";
|
||||
import { RiskEtiketi } from "@/components/risk-etiketi";
|
||||
@@ -198,7 +199,7 @@ export function ListeCekmecesi() {
|
||||
</span>
|
||||
) : (
|
||||
<>
|
||||
{liste.length} / {MANUEL_LISTE_MAX} tercih
|
||||
<SayiRulo deger={liste.length} /> / {MANUEL_LISTE_MAX} tercih
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { ArrowRight, Plus } from "lucide-react";
|
||||
import { SayiRulo } from "@/components/ui/sayi-rulo";
|
||||
import {
|
||||
MANUEL_LISTE_MAX,
|
||||
cekmeceAc,
|
||||
@@ -41,9 +42,17 @@ export function ListeDurumuSatiri() {
|
||||
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]"
|
||||
>
|
||||
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">
|
||||
{kalan > 0 ? `· ${kalan} yer kaldı` : "· liste tamam"}
|
||||
{kalan > 0 ? (
|
||||
<>
|
||||
· <SayiRulo deger={kalan} /> yer kaldı
|
||||
</>
|
||||
) : (
|
||||
"· liste tamam"
|
||||
)}
|
||||
</span>
|
||||
<ArrowRight
|
||||
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.)
|
||||
|
||||
import { ListChecks } from "lucide-react";
|
||||
import { motion, useReducedMotion } from "motion/react";
|
||||
import { SayiRulo } from "@/components/ui/sayi-rulo";
|
||||
import {
|
||||
MANUEL_LISTE_MAX,
|
||||
cekmeceDegistir,
|
||||
@@ -17,7 +17,6 @@ import { preloadListeCekmecesi } from "./liste-cekmecesi-lazy";
|
||||
export function ListemButonu() {
|
||||
const liste = useManuelListe();
|
||||
const acik = useCekmeceAcik();
|
||||
const azMotion = useReducedMotion();
|
||||
|
||||
// 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.
|
||||
@@ -40,16 +39,11 @@ export function ListemButonu() {
|
||||
aria-label anlamı taşıyor. UserNav'daki pill'le aynı desen. */}
|
||||
<span className="hidden">Kendi Listem</span>
|
||||
{liste.length > 0 ? (
|
||||
// key=length: her eklemede rozet yeniden doğar ve spring ile oturur
|
||||
<motion.span
|
||||
key={liste.length}
|
||||
initial={azMotion ? false : { scale: 0.6 }}
|
||||
animate={{ scale: 1 }}
|
||||
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>
|
||||
// Rozet yerinde durur, rakam rulo gibi döner (eklemede yukarı,
|
||||
// çıkarmada aşağı) — Arc "Number field" rulosu, bkz. sayi-rulo.tsx.
|
||||
<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">
|
||||
<SayiRulo deger={liste.length} />
|
||||
</span>
|
||||
) : null}
|
||||
</button>
|
||||
);
|
||||
|
||||
@@ -22,6 +22,7 @@ import type {
|
||||
} from "@/types/yokatlas";
|
||||
import { DILIM_ETIKET, riskHesapla } from "@/lib/risk";
|
||||
import { bolumSayfaSlug, ilSayfaHref, trBaslikDuzeni, uniSayfaSlug } from "@/lib/slug";
|
||||
import { SayiRulo } from "@/components/ui/sayi-rulo";
|
||||
import { TurCipi } from "@/components/tur-cipi";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { VERI_YIL_SAYISI } from "@/lib/veri-yillari";
|
||||
@@ -306,7 +307,7 @@ export function ProgramTablosu({
|
||||
<ChevronDown className="size-4" aria-hidden />
|
||||
</Button>
|
||||
<span className="text-sm tabular-nums text-slate-500">
|
||||
{satirlar.length.toLocaleString("tr-TR")} /{" "}
|
||||
<SayiRulo deger={satirlar.length} /> /{" "}
|
||||
{toplam.toLocaleString("tr-TR")} program
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
} from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { SayiRulo } from "@/components/ui/sayi-rulo";
|
||||
import { AnimasyonluMark } from "@/components/animasyonlu-mark";
|
||||
import { AracPilleri, aracPilleri } from "./arac-pilleri";
|
||||
import { SOHBET_MARKDOWN_BILESENLERI, baglantilariIsaretle } from "./sohbet-baglanti";
|
||||
@@ -643,7 +644,7 @@ export function SohbetClient({
|
||||
title="Kredilerin — yüklemek için tıkla"
|
||||
>
|
||||
<Coins className="size-3.5" aria-hidden />
|
||||
{kredi} kredi
|
||||
<SayiRulo deger={kredi} /> kredi
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useRouter } from "next/navigation";
|
||||
import { ArrowRight, Trophy, X } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { SecimHaplari } from "@/components/ui/secim-haplari";
|
||||
import {
|
||||
Drawer,
|
||||
DrawerClose,
|
||||
@@ -37,6 +38,7 @@ const PUAN_TURLERI_SECENEK = [
|
||||
{ deger: "dil", etiket: "Dil" },
|
||||
{ deger: "tyt", etiket: "TYT" },
|
||||
] as const;
|
||||
type PuanTuru = (typeof PUAN_TURLERI_SECENEK)[number]["deger"];
|
||||
|
||||
// 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.
|
||||
@@ -138,7 +140,7 @@ export function CtaSiraForm({
|
||||
const [acik, setAcik] = useState(false);
|
||||
const [adim, setAdim] = useState<"sira" | "sihirbaz">("sira");
|
||||
const [siralama, setSiralama] = useState("");
|
||||
const [tur, setTur] = useState<string>("say");
|
||||
const [tur, setTur] = useState<PuanTuru>("say");
|
||||
const [sira, setSira] = useState<number | null>(null);
|
||||
const [facetler, setFacetler] = useState<SihirbazFacetleri | null>(null);
|
||||
const [yukleniyor, setYukleniyor] = useState(false);
|
||||
@@ -236,28 +238,14 @@ export function CtaSiraForm({
|
||||
: "flex flex-col gap-3"
|
||||
}
|
||||
>
|
||||
<div
|
||||
role="radiogroup"
|
||||
aria-label="Puan türü"
|
||||
className={cn("flex flex-wrap gap-1.5", kompakt && "lg:shrink-0")}
|
||||
>
|
||||
{PUAN_TURLERI_SECENEK.map((t) => (
|
||||
<button
|
||||
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>
|
||||
{/* Seçili vurgu çipten çipe kayar (Arc kayan vurgu, secim-haplari.tsx). */}
|
||||
<SecimHaplari
|
||||
ariaLabel="Puan türü"
|
||||
secenekler={PUAN_TURLERI_SECENEK}
|
||||
deger={tur}
|
||||
onChange={setTur}
|
||||
className={cn(kompakt && "lg:shrink-0")}
|
||||
/>
|
||||
|
||||
<label htmlFor={alanId} className="sr-only">
|
||||
YKS başarı sıralaması
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useRouter } from "next/navigation";
|
||||
import { ArrowRight } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { SecimHaplari } from "@/components/ui/secim-haplari";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -35,10 +36,11 @@ const PUAN_TURLERI_SECENEK = [
|
||||
{ deger: "dil", etiket: "Dil" },
|
||||
{ deger: "tyt", etiket: "TYT" },
|
||||
] as const;
|
||||
type PuanTuru = (typeof PUAN_TURLERI_SECENEK)[number]["deger"];
|
||||
|
||||
export function HeroForm() {
|
||||
const [siralama, setSiralama] = useState("");
|
||||
const [tur, setTur] = useState<string>("say");
|
||||
const [tur, setTur] = useState<PuanTuru>("say");
|
||||
const [sira, setSira] = useState<number | null>(null);
|
||||
const [facetler, setFacetler] = useState<SihirbazFacetleri | null>(null);
|
||||
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
|
||||
// (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
|
||||
role="radiogroup"
|
||||
aria-label="Puan türü"
|
||||
className="flex flex-wrap justify-center gap-1.5"
|
||||
>
|
||||
{PUAN_TURLERI_SECENEK.map((t) => (
|
||||
<button
|
||||
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>
|
||||
{/* Seçili vurgu çipten çipe kayar (Arc kayan vurgu, secim-haplari.tsx). */}
|
||||
<SecimHaplari
|
||||
ariaLabel="Puan türü"
|
||||
secenekler={PUAN_TURLERI_SECENEK}
|
||||
deger={tur}
|
||||
onChange={setTur}
|
||||
className="justify-center"
|
||||
/>
|
||||
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
|
||||
Reference in New Issue
Block a user