diff --git a/.claude/skills/better-ui/SKILL.md b/.claude/skills/better-ui/SKILL.md new file mode 100644 index 0000000..55d8fda --- /dev/null +++ b/.claude/skills/better-ui/SKILL.md @@ -0,0 +1,106 @@ +--- +name: better-ui +description: Design engineering principles for making interfaces feel polished. Use when building UI components, reviewing frontend code, implementing animations, hover states, shadows, borders, micro-interactions, enter/exit animations, or any visual detail work. Triggers on UI polish, design details, "make it feel better", "feels off", stagger animations, border radius, optical alignment, image outlines, box shadows. +--- + +> **KolayTercih notu (25 Eyl 2026, Bilal'in isteğiyle eklendi).** Kaynak: Jakub Krehel, +> https://github.com/jakubkrehel/skills (ui-skills.com üzerinden okundu, elle kopyalandı). +> Bu projede `docs/tasarim/DESIGN.md` önceliklidir. Çakışan iki nokta: +> basınca küçülme bizde `scale-[0.97]` (geniş satır `0.99`, çarpı `0.96`), buradaki `0.96` +> değil; kart ve satırlarda yapıyı `border-slate-200` verir, gölge yalnız §4.4'teki kadar. +> Görsel ayar yaparken önce DESIGN.md, sonra bu dosya. + +# UI polish + +Polish comes from a pile of small details that compound. This skill is the reference for which are worth having and what values they take. + +When reviewing, slow the interface down. What feels off at 10% speed is what is subtly wrong at full speed. + +Keep the project's component library, tokens and density, and match its motion language except where a rule below prescribes an exact interaction. + +Every duration, curve, scale and blur below is a specific value, not a range to approximate. cubic-bezier(0.2, 0, 0, 1) is not cubic-bezier(0.4, 0, 0.2, 1), and 0.96 is not 0.95. Use what is written. + +## Concentric border radius + +Outer radius = inner radius + padding. Mismatched radii on nested elements is the most common thing that makes an interface feel off. + +## Optical over geometric alignment + +When geometric centering looks off, align optically. Buttons with icons, play triangles and asymmetric icons all need a manual nudge. + +## Shadows for elevation, borders for structure + +Where a border exists only to create depth, prefer layered transparent box-shadow values. Keep borders that communicate structure or state: dividers, separators and selected or focus states. + +## Interruptible animations + +Use CSS transitions for interactive state changes, because they can be interrupted mid-animation. Reserve keyframes for staged sequences that run once. + +## Split and stagger enter animations + +For an infrequent staged entrance where sequence communicates hierarchy, break the content into semantic chunks and stagger them by ~100ms. Animating one container gets you less for the same cost. Leave high-frequency interactions unstaggered. + +## Subtle exit animations + +Use a small fixed translateY rather than full height. Exits should be softer than enters. Use ease-out for both directions. + +## Contextual icon animations + +Animate icons with opacity, scale and blur rather than toggling visibility. Use exactly these values: scale 0.25 to 1, opacity 0 to 1, blur 4px to 0px. + +With a motion library (motion or framer-motion in package.json), match that package's import path. Use transition: { type: "spring", duration: 0.3, bounce: 0 }. Bounce is always 0. + +Without one, keep both icons in the DOM with one absolutely positioned, and cross-fade with cubic-bezier(0.2, 0, 0, 1). + +## Image outlines + +Give images a 1px outline at low opacity for consistent depth. Pure black in light mode (oklch(0 0 0 / 0.1)), pure white in dark (oklch(1 0 0 / 0.1)). Never a near-black like slate or zinc and never a tinted neutral. + +## Scale on press + +A scale on click gives a button tactile feedback (upstream value 0.96; this project uses 0.97, see note above). Add a static prop to switch it off where motion would distract. + +## Skip animation on page load + +Use initial={false} on AnimatePresence to keep enter animations off the first render. Check that it leaves intentional page entrances intact. + +## Suppress transitions on theme switch + +A theme flip changes color, background, border and shadow on nearly every element at once. Inject *,*::before,*::after{transition:none !important}, force a reflow, then remove it on the next frame. + +## Transition only what changes + +Always name the exact properties: transition-property: scale, opacity. Tailwind's transition-transform covers transform, translate, scale, rotate. + +## Use will-change sparingly + +Only for transform, opacity and filter. Never will-change: all. Add it when you see first-frame stutter, not before. + +## Match icon stroke to text weight + +1.5px stroke beside regular (400) text, 2px beside semibold (600). One stroke weight per icon set and one icon library per surface. + +## One SVG, recolored per state + +Icons use currentColor and take hover, selected and disabled states from CSS color and opacity. Outline is the default variant; fill marks the active state. + +## Motion restraint + +Give high-frequency interactions instant feedback, or a transition of 150ms or less on opacity and color. Every animated state change also needs a static cue: color, an icon, or a label. Motion is never the only feedback channel. + +## Before you finish + +| Mistake | Fix | +|---|---| +| Icons look off-center | Nudge optically with padding, or fix the SVG | +| Jarring staged entrance or exit | Stagger infrequent entrances; keep exits subtle | +| Theme toggle crossfades the whole page | Disable transitions for the swap, force a reflow, restore on the next frame | +| transition: all on elements | Specify exact properties | +| First-frame animation stutter | Add will-change: transform (sparingly) | +| Hairline icon beside bold text | Match the stroke width to the text weight | + +## Reporting + +Severity. HIGH breaks an interaction, makes motion unusable, or leaves a state change visible only while the animation runs. MEDIUM is a visible inconsistency in surfaces, icons, or motion. LOW is isolated polish. + +Format: group findings under the principle each violates, ordered by severity, one row per root cause: Severity | Location | Before | After | Why. End with Block when any HIGH remains, Approve otherwise. Report every check you could not run as Not verified. diff --git a/src/features/liste/components/liste-cekmecesi.tsx b/src/features/liste/components/liste-cekmecesi.tsx index 4332389..ab1960e 100644 --- a/src/features/liste/components/liste-cekmecesi.tsx +++ b/src/features/liste/components/liste-cekmecesi.tsx @@ -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 ? (