feat(sohbet): liste revizyonu sohbete taşındı — soru formu ve yapısal kural
Eski revizyon kutusu kaldırıldı: serbest metin modele gidiyor, sıra ve havuz koddan belirlendiği için "ilk 5 X ilinden olsun" gibi isteklerin karşılığı yoktu. Danışman artık belirsiz isteği soru formuyla (soru_sor → shadcn Questionnaire, proje diline çevrildi) netleştirir, sonra liste_revize ile yapısal kural verir: il/alan/tip farkı, "ilk N" ölçütü, çıkar/ekle, iskelet (lib/revizyon-kurali.ts). Kurallar havuz, seçim ve sırada koddan uygulanır; "ilk N" ölçütünün ili/alanı süzgece kendiliğinden eklenir. - chat_messages.meta sütunu (göç 0006): form, form cevabı ve revizyon sonucu kayıttan geri kurulur; form cevabı kredi harcamaz. - Akış korumaları: form açılınca başka araç yok, revizyonda zaman aşımı uzar, "yeniden kurdum" beyanı araçsızsa düzeltme notu. - /listem "Revize et" sohbeti odaklar; başarılı revizyonda liste yenilenir. - /dev/sohbet senaryosu, DESIGN.md §4.20 notu ve §4.33. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
329
src/components/ui/questionnaire.tsx
Normal file
329
src/components/ui/questionnaire.tsx
Normal file
@@ -0,0 +1,329 @@
|
||||
"use client";
|
||||
|
||||
// shadcn `questionnaire` (https://ui.shadcn.com/docs/react/questionnaire,
|
||||
// radix-nova kayıt öğesi; ilkel: @shadcn/react/questionnaire), sitenin diline
|
||||
// çevrildi (DESIGN.md §4.6, §4.17, §4.24): `border-input / bg-muted /
|
||||
// text-muted-foreground` token'ları yerine slate ölçeği ve `bg-card`; seçili
|
||||
// seçenek sihirbaz çipi gibi `border-primary bg-primary text-white`; hover
|
||||
// tek turuncu (§2.1); dokunma hedefi 44 px; köşe `rounded-2xl` (§4.4 sade
|
||||
// kutu); düğmeler sitenin `Button`'u. Yapı ve davranış (tek tek soru,
|
||||
// ilerleme, doğrulama, klavye kısayolu, atlama) ilkelden gelir. Danışman
|
||||
// sohbetindeki soru formu bunu kullanır (features/rapor/components/sohbet-anketi.tsx).
|
||||
|
||||
import * as React from "react";
|
||||
import { Questionnaire as QuestionnairePrimitive } from "@shadcn/react/questionnaire";
|
||||
import { Check } from "lucide-react";
|
||||
import { buttonVariants, type Button } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Questionnaire({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof QuestionnairePrimitive.Root>) {
|
||||
return (
|
||||
<QuestionnairePrimitive.Root
|
||||
data-slot="questionnaire"
|
||||
className={cn("flex w-full min-w-0 flex-col gap-4", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function QuestionnaireProgress({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof QuestionnairePrimitive.Progress>) {
|
||||
return (
|
||||
<QuestionnairePrimitive.Progress
|
||||
data-slot="questionnaire-progress"
|
||||
className={cn(
|
||||
"min-h-[1lh] w-fit min-w-[14ch] text-xs font-semibold text-slate-500 tabular-nums",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function QuestionnaireItem({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof QuestionnairePrimitive.Item>) {
|
||||
return (
|
||||
<QuestionnairePrimitive.Item
|
||||
data-slot="questionnaire-item"
|
||||
className={cn("flex min-w-0 flex-col gap-3 border-0 p-0 outline-none", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function QuestionnaireTitle({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof QuestionnairePrimitive.Title>) {
|
||||
return (
|
||||
<QuestionnairePrimitive.Title
|
||||
data-slot="questionnaire-title"
|
||||
className={cn(
|
||||
"font-heading text-base leading-snug font-bold text-pretty text-slate-900 [&:not(:has(~[data-slot=questionnaire-description]))]:mb-1",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function QuestionnaireDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof QuestionnairePrimitive.Description>) {
|
||||
return (
|
||||
<QuestionnairePrimitive.Description
|
||||
data-slot="questionnaire-description"
|
||||
className={cn("text-sm text-pretty leading-5 text-slate-600", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function QuestionnaireChoices({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof QuestionnairePrimitive.Choices>) {
|
||||
return (
|
||||
<QuestionnairePrimitive.Choices
|
||||
data-slot="questionnaire-choices"
|
||||
className={cn("group/questionnaire-choices grid min-w-0 gap-2", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Seçenek satırı: 44 px sade kutu; seçilince sihirbaz çipi gibi mavi dolar ve
|
||||
* başındaki işaret tik olur — seçili hâl renk + tik'tir (§2.2). Klavye
|
||||
* kısayolu (harf) sağda küçük bir kutuda durur.
|
||||
*/
|
||||
function QuestionnaireChoice({
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof QuestionnairePrimitive.Choice>) {
|
||||
return (
|
||||
<QuestionnairePrimitive.Choice
|
||||
data-slot="questionnaire-choice"
|
||||
className={cn(
|
||||
"group/questionnaire-choice relative flex min-h-11 cursor-pointer items-start gap-2.5 rounded-2xl border border-slate-200 bg-card px-3.5 py-2.5 text-start text-sm font-semibold text-slate-700 transition-[background-color,border-color,color,transform] duration-150 outline-none select-none",
|
||||
"hover:border-orange-500 hover:bg-orange-50/60 hover:text-orange-500 active:scale-[0.99]",
|
||||
"has-[>input:focus-visible]:ring-3 has-[>input:focus-visible]:ring-ring/50",
|
||||
"data-checked:border-primary data-checked:bg-primary data-checked:text-white data-checked:hover:border-primary data-checked:hover:bg-primary data-checked:hover:text-white",
|
||||
"data-invalid:border-red-500",
|
||||
"data-disabled:pointer-events-none data-disabled:cursor-not-allowed data-disabled:opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<QuestionnairePrimitive.ChoiceInput
|
||||
data-slot="questionnaire-choice-input"
|
||||
className="absolute inset-0 z-10 size-full cursor-pointer opacity-0"
|
||||
/>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
data-slot="questionnaire-choice-indicator"
|
||||
className="pointer-events-none relative mt-0.5 flex size-4 shrink-0 items-center justify-center rounded-[4px] border border-slate-300 bg-card group-data-[type=radio]/questionnaire-choice:rounded-full group-data-checked/questionnaire-choice:border-primary-foreground group-data-checked/questionnaire-choice:bg-primary-foreground group-data-checked/questionnaire-choice:text-primary"
|
||||
>
|
||||
<Check
|
||||
data-slot="questionnaire-choice-indicator-check"
|
||||
className="hidden size-3 group-data-checked/questionnaire-choice:block"
|
||||
strokeWidth={3}
|
||||
/>
|
||||
</span>
|
||||
<QuestionnairePrimitive.ChoiceLabel
|
||||
data-slot="questionnaire-choice-label"
|
||||
className="flex min-w-0 flex-1 flex-col gap-0.5 leading-snug"
|
||||
>
|
||||
{children}
|
||||
</QuestionnairePrimitive.ChoiceLabel>
|
||||
<QuestionnairePrimitive.ChoiceShortcut
|
||||
data-slot="questionnaire-choice-shortcut"
|
||||
className="pointer-events-none ms-auto mt-0.5 hidden size-5 shrink-0 items-center justify-center rounded-md border border-slate-200 bg-card font-mono text-[0.625rem] leading-none font-medium text-slate-500 group-data-checked/questionnaire-choice:border-white/40 group-data-checked/questionnaire-choice:bg-white/15 group-data-checked/questionnaire-choice:text-white group-data-[shortcut]/questionnaire-choice:inline-flex"
|
||||
/>
|
||||
</QuestionnairePrimitive.Choice>
|
||||
);
|
||||
}
|
||||
|
||||
function QuestionnaireChoiceDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
data-slot="questionnaire-choice-description"
|
||||
className={cn(
|
||||
"text-xs font-normal text-slate-500 group-data-checked/questionnaire-choice:text-white/80",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
/** Serbest cevap alanı: §4.6 metin alanı hapı (44 px, bu kutuda bir kademe kısa). */
|
||||
function QuestionnaireInput({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof QuestionnairePrimitive.Input>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="questionnaire-input-wrapper"
|
||||
className="group/questionnaire-input relative w-full min-w-0"
|
||||
>
|
||||
<QuestionnairePrimitive.Input
|
||||
data-slot="questionnaire-input"
|
||||
className={cn(
|
||||
"h-11 w-full min-w-0 rounded-full border border-slate-200 bg-card px-4 text-base text-slate-900 transition-[border-color,box-shadow] 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}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function QuestionnaireError({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof QuestionnairePrimitive.Error>) {
|
||||
return (
|
||||
<QuestionnairePrimitive.Error
|
||||
data-slot="questionnaire-error"
|
||||
className={cn("text-sm font-medium text-red-600", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function QuestionnaireActions({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="questionnaire-actions"
|
||||
className={cn(
|
||||
"grid min-h-11 w-full grid-cols-[minmax(0,1fr)_auto_auto] items-center gap-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
type DugmeProps = Pick<React.ComponentProps<typeof Button>, "size" | "variant">;
|
||||
|
||||
function QuestionnairePrevious({
|
||||
children,
|
||||
className,
|
||||
size = "default",
|
||||
variant = "outline",
|
||||
...props
|
||||
}: React.ComponentProps<typeof QuestionnairePrimitive.Previous> & DugmeProps) {
|
||||
return (
|
||||
<QuestionnairePrimitive.Previous
|
||||
data-slot="questionnaire-previous"
|
||||
className={cn(
|
||||
buttonVariants({ size, variant }),
|
||||
"col-start-1 row-start-1 cursor-pointer justify-self-start",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children ?? "Geri"}
|
||||
</QuestionnairePrimitive.Previous>
|
||||
);
|
||||
}
|
||||
|
||||
function QuestionnaireSkip({
|
||||
children,
|
||||
className,
|
||||
size = "default",
|
||||
variant = "ghost",
|
||||
...props
|
||||
}: React.ComponentProps<typeof QuestionnairePrimitive.Skip> & DugmeProps) {
|
||||
return (
|
||||
<QuestionnairePrimitive.Skip
|
||||
data-slot="questionnaire-skip"
|
||||
className={cn(
|
||||
buttonVariants({ size, variant }),
|
||||
"col-start-2 row-start-1 cursor-pointer justify-self-end",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children ?? "Atla"}
|
||||
</QuestionnairePrimitive.Skip>
|
||||
);
|
||||
}
|
||||
|
||||
function QuestionnaireNext({
|
||||
children,
|
||||
className,
|
||||
size = "default",
|
||||
variant = "default",
|
||||
...props
|
||||
}: React.ComponentProps<typeof QuestionnairePrimitive.Next> & DugmeProps) {
|
||||
return (
|
||||
<QuestionnairePrimitive.Next
|
||||
data-slot="questionnaire-next"
|
||||
className={cn(
|
||||
buttonVariants({ size, variant }),
|
||||
"col-start-3 row-start-1 cursor-pointer justify-self-end",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children ?? "İleri"}
|
||||
</QuestionnairePrimitive.Next>
|
||||
);
|
||||
}
|
||||
|
||||
function QuestionnaireSubmit({
|
||||
children,
|
||||
className,
|
||||
size = "default",
|
||||
variant = "default",
|
||||
...props
|
||||
}: React.ComponentProps<typeof QuestionnairePrimitive.Submit> & DugmeProps) {
|
||||
return (
|
||||
<QuestionnairePrimitive.Submit
|
||||
data-slot="questionnaire-submit"
|
||||
className={cn(
|
||||
buttonVariants({ size, variant }),
|
||||
"col-start-3 row-start-1 cursor-pointer justify-self-end",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children ?? "Gönder"}
|
||||
</QuestionnairePrimitive.Submit>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
Questionnaire,
|
||||
QuestionnaireActions,
|
||||
QuestionnaireChoice,
|
||||
QuestionnaireChoiceDescription,
|
||||
QuestionnaireChoices,
|
||||
QuestionnaireDescription,
|
||||
QuestionnaireError,
|
||||
QuestionnaireInput,
|
||||
QuestionnaireItem,
|
||||
QuestionnaireNext,
|
||||
QuestionnairePrevious,
|
||||
QuestionnaireProgress,
|
||||
QuestionnaireSkip,
|
||||
QuestionnaireSubmit,
|
||||
QuestionnaireTitle,
|
||||
};
|
||||
Reference in New Issue
Block a user