feat: update app.db and BACKLOG.md with new decisions and adjustments
All checks were successful
Deploy / deploy (push) Successful in 7m45s
All checks were successful
Deploy / deploy (push) Successful in 7m45s
- Updated the app.db file to reflect recent changes in the application state. - Modified BACKLOG.md to include new entries regarding the removal of certain wizard steps, adjustments to priority chips, and the introduction of special quota conditions for candidates. - Added details on the decision to link priority chips to data sources and clarified the implications for user experience and data handling.
This commit is contained in:
@@ -4,14 +4,19 @@ import { useEffect, useRef } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
// public/kolay-tercih-mark.svg'nin inline, animasyonlu karşılığı.
|
||||
// İki varyant:
|
||||
// Üç varyant:
|
||||
//
|
||||
// - "rota" (varsayılan): her kare sıradakinin pozisyonuna süzülür (mavi →
|
||||
// şeftalinin yeri, şeftali → turuncunun, turuncu → mavinin), üç adımda
|
||||
// herkes başlangıç formuna döner. Uçuş sırasında kare hafifçe küçülüp
|
||||
// solar, varışta toparlanır. Site header'daki logo hover'da bunu oynatır.
|
||||
// - "zincir": aynı rota, ama kareler aynı anda değil sırayla yürür — bir kare
|
||||
// hedefindeki karenin üzerine konarken alttaki yola çıkar, üçüncü kare boş
|
||||
// kalan yere oturur ve zincir baştan başlar. Danışman avatarı yalnız
|
||||
// danışman veriye bakarken (araç çalışırken) bunu oynatır.
|
||||
// - "nabiz": kareler yerlerinden ayrılmaz; 120° faz farkıyla dalga gibi
|
||||
// küçülüp büyür (danışman panel başlığının sakin, sürekli hâli).
|
||||
// küçülüp büyür (danışman panel başlığının sakin, sürekli hâli; danışman
|
||||
// avatarı da cevap beklenirken böyle atar).
|
||||
//
|
||||
// Performans notu: kareler SVG <rect> değil HTML <span>, animasyon da Web
|
||||
// Animations API ile yalnızca `translate` / `scale` / `opacity` üzerinden
|
||||
@@ -49,6 +54,26 @@ const ROTA_NABIZ_ZAMANLARI = [
|
||||
const ROTA_NABIZ_SCALE = [1, 0.84, 1, 1, 0.84, 1, 1, 0.84, 1, 1];
|
||||
const ROTA_NABIZ_OPACITY = [1, 0.6, 1, 1, 0.6, 1, 1, 0.6, 1, 1];
|
||||
|
||||
// Zincir: iki kalkış arası 450 ms, tek yolculuk 600 ms. Sıradaki kare, gelen
|
||||
// kare yolun ~%94'ündeyken (üzerine konmuşken) kalkar; son 150 ms'de biri
|
||||
// inerken diğeri hızlanır, akış hiç durmaz. Her kare turda 3 yolculuk yapar.
|
||||
const ZINCIR_ADIM = 450;
|
||||
const ZINCIR_YOL = 600;
|
||||
const ZINCIR_SURE = ZINCIR_ADIM * 9;
|
||||
const ZINCIR_YOL_PAYI = ZINCIR_YOL / ZINCIR_SURE;
|
||||
const ZINCIR_KONUM_ZAMANLARI = [
|
||||
...[0, 1, 2].flatMap((etap) => [etap / 3, etap / 3 + ZINCIR_YOL_PAYI]),
|
||||
1,
|
||||
];
|
||||
const ZINCIR_NABIZ_ZAMANLARI = [
|
||||
...[0, 1, 2].flatMap((etap) => [
|
||||
etap / 3,
|
||||
etap / 3 + ZINCIR_YOL_PAYI / 2,
|
||||
etap / 3 + ZINCIR_YOL_PAYI,
|
||||
]),
|
||||
1,
|
||||
];
|
||||
|
||||
// Yerinde nabız: kesintisiz tam-sinüs (dinlenme yok), delay'ler döngüyü üçe
|
||||
// böler — biri büyürken sıradaki küçülür, hiçbir kare beklemez.
|
||||
const NABIZ_SURE = 1800;
|
||||
@@ -60,6 +85,10 @@ const YOL = "cubic-bezier(0.65, 0, 0.35, 1)";
|
||||
// Hover bitince yerine oturma (easeOutQuint): hızlı toparlanır, sarsmadan durur.
|
||||
const DONUS = "cubic-bezier(0.22, 1, 0.36, 1)";
|
||||
const DONUS_SURE = 500;
|
||||
// `oynat` kapanınca yerine oturma: hover'daki kadar aceleci değil — duran kare
|
||||
// yumuşak kalkar, uzun ve yumuşak iner.
|
||||
const YERLES = "cubic-bezier(0.4, 0, 0.2, 1)";
|
||||
const YERLES_SURE = 600;
|
||||
|
||||
/** Karenin `translate` değeri: yüzdeler karenin kendi boyutuna göredir, bu
|
||||
* yüzden işaret hangi boyutta çizilirse çizilsin aynı oran korunur. */
|
||||
@@ -76,10 +105,12 @@ function rota(baslangic: number, boyut: number): string[] {
|
||||
return [p0, p1, p1, p2, p2, p0, p0];
|
||||
}
|
||||
|
||||
type Varyant = "rota" | "zincir" | "nabiz";
|
||||
|
||||
function donguyuBaslat(
|
||||
el: HTMLElement,
|
||||
i: number,
|
||||
varyant: "rota" | "nabiz",
|
||||
varyant: Varyant,
|
||||
): Animation[] {
|
||||
const k = KARELER[i];
|
||||
if (varyant === "nabiz") {
|
||||
@@ -94,9 +125,10 @@ function donguyuBaslat(
|
||||
),
|
||||
];
|
||||
}
|
||||
const zincir = varyant === "zincir";
|
||||
const secenek = {
|
||||
duration: ROTA_SURE,
|
||||
delay: i * ROTA_GECIKME_ADIMI,
|
||||
duration: zincir ? ZINCIR_SURE : ROTA_SURE,
|
||||
delay: i * (zincir ? ZINCIR_ADIM : ROTA_GECIKME_ADIMI),
|
||||
iterations: Infinity,
|
||||
};
|
||||
// Konum ve nabız ayrı animasyonlar: konum etabı tek parça eğriyle akar,
|
||||
@@ -105,7 +137,7 @@ function donguyuBaslat(
|
||||
el.animate(
|
||||
{
|
||||
translate: rota(k.baslangic, k.boyut),
|
||||
offset: KONUM_ZAMANLARI,
|
||||
offset: zincir ? ZINCIR_KONUM_ZAMANLARI : KONUM_ZAMANLARI,
|
||||
easing: YOL,
|
||||
},
|
||||
secenek,
|
||||
@@ -113,8 +145,10 @@ function donguyuBaslat(
|
||||
el.animate(
|
||||
{
|
||||
scale: ROTA_NABIZ_SCALE.map(String),
|
||||
opacity: ROTA_NABIZ_OPACITY,
|
||||
offset: ROTA_NABIZ_ZAMANLARI,
|
||||
// Zincirde kare solmaz: başka bir karenin üzerine konar, yarı saydam
|
||||
// olursa alttaki renk içinden görünür.
|
||||
...(zincir ? {} : { opacity: ROTA_NABIZ_OPACITY }),
|
||||
offset: zincir ? ZINCIR_NABIZ_ZAMANLARI : ROTA_NABIZ_ZAMANLARI,
|
||||
easing: SINE,
|
||||
},
|
||||
secenek,
|
||||
@@ -126,22 +160,35 @@ function donguyuBaslat(
|
||||
* Animasyonlu KolayTercih işareti. Varsayılanda koreografi sürekli döner;
|
||||
* `sadeceHover` ile yalnızca işaretin üzerine gelindiğinde oynar (ör. site
|
||||
* header'daki logo linki) ve hover bitince kareler yumuşakça yerine oturur.
|
||||
* `oynat` ile döngü dışarıdan açılıp kapatılır, `varyant` da çalışırken
|
||||
* değişebilir (ör. danışman avatarı: cevap beklenirken nabız, veriye bakarken
|
||||
* zincir); her geçişte kareler önce yumuşakça yerine oturur, yeni döngü
|
||||
* yerinden başlar.
|
||||
*/
|
||||
export function AnimasyonluMark({
|
||||
className,
|
||||
sadeceHover = false,
|
||||
oynat = true,
|
||||
varyant = "rota",
|
||||
renk = "marka",
|
||||
}: {
|
||||
className?: string;
|
||||
sadeceHover?: boolean;
|
||||
varyant?: "rota" | "nabiz";
|
||||
/** `sadeceHover` yokken döngü açık mı. `false` iken işaret durağandır. */
|
||||
oynat?: boolean;
|
||||
varyant?: Varyant;
|
||||
/** "marka": logodaki üç renk; "turuncu": tüm kareler sitenin CTA turuncusu
|
||||
* (orange-500) — ör. danışman panel başlığı. */
|
||||
renk?: "marka" | "turuncu";
|
||||
}) {
|
||||
const kokRef = useRef<HTMLSpanElement>(null);
|
||||
const kontrolRef = useRef<{
|
||||
ayarla: (hedef: Varyant | null, cevik?: boolean) => void;
|
||||
} | null>(null);
|
||||
|
||||
// Animasyonların sahibi: bileşen yaşadıkça bir kez kurulur. Prop değişimi
|
||||
// bunu söküp kurmaz (kareler sıçrardı); aşağıdaki efekt `ayarla` ile hedefi
|
||||
// bildirir.
|
||||
useEffect(() => {
|
||||
const kok = kokRef.current;
|
||||
if (!kok || typeof kok.animate !== "function") return;
|
||||
@@ -150,32 +197,25 @@ export function AnimasyonluMark({
|
||||
|
||||
let dongu: Animation[] = [];
|
||||
let donus: Animation[] = [];
|
||||
let istekli = false;
|
||||
/** Şu an dönen döngü ve istenen döngü (`null`: durağan). */
|
||||
let aktif: Varyant | null = null;
|
||||
let hedef: Varyant | null = null;
|
||||
// Döngü bir mikro görev sonra başlar; o arada efekt sökülmüşse (React
|
||||
// geliştirme modunda her efekti kur-sök-kur yapar) başlamamalı — yoksa
|
||||
// kimsenin durduramadığı sahipsiz bir döngü kalır.
|
||||
let sokuldu = false;
|
||||
|
||||
const baslat = () => {
|
||||
if (istekli) return;
|
||||
istekli = true;
|
||||
if (azHareket.matches) return;
|
||||
// Kareler hâlâ yerine dönüyorsa (hızlı çık-gir) dönüşün bitmesi
|
||||
// beklenir; döngü hep yerinden başlar, sıçrama olmaz.
|
||||
Promise.all(donus.map((a) => a.finished))
|
||||
.then(() => {
|
||||
if (!istekli || dongu.length > 0) return;
|
||||
dongu = kareler.flatMap((el, i) => donguyuBaslat(el, i, varyant));
|
||||
})
|
||||
.catch(() => {});
|
||||
};
|
||||
|
||||
const durdur = () => {
|
||||
istekli = false;
|
||||
if (dongu.length === 0) return;
|
||||
// Kareler o an neredeyse oradan yerlerine süzülür.
|
||||
/** Döngüyü keser; kareler o an neredeyse oradan yerlerine süzülür. */
|
||||
const yerles = (cevik: boolean) => {
|
||||
const anlik = kareler.map((el) => {
|
||||
const cs = getComputedStyle(el);
|
||||
return { translate: cs.translate, scale: cs.scale, opacity: cs.opacity };
|
||||
});
|
||||
// Nabızda kareler zaten yerindedir, yalnız boyut toparlanır: kısa sürer.
|
||||
const sure = aktif === "nabiz" ? YERLES_SURE / 2 : YERLES_SURE;
|
||||
dongu.forEach((a) => a.cancel());
|
||||
dongu = [];
|
||||
aktif = null;
|
||||
donus = kareler.map((el, i) =>
|
||||
el.animate(
|
||||
[
|
||||
@@ -186,38 +226,73 @@ export function AnimasyonluMark({
|
||||
opacity: 1,
|
||||
},
|
||||
],
|
||||
{ duration: DONUS_SURE, easing: DONUS },
|
||||
cevik
|
||||
? { duration: DONUS_SURE, easing: DONUS }
|
||||
: { duration: sure, easing: YERLES },
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
if (!sadeceHover) {
|
||||
baslat();
|
||||
return () => {
|
||||
dongu.forEach((a) => a.cancel());
|
||||
};
|
||||
}
|
||||
const ayarla = (yeni: Varyant | null, cevik = false) => {
|
||||
if (yeni === hedef) return;
|
||||
hedef = yeni;
|
||||
if (aktif !== null) yerles(cevik);
|
||||
if (yeni === null || azHareket.matches) return;
|
||||
// Kareler hâlâ yerine dönüyorsa dönüşün bitmesi beklenir; döngü hep
|
||||
// yerinden başlar, sıçrama olmaz.
|
||||
Promise.all(donus.map((a) => a.finished))
|
||||
.then(() => {
|
||||
if (sokuldu || hedef !== yeni || aktif !== null) return;
|
||||
dongu = kareler.flatMap((el, i) => donguyuBaslat(el, i, yeni));
|
||||
aktif = yeni;
|
||||
})
|
||||
.catch(() => {});
|
||||
};
|
||||
|
||||
const kontrol = { ayarla };
|
||||
kontrolRef.current = kontrol;
|
||||
return () => {
|
||||
sokuldu = true;
|
||||
if (kontrolRef.current === kontrol) kontrolRef.current = null;
|
||||
[...dongu, ...donus].forEach((a) => a.cancel());
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Üstteki efektten sonra koşar (aynı bileşende efektler sırayla çalışır),
|
||||
// o yüzden kontrolRef burada doludur.
|
||||
useEffect(() => {
|
||||
const kok = kokRef.current;
|
||||
if (!kok) return;
|
||||
if (!sadeceHover) {
|
||||
kontrolRef.current?.ayarla(oynat ? varyant : null);
|
||||
return;
|
||||
}
|
||||
kontrolRef.current?.ayarla(null, true);
|
||||
// Hover hedefi işaretin kendisi değil, en yakın `group` ebeveyni (site
|
||||
// header'da logo + "KolayTercih" yazısını saran Link): yazının üzerine
|
||||
// gelindiğinde de animasyon başlar. `group`'suz kullanımda köke düşer.
|
||||
const hedef = kok.closest(".group") ?? kok;
|
||||
hedef.addEventListener("mouseenter", baslat);
|
||||
hedef.addEventListener("mouseleave", durdur);
|
||||
const hoverHedefi = kok.closest(".group") ?? kok;
|
||||
const gir = () => kontrolRef.current?.ayarla(varyant, true);
|
||||
const cik = () => kontrolRef.current?.ayarla(null, true);
|
||||
hoverHedefi.addEventListener("mouseenter", gir);
|
||||
hoverHedefi.addEventListener("mouseleave", cik);
|
||||
return () => {
|
||||
hedef.removeEventListener("mouseenter", baslat);
|
||||
hedef.removeEventListener("mouseleave", durdur);
|
||||
[...dongu, ...donus].forEach((a) => a.cancel());
|
||||
hoverHedefi.removeEventListener("mouseenter", gir);
|
||||
hoverHedefi.removeEventListener("mouseleave", cik);
|
||||
};
|
||||
}, [sadeceHover, varyant]);
|
||||
}, [oynat, sadeceHover, varyant]);
|
||||
|
||||
return (
|
||||
<span
|
||||
ref={kokRef}
|
||||
aria-hidden="true"
|
||||
className={cn("relative block shrink-0", className)}
|
||||
className={cn(
|
||||
"relative block shrink-0",
|
||||
varyant !== "rota" && "isolate",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{KARELER.map((k) => (
|
||||
{KARELER.map((k, i) => (
|
||||
<span
|
||||
key={k.renk}
|
||||
className={cn(
|
||||
@@ -230,6 +305,11 @@ export function AnimasyonluMark({
|
||||
borderRadius: `${(13 / k.boyut) * 100}%`,
|
||||
backgroundColor: renk === "marka" ? k.renk : undefined,
|
||||
translate: yer(k.baslangic, k.boyut),
|
||||
// Zincirde yürüyen kare hep hedefindekinin üstüne konar: sırası
|
||||
// önce gelen üstte durur (mavi > şeftali > turuncu). Avatar nabız
|
||||
// ile zincir arasında gidip geldiği için sıra nabızda da aynıdır
|
||||
// (orada kareler üst üste gelmez); header'daki rota'ya dokunulmaz.
|
||||
zIndex: varyant !== "rota" ? KARELER.length - i : undefined,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -110,6 +110,9 @@ export function DevPanel() {
|
||||
}
|
||||
// Verify URL'i oturumu kurup callback'e yönlendirir
|
||||
window.location.assign(sonuc.url);
|
||||
} catch (e) {
|
||||
// Sunucu hatası (ör. e-posta gönderimi) sessiz kalmasın (6 Eki 2026 QA)
|
||||
toast.error(e instanceof Error ? e.message : "Anında giriş başarısız.");
|
||||
} finally {
|
||||
setMesgul(false);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,32 @@
|
||||
import type { ReactNode } from "react";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
|
||||
const sayi = (n: number) => n.toLocaleString("tr-TR");
|
||||
|
||||
/**
|
||||
* Özet kartında iri sayı telefonda ~94 px'e sığmalı (DESIGN.md §4.8):
|
||||
* 36 px Cal Sans'ta "1.618.988" 123 px ölçüldü, ikonun altına giriyor. 6 ve
|
||||
* 7 haneli sıralar birimle kısalır: 245.300 → "245" + "bin",
|
||||
* 1.618.988 → "1,62" + "milyon". Üniversite ve bölüm sayfası buradan okur.
|
||||
*/
|
||||
export function kartSayisi(n: number): { deger: string; birim?: string } {
|
||||
// 999.500+ "1.000 bin" olmasın diye milyona geçer
|
||||
if (n >= 999_500) {
|
||||
return {
|
||||
deger: (n / 1e6).toLocaleString("tr-TR", { maximumFractionDigits: 2 }),
|
||||
birim: "milyon",
|
||||
};
|
||||
}
|
||||
if (n >= 100_000) return { deger: sayi(Math.round(n / 1000)), birim: "bin" };
|
||||
return { deger: sayi(n) };
|
||||
}
|
||||
|
||||
/** Metin içinde kısa sıra: 1.618.988 → "1,62 milyon", 105.215 → "105 bin", 4.560 → "4.560". */
|
||||
export function kisaSayi(n: number): string {
|
||||
const k = kartSayisi(n);
|
||||
return k.birim ? `${k.deger} ${k.birim}` : k.deger;
|
||||
}
|
||||
|
||||
/**
|
||||
* Özet kartı (DESIGN.md §4.8 — Apple Fitness özet kartlarının düzeni,
|
||||
* sitenin ölçüleriyle): kartın en baskın öğesi iri sayı; ikonu sağında,
|
||||
|
||||
37
src/components/ui/collapsible.tsx
Normal file
37
src/components/ui/collapsible.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
"use client"
|
||||
|
||||
import { Collapsible as CollapsiblePrimitive } from "radix-ui"
|
||||
|
||||
// shadcn `collapsible` (radix-ui tek paket). prompt-kit `steps.tsx` bunu ister;
|
||||
// stil taşımaz, açılma/kapanma animasyonu tw-animate-css'in
|
||||
// `animate-collapsible-down/up` sınıflarıyla içerikte verilir.
|
||||
|
||||
function Collapsible({
|
||||
...props
|
||||
}: React.ComponentProps<typeof CollapsiblePrimitive.Root>) {
|
||||
return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />
|
||||
}
|
||||
|
||||
function CollapsibleTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleTrigger>) {
|
||||
return (
|
||||
<CollapsiblePrimitive.CollapsibleTrigger
|
||||
data-slot="collapsible-trigger"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CollapsibleContent({
|
||||
...props
|
||||
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleContent>) {
|
||||
return (
|
||||
<CollapsiblePrimitive.CollapsibleContent
|
||||
data-slot="collapsible-content"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Collapsible, CollapsibleTrigger, CollapsibleContent }
|
||||
@@ -41,7 +41,8 @@ function extractLanguage(className?: string): string {
|
||||
return match ? match[1] : "plaintext"
|
||||
}
|
||||
|
||||
const INITIAL_COMPONENTS: Partial<Components> = {
|
||||
// Dışa açık: sohbet (sohbet-baglanti.tsx) bu varsayılanı bağlantı haplarıyla genişletir.
|
||||
export const INITIAL_COMPONENTS: Partial<Components> = {
|
||||
code: function CodeComponent({ className, children, ...props }) {
|
||||
const isInline =
|
||||
!props.node?.position?.start.line ||
|
||||
|
||||
123
src/components/ui/steps.tsx
Normal file
123
src/components/ui/steps.tsx
Normal file
@@ -0,0 +1,123 @@
|
||||
"use client"
|
||||
|
||||
// prompt-kit `steps` (https://prompt-kit.com/c/steps.json), sitenin paletine
|
||||
// çevrildi: `muted/foreground/popover` token'ları yerine slate ölçeği
|
||||
// (DESIGN.md §2, §2.3 — koyu temada token'lar çevrilir, `dark:` yok). Yapı
|
||||
// aynı: Steps (Collapsible) → StepsTrigger (başlık + ok) → StepsContent
|
||||
// (solda dikey çubuk, sağda adımlar). Danışman sohbetinde araç adımlarını
|
||||
// listeler (features/rapor/components/arac-pilleri.tsx, §4.20).
|
||||
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from "@/components/ui/collapsible"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { ChevronDown } from "lucide-react"
|
||||
|
||||
export type StepsItemProps = React.ComponentProps<"div">
|
||||
|
||||
export const StepsItem = ({
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}: StepsItemProps) => (
|
||||
<div className={cn("text-sm text-slate-600", className)} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
|
||||
export type StepsTriggerProps = React.ComponentProps<
|
||||
typeof CollapsibleTrigger
|
||||
> & {
|
||||
leftIcon?: React.ReactNode
|
||||
swapIconOnHover?: boolean
|
||||
}
|
||||
|
||||
export const StepsTrigger = ({
|
||||
children,
|
||||
className,
|
||||
leftIcon,
|
||||
swapIconOnHover = true,
|
||||
...props
|
||||
}: StepsTriggerProps) => (
|
||||
<CollapsibleTrigger
|
||||
className={cn(
|
||||
"group flex w-full cursor-pointer items-center justify-start gap-1 text-sm text-slate-500 transition-colors hover:text-slate-800",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
{leftIcon ? (
|
||||
<span className="relative inline-flex size-4 items-center justify-center">
|
||||
<span
|
||||
className={cn(
|
||||
"transition-opacity",
|
||||
swapIconOnHover && "group-hover:opacity-0"
|
||||
)}
|
||||
>
|
||||
{leftIcon}
|
||||
</span>
|
||||
{swapIconOnHover && (
|
||||
<ChevronDown className="absolute size-4 opacity-0 transition-opacity group-hover:opacity-100 group-data-[state=open]:rotate-180" />
|
||||
)}
|
||||
</span>
|
||||
) : null}
|
||||
<span>{children}</span>
|
||||
</div>
|
||||
{!leftIcon && (
|
||||
<ChevronDown className="size-4 transition-transform group-data-[state=open]:rotate-180" />
|
||||
)}
|
||||
</CollapsibleTrigger>
|
||||
)
|
||||
|
||||
export type StepsContentProps = React.ComponentProps<
|
||||
typeof CollapsibleContent
|
||||
> & {
|
||||
bar?: React.ReactNode
|
||||
}
|
||||
|
||||
export const StepsContent = ({
|
||||
children,
|
||||
className,
|
||||
bar,
|
||||
...props
|
||||
}: StepsContentProps) => {
|
||||
return (
|
||||
<CollapsibleContent
|
||||
className={cn(
|
||||
"overflow-hidden text-slate-800 data-[state=closed]:animate-collapsible-up data-[state=open]:animate-collapsible-down motion-reduce:animate-none",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div className="mt-2 grid max-w-full min-w-0 grid-cols-[min-content_minmax(0,1fr)] items-start gap-x-3">
|
||||
<div className="min-w-0 self-stretch">{bar ?? <StepsBar />}</div>
|
||||
<div className="min-w-0 space-y-1.5">{children}</div>
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
)
|
||||
}
|
||||
|
||||
export type StepsBarProps = React.HTMLAttributes<HTMLDivElement>
|
||||
|
||||
export const StepsBar = ({ className, ...props }: StepsBarProps) => (
|
||||
<div
|
||||
className={cn("h-full w-0.5 rounded-full bg-slate-200", className)}
|
||||
aria-hidden
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
|
||||
export type StepsProps = React.ComponentProps<typeof Collapsible>
|
||||
|
||||
export function Steps({ defaultOpen = true, className, ...props }: StepsProps) {
|
||||
return (
|
||||
<Collapsible
|
||||
className={cn(className)}
|
||||
defaultOpen={defaultOpen}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user