feat(tasarim): form alanları hap, CTA bandı tek kaynak, masonry piksel zemin, çekmeceler §4.10
- Input/Select/Textarea: shadcn'in ince varsayılanı yerine üst çubuk haplarıyla aynı kalıp (h-12, rounded-full, slate-200, 16 px; md:text-sm yok); kullanım yerleri yalnız yerleşim sınıfı taşır. DESIGN.md §4.6 tam sınıf dizisi; denetime ince-form-alani (hata) ve ham-input (uyarı) - CtaBandi (mavi zemin + piksel doku) tek kaynak: /sonuc iki panel, katalog altı kart, "Kendi Listem" çekmecesi altı. Ana sayfa kapanışı koyu PikselCtaYuzeyi + 24'lük piksel liste kreatifiyle kalır (Bilal: "orası bayağı iyiydi"); yazılıp silinen satır kaldırıldı - PixelMasonry: CTA arka planları büyüklü küçüklü, hafif kaymış kareler; eşit ızgaralı PixelField kaldırıldı - Çekmece başlığındaki 12×2 piksel liste kaldırıldı (çok dolu); sayı + ilerleme çubuğu + risk dağılımı geri geldi - "Saniyede gör" çekmecesi §4.10 düzeninde: ortalı DrawerTitle + bağlam, rozet ve çizgili "neler alacaksın" listesi yok; kapı modalındaki iki rozet kalktı; denetime modal-rozet (hata) - Kapı panelinde 3 px odak halkası kırpılmıyor (px-px → p-1) - Liste satırı: üniversite adı + tür/il/süre adın altında tek akan satır, telefonda text-xs (SATIR_ALT_GRUP); katalog listelerinde ad kendi satırında (SATIR_GRUP space-y-2). Ölçüm 375 px canlı bölüm sayfası: satır yüksekliğini metin değil alt alta iki 44 px düğme belirliyor Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
59
src/components/cta-bandi.tsx
Normal file
59
src/components/cta-bandi.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
"use client";
|
||||
|
||||
import type { ComponentProps, Ref } from "react";
|
||||
import { PixelMasonry } from "@/components/pixel-decor";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
/**
|
||||
* CTA bandı — DESIGN.md §10.8 kanonik yüzey: sitenin mavisi (`bg-primary`),
|
||||
* arka planda büyüklü küçüklü, masonry gibi kaymış ve imleç yaklaşınca
|
||||
* turuncuya ısınan piksel dokusu (`PixelMasonry`), beyaz yazı, tek turuncu buton.
|
||||
*
|
||||
* - `buyuk` (varsayılan): sayfa kapanışı / panel — ortalı, iri boşluk.
|
||||
* - `kompakt`: çekmece altı gibi dar yerler — sola yaslı, alçak.
|
||||
*
|
||||
* İçerik (başlık, paragraf, buton) çağıranındır; yalnız kabuk burada durur.
|
||||
* Bilal'in kararı (24 Eyl 2026): koyu slate-950 yüzey yerine bu band.
|
||||
*/
|
||||
export function CtaBandi({
|
||||
children,
|
||||
className,
|
||||
seed = 8,
|
||||
boyut = "buyuk",
|
||||
etiket = "section",
|
||||
ref,
|
||||
...props
|
||||
}: Omit<ComponentProps<"div">, "ref"> & {
|
||||
/** Piksel dokusunun deseni; aynı sayfadaki iki band farklı olsun */
|
||||
seed?: number;
|
||||
boyut?: "buyuk" | "kompakt";
|
||||
/** Çekmece altı gibi başlıksız yerlerde `div` */
|
||||
etiket?: "section" | "div";
|
||||
ref?: Ref<HTMLElement>;
|
||||
}) {
|
||||
const buyuk = boyut === "buyuk";
|
||||
// İki etiket de HTMLElement'tir; JSX tipi için div'e daraltılır.
|
||||
const Etiket = etiket as "div";
|
||||
return (
|
||||
<Etiket
|
||||
ref={ref as Ref<HTMLDivElement>}
|
||||
className={cn(
|
||||
"relative isolate overflow-hidden rounded-2xl bg-primary text-white",
|
||||
buyuk ? "px-5 py-8 text-center sm:p-12" : "px-5 py-4 text-left sm:py-5",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div
|
||||
aria-hidden
|
||||
className={cn(
|
||||
"pointer-events-none absolute inset-x-0 -z-10 text-white",
|
||||
buyuk ? "-inset-y-10" : "-inset-y-6",
|
||||
)}
|
||||
>
|
||||
<PixelMasonry seed={seed} />
|
||||
</div>
|
||||
{children}
|
||||
</Etiket>
|
||||
);
|
||||
}
|
||||
@@ -13,23 +13,32 @@ export const SATIR_AD =
|
||||
export const SATIR_AD_DUZ =
|
||||
"block text-base font-semibold leading-snug text-slate-900";
|
||||
|
||||
/** Adın altındaki ikinci satır (varyant ya da üniversite adı). */
|
||||
export const SATIR_ALT_AD = "mt-0.5 block text-sm leading-snug text-slate-600";
|
||||
/** Adın altındaki ikinci satır (varyant ya da üniversite adı). Telefonda
|
||||
* bir kademe küçük (Bilal, 24 Eyl 2026: "mobilde çok şişko, üni adını daha
|
||||
* küçük yazabiliriz"); tür/il/süre ile aynı akan satırda (SATIR_GRUP). */
|
||||
export const SATIR_ALT_AD =
|
||||
"block text-xs leading-snug text-slate-600 sm:text-sm";
|
||||
|
||||
/** Üniversite adı (SATIR_ALT_AD) + tür/il/süre (SATIR_META) — adın altında
|
||||
* her genişlikte tek akan satır, sığmazsa sarar. /sonuc, rapor ve tadımlık
|
||||
* satırı kullanır. Bilal (24 Eyl 2026): telefonda katmanlar "iç içe". */
|
||||
export const SATIR_ALT_GRUP =
|
||||
"mt-1 flex flex-wrap items-center gap-x-2 gap-y-0.5 sm:mt-0.5 sm:gap-x-3 sm:gap-y-1";
|
||||
|
||||
/** Satırdaki ikincil link (ör. üniversite adı) — §2.1 tek turuncu. */
|
||||
export const SATIR_IKINCIL_LINK =
|
||||
"transition-colors hover:text-orange-500 hover:underline";
|
||||
|
||||
/** Tür/il/süre satırı. Geniş ekranda (sm+) üstündeki adla aynı satıra
|
||||
* çıkar (SATIR_GRUP), telefonda altına iner. */
|
||||
/** Tür/il/süre satırı. Kenar boşluğu sarmaldan gelir (SATIR_GRUP /
|
||||
* SATIR_ALT_GRUP); telefonda üniversite adı gibi bir kademe küçük. */
|
||||
export const SATIR_META =
|
||||
"mt-2 flex flex-wrap items-center gap-x-2 gap-y-1 text-sm text-slate-500 sm:mt-0";
|
||||
"flex flex-wrap items-center gap-x-2 gap-y-1 text-xs text-slate-500 sm:text-sm";
|
||||
|
||||
/** Ad (ya da üniversite adı) + tür/il/süre satırını geniş ekranda yan yana
|
||||
* dizen sarmal. Bilal'in isteği (25 Eyl 2026): geniş ekranda satırlar üç
|
||||
* katlı ve "çok kalın" duruyordu. Telefonda alt alta kalır. */
|
||||
/** Ana ad + tür/il/süre sarmalı (katalog listeleri: ad zaten üniversite ya da
|
||||
* bölüm). Telefonda ad kendi satırında kalır, çipler altına iner (8 px);
|
||||
* geniş ekranda (Bilal, 25 Eyl 2026: satırlar "çok kalın") aynı satıra çıkar. */
|
||||
export const SATIR_GRUP =
|
||||
"sm:flex sm:flex-wrap sm:items-center sm:gap-x-3 sm:gap-y-1";
|
||||
"space-y-2 sm:flex sm:flex-wrap sm:items-center sm:gap-x-3 sm:gap-y-1 sm:space-y-0";
|
||||
|
||||
/** Tür çipi (Devlet, Vakıf, SAY…). */
|
||||
export const SATIR_CIP =
|
||||
|
||||
@@ -2,38 +2,28 @@
|
||||
|
||||
import { useState, type ReactNode } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { TypingAnimation } from "@/components/ui/typing-animation";
|
||||
import { FUNNEL_METINLERI } from "@/lib/funnel-metinleri";
|
||||
import { PixelMasonry } from "@/components/pixel-decor";
|
||||
import { PikselListe, PikselListeAciklama } from "@/components/piksel-liste";
|
||||
|
||||
/** Listeyi `ofset` kadar döndürür: aynı sayfadaki yüzeyler farklı satırla
|
||||
* başlar. Rastgele değil, deterministik — sunucu ve istemci aynı çizer. */
|
||||
function dondur<T>(dizi: readonly T[], ofset: number): T[] {
|
||||
const n = ofset % dizi.length;
|
||||
return [...dizi.slice(n), ...dizi.slice(0, n)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Funnel CTA yüzeyi (DESIGN.md §10.8) — arama penceresindeki kartın dili:
|
||||
* koyu slate-950 zemin, ince mavi kenar, yazılıp silinen kısa satırlar,
|
||||
* turuncu CTA. Pikseller bir köşede kümelenmez; yüzeyin kreatifi 24'lük
|
||||
* listenin kendisidir (PikselListe). Kartın içindeki bir düğmeye ya da
|
||||
* linke imleç/odak gelince 24 kare birden dolar — "listen böyle hazır olur".
|
||||
* Kapanış yüzeyi (DESIGN.md §10.8) — arama penceresindeki kartın dili:
|
||||
* koyu slate-950 zemin, ince mavi kenar, turuncu CTA. Arka planda büyüklü
|
||||
* küçüklü, masonry gibi kaymış piksel dokusu (PixelMasonry); yüzeyin
|
||||
* kreatifi 24'lük listenin kendisidir (PikselListe). Kartın içindeki bir
|
||||
* düğmeye ya da linke imleç/odak gelince 24 kare birden dolar — "listen
|
||||
* böyle hazır olur". Yazılıp silinen satır yok (Bilal, 24 Eyl 2026).
|
||||
*/
|
||||
export function PikselCtaYuzeyi({
|
||||
children,
|
||||
className,
|
||||
metinOfseti = 0,
|
||||
kayanMetin = true,
|
||||
seed = 7,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
/** Aynı sayfadaki iki yüzey aynı satırla başlamasın */
|
||||
metinOfseti?: number;
|
||||
kayanMetin?: boolean;
|
||||
/** Piksel dokusunun deseni; aynı sayfadaki iki yüzey farklı olsun */
|
||||
seed?: number;
|
||||
}) {
|
||||
const [tamamla, setTamamla] = useState(false);
|
||||
const [metinler] = useState(() => dondur(FUNNEL_METINLERI, metinOfseti));
|
||||
|
||||
const hedefMi = (t: EventTarget | null) =>
|
||||
t instanceof Element && t.closest("a, button") != null;
|
||||
@@ -49,20 +39,15 @@ export function PikselCtaYuzeyi({
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div
|
||||
aria-hidden
|
||||
className="pointer-events-none absolute inset-x-0 -inset-y-10 -z-10 text-white"
|
||||
>
|
||||
<PixelMasonry seed={seed} />
|
||||
</div>
|
||||
<div className="grid items-center gap-8 px-5 py-10 sm:px-10 sm:py-12 lg:grid-cols-[minmax(0,1fr)_16rem] lg:gap-12">
|
||||
<div className="flex min-w-0 flex-col items-center text-center lg:items-start lg:text-left">
|
||||
{children}
|
||||
{kayanMetin ? (
|
||||
<TypingAnimation
|
||||
as="p"
|
||||
words={metinler}
|
||||
loop
|
||||
typeSpeed={38}
|
||||
deleteSpeed={18}
|
||||
pauseDelay={2600}
|
||||
className="mt-5 min-h-[2.75em] font-heading text-base font-bold leading-snug tracking-[-0.02em] text-slate-300 sm:min-h-[1.4em]"
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="order-first flex flex-col items-center lg:order-none">
|
||||
<PikselListe
|
||||
|
||||
@@ -72,11 +72,14 @@ function pixelRect(
|
||||
}
|
||||
|
||||
/**
|
||||
* Geniş alanlar için seyrek piksel zemini (ör. koyu CTA bandı).
|
||||
* İçeriğin oturduğu orta bölge tamamen boş bırakılır; pikseller
|
||||
* kenarlara doğru yoğunlaşır.
|
||||
* CTA yüzeylerinin piksel zemini: büyüklü küçüklü kareler, masonry gibi
|
||||
* hafif kaymış (Bilal, 24 Eyl 2026: "arka planda büyüklü küçüklü, masonry
|
||||
* şeklinde"). İçeriğin oturduğu orta bant boş kalır; kareler kenarlara doğru
|
||||
* yoğunlaşır. 3×3 hücrelik makro hücrelere 1–3 hücrelik tek kare düşer ve
|
||||
* makro hücre içinde kaydırılır: kareler çakışmaz ama hizaya da girmez
|
||||
* (§1.1 "doğal duruş"). Her kare imleç ısı fırçasına katılır.
|
||||
*/
|
||||
export function PixelField({
|
||||
export function PixelMasonry({
|
||||
seed = 7,
|
||||
className,
|
||||
}: {
|
||||
@@ -84,42 +87,99 @@ export function PixelField({
|
||||
className?: string;
|
||||
}) {
|
||||
const svgRef = useRef<SVGSVGElement>(null);
|
||||
usePixelHeat(svgRef);
|
||||
usePixelHeat(svgRef, { radius: 44 });
|
||||
|
||||
const COLS = 42;
|
||||
const ROWS = 12;
|
||||
const MAKRO = 3;
|
||||
|
||||
// Çıktı seed'e göre deterministik; 504 iterasyon + ~150 element üretimini
|
||||
// her render'da (ör. çekmece reorder'ında) tekrarlama (rendering-hoist-jsx)
|
||||
// Çıktı seed'e göre deterministik; render başına yeniden üretme
|
||||
const pixels = useMemo(() => {
|
||||
const cx = (COLS - 1) / 2;
|
||||
const cy = (ROWS - 1) / 2;
|
||||
const maxD = Math.sqrt(cx * cx + cy * cy);
|
||||
const BAND_MIN = COLS * 0.22;
|
||||
const BAND_MAX = COLS * 0.78;
|
||||
|
||||
const sonuc: ReactNode[] = [];
|
||||
let accentIdx = 0;
|
||||
|
||||
// Hero serpintisiyle aynı kural: içeriğin oturduğu orta kolon
|
||||
// (genişliğin %22–78 bandı) tüm yükseklik boyunca tamamen boş kalır
|
||||
const BAND_MIN = COLS * 0.22;
|
||||
const BAND_MAX = COLS * 0.78;
|
||||
|
||||
for (let y = 0; y < ROWS; y++) {
|
||||
for (let x = 0; x < COLS; x++) {
|
||||
if (x > BAND_MIN && x < BAND_MAX) continue;
|
||||
// Kenarlara doğru artan doluluk
|
||||
const d = Math.sqrt((x - cx) ** 2 + (y - cy) ** 2) / maxD;
|
||||
const density = 0.1 + 0.42 * d * d;
|
||||
if (hash(x, y, seed) > density) continue;
|
||||
const accent = hash(x, y, seed + 99) > 0.96;
|
||||
sonuc.push(
|
||||
pixelRect(x, y, {
|
||||
key: `${x}-${y}`,
|
||||
accent,
|
||||
accentDelay: accent ? (accentIdx++ % 8) * 0.45 : 0,
|
||||
}),
|
||||
const kare = (
|
||||
x: number,
|
||||
y: number,
|
||||
n: number,
|
||||
key: string,
|
||||
accent: boolean,
|
||||
) => {
|
||||
const boy = n * CELL - 2 * INSET;
|
||||
const shared = {
|
||||
x: x * CELL + INSET,
|
||||
y: y * CELL + INSET,
|
||||
width: boy,
|
||||
height: boy,
|
||||
rx: 2 * n,
|
||||
};
|
||||
if (accent) {
|
||||
return (
|
||||
<rect
|
||||
key={key}
|
||||
{...shared}
|
||||
className="animate-pulse fill-orange-500/70 motion-reduce:animate-none"
|
||||
style={{
|
||||
animationDuration: "3.6s",
|
||||
animationDelay: `${(accentIdx++ % 8) * 0.45}s`,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
// İri kare daha silik: yazının yanında bağırmasın
|
||||
const op = opacityFor(x, y) * (n === 1 ? 1 : n === 2 ? 0.8 : 0.6);
|
||||
return (
|
||||
<rect
|
||||
key={key}
|
||||
{...shared}
|
||||
data-px=""
|
||||
data-cx={x * CELL + (n * CELL) / 2}
|
||||
data-cy={y * CELL + (n * CELL) / 2}
|
||||
data-op={op}
|
||||
fill="currentColor"
|
||||
fillOpacity={op}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
for (let my = 0; my < ROWS / MAKRO; my++) {
|
||||
for (let mx = 0; mx < COLS / MAKRO; mx++) {
|
||||
const ox = mx * MAKRO;
|
||||
const oy = my * MAKRO;
|
||||
const mcx = ox + MAKRO / 2;
|
||||
if (mcx > BAND_MIN && mcx < BAND_MAX) continue;
|
||||
// Kenarlara doğru artan doluluk
|
||||
const d = Math.sqrt((mcx - cx) ** 2 + (oy + MAKRO / 2 - cy) ** 2) / maxD;
|
||||
const density = 0.3 + 0.6 * d * d;
|
||||
if (hash(mx, my, seed) > density) {
|
||||
// Boş makro hücrede ara sıra tek küçük piksel: doku kopmasın
|
||||
if (hash(mx, my, seed + 3) < 0.25) {
|
||||
const px = ox + Math.floor(hash(mx, my, seed + 4) * MAKRO);
|
||||
const py = oy + Math.floor(hash(mx, my, seed + 5) * MAKRO);
|
||||
sonuc.push(kare(px, py, 1, `k-${mx}-${my}`, false));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
const r = hash(mx, my, seed + 7);
|
||||
const n = r < 0.5 ? 1 : r < 0.85 ? 2 : 3;
|
||||
const serbest = MAKRO - n;
|
||||
const kx = ox + Math.floor(hash(mx, my, seed + 11) * (serbest + 1));
|
||||
const ky = oy + Math.floor(hash(mx, my, seed + 13) * (serbest + 1));
|
||||
const accent = n < 3 && hash(mx, my, seed + 99) > 0.94;
|
||||
sonuc.push(kare(kx, ky, n, `m-${mx}-${my}`, accent));
|
||||
// Büyük karenin yanına küçük bir eş: büyüklü küçüklü ritim
|
||||
if (n < 3 && hash(mx, my, seed + 17) < 0.45) {
|
||||
const ex = kx + n < ox + MAKRO ? kx + n : kx - 1;
|
||||
const ey = ky + n < oy + MAKRO ? ky + n : ky - 1;
|
||||
if (ex >= ox && ey >= oy) sonuc.push(kare(ex, ey, 1, `e-${mx}-${my}`, false));
|
||||
}
|
||||
}
|
||||
}
|
||||
return sonuc;
|
||||
}, [seed]);
|
||||
@@ -156,7 +216,7 @@ export function PixelDivider({
|
||||
const COLS = 12;
|
||||
const ROWS = 2;
|
||||
|
||||
// Deterministik çıktı — render başına yeniden üretme (PixelField ile aynı)
|
||||
// Deterministik çıktı — render başına yeniden üretme (PixelMasonry ile aynı)
|
||||
const pixels = useMemo(() => {
|
||||
const cx = (COLS - 1) / 2;
|
||||
const sonuc: ReactNode[] = [];
|
||||
|
||||
@@ -2,13 +2,18 @@ import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
/**
|
||||
* Metin alanı — DESIGN.md §4.6. Üst çubuktaki arama/kullanıcı haplarıyla aynı
|
||||
* kalıp: 48 px, tam yuvarlak, slate-200 kenarlık, 16 px yazı (telefonda da).
|
||||
* Başta ikon gerekiyorsa kapsayıcı `relative`, ikon `left-5`, alan `pl-12`.
|
||||
*/
|
||||
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
data-slot="input"
|
||||
className={cn(
|
||||
"h-8 w-full min-w-0 rounded-lg border border-input bg-transparent px-2.5 py-1 text-base transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
|
||||
"h-12 w-full min-w-0 rounded-full border border-slate-200 bg-white px-5 text-base text-slate-900 transition-colors duration-150 outline-none placeholder:text-slate-400 hover:border-slate-300 focus-visible:border-primary focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-slate-50 disabled:opacity-50 aria-invalid:border-red-500 aria-invalid:ring-3 aria-invalid:ring-red-500/20",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -44,14 +44,14 @@ function SelectTrigger({
|
||||
data-slot="select-trigger"
|
||||
data-size={size}
|
||||
className={cn(
|
||||
"flex w-fit items-center justify-between gap-1.5 rounded-lg border border-input bg-transparent py-2 pr-2 pl-2.5 text-sm whitespace-nowrap transition-colors outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-placeholder:text-muted-foreground data-[size=default]:h-8 data-[size=sm]:h-7 data-[size=sm]:rounded-[min(var(--radius-md),10px)] *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-1.5 dark:bg-input/30 dark:hover:bg-input/50 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
"flex w-fit cursor-pointer items-center justify-between gap-2 rounded-full border border-slate-200 bg-white py-2 pr-4 pl-5 text-base text-slate-900 whitespace-nowrap transition-colors duration-150 outline-none select-none hover:border-slate-300 focus-visible:border-primary focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-red-500 aria-invalid:ring-3 aria-invalid:ring-red-500/20 data-placeholder:text-slate-400 data-[size=default]:h-12 data-[size=sm]:h-9 data-[size=sm]:pr-3 data-[size=sm]:pl-4 data-[size=sm]:text-sm *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-1.5 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SelectPrimitive.Icon asChild>
|
||||
<ChevronDownIcon className="pointer-events-none size-4 text-muted-foreground" />
|
||||
<ChevronDownIcon className="pointer-events-none size-4 text-slate-400" />
|
||||
</SelectPrimitive.Icon>
|
||||
</SelectPrimitive.Trigger>
|
||||
)
|
||||
@@ -69,7 +69,7 @@ function SelectContent({
|
||||
<SelectPrimitive.Content
|
||||
data-slot="select-content"
|
||||
data-align-trigger={position === "item-aligned"}
|
||||
className={cn("relative z-50 max-h-(--radix-select-content-available-height) min-w-36 origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-lg bg-popover text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[align-trigger=true]:animate-none data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95", position ==="popper"&&"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1", className )}
|
||||
className={cn("relative z-50 max-h-(--radix-select-content-available-height) min-w-36 origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-2xl border border-slate-200 bg-white text-slate-900 shadow-lg duration-100 data-[align-trigger=true]:animate-none data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95", position ==="popper"&&"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1", className )}
|
||||
position={position}
|
||||
align={align}
|
||||
{...props}
|
||||
@@ -78,7 +78,7 @@ function SelectContent({
|
||||
<SelectPrimitive.Viewport
|
||||
data-position={position}
|
||||
className={cn(
|
||||
"data-[position=popper]:h-(--radix-select-trigger-height) data-[position=popper]:w-full data-[position=popper]:min-w-(--radix-select-trigger-width)",
|
||||
"p-1.5 data-[position=popper]:h-(--radix-select-trigger-height) data-[position=popper]:w-full data-[position=popper]:min-w-(--radix-select-trigger-width)",
|
||||
position === "popper" && ""
|
||||
)}
|
||||
>
|
||||
@@ -97,7 +97,7 @@ function SelectLabel({
|
||||
return (
|
||||
<SelectPrimitive.Label
|
||||
data-slot="select-label"
|
||||
className={cn("px-1.5 py-1 text-xs text-muted-foreground", className)}
|
||||
className={cn("px-4 py-1.5 text-sm font-semibold text-slate-500", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
@@ -112,12 +112,12 @@ function SelectItem({
|
||||
<SelectPrimitive.Item
|
||||
data-slot="select-item"
|
||||
className={cn(
|
||||
"relative flex w-full cursor-default items-center gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
|
||||
"relative flex w-full cursor-pointer items-center gap-2 rounded-full py-2 pr-9 pl-4 text-base text-slate-900 outline-hidden select-none focus:bg-slate-100 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="pointer-events-none absolute right-2 flex size-4 items-center justify-center">
|
||||
<span className="pointer-events-none absolute right-3 flex size-4 items-center justify-center text-primary">
|
||||
<SelectPrimitive.ItemIndicator>
|
||||
<CheckIcon className="pointer-events-none" />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
@@ -134,7 +134,7 @@ function SelectSeparator({
|
||||
return (
|
||||
<SelectPrimitive.Separator
|
||||
data-slot="select-separator"
|
||||
className={cn("pointer-events-none -mx-1 my-1 h-px bg-border", className)}
|
||||
className={cn("pointer-events-none -mx-1.5 my-1 h-px bg-slate-200", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
@@ -148,7 +148,7 @@ function SelectScrollUpButton({
|
||||
<SelectPrimitive.ScrollUpButton
|
||||
data-slot="select-scroll-up-button"
|
||||
className={cn(
|
||||
"z-10 flex cursor-default items-center justify-center bg-popover py-1 [&_svg:not([class*='size-'])]:size-4",
|
||||
"z-10 flex cursor-default items-center justify-center bg-white py-1 text-slate-400 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
@@ -167,7 +167,7 @@ function SelectScrollDownButton({
|
||||
<SelectPrimitive.ScrollDownButton
|
||||
data-slot="select-scroll-down-button"
|
||||
className={cn(
|
||||
"z-10 flex cursor-default items-center justify-center bg-popover py-1 [&_svg:not([class*='size-'])]:size-4",
|
||||
"z-10 flex cursor-default items-center justify-center bg-white py-1 text-slate-400 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -2,12 +2,17 @@ import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
/**
|
||||
* Çok satırlı alan — DESIGN.md §4.6. Hap değil sade kutu (§4.4): rounded-2xl,
|
||||
* slate-200 kenarlık, 16 px yazı. Hover/odak/hata hâli `Input` ile aynı.
|
||||
* Sohbet kutusu (`prompt-input.tsx`) kenarlıksız/şeffaf üstüne yazar.
|
||||
*/
|
||||
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
|
||||
return (
|
||||
<textarea
|
||||
data-slot="textarea"
|
||||
className={cn(
|
||||
"flex field-sizing-content min-h-16 w-full rounded-lg border border-input bg-transparent px-2.5 py-2 text-base transition-colors outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
|
||||
"flex field-sizing-content min-h-24 w-full rounded-2xl border border-slate-200 bg-white px-5 py-3 text-base text-slate-900 transition-colors duration-150 outline-none placeholder:text-slate-400 hover:border-slate-300 focus-visible:border-primary focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:bg-slate-50 disabled:opacity-50 aria-invalid:border-red-500 aria-invalid:ring-3 aria-invalid:ring-red-500/20",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
Reference in New Issue
Block a user