this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(ui): simplify app surfaces and remove login route

+466 -263
-1
README.md
··· 107 107 ## Product surfaces 108 108 109 109 ### Creator app 110 - - `/login` 111 110 - `/dashboard` 112 111 - `/forms/[id]/edit` 113 112 - `/forms/[id]/responses`
+1 -1
app/(creator)/actions.ts
··· 9 9 const session = await getServerAuthSession(); 10 10 11 11 if (!session?.user?.id) { 12 - redirect("/login"); 12 + redirect("/"); 13 13 } 14 14 15 15 const form = await createDraftForm(session.user.id);
+11 -12
app/(creator)/dashboard/page.tsx
··· 16 16 17 17 return ( 18 18 <div className="space-y-8"> 19 - <section className="flex flex-col gap-4 rounded-[32px] border border-black/8 bg-[linear-gradient(180deg,rgba(255,255,255,0.95),rgba(235,247,235,0.92))] px-8 py-8 shadow-[0_24px_70px_rgba(15,23,42,0.06)] lg:flex-row lg:items-end lg:justify-between"> 20 - <div className="space-y-3"> 21 - <p className="text-xs font-semibold uppercase tracking-[0.28em] text-[var(--accent)]">Creator dashboard</p> 22 - <h1 className="font-display text-5xl leading-tight text-[var(--ink)]">Build linear forms that feel composed.</h1> 23 - <p className="max-w-2xl text-sm leading-7 text-[var(--muted)]"> 24 - Start drafts fast, keep editing focused, and send people into a one-screen-at-a-time runner that stays 25 - anonymous on submission. 19 + <section className="flex flex-col gap-4 border-b border-black/8 pb-6 lg:flex-row lg:items-end lg:justify-between"> 20 + <div className="space-y-2"> 21 + <p className="text-xs font-semibold uppercase tracking-[0.24em] text-[var(--accent)]">Dashboard</p> 22 + <h1 className="font-display text-4xl leading-tight text-[var(--ink)]">Your forms</h1> 23 + <p className="max-w-2xl text-sm leading-6 text-[var(--muted)]"> 24 + Create a form, open a draft, or review responses. 26 25 </p> 27 26 </div> 28 27 <form action={createFormAction}> ··· 36 35 {forms.length === 0 ? ( 37 36 <EmptyState 38 37 eyebrow="No forms yet" 39 - title="Start with a draft and shape the conversation." 40 - description="Every new form begins with a welcome text block and a first question so you can immediately refine the flow." 38 + title="Create your first form" 39 + description="New forms start as drafts so you can edit before publishing." 41 40 action={ 42 41 <form action={createFormAction}> 43 42 <Button type="submit"> 44 43 <Plus className="size-4" /> 45 - Create your first form 44 + New form 46 45 </Button> 47 46 </form> 48 47 } ··· 57 56 {form.status === "PUBLISHED" ? "Published" : "Draft"} 58 57 </Badge> 59 58 <h2 className="mt-4 font-display text-3xl text-[var(--ink)]">{form.title}</h2> 60 - <p className="mt-3 max-w-xl text-sm leading-7 text-[var(--muted)]"> 61 - {form.description || "No description yet — open the builder to shape the story of the form."} 59 + <p className="mt-3 max-w-xl text-sm leading-6 text-[var(--muted)]"> 60 + {form.description || "No description yet."} 62 61 </p> 63 62 </div> 64 63 <Link href={`/forms/${form.id}/edit`}>
+2 -2
app/(creator)/forms/[id]/responses/[responseId]/page.tsx
··· 31 31 32 32 return ( 33 33 <div className="space-y-6"> 34 - <section className="flex flex-col gap-4 rounded-[30px] border border-black/8 bg-white/70 px-6 py-5 backdrop-blur-sm lg:flex-row lg:items-end lg:justify-between"> 34 + <section className="flex flex-col gap-4 border-b border-black/8 pb-5 lg:flex-row lg:items-end lg:justify-between"> 35 35 <div> 36 36 <p className="text-xs font-semibold uppercase tracking-[0.28em] text-[var(--accent)]">Submission replay</p> 37 37 <h1 className="mt-3 font-display text-4xl text-[var(--ink)]">{formatDate(response.submittedAt)}</h1> 38 - <p className="mt-2 text-sm leading-7 text-[var(--muted)]">Text blocks appear as context markers while answerable steps show the submitted value.</p> 38 + <p className="mt-2 text-sm leading-6 text-[var(--muted)]">Review submitted answers in form order.</p> 39 39 </div> 40 40 <Link href={`/forms/${id}/responses`}> 41 41 <Button variant="secondary">Back to responses</Button>
+23 -20
app/(creator)/forms/[id]/responses/page.tsx
··· 31 31 } 32 32 33 33 return ( 34 - <div className="space-y-6"> 35 - <section className="flex flex-col gap-4 rounded-[30px] border border-black/8 bg-white/70 px-6 py-5 backdrop-blur-sm lg:flex-row lg:items-end lg:justify-between"> 34 + <div className="space-y-5"> 35 + <section className="flex flex-col gap-4 border-b border-black/8 pb-5 lg:flex-row lg:items-end lg:justify-between"> 36 36 <div> 37 37 <p className="text-xs font-semibold uppercase tracking-[0.28em] text-[var(--accent)]">Responses</p> 38 38 <h1 className="mt-3 font-display text-4xl text-[var(--ink)]">{form.title}</h1> 39 - <p className="mt-2 text-sm leading-7 text-[var(--muted)]">Read each anonymous submission in form order and keep the conversational context intact.</p> 39 + <p className="mt-2 text-sm leading-6 text-[var(--muted)]">Review anonymous submissions in form order.</p> 40 40 </div> 41 41 <div className="flex gap-3"> 42 42 <Badge>{responses.length} submissions</Badge> ··· 58 58 } 59 59 /> 60 60 ) : ( 61 - <div className="grid gap-4"> 62 - {responses.map((response) => ( 63 - <Card key={response.id} className="p-6"> 64 - <div className="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between"> 65 - <div> 66 - <p className="text-xs font-semibold uppercase tracking-[0.24em] text-[var(--accent)]">Anonymous response</p> 67 - <h2 className="mt-3 font-display text-3xl text-[var(--ink)]">{formatDate(response.submittedAt)}</h2> 68 - <p className="mt-2 text-sm text-[var(--muted)]">{response.answerCount} answers captured</p> 61 + <Card className="overflow-hidden p-0"> 62 + <div className="divide-y divide-black/8"> 63 + {responses.map((response) => ( 64 + <div key={response.id} className="flex flex-col gap-3 px-5 py-4 sm:flex-row sm:items-center sm:justify-between sm:gap-4"> 65 + <div className="min-w-0"> 66 + <div className="flex flex-col gap-1 sm:flex-row sm:items-center sm:gap-3"> 67 + <h2 className="font-display text-2xl text-[var(--ink)]">{formatDate(response.submittedAt)}</h2> 68 + <p className="text-sm text-[var(--muted)]">{response.answerCount} answers</p> 69 + </div> 70 + </div> 71 + <div className="sm:flex sm:justify-end"> 72 + <Link href={`/forms/${form.id}/responses/${response.id}`}> 73 + <Button size="sm" variant="secondary"> 74 + Open 75 + <ArrowRight className="size-4" /> 76 + </Button> 77 + </Link> 69 78 </div> 70 - <Link href={`/forms/${form.id}/responses/${response.id}`}> 71 - <Button> 72 - Open response 73 - <ArrowRight className="size-4" /> 74 - </Button> 75 - </Link> 76 79 </div> 77 - </Card> 78 - ))} 79 - </div> 80 + ))} 81 + </div> 82 + </Card> 80 83 )} 81 84 </div> 82 85 );
+1 -1
app/(creator)/forms/new/route.ts
··· 7 7 const session = await getServerAuthSession(); 8 8 9 9 if (!session?.user?.id) { 10 - redirect("/login"); 10 + redirect("/"); 11 11 } 12 12 13 13 const form = await createDraftForm(session.user.id);
+5 -13
app/(creator)/layout.tsx
··· 8 8 const session = await getServerAuthSession(); 9 9 10 10 if (!session?.user?.id) { 11 - redirect("/login"); 11 + redirect("/"); 12 12 } 13 13 14 14 return ( 15 15 <main className="mx-auto min-h-screen w-full max-w-7xl px-6 py-8 lg:px-10"> 16 - <header className="mb-8 flex flex-col gap-4 rounded-[30px] border border-black/8 bg-white/70 px-6 py-5 backdrop-blur-sm lg:flex-row lg:items-center lg:justify-between"> 17 - <div className="flex items-center gap-4"> 18 - <Link href="/dashboard" className="space-y-1"> 19 - <p className="text-xs font-semibold uppercase tracking-[0.32em] text-[var(--accent)]">Lively Forms</p> 20 - <p className="text-sm text-[var(--muted)]">Creator workspace</p> 21 - </Link> 22 - <div className="hidden h-12 w-px bg-black/8 lg:block" /> 23 - <div className="hidden lg:block"> 24 - <p className="font-display text-2xl text-[var(--ink)]">{session.user.name ?? session.user.email ?? "Creator"}</p> 25 - <p className="text-sm text-[var(--muted)]">Own forms, publish calmly, review responses.</p> 26 - </div> 16 + <header className="mb-8 flex flex-col gap-4 border-b border-black/8 pb-5 lg:flex-row lg:items-center lg:justify-between"> 17 + <div> 18 + <p className="font-display text-2xl text-[var(--ink)]">Hi, {session.user.name ?? session.user.email ?? "Creator"}</p> 27 19 </div> 28 - <div className="flex items-center gap-3"> 20 + <div className="flex items-center gap-5"> 29 21 <Link href="/dashboard" className="text-sm font-medium text-[var(--muted)] transition hover:text-[var(--ink)]"> 30 22 Dashboard 31 23 </Link>
+18 -1
app/api/forms/[formId]/route.ts
··· 2 2 3 3 import { handleRouteError } from "@/lib/api"; 4 4 import { getServerAuthSession } from "@/lib/auth"; 5 - import { updateOwnedFormMetadata } from "@/lib/forms"; 5 + import { deleteOwnedForm, updateOwnedFormMetadata } from "@/lib/forms"; 6 6 7 7 export async function PATCH(request: Request, context: { params: Promise<{ formId: string }> }) { 8 8 const session = await getServerAuthSession(); ··· 21 21 return handleRouteError(error); 22 22 } 23 23 } 24 + 25 + export async function DELETE(_request: Request, context: { params: Promise<{ formId: string }> }) { 26 + const session = await getServerAuthSession(); 27 + 28 + if (!session?.user?.id) { 29 + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); 30 + } 31 + 32 + try { 33 + const { formId } = await context.params; 34 + await deleteOwnedForm(session.user.id, formId); 35 + 36 + return NextResponse.json({ ok: true }); 37 + } catch (error) { 38 + return handleRouteError(error); 39 + } 40 + }
+10 -24
app/globals.css
··· 1 1 @import "tailwindcss"; 2 2 3 3 :root { 4 - --bg: #f3f8f1; 5 - --bg-strong: #e3f0e0; 6 - --surface: rgba(255, 255, 255, 0.72); 7 - --surface-strong: rgba(255, 255, 255, 0.92); 8 - --ink: #162015; 9 - --muted: #5f7060; 10 - --accent: #74ad7c; 11 - --accent-soft: #b9dfb9; 12 - --line: rgba(22, 32, 21, 0.08); 4 + --bg: #f5f5f1; 5 + --bg-strong: #ecece6; 6 + --surface: rgba(255, 255, 255, 0.88); 7 + --surface-strong: rgba(255, 255, 255, 0.96); 8 + --ink: #171717; 9 + --muted: #5f5f5a; 10 + --accent: #4f7a58; 11 + --accent-soft: #dbe6dd; 12 + --line: rgba(23, 23, 23, 0.08); 13 13 } 14 14 15 15 @theme inline { ··· 25 25 26 26 body { 27 27 min-height: 100vh; 28 - background: 29 - radial-gradient(circle at top, rgba(255, 255, 255, 0.82), transparent 42%), 30 - linear-gradient(180deg, #fbfef9 0%, #f3f8f1 48%, #e7f2e5 100%); 28 + background: linear-gradient(180deg, #f8f8f4 0%, #f2f1eb 100%); 31 29 color: var(--ink); 32 30 font-family: var(--font-manrope), sans-serif; 33 - } 34 - 35 - body::before { 36 - position: fixed; 37 - inset: 0; 38 - pointer-events: none; 39 - content: ""; 40 - background-image: linear-gradient(rgba(255,255,255,0.16) 1px, transparent 1px), 41 - linear-gradient(90deg, rgba(255,255,255,0.16) 1px, transparent 1px); 42 - background-size: 32px 32px; 43 - mask-image: radial-gradient(circle at center, black, transparent 82%); 44 - opacity: 0.5; 45 31 } 46 32 47 33 main {
+1 -1
app/layout.tsx
··· 14 14 15 15 export const metadata: Metadata = { 16 16 title: "Lively Forms", 17 - description: "Lively Forms is a conversational form builder with an elegant creator workflow.", 17 + description: "Lively Forms is a conversational form builder for creating, publishing, and reviewing forms.", 18 18 }; 19 19 20 20 export default function RootLayout({
-40
app/login/page.tsx
··· 1 - import { redirect } from "next/navigation"; 2 - 3 - import { GoogleSignInButton } from "@/components/auth/google-sign-in-button"; 4 - import { Card } from "@/components/ui/card"; 5 - import { getServerAuthSession } from "@/lib/auth"; 6 - 7 - export default async function LoginPage() { 8 - const session = await getServerAuthSession(); 9 - 10 - if (session?.user?.id) { 11 - redirect("/dashboard"); 12 - } 13 - 14 - return ( 15 - <main className="mx-auto flex min-h-screen max-w-4xl items-center px-6 py-16"> 16 - <Card className="grid w-full overflow-hidden lg:grid-cols-[1.1fr_0.9fr]"> 17 - <div className="bg-[#191611] p-10 text-white lg:p-14"> 18 - <p className="text-xs font-semibold uppercase tracking-[0.3em] text-[#b9dfb9]">Lively Forms creator access</p> 19 - <h1 className="mt-8 font-display text-5xl leading-tight">Sign in to craft the form experience.</h1> 20 - <p className="mt-6 max-w-md text-sm leading-7 text-white/70"> 21 - Creator auth is used only for owning, editing, publishing, and reviewing forms. Public respondents never 22 - log in and their submissions stay anonymous. 23 - </p> 24 - </div> 25 - <div className="flex items-center justify-center bg-[linear-gradient(180deg,rgba(255,255,255,0.92),rgba(237,247,237,0.96))] p-10 lg:p-14"> 26 - <div className="w-full max-w-sm space-y-6"> 27 - <div> 28 - <p className="text-xs font-semibold uppercase tracking-[0.28em] text-[var(--accent)]">Lively Forms login</p> 29 - <h2 className="mt-3 font-display text-4xl text-[var(--ink)]">Use Google to continue.</h2> 30 - </div> 31 - <p className="text-sm leading-7 text-[var(--muted)]"> 32 - Keep auth simple in v0 and stay focused on the form builder and conversational runner. 33 - </p> 34 - <GoogleSignInButton /> 35 - </div> 36 - </div> 37 - </Card> 38 - </main> 39 - ); 40 - }
+27 -76
app/page.tsx
··· 1 1 import Link from "next/link"; 2 - import { ArrowRight, Sparkles } from "lucide-react"; 2 + import { ArrowRight } from "lucide-react"; 3 3 4 + import { GoogleSignInButton } from "@/components/auth/google-sign-in-button"; 4 5 import { Button } from "@/components/ui/button"; 5 6 import { Card } from "@/components/ui/card"; 6 7 import { getServerAuthSession } from "@/lib/auth"; ··· 9 10 const session = await getServerAuthSession(); 10 11 11 12 return ( 12 - <main className="mx-auto flex min-h-screen w-full max-w-6xl flex-col px-6 py-10 lg:px-10"> 13 - <header className="flex items-center justify-between rounded-full border border-black/8 bg-white/60 px-5 py-3 backdrop-blur-sm"> 14 - <div> 15 - <p className="text-xs font-semibold uppercase tracking-[0.32em] text-[var(--accent)]">Lively Forms</p> 16 - <p className="text-xs text-[var(--muted)]">Conversational forms with a softer, brighter rhythm.</p> 17 - </div> 18 - <Link href={session?.user ? "/dashboard" : "/login"}> 19 - <Button variant="secondary" size="sm"> 20 - {session?.user ? "Open dashboard" : "Creator login"} 21 - </Button> 22 - </Link> 23 - </header> 24 - 25 - <section className="grid flex-1 items-center gap-8 py-12 lg:grid-cols-[1.25fr_0.9fr] lg:py-20"> 26 - <div className="space-y-8"> 27 - <div className="inline-flex items-center gap-2 rounded-full border border-black/8 bg-white/70 px-4 py-2 text-xs font-semibold uppercase tracking-[0.24em] text-[var(--accent)] backdrop-blur-sm"> 28 - <Sparkles className="size-3.5" /> 29 - Lively Forms v0 30 - </div> 31 - <div className="space-y-5"> 32 - <h1 className="max-w-4xl font-display text-6xl leading-none text-[var(--ink)] md:text-7xl"> 33 - Build forms that feel like a calm conversation. 13 + <main className="mx-auto flex min-h-screen w-full max-w-4xl flex-col px-6 py-8 lg:px-10 lg:py-10"> 14 + <section className="flex flex-1 items-center py-16 lg:py-24"> 15 + <Card className="w-full p-8 lg:p-10"> 16 + <div className="max-w-2xl"> 17 + <p className="text-xs font-semibold uppercase tracking-[0.28em] text-[var(--accent)]">Lively Forms</p> 18 + <h1 className="mt-4 font-display text-5xl leading-tight text-[var(--ink)] sm:text-6xl"> 19 + Create, publish, and review forms. 34 20 </h1> 35 - <p className="max-w-2xl text-lg leading-8 text-[var(--muted)]"> 36 - Create linear, elegant flows with focused screens, anonymous public submissions, and a polished 37 - creator workflow made for shipping quickly. 21 + <p className="mt-5 text-base leading-7 text-[var(--muted)] sm:text-lg"> 22 + Lively Forms is a focused tool for building conversational forms, sharing them publicly, and reviewing 23 + anonymous responses. 38 24 </p> 39 - </div> 40 - <div className="flex flex-col gap-3 sm:flex-row"> 41 - <Link href={session?.user ? "/dashboard" : "/login"}> 42 - <Button size="lg"> 43 - {session?.user ? "Go to dashboard" : "Start with Google"} 44 - <ArrowRight className="size-4" /> 45 - </Button> 46 - </Link> 47 - <a href="#surfaces"> 48 - <Button variant="secondary" size="lg"> 49 - See product surfaces 50 - </Button> 51 - </a> 52 - </div> 53 - </div> 25 + 26 + <div className="mt-8 flex flex-col gap-3 sm:flex-row"> 27 + {session?.user ? ( 28 + <Link href="/dashboard"> 29 + <Button size="lg"> 30 + Open dashboard 31 + <ArrowRight className="size-4" /> 32 + </Button> 33 + </Link> 34 + ) : ( 35 + <GoogleSignInButton /> 36 + )} 37 + </div> 54 38 55 - <Card className="overflow-hidden bg-[linear-gradient(180deg,rgba(255,255,255,0.97),rgba(235,247,235,0.94))] p-6"> 56 - <div className="rounded-[24px] border border-black/8 bg-[#171411] p-6 text-white shadow-[0_24px_70px_rgba(23,20,17,0.26)]"> 57 - <p className="text-xs font-semibold uppercase tracking-[0.24em] text-[#b9dfb9]">Respondent experience</p> 58 - <div className="mt-8 space-y-6"> 59 - <div className="h-1.5 rounded-full bg-white/10"> 60 - <div className="h-full w-1/2 rounded-full bg-[#9fd7a4]" /> 61 - </div> 62 - <div className="space-y-4"> 63 - <p className="text-sm uppercase tracking-[0.3em] text-white/45">Step 2 of 4</p> 64 - <h2 className="font-display text-4xl leading-tight">What should we call you?</h2> 65 - <p className="max-w-sm text-sm leading-7 text-white/70"> 66 - Every step gets the full stage — large type, clean focus, and motion that keeps the flow moving. 67 - </p> 68 - </div> 69 - <div className="rounded-[24px] border border-white/10 bg-white/5 p-4 text-white/55">Type your answer here…</div> 70 - <div className="flex items-center justify-between"> 71 - <Button variant="secondary" className="border border-white/12 bg-white/8 text-white hover:bg-white/14"> 72 - Back 73 - </Button> 74 - <Button className="bg-[#9fd7a4] text-[#122012] hover:bg-[#b8e4bb]">Continue</Button> 75 - </div> 39 + <div className="mt-10 border-t border-black/8 pt-6 text-sm text-[var(--muted)]"> 40 + <p>Core workflow: create a form, publish it, and review responses.</p> 76 41 </div> 77 42 </div> 78 43 </Card> 79 - </section> 80 - 81 - <section id="surfaces" className="grid gap-6 pb-8 md:grid-cols-3"> 82 - {[ 83 - ["Creator dashboard", "Start new forms, manage drafts, and jump straight into editing without extra ceremony."], 84 - ["Block builder", "A master-detail editor keeps structure simple while the public runner gets the design spotlight."], 85 - ["Response review", "Read each submission in form order and keep the conversational context intact."], 86 - ].map(([title, description]) => ( 87 - <Card key={title} className="p-6"> 88 - <p className="text-xs font-semibold uppercase tracking-[0.26em] text-[var(--accent)]">Surface</p> 89 - <h3 className="mt-4 font-display text-3xl">{title}</h3> 90 - <p className="mt-3 text-sm leading-7 text-[var(--muted)]">{description}</p> 91 - </Card> 92 - ))} 93 44 </section> 94 45 </main> 95 46 );
-1
components/auth/google-sign-in-button.tsx
··· 11 11 12 12 return ( 13 13 <Button 14 - className="w-full" 15 14 size="lg" 16 15 onClick={() => 17 16 startTransition(() => {
+5 -5
components/empty-state.tsx
··· 4 4 5 5 export function EmptyState({ eyebrow, title, description, action }: { eyebrow: string; title: string; description: string; action?: ReactNode }) { 6 6 return ( 7 - <Card className="border-dashed px-8 py-10 text-center"> 8 - <p className="text-xs font-semibold uppercase tracking-[0.28em] text-[var(--accent)]">{eyebrow}</p> 9 - <h3 className="mt-4 font-display text-3xl text-[var(--ink)]">{title}</h3> 10 - <p className="mx-auto mt-3 max-w-lg text-sm leading-7 text-[var(--muted)]">{description}</p> 11 - {action ? <div className="mt-6 flex justify-center">{action}</div> : null} 7 + <Card className="px-8 py-8"> 8 + <p className="text-xs font-semibold uppercase tracking-[0.24em] text-[var(--accent)]">{eyebrow}</p> 9 + <h3 className="mt-3 font-display text-2xl text-[var(--ink)]">{title}</h3> 10 + <p className="mt-2 max-w-xl text-sm leading-6 text-[var(--muted)]">{description}</p> 11 + {action ? <div className="mt-5 flex">{action}</div> : null} 12 12 </Card> 13 13 ); 14 14 }
+77 -64
components/form-builder.tsx
··· 22 22 Link as LinkIcon, 23 23 LoaderCircle, 24 24 Plus, 25 + MessagesSquare, 25 26 Radio, 26 27 Rows3, 27 28 Save, ··· 38 39 import { Button } from "@/components/ui/button"; 39 40 import { Card } from "@/components/ui/card"; 40 41 import { Input } from "@/components/ui/input"; 42 + import { ToastViewport, type ToastData } from "@/components/ui/toast"; 41 43 import { Textarea } from "@/components/ui/textarea"; 42 44 import { cn } from "@/lib/utils"; 43 45 ··· 232 234 const [selection, setSelection] = useState<Selection>(() => 233 235 initialForm.blocks[0] ? { kind: "block", blockId: initialForm.blocks[0].id } : { kind: "form" }, 234 236 ); 235 - const [message, setMessage] = useState<string | null>(null); 236 - const [error, setError] = useState<string | null>(null); 237 + const [toasts, setToasts] = useState<ToastData[]>([]); 237 238 const [busy, setBusy] = useState<string | null>(null); 238 239 const [isDndReady, setIsDndReady] = useState(false); 239 240 ··· 268 269 setIsDndReady(true); 269 270 }, []); 270 271 272 + function showToast(message: string, variant: ToastData["variant"] = "success") { 273 + const id = crypto.randomUUID(); 274 + setToasts((current) => [...current, { id, message, variant }]); 275 + } 276 + 277 + function dismissToast(id: string) { 278 + setToasts((current) => current.filter((toast) => toast.id !== id)); 279 + } 280 + 271 281 async function withTask(task: string, runner: () => Promise<void>) { 272 282 setBusy(task); 273 - setError(null); 274 283 275 284 try { 276 285 await runner(); 277 286 } catch (caughtError) { 278 287 const nextError = caughtError instanceof Error ? caughtError.message : "Something went wrong."; 279 - setError(nextError); 288 + showToast(nextError, "error"); 280 289 } finally { 281 290 setBusy(null); 282 291 } ··· 290 299 }); 291 300 292 301 setForm(payload.form); 293 - setMessage("Form settings saved."); 302 + showToast("Saved form settings"); 294 303 }); 295 304 } 296 305 ··· 306 315 blocks: [...current.blocks, payload.block], 307 316 })); 308 317 setSelection({ kind: "block", blockId: payload.block.id }); 309 - setMessage(`${blockLabels[type]} added.`); 318 + showToast(`Added ${blockLabels[type].toLowerCase()}`); 310 319 }); 311 320 } 312 321 ··· 330 339 ...current, 331 340 blocks: current.blocks.map((block) => (block.id === payload.block.id ? payload.block : block)), 332 341 })); 333 - setMessage("Block saved."); 342 + showToast("Saved block"); 334 343 }); 335 344 } 336 345 ··· 350 359 })), 351 360 })); 352 361 setSelection({ kind: "form" }); 353 - setMessage("Block removed."); 362 + showToast("Removed block"); 354 363 }); 355 364 } 356 365 ··· 364 373 }); 365 374 366 375 setForm(payload.form); 367 - setMessage(payload.form.status === "PUBLISHED" ? "Form published." : "Form moved back to draft."); 376 + showToast(payload.form.status === "PUBLISHED" ? "Form published" : "Form moved back to draft"); 368 377 router.refresh(); 369 378 }); 370 379 } ··· 372 381 async function copyShareLink() { 373 382 const url = `${window.location.origin}/f/${form.slug}`; 374 383 await navigator.clipboard.writeText(url); 375 - setMessage("Share link copied."); 384 + showToast("Copied share link"); 385 + } 386 + 387 + async function deleteForm() { 388 + const confirmed = window.confirm( 389 + "Delete this form permanently? This will also delete all blocks and responses. This action cannot be undone.", 390 + ); 391 + 392 + if (!confirmed) { 393 + return; 394 + } 395 + 396 + await withTask("delete-form", async () => { 397 + await fetchJson(`/api/forms/${form.id}`, { 398 + method: "DELETE", 399 + }); 400 + 401 + router.push("/dashboard"); 402 + router.refresh(); 403 + }); 376 404 } 377 405 378 406 async function handleDragEnd(event: DragEndEvent) { ··· 400 428 }); 401 429 402 430 setForm(payload.form); 403 - setMessage("Block order updated."); 431 + showToast("Updated block order"); 404 432 } catch (caughtError) { 405 433 setForm((current) => ({ ...current, blocks: previous })); 406 - setError(caughtError instanceof Error ? caughtError.message : "Could not reorder blocks."); 434 + showToast(caughtError instanceof Error ? caughtError.message : "Could not reorder blocks.", "error"); 407 435 } 408 436 } 409 437 410 438 const shareHref = `/f/${form.slug}`; 411 439 412 440 return ( 413 - <div className="space-y-6"> 414 - <section className="flex flex-col gap-4 rounded-[30px] border border-black/8 bg-white/70 px-6 py-5 backdrop-blur-sm xl:flex-row xl:items-center xl:justify-between"> 441 + <> 442 + <ToastViewport toasts={toasts} onDismiss={dismissToast} /> 443 + <div className="space-y-6"> 444 + <section className="flex flex-col gap-4 border-b border-black/8 pb-5 lg:flex-row lg:items-end lg:justify-between"> 415 445 <div> 416 - <p className="text-xs font-semibold uppercase tracking-[0.28em] text-[var(--accent)]">Builder</p> 417 - <h1 className="mt-3 font-display text-4xl text-[var(--ink)]">{form.title}</h1> 418 - <p className="mt-2 text-sm leading-7 text-[var(--muted)]"> 419 - Left: structure and flow. Right: the selected block or form settings. 420 - </p> 446 + <p className="text-xs font-semibold uppercase tracking-[0.24em] text-[var(--accent)]">Builder</p> 447 + <div className="mt-3 flex flex-wrap items-center gap-3"> 448 + <h1 className="font-display text-4xl text-[var(--ink)]">{form.title}</h1> 449 + <Badge className={cn(form.status === "PUBLISHED" && "bg-[var(--accent-soft)] text-[#2f5d35]")}>{form.status}</Badge> 450 + </div> 451 + <p className="mt-2 text-sm leading-6 text-[var(--muted)]">Edit blocks, update settings, and review responses.</p> 421 452 </div> 422 - <div className="flex flex-wrap items-center gap-3"> 423 - <Badge className={cn(form.status === "PUBLISHED" && "bg-[var(--accent-soft)] text-[#2f5d35]")}>{form.status}</Badge> 453 + <div className="flex flex-wrap items-center gap-3 lg:justify-end"> 454 + <Button 455 + variant={selection.kind === "form" ? "default" : "secondary"} 456 + onClick={() => setSelection({ kind: "form" })} 457 + > 458 + <Settings2 className="size-4" /> 459 + Settings 460 + </Button> 424 461 <Link href={`/forms/${form.id}/responses`}> 425 - <Button variant="secondary">Responses ({form.responseCount})</Button> 462 + <Button variant="secondary"> 463 + <MessagesSquare className="size-4" /> 464 + Responses ({form.responseCount}) 465 + </Button> 426 466 </Link> 427 467 <Button variant={form.status === "PUBLISHED" ? "secondary" : "default"} onClick={togglePublished}> 428 468 {busy === "publish" ? <LoaderCircle className="size-4 animate-spin" /> : <LinkIcon className="size-4" />} ··· 431 471 </div> 432 472 </section> 433 473 434 - {(message || error) && ( 435 - <div 436 - className={cn( 437 - "rounded-2xl border px-4 py-3 text-sm", 438 - error ? "border-rose-200 bg-rose-50 text-rose-700" : "border-[#cfe9d0] bg-[#eef9ef] text-[#2f5d35]", 439 - )} 440 - > 441 - {error ?? message} 442 - </div> 443 - )} 444 - 445 - <div className="grid gap-6 lg:grid-cols-[340px_minmax(0,1fr)]"> 474 + <div className="grid gap-6 lg:grid-cols-[340px_minmax(0,1fr)]"> 446 475 <Card className="p-5"> 447 476 <div className="flex items-center justify-between gap-3"> 448 - <div> 449 - <p className="text-xs font-semibold uppercase tracking-[0.24em] text-[var(--accent)]">Flow</p> 450 - <h2 className="mt-2 font-display text-3xl text-[var(--ink)]">Blocks</h2> 451 - </div> 477 + <h2 className="font-display text-3xl text-[var(--ink)]">Blocks</h2> 452 478 <Badge>{form.blocks.length} steps</Badge> 453 479 </div> 454 480 455 - <button 456 - type="button" 457 - onClick={() => setSelection({ kind: "form" })} 458 - className={cn( 459 - "mt-5 flex w-full items-center gap-3 rounded-[22px] border px-4 py-4 text-left transition", 460 - selection.kind === "form" ? "border-[var(--accent)] bg-white shadow-[0_18px_44px_rgba(15,23,42,0.08)]" : "border-black/8 bg-white/60 hover:bg-white", 461 - )} 462 - > 463 - <div className="rounded-full border border-black/8 p-2 text-[var(--muted)]"> 464 - <Settings2 className="size-4" /> 465 - </div> 466 - <div> 467 - <p className="text-xs font-semibold uppercase tracking-[0.24em] text-[var(--accent)]">Form settings</p> 468 - <p className="mt-1 font-medium text-[var(--ink)]">Title, slug, publish state</p> 469 - </div> 470 - </button> 471 - 472 481 <div className="mt-5 space-y-3"> 473 482 {isDndReady ? ( 474 483 <DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}> ··· 522 531 {selection.kind === "form" ? ( 523 532 <div className="space-y-6"> 524 533 <div> 525 - <p className="text-xs font-semibold uppercase tracking-[0.24em] text-[var(--accent)]">Form settings</p> 526 - <h2 className="mt-3 font-display text-4xl text-[var(--ink)]">Shape the live version.</h2> 534 + <h2 className="font-display text-4xl text-[var(--ink)]">Settings</h2> 527 535 </div> 528 536 529 537 <div className="grid gap-5"> ··· 549 557 <p className="text-xs font-semibold uppercase tracking-[0.22em] text-[var(--accent)]">Public route</p> 550 558 <p className="mt-2 truncate text-sm text-[var(--ink)]">{shareHref}</p> 551 559 <p className="mt-1 text-xs text-[var(--muted)]"> 552 - Only published forms accept anonymous submissions. Editing a published form updates the live experience. 560 + Published forms accept anonymous submissions. Changes to a published form go live immediately. 553 561 </p> 554 562 </div> 555 563 <Button variant="secondary" onClick={copyShareLink}> ··· 564 572 </Link> 565 573 </div> 566 574 567 - <div className="flex justify-end"> 575 + <div className="flex flex-wrap items-center justify-between gap-3 border-t border-black/8 pt-6"> 576 + <Button variant="danger" onClick={deleteForm}> 577 + {busy === "delete-form" ? <LoaderCircle className="size-4 animate-spin" /> : <Trash2 className="size-4" />} 578 + Delete form 579 + </Button> 568 580 <Button onClick={saveMetadata}> 569 581 {busy === "metadata" ? <LoaderCircle className="size-4 animate-spin" /> : <Save className="size-4" />} 570 582 Save form settings ··· 679 691 ) : ( 680 692 <div className="flex min-h-[420px] items-center justify-center rounded-[28px] border border-dashed border-black/10 bg-[var(--bg-strong)] p-10 text-center"> 681 693 <div> 682 - <p className="text-xs font-semibold uppercase tracking-[0.28em] text-[var(--accent)]">Nothing selected</p> 683 - <h2 className="mt-4 font-display text-4xl text-[var(--ink)]">Pick a block or open form settings.</h2> 684 - <p className="mx-auto mt-3 max-w-lg text-sm leading-7 text-[var(--muted)]"> 685 - The builder stays intentionally simple in v0: organize the flow on the left, edit the selected step on the right. 694 + <p className="text-xs font-semibold uppercase tracking-[0.24em] text-[var(--accent)]">Nothing selected</p> 695 + <h2 className="mt-4 font-display text-4xl text-[var(--ink)]">Select a block or open form settings.</h2> 696 + <p className="mx-auto mt-3 max-w-lg text-sm leading-6 text-[var(--muted)]"> 697 + Use the left panel to choose what you want to edit. 686 698 </p> 687 699 <div className="mt-6 flex justify-center"> 688 700 <Button variant="secondary" onClick={() => addBlock("SHORT_TEXT")}> ··· 694 706 </div> 695 707 )} 696 708 </Card> 709 + </div> 697 710 </div> 698 - </div> 711 + </> 699 712 ); 700 713 }
+79
components/ui/toast.tsx
··· 1 + "use client"; 2 + 3 + import { useEffect } from "react"; 4 + import { AlertCircle, CheckCircle2, X } from "lucide-react"; 5 + import { AnimatePresence, motion } from "framer-motion"; 6 + 7 + import { cn } from "@/lib/utils"; 8 + 9 + export type ToastData = { 10 + id: string; 11 + message: string; 12 + variant: "success" | "error"; 13 + duration?: number; 14 + }; 15 + 16 + function ToastItem({ 17 + toast, 18 + onDismiss, 19 + }: { 20 + toast: ToastData; 21 + onDismiss: (id: string) => void; 22 + }) { 23 + useEffect(() => { 24 + const timeout = window.setTimeout(() => { 25 + onDismiss(toast.id); 26 + }, toast.duration ?? (toast.variant === "error" ? 5000 : 2600)); 27 + 28 + return () => window.clearTimeout(timeout); 29 + }, [onDismiss, toast.duration, toast.id, toast.variant]); 30 + 31 + const Icon = toast.variant === "error" ? AlertCircle : CheckCircle2; 32 + 33 + return ( 34 + <motion.div 35 + layout 36 + initial={{ opacity: 0, y: -16, scale: 0.98 }} 37 + animate={{ opacity: 1, y: 0, scale: 1 }} 38 + exit={{ opacity: 0, x: 24, y: -8, scale: 0.98 }} 39 + transition={{ duration: 0.18, ease: "easeOut" }} 40 + className={cn( 41 + "pointer-events-auto flex items-start gap-3 rounded-2xl border bg-white px-4 py-3 shadow-[0_18px_50px_rgba(15,23,42,0.12)]", 42 + toast.variant === "error" 43 + ? "border-rose-200 text-rose-700" 44 + : "border-[#cfe9d0] text-[#2f5d35]", 45 + )} 46 + role="status" 47 + aria-live="polite" 48 + > 49 + <Icon className="mt-0.5 size-4 shrink-0" /> 50 + <p className="min-w-0 flex-1 text-sm leading-6">{toast.message}</p> 51 + <button 52 + type="button" 53 + onClick={() => onDismiss(toast.id)} 54 + className="rounded-full p-1 opacity-60 transition hover:bg-black/5 hover:opacity-100" 55 + aria-label="Dismiss notification" 56 + > 57 + <X className="size-4" /> 58 + </button> 59 + </motion.div> 60 + ); 61 + } 62 + 63 + export function ToastViewport({ 64 + toasts, 65 + onDismiss, 66 + }: { 67 + toasts: ToastData[]; 68 + onDismiss: (id: string) => void; 69 + }) { 70 + return ( 71 + <div className="pointer-events-none fixed right-6 top-6 z-50 flex w-[min(24rem,calc(100vw-3rem))] flex-col gap-3"> 72 + <AnimatePresence initial={false}> 73 + {toasts.map((toast) => ( 74 + <ToastItem key={toast.id} toast={toast} onDismiss={onDismiss} /> 75 + ))} 76 + </AnimatePresence> 77 + </div> 78 + ); 79 + }
+1 -1
lib/auth.ts
··· 12 12 strategy: "database", 13 13 }, 14 14 pages: { 15 - signIn: "/login", 15 + signIn: "/", 16 16 }, 17 17 providers: [ 18 18 GoogleProvider({
+8
lib/forms.ts
··· 375 375 return toBuilderForm(form); 376 376 } 377 377 378 + export async function deleteOwnedForm(userId: string, formId: string) { 379 + await assertOwner(userId, formId); 380 + 381 + await db.form.delete({ 382 + where: { id: formId }, 383 + }); 384 + } 385 + 378 386 export async function setOwnedFormPublished(userId: string, formId: string, payload: unknown) { 379 387 const parsed = publishFormSchema.parse(payload); 380 388 const form = await assertOwner(userId, formId);
+2
openspec/changes/archive/2026-04-08-simplify-ui/.openspec.yaml
··· 1 + schema: spec-driven 2 + created: 2026-04-08
+73
openspec/changes/archive/2026-04-08-simplify-ui/design.md
··· 1 + ## Context 2 + 3 + Lively Forms already supports the core creator and respondent workflows, but several key surfaces still read like a promotional landing experience rather than a focused internal tool. The current home page, login screen, creator layout, dashboard, and builder all use decorative gradients, showcase blocks, and persuasive copy that add visual weight without adding functional value. 4 + 5 + This change is intentionally presentation-only. The routes, auth model, data flow, and builder/respondent behavior stay the same. The design work therefore needs to be implemented by simplifying component structure, trimming copy, and tightening visual styling without disturbing existing user flows. 6 + 7 + ## Goals / Non-Goals 8 + 9 + **Goals:** 10 + - Make the home page a concise product entry surface with clear next actions. 11 + - Remove or reduce decorative sections, promotional messaging, and showcase panels from entry and creator-facing surfaces. 12 + - Align the creator shell, dashboard, and builder with a quieter, more utility-first visual language. 13 + - Preserve all current workflows, routes, and permissions while changing only presentation and copy emphasis. 14 + 15 + **Non-Goals:** 16 + - Changing authentication, publishing, response review, or form submission behavior. 17 + - Redesigning the underlying builder interaction model or adding new product capabilities. 18 + - Reworking the public runner experience beyond any styling inherited from shared tokens. 19 + - Introducing a new design system, dependency, or theming framework. 20 + 21 + ## Decisions 22 + 23 + ### Decision: Simplify in place rather than introducing a separate "minimal" theme 24 + The implementation will update the existing pages and shared styling tokens directly instead of adding a mode switch or alternate theme. 25 + 26 + **Rationale:** The request is to move the product in a clearer minimal direction overall, not to support multiple visual identities. Updating the existing surfaces avoids unnecessary complexity and keeps future maintenance simple. 27 + 28 + **Alternatives considered:** 29 + - Add a second theme or style variant: rejected because it introduces maintenance overhead for a single desired direction. 30 + - Limit the change to copy only: rejected because the visual hierarchy and decorative layouts also contribute to the ad-like feel. 31 + 32 + ### Decision: Preserve page structure and routes while reducing section count and chrome 33 + The home page, login page, dashboard, creator layout, and builder will keep their current route responsibilities, but unnecessary secondary sections and large promotional headers will be collapsed into compact, action-oriented layouts. 34 + 35 + **Rationale:** This preserves behavior and navigation while delivering the requested tone shift with low implementation risk. 36 + 37 + **Alternatives considered:** 38 + - Merge or remove routes entirely: rejected because the problem is presentation, not information architecture. 39 + - Leave layouts intact and only shorten text: rejected because oversized hero treatments and showcase cards would still dominate the experience. 40 + 41 + ### Decision: Use concise functional copy as a product rule on key surfaces 42 + Labels and helper text on the affected surfaces will describe what the user can do next, not why the product is appealing. 43 + 44 + **Rationale:** The primary complaint is that the interface feels like an ad. Functional copy directly addresses that without changing capabilities. 45 + 46 + **Alternatives considered:** 47 + - Keep existing poetic brand voice in a lighter form: rejected because it still risks reading as promotional. 48 + - Remove nearly all copy: rejected because users still need short contextual cues for auth, dashboard actions, and builder state. 49 + 50 + ### Decision: Reduce ornamental global styling before tuning page-level components 51 + Global background treatments, glassmorphism effects, and strong decorative gradients will be softened or removed first, then page components will be simplified to match. 52 + 53 + **Rationale:** A minimal tone will not hold if page markup is simplified but the shared visual environment remains highly styled. 54 + 55 + **Alternatives considered:** 56 + - Only update individual components: rejected because the background and shared surfaces would keep the same overall mood. 57 + 58 + ## Risks / Trade-offs 59 + 60 + - **Risk:** Over-simplifying could make the app feel unfinished rather than intentionally minimal. → **Mitigation:** Keep strong spacing, typography, and clear action grouping even while removing decorative sections. 61 + - **Risk:** Copy reductions could remove useful orientation for first-time creators. → **Mitigation:** Replace promotional text with short task-oriented guidance instead of deleting context entirely. 62 + - **Risk:** Shared style token changes could unintentionally affect lower-priority screens. → **Mitigation:** Validate all major surfaces after token updates and use local overrides where shared simplification causes regressions. 63 + - **Risk:** Minimal dashboard and builder chrome may reduce visual separation between sections. → **Mitigation:** Preserve information hierarchy through borders, spacing, headings, and status/action clusters rather than decorative cards. 64 + 65 + ## Migration Plan 66 + 67 + - Implement as a standard UI-only change with no schema, API, or data migration. 68 + - Validate the home page, login page, dashboard, builder, responses, and public form runner visually before merge. 69 + - Roll back by reverting the changed UI files if the reduced treatment removes needed clarity. 70 + 71 + ## Open Questions 72 + 73 + - None at proposal time; the requested direction is specific enough to proceed with implementation decisions during the apply phase.
+25
openspec/changes/archive/2026-04-08-simplify-ui/proposal.md
··· 1 + ## Why 2 + 3 + The current UI leans toward a polished landing-page aesthetic with promotional copy, decorative panels, and feature-selling language. For this project, that tone distracts from the actual goal: letting creators get in, manage forms, and review responses with as little friction and visual noise as possible. 4 + 5 + ## What Changes 6 + 7 + - Simplify the public home page so it acts as a direct entry point into the product instead of a marketing surface. 8 + - Remove promotional sections and ad-like framing such as feature sales copy, showcase panels, and non-essential product-surface highlights. 9 + - Simplify creator-facing chrome in the authenticated layout and dashboard so navigation emphasizes core actions over descriptive brand messaging. 10 + - Replace decorative or persuasive copy with short, functional labels and guidance across key entry surfaces. 11 + - Reduce ornamental visual treatments where they compete with clarity, while preserving the existing product functionality and route structure. 12 + 13 + ## Capabilities 14 + 15 + ### New Capabilities 16 + - `minimal-ui-surfaces`: Defines a minimal, utility-first presentation for the home page, login page, creator layout, and dashboard so core actions remain clear without promotional framing. 17 + 18 + ### Modified Capabilities 19 + - `conversational-form-builder`: Align the creator builder surface with the same minimal product chrome and reduce non-essential explanatory copy around editing actions. 20 + 21 + ## Impact 22 + 23 + - Affected code: `app/page.tsx`, `app/login/page.tsx`, `app/(creator)/layout.tsx`, `app/(creator)/dashboard/page.tsx`, `components/empty-state.tsx`, `components/form-builder.tsx`, and shared visual styling in `app/globals.css`. 24 + - No API, database, auth, or dependency changes are expected. 25 + - Existing routes and creator/respondent workflows remain intact; this change is limited to presentation, copy, and interface emphasis.
+12
openspec/changes/archive/2026-04-08-simplify-ui/specs/conversational-form-builder/spec.md
··· 1 + ## ADDED Requirements 2 + 3 + ### Requirement: Builder chrome stays focused on editing 4 + The system SHALL present the creator builder with concise, editing-focused chrome that emphasizes form status, navigation, and editing actions. The builder SHALL avoid promotional hero copy or decorative overview treatments that compete with the editing workflow. 5 + 6 + #### Scenario: Creator opens the builder 7 + - **WHEN** an authenticated creator opens an owned form in the builder 8 + - **THEN** the system shows the form title, current status, and core actions needed to edit, publish, copy the link, or review responses without marketing-style framing 9 + 10 + #### Scenario: Creator reads helper copy in the builder 11 + - **WHEN** an authenticated creator views builder headings, descriptions, or empty states 12 + - **THEN** the system uses short task-oriented copy that explains the current editing context without persuasive or ornamental language
+27
openspec/changes/archive/2026-04-08-simplify-ui/specs/minimal-ui-surfaces/spec.md
··· 1 + ## ADDED Requirements 2 + 3 + ### Requirement: Home page acts as a direct entry surface 4 + The system SHALL present the home page as a concise entry point into the product with product identity, a short functional description, and a primary action to enter creator flows. The home page SHALL NOT depend on promotional showcase sections or marketing-style product demos to explain the product. 5 + 6 + #### Scenario: Visitor opens the home page 7 + - **WHEN** a visitor navigates to the home page 8 + - **THEN** the system displays the product name, a short functional description, and a primary action that leads directly to creator sign-in or the dashboard depending on session state 9 + 10 + #### Scenario: Visitor views the full home page 11 + - **WHEN** a visitor scrolls the home page 12 + - **THEN** the system does not show promotional feature grids, respondent showcase panels, or other ad-like sections that are not required for entry into the product 13 + 14 + ### Requirement: Creator shell and dashboard prioritize management actions 15 + The system SHALL present authenticated creator surfaces with concise identity, navigation, and management actions. Dashboard content SHALL focus on form creation, form status, and response access without promotional taglines or decorative overview blocks. 16 + 17 + #### Scenario: Creator opens the authenticated shell 18 + - **WHEN** an authenticated creator navigates to a creator route 19 + - **THEN** the system shows app identity, essential navigation, and sign-out access without promotional tagline text in the header 20 + 21 + #### Scenario: Creator opens the dashboard 22 + - **WHEN** an authenticated creator opens the dashboard 23 + - **THEN** the system emphasizes creating forms, viewing existing forms, and checking status data without marketing-style hero framing or non-essential decorative cards 24 + 25 + #### Scenario: Creator has no forms yet 26 + - **WHEN** an authenticated creator opens an empty dashboard 27 + - **THEN** the system shows a concise empty state that explains the next action without sales-oriented or poetic product language
+20
openspec/changes/archive/2026-04-08-simplify-ui/tasks.md
··· 1 + ## 1. Shared visual language 2 + 3 + - [x] 1.1 Simplify shared background, surface, and metadata styling in `app/globals.css` and `app/layout.tsx` to support a quieter default presentation 4 + - [x] 1.2 Update shared creator-facing support components such as `components/empty-state.tsx` so headings and helper copy stay concise and functional 5 + 6 + ## 2. Entry surfaces 7 + 8 + - [x] 2.1 Rewrite `app/page.tsx` into a direct product entry page with a short description, a primary action, and no promotional showcase sections 9 + - [x] 2.2 Simplify `app/login/page.tsx` so creator authentication is explained with compact, utility-first copy and layout 10 + 11 + ## 3. Creator surfaces 12 + 13 + - [x] 3.1 Simplify `app/(creator)/layout.tsx` header chrome so it keeps only essential identity, navigation, and sign-out context 14 + - [x] 3.2 Simplify `app/(creator)/dashboard/page.tsx` so form creation, form status, and response access are primary while decorative hero treatments and promotional copy are removed 15 + - [x] 3.3 Update `components/form-builder.tsx` so builder headings, helper text, and top-level chrome are editing-focused and consistent with the minimal direction 16 + 17 + ## 4. Validation 18 + 19 + - [x] 4.1 Visually verify the home page, login page, dashboard, builder, responses, and public runner after shared styling changes to confirm workflows still read clearly 20 + - [x] 4.2 Run the project checks needed for a UI-only change and confirm there are no regressions in the touched surfaces
+11
openspec/specs/conversational-form-builder/spec.md
··· 58 58 #### Scenario: Creator unpublishes a form 59 59 - **WHEN** an authenticated creator unpublishes an owned form 60 60 - **THEN** the system marks the form as unavailable for new public submissions 61 + 62 + ### Requirement: Builder chrome stays focused on editing 63 + The system SHALL present the creator builder with concise, editing-focused chrome that emphasizes form status, navigation, and editing actions. The builder SHALL avoid promotional hero copy or decorative overview treatments that compete with the editing workflow. 64 + 65 + #### Scenario: Creator opens the builder 66 + - **WHEN** an authenticated creator opens an owned form in the builder 67 + - **THEN** the system shows the form title, current status, and core actions needed to edit, publish, copy the link, or review responses without marketing-style framing 68 + 69 + #### Scenario: Creator reads helper copy in the builder 70 + - **WHEN** an authenticated creator views builder headings, descriptions, or empty states 71 + - **THEN** the system uses short task-oriented copy that explains the current editing context without persuasive or ornamental language
+27
openspec/specs/minimal-ui-surfaces/spec.md
··· 1 + ## ADDED Requirements 2 + 3 + ### Requirement: Home page acts as a direct entry surface 4 + The system SHALL present the home page as a concise entry point into the product with product identity, a short functional description, and a primary action to enter creator flows. The home page SHALL NOT depend on promotional showcase sections or marketing-style product demos to explain the product. 5 + 6 + #### Scenario: Visitor opens the home page 7 + - **WHEN** a visitor navigates to the home page 8 + - **THEN** the system displays the product name, a short functional description, and a primary action that leads directly to creator sign-in or the dashboard depending on session state 9 + 10 + #### Scenario: Visitor views the full home page 11 + - **WHEN** a visitor scrolls the home page 12 + - **THEN** the system does not show promotional feature grids, respondent showcase panels, or other ad-like sections that are not required for entry into the product 13 + 14 + ### Requirement: Creator shell and dashboard prioritize management actions 15 + The system SHALL present authenticated creator surfaces with concise identity, navigation, and management actions. Dashboard content SHALL focus on form creation, form status, and response access without promotional taglines or decorative overview blocks. 16 + 17 + #### Scenario: Creator opens the authenticated shell 18 + - **WHEN** an authenticated creator navigates to a creator route 19 + - **THEN** the system shows app identity, essential navigation, and sign-out access without promotional tagline text in the header 20 + 21 + #### Scenario: Creator opens the dashboard 22 + - **WHEN** an authenticated creator opens the dashboard 23 + - **THEN** the system emphasizes creating forms, viewing existing forms, and checking status data without marketing-style hero framing or non-essential decorative cards 24 + 25 + #### Scenario: Creator has no forms yet 26 + - **WHEN** an authenticated creator opens an empty dashboard 27 + - **THEN** the system shows a concise empty state that explains the next action without sales-oriented or poetic product language