From 3f4095e61571a2846f01d74e559c16bf81c47d5b Mon Sep 17 00:00:00 2001
From: bilalgursen
Date: Wed, 7 Oct 2026 23:14:34 +0300
Subject: [PATCH] =?UTF-8?q?refactor(hareket):=20sihirbaz=20ad=C4=B1m=20ge?=
=?UTF-8?q?=C3=A7i=C5=9Fi=20sabitleri=20lib/adim-gecisi.ts'e;=20soru=20for?=
=?UTF-8?q?mu=20ayn=C4=B1=20ge=C3=A7i=C5=9Fle?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- adimGecisiVaryantlari / adimGecisi tek kaynak (spring 300/30, opaklık 0,2 s;
hareketi azalt'ta 0,12 s çapraz solma). Sihirbaz davranışı aynı; center
varyantına `position: relative` eklendi ki geri dönen panel mutlak kalmasın.
- SohbetAnketi kontrollü (item/onItemChange); her soru motion.div içinde,
pasif sorular hidden={false} + mutlak/saydam park eder (ilkelin inert'i
kalır), aktif fieldset useMeasure ile ölçülüp boy yayla uzar. Çıkış animasyonu
da çalışır: mergeProps'ta kullanıcı prop'u kazanıyor.
Co-Authored-By: Claude Fable 5.1
---
.../rapor/components/sohbet-anketi.tsx | 106 +++++++++++++-----
.../sihirbaz/components/sihirbaz-adimlar.tsx | 45 ++------
src/lib/adim-gecisi.ts | 65 +++++++++++
3 files changed, 148 insertions(+), 68 deletions(-)
create mode 100644 src/lib/adim-gecisi.ts
diff --git a/src/features/rapor/components/sohbet-anketi.tsx b/src/features/rapor/components/sohbet-anketi.tsx
index e972acd..3636519 100644
--- a/src/features/rapor/components/sohbet-anketi.tsx
+++ b/src/features/rapor/components/sohbet-anketi.tsx
@@ -9,10 +9,21 @@
// danışman mesajındayken açıktır: cevaplandıysa ya da aday başka bir şey
// yazdıysa kapanır ve tek satırlık durumla kalır (sayfa yenilenince kayıttan
// aynı hâlde döner).
+//
+// Sorular arası geçiş sihirbaz adımlarıyla birebir aynı yoldan (lib/adim-gecisi.ts):
+// ileri giderken yeni soru sağdan gelir, eskisi sola çıkar; geri dönerken
+// tersi; boy ölçülen yüksekliğe yayla uzar. İlkel pasif soruyu `hidden`
+// yapar; burada `hidden={false}` ile görünürlük bize geçer (mergeProps'ta
+// kullanıcı prop'u kazanır — @shadcn/react chunk-HBS6WEDP.js `b`), pasif
+// sorular mutlak konumda saydam durur ve ilkelin verdiği `inert`i korur;
+// DOM'da kaldıkları için gönderimde FormData önceki cevapları da toplar.
-import { useId } from "react";
+import { useId, useState } from "react";
import { Check, MessageSquareText } from "lucide-react";
+import { motion, useReducedMotion } from "motion/react";
+import useMeasure from "react-use-measure";
import type { Anket, AnketCevaplari } from "@/lib/ai/sohbet-meta";
+import { adimGecisi, adimGecisiVaryantlari } from "@/lib/adim-gecisi";
import {
Questionnaire,
QuestionnaireActions,
@@ -43,6 +54,12 @@ export function SohbetAnketi({
onGonder: (cevaplar: AnketCevaplari) => void;
}) {
const kimlik = useId();
+ // Aktif soru bizde (kontrollü ilkel): geçiş yönü sorunun sırasından çıkar.
+ const [aktif, setAktif] = useState(anket.sorular[0]?.ad);
+ const [gecisBasladi, setGecisBasladi] = useState(false);
+ // Yalnız aktif sorunun fieldset'i ölçülür; ref soru değişince ona geçer.
+ const [olcumRef, olculer] = useMeasure();
+ const hareketiAzalt = !!useReducedMotion();
if (durum === "cevaplandi" || durum === "kapandi") {
return (
@@ -63,6 +80,11 @@ export function SohbetAnketi({
choices: s.secenekler.map((o) => ({ value: o.deger })),
}));
+ const aktifIndeks = anket.sorular.findIndex((s) => s.ad === aktif);
+ const yukseklik = gecisBasladi && olculer.height > 0 ? olculer.height : "auto";
+ const varyantlar = adimGecisiVaryantlari({ hareketiAzalt, yukseklik });
+ const gecis = adimGecisi(hareketiAzalt);
+
function gonder(e: React.FormEvent) {
e.preventDefault();
if (durum === "gonderiliyor") return;
@@ -86,7 +108,11 @@ export function SohbetAnketi({
aria-labelledby={anket.baslik ? `${kimlik}-baslik` : undefined}
aria-label={anket.baslik ? undefined : "Danışmanın sorusu"}
className="rounded-2xl border border-slate-200 bg-card p-4"
- defaultItem={anket.sorular[0]?.ad}
+ item={aktif}
+ onItemChange={(ad) => {
+ setGecisBasladi(true);
+ setAktif(ad);
+ }}
items={items}
shortcuts="letters"
onSubmit={gonder}
@@ -103,34 +129,54 @@ export function SohbetAnketi({
)}
/>
- {anket.sorular.map((s) => (
-
- {s.soru}
- {s.aciklama ? {s.aciklama} : null}
-
- {s.secenekler.map((o) => (
-
- {o.etiket}
- {o.aciklama ? (
- {o.aciklama}
- ) : null}
-
- ))}
- {s.serbest ? (
-
- ) : null}
-
-
- {s.coklu ? "En az bir seçenek işaretle." : "Bir seçenek seç."}
-
-
- ))}
+ {/* Kayan sorular taşmasın diye kırpılır; 4 px'lik negatif kenar +
+ fieldset'teki p-1 odak halkasına (ring-3) yer bırakır. */}
+
+ {anket.sorular.map((s, i) => {
+ const buAktif = s.ad === aktif;
+ return (
+
+
+ {s.soru}
+ {s.aciklama ? {s.aciklama} : null}
+
+ {s.secenekler.map((o) => (
+
+ {o.etiket}
+ {o.aciklama ? (
+ {o.aciklama}
+ ) : null}
+
+ ))}
+ {s.serbest ? (
+
+ ) : null}
+
+
+ {s.coklu ? "En az bir seçenek işaretle." : "Bir seçenek seç."}
+
+
+
+ );
+ })}
+
diff --git a/src/features/sihirbaz/components/sihirbaz-adimlar.tsx b/src/features/sihirbaz/components/sihirbaz-adimlar.tsx
index 864c6bd..52caba2 100644
--- a/src/features/sihirbaz/components/sihirbaz-adimlar.tsx
+++ b/src/features/sihirbaz/components/sihirbaz-adimlar.tsx
@@ -14,6 +14,7 @@ import useMeasure from "react-use-measure";
import { toast } from "@/components/ui/bildirim";
import { cities } from "turkey-map-react/lib/data";
import { TransitionPanel } from "@/components/core/transition-panel";
+import { adimGecisi, adimGecisiVaryantlari } from "@/lib/adim-gecisi";
import { Button } from "@/components/ui/button";
import { IlSecimHaritasi } from "./il-secim-haritasi";
import { normalizeIlAdi } from "@/lib/harita";
@@ -249,47 +250,15 @@ export function SihirbazAdimlar({
+ {/* Geçiş sabitleri soru formuyla ortak (lib/adim-gecisi.ts) */}
({
- x: hareketiAzalt ? 0 : gecisYonu > 0 ? "100%" : "-100%",
- opacity: 0,
- height: panelYuksekligi,
- position: "relative",
- }),
- center: {
- zIndex: 1,
- x: 0,
- opacity: 1,
- height: panelYuksekligi,
- },
- exit: (gecisYonu: number) => ({
- zIndex: 0,
- x: hareketiAzalt ? 0 : gecisYonu < 0 ? "100%" : "-100%",
- opacity: 0,
- position: "absolute",
- top: 0,
- left: 0,
- width: "100%",
- }),
- }}
- transition={
- hareketiAzalt
- ? // Azaltılmış harekette yalnızca kısa çapraz solma; height/x
- // varsayılan spring'e düşüp oynamasın diye açıkça sıfırlanır.
- {
- opacity: { duration: 0.12 },
- x: { duration: 0 },
- height: { duration: 0 },
- }
- : {
- x: { type: "spring", stiffness: 300, damping: 30 },
- height: { type: "spring", stiffness: 300, damping: 30 },
- opacity: { duration: 0.2 },
- }
- }
+ variants={adimGecisiVaryantlari({
+ hareketiAzalt: !!hareketiAzalt,
+ yukseklik: panelYuksekligi,
+ })}
+ transition={adimGecisi(!!hareketiAzalt)}
>
diff --git a/src/lib/adim-gecisi.ts b/src/lib/adim-gecisi.ts
new file mode 100644
index 0000000..7096570
--- /dev/null
+++ b/src/lib/adim-gecisi.ts
@@ -0,0 +1,65 @@
+// Adım adım ilerleyen panellerin ortak geçişi (DESIGN.md §6): sihirbazın
+// alan → il → öncelik adımları (sihirbaz-adimlar.tsx, TransitionPanel) ile
+// danışmanın soru formundaki sorular (sohbet-anketi.tsx) aynı yoldan gelir
+// gider — yeni adım ileri giderken sağdan, geri dönerken soldan kayar; boy
+// ölçülen yüksekliğe yayla uzar. Sabitler yalnız burada; iki yerde de bu iki
+// fonksiyon çağrılır, kopyası yazılmaz. Yay sekmesiz (stiffness 300 /
+// damping 30), opaklık 0,2 s. "Hareketi azalt"ta kayma ve boy yayı yok,
+// yalnız 0,12 s'lik çapraz solma.
+
+import type { Transition, Variants } from "motion/react";
+
+/**
+ * enter / center / exit varyantları; `custom` geçiş yönüdür (1 ileri, -1
+ * geri). Giren panel akışta (`relative`) durur, çıkan panel mutlak konuma
+ * alınır ki ikisi aynı anda üst üste kayabilsin. `yukseklik` ölçülen boy
+ * (ilk ölçümden önce "auto").
+ */
+export function adimGecisiVaryantlari({
+ hareketiAzalt,
+ yukseklik,
+}: {
+ hareketiAzalt: boolean;
+ yukseklik: number | "auto";
+}): Variants {
+ return {
+ enter: (gecisYonu: number) => ({
+ x: hareketiAzalt ? 0 : gecisYonu > 0 ? "100%" : "-100%",
+ opacity: 0,
+ height: yukseklik,
+ position: "relative",
+ }),
+ center: {
+ zIndex: 1,
+ x: 0,
+ opacity: 1,
+ height: yukseklik,
+ position: "relative",
+ },
+ exit: (gecisYonu: number) => ({
+ zIndex: 0,
+ x: hareketiAzalt ? 0 : gecisYonu < 0 ? "100%" : "-100%",
+ opacity: 0,
+ position: "absolute",
+ top: 0,
+ left: 0,
+ width: "100%",
+ }),
+ };
+}
+
+/** Geçişin zamanlaması; azaltılmış harekette x/height açıkça sıfırlanır ki
+ * varsayılan yaya düşüp oynamasın. */
+export function adimGecisi(hareketiAzalt: boolean): Transition {
+ return hareketiAzalt
+ ? {
+ opacity: { duration: 0.12 },
+ x: { duration: 0 },
+ height: { duration: 0 },
+ }
+ : {
+ x: { type: "spring", stiffness: 300, damping: 30 },
+ height: { type: "spring", stiffness: 300, damping: 30 },
+ opacity: { duration: 0.2 },
+ };
+}