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:
106
.claude/skills/better-ui/SKILL.md
Normal file
106
.claude/skills/better-ui/SKILL.md
Normal file
@@ -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.
|
||||
@@ -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]"
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user