feat(ux): listeden silme geri alınabilir; better-ui skill'i eklendi

- Çekmecede "Listeyi temizle" ve satır çıkarma (düğme ve kaydırma)
  anında siliyordu, geri dönüş yoktu. Artık 8 sn "Geri al" bildirimi;
  çıkanlar eski sıralarına döner, arada eklenenler korunur
- Çekmece kaydırması sayfaya taşmıyor (overscroll-contain)
- .claude/skills/better-ui: Jakub Krehel'in UI cilası skill'i (ui-skills.com);
  başına DESIGN.md önceliği ve çakışan iki değer notu eklendi

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
bilalgursen
2026-09-24 01:29:51 +03:00
parent 169080df9d
commit aef4adc90b
2 changed files with 149 additions and 4 deletions

View File

@@ -12,6 +12,7 @@
// sadeleşir; X butonu her durumda kalır.
import { useState } from "react";
import { toast } from "sonner";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { PixelField } from "@/components/pixel-decor";
@@ -60,6 +61,7 @@ import {
cekmeceAc,
cekmeceKapat,
cikar,
listeOku,
temizle,
useCekmeceAcik,
useManuelListe,
@@ -79,6 +81,36 @@ function uniHref(ad: string): string {
return `/universite/${turkishSlugify(ad.replace(/\s*\([^)]*\)\s*$/, "").trim())}`;
}
/**
* Geri alınabilir silme (Vercel arayüz ilkeleri: yıkıcı işlem ya onay ya
* geri al penceresi ister; Apple: beklenen işlemde uyarı penceresi açma).
* Çıkan tercihler eski sıralarına geri konur; arada eklenenler kaybolmaz.
*/
function geriAlinabilirCikar(ids: string[], baslik: string) {
const onceki = listeOku();
const cikanlar = onceki
.map((t, sira) => ({ t, sira }))
.filter(({ t }) => ids.includes(t.id));
if (cikanlar.length === 0) return;
if (ids.length === onceki.length) temizle();
else for (const id of ids) cikar(id);
toast(baslik, {
duration: 8000,
action: {
label: "Geri al",
onClick: () => {
const simdi = listeOku().filter(
(t) => !cikanlar.some((c) => c.t.id === t.id),
);
for (const { t, sira } of cikanlar) {
simdi.splice(Math.min(sira, simdi.length), 0, t);
}
yenidenSirala(simdi.slice(0, MANUEL_LISTE_MAX));
},
},
});
}
export function ListeCekmecesi() {
const router = useRouter();
const acik = useCekmeceAcik();
@@ -132,7 +164,12 @@ export function ListeCekmecesi() {
{liste.length > 0 ? (
<button
type="button"
onClick={temizle}
onClick={() =>
geriAlinabilirCikar(
liste.map((t) => t.id),
`Listen temizlendi · ${liste.length} program çıkarıldı`,
)
}
title="Listeyi temizle"
className="absolute top-3 left-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]"
>
@@ -201,7 +238,7 @@ export function ListeCekmecesi() {
</DrawerHeader>
<div
className="min-h-0 flex-1 overflow-x-hidden overflow-y-auto px-4 py-2"
className="min-h-0 flex-1 overflow-x-hidden overflow-y-auto overscroll-contain px-4 py-2"
>
{liste.length === 0 ? (
<div className="relative flex flex-col items-center justify-center gap-2 overflow-hidden px-4 py-16 text-center">
@@ -361,7 +398,7 @@ function CekmeceSatiri({
info.offset.x > SIL_ESIK ||
(info.offset.x > 32 && info.velocity.x > 500)
) {
cikar(tercih.id);
geriAlinabilirCikar([tercih.id], `${tercih.isim} çıkarıldı`);
}
};
@@ -450,7 +487,9 @@ function CekmeceSatiri({
</span>
<button
type="button"
onClick={() => cikar(tercih.id)}
onClick={() =>
geriAlinabilirCikar([tercih.id], `${tercih.isim} çıkarıldı`)
}
onPointerDown={(e) => e.stopPropagation()} // kaydırmayı tetiklemesin
className="inline-flex size-11 shrink-0 cursor-pointer items-center justify-center rounded-full border border-slate-200 bg-white text-slate-500 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.92]"
>