Form
Free, copy-and-go Form components built on the SevenUI Form primitive.Read the primitive docs.
"use client";
import * as React from "react";
import { Button } from "@/components/ui/button";
import {
Field,
FieldDescription,
FieldError,
FieldGroup,
FieldLabel,
} from "@/components/ui/field";
import { Form } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
export default function Form01() {
const [signedInAs, setSignedInAs] = React.useState<string | null>(null);
return (
<div className="w-full max-w-sm">
<Form
aria-labelledby="form-01-title"
onFormSubmit={(values) => setSignedInAs(String(values.email))}
>
<div className="grid gap-1">
<h2 id="form-01-title" className="text-lg font-semibold">
Sign in to Ledgerly
</h2>
<p className="text-sm text-muted-foreground">
Use the email your workspace admin invited.
</p>
</div>
<FieldGroup className="gap-4">
<Field name="email">
<FieldLabel>Work email</FieldLabel>
<Input
required
type="email"
autoComplete="email"
placeholder="dana@northwind.io"
/>
<FieldError match="valueMissing">
Enter the email you use for work.
</FieldError>
<FieldError match="typeMismatch">
That doesn't look like an email address.
</FieldError>
</Field>
<Field name="password">
<div className="flex items-center justify-between gap-2">
<FieldLabel>Password</FieldLabel>
<a
href="#reset-password"
className="rounded-sm text-sm text-muted-foreground underline-offset-4 outline-none hover:text-foreground hover:underline focus-visible:ring-3 focus-visible:ring-ring/50"
>
Forgot password?
</a>
</div>
<Input
required
type="password"
minLength={8}
autoComplete="current-password"
/>
<FieldDescription>At least 8 characters.</FieldDescription>
<FieldError match="valueMissing">Enter your password.</FieldError>
<FieldError match="tooShort">
Passwords are at least 8 characters long.
</FieldError>
</Field>
</FieldGroup>
<Button type="submit" className="w-full">
Sign in
</Button>
<p
role="status"
className="min-h-5 text-center text-sm text-muted-foreground"
>
{signedInAs ? `Signed in as ${signedInAs}` : null}
</p>
</Form>
</div>
);
}
npx shadcn@latest add @sevenui/component/form-01pnpm dlx shadcn@latest add @sevenui/component/form-01yarn dlx shadcn@latest add @sevenui/component/form-01bunx --bun shadcn@latest add @sevenui/component/form-01Validate
Errors appear after the first submit, then update as you type.
"use client";
import * as React from "react";
import { Button } from "@/components/ui/button";
import {
Field,
FieldDescription,
FieldError,
FieldGroup,
FieldLabel,
} from "@/components/ui/field";
import { Form } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
type Mode = "onSubmit" | "onBlur" | "onChange";
const modes: { value: Mode; label: string; hint: string }[] = [
{
value: "onSubmit",
label: "On submit",
hint: "Errors appear after the first submit, then update as you type.",
},
{
value: "onBlur",
label: "On blur",
hint: "Each field checks itself when you leave it.",
},
{
value: "onChange",
label: "On change",
hint: "Fields check on every keystroke, debounced by 300 ms.",
},
];
function validateHandle(value: unknown) {
const handle = String(value ?? "");
if (handle.length < 3) return "Use at least 3 characters.";
if (!/^[a-z0-9_]+$/.test(handle)) {
return "Only lowercase letters, numbers and underscores.";
}
return null;
}
export default function Form02() {
const [mode, setMode] = React.useState<Mode>("onSubmit");
const [claimed, setClaimed] = React.useState<string | null>(null);
const active = modes.find((item) => item.value === mode) ?? modes[0];
return (
<div className="grid w-full max-w-sm gap-5">
<div className="grid gap-2">
<span id="form-02-mode" className="text-sm font-medium">
Validate
</span>
<ToggleGroup
aria-labelledby="form-02-mode"
variant="outline"
size="sm"
spacing={0}
value={[mode]}
onValueChange={(value) => {
const next = value[0] as Mode | undefined;
if (next) {
setMode(next);
setClaimed(null);
}
}}
className="w-full"
>
{modes.map((item) => (
<ToggleGroupItem
key={item.value}
value={item.value}
className="flex-1"
>
{item.label}
</ToggleGroupItem>
))}
</ToggleGroup>
<p className="text-sm text-muted-foreground">{active.hint}</p>
</div>
<Form
key={mode}
validationMode={mode}
aria-label="Claim your handle"
className="rounded-xl border border-border bg-card p-5"
onFormSubmit={(values) => setClaimed(String(values.handle))}
>
<FieldGroup className="gap-4">
<Field
name="handle"
validate={validateHandle}
validationDebounceTime={mode === "onChange" ? 300 : 0}
>
<FieldLabel>Handle</FieldLabel>
<Input
autoComplete="off"
spellCheck={false}
placeholder="dana_builds"
/>
<FieldDescription>Your public profile address.</FieldDescription>
<FieldError />
</Field>
<Field name="email">
<FieldLabel>Email</FieldLabel>
<Input
required
type="email"
autoComplete="email"
placeholder="dana@northwind.io"
/>
<FieldError match="valueMissing">
We need an email to confirm the handle.
</FieldError>
<FieldError match="typeMismatch">
Enter an address like name@company.com.
</FieldError>
</Field>
</FieldGroup>
<Button type="submit">Claim handle</Button>
<p role="status" className="min-h-5 text-sm text-muted-foreground">
{claimed ? (
<>
<span className="font-medium text-foreground">@{claimed}</span>{" "}
is yours.
</>
) : null}
</p>
</Form>
</div>
);
}
npx shadcn@latest add @sevenui/component/form-02pnpm dlx shadcn@latest add @sevenui/component/form-02yarn dlx shadcn@latest add @sevenui/component/form-02bunx --bun shadcn@latest add @sevenui/component/form-02"use client";
import * as React from "react";
import { Button } from "@/components/ui/button";
import {
Field,
FieldDescription,
FieldError,
FieldGroup,
FieldLabel,
} from "@/components/ui/field";
import { Form } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
type Errors = Record<string, string | string[]>;
const takenUsernames = ["admin", "support", "dana"];
const breachedPasswords = ["password123", "qwerty2024", "letmein!"];
// Stands in for the response of a sign-up endpoint. A real server returns the
// same shape: messages keyed by field name, one string or a list per field.
function checkOnServer(values: Record<string, unknown>): Errors {
const username = String(values.username ?? "").toLowerCase();
const password = String(values.password ?? "");
const errors: Errors = {};
if (takenUsernames.includes(username)) {
errors.username = `“${username}” is taken. Try ${username}-studio.`;
}
const passwordIssues: string[] = [];
if (password.length < 12) passwordIssues.push("Use at least 12 characters.");
if (!/\d/.test(password)) passwordIssues.push("Add at least one number.");
if (breachedPasswords.includes(password.toLowerCase())) {
passwordIssues.push("This password appeared in a known data breach.");
}
if (username && password.toLowerCase().includes(username)) {
passwordIssues.push("Don't include your username.");
}
if (passwordIssues.length) errors.password = passwordIssues;
return errors;
}
export default function Form03() {
const [errors, setErrors] = React.useState<Errors>({});
const [created, setCreated] = React.useState<string | null>(null);
return (
<div className="w-full max-w-sm">
<Form
aria-label="Create account"
errors={errors}
onFormSubmit={(values) => {
const next = checkOnServer(values);
setErrors(next);
setCreated(
Object.keys(next).length ? null : String(values.username ?? ""),
);
}}
>
<FieldGroup className="gap-4">
<Field name="username">
<FieldLabel>Username</FieldLabel>
<Input defaultValue="admin" autoComplete="username" />
<FieldError />
</Field>
<Field name="password">
<FieldLabel>Password</FieldLabel>
<Input
type="password"
defaultValue="password123"
autoComplete="new-password"
/>
<FieldDescription>
Checked by the server when you submit.
</FieldDescription>
<FieldError className="[&_ul]:ml-4 [&_ul]:flex [&_ul]:list-disc [&_ul]:flex-col [&_ul]:gap-1" />
</Field>
</FieldGroup>
<Button type="submit">Create account</Button>
<p role="status" className="min-h-5 text-sm text-muted-foreground">
{created ? `Account created for ${created}.` : null}
</p>
</Form>
</div>
);
}
npx shadcn@latest add @sevenui/component/form-03pnpm dlx shadcn@latest add @sevenui/component/form-03yarn dlx shadcn@latest add @sevenui/component/form-03bunx --bun shadcn@latest add @sevenui/component/form-03"use client";
import * as React from "react";
import { CircleAlertIcon, CircleCheckIcon } from "lucide-react";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import {
Field,
FieldDescription,
FieldError,
FieldGroup,
FieldLabel,
} from "@/components/ui/field";
import { Form } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Spinner } from "@/components/ui/spinner";
type Status = "idle" | "submitting" | "error" | "success";
export default function Form04() {
const [status, setStatus] = React.useState<Status>("idle");
const [email, setEmail] = React.useState("");
const attempts = React.useRef(0);
const timer = React.useRef<ReturnType<typeof setTimeout> | null>(null);
React.useEffect(
() => () => {
if (timer.current) clearTimeout(timer.current);
},
[],
);
const submitting = status === "submitting";
if (status === "success") {
return (
<div
role="status"
className="flex w-full max-w-sm animate-in flex-col items-center gap-3 rounded-xl border border-border bg-card p-6 text-center duration-300 fade-in-0 zoom-in-95 motion-reduce:animate-none"
>
<span className="flex size-10 items-center justify-center rounded-full bg-success/10">
<CircleCheckIcon aria-hidden="true" className="size-5 text-success" />
</span>
<div className="grid gap-1">
<h2 className="font-semibold">Check your inbox</h2>
<p className="text-sm text-balance text-muted-foreground">
We sent a confirmation link to{" "}
<span className="font-medium text-foreground">{email}</span>. It
expires in 24 hours.
</p>
</div>
<Button
variant="outline"
size="sm"
onClick={() => {
attempts.current = 0;
setEmail("");
setStatus("idle");
}}
>
Use a different email
</Button>
</div>
);
}
return (
<div className="w-full max-w-sm rounded-xl border border-border bg-card p-5">
<Form
aria-labelledby="form-04-title"
aria-busy={submitting}
onFormSubmit={(values) => {
setEmail(String(values.email));
setStatus("submitting");
attempts.current += 1;
// The first attempt fails so every state of the form is visible.
const fails = attempts.current === 1;
timer.current = setTimeout(() => {
setStatus(fails ? "error" : "success");
}, 1200);
}}
>
<div className="grid gap-1">
<h2 id="form-04-title" className="font-semibold">
Get the release notes
</h2>
<p className="text-sm text-muted-foreground">
One email per release. Unsubscribe anytime.
</p>
</div>
{status === "error" ? (
<Alert variant="destructive" className="animate-in fade-in-0 motion-reduce:animate-none">
<CircleAlertIcon aria-hidden="true" />
<AlertTitle>We couldn't reach the server</AlertTitle>
<AlertDescription>
Your details are still here. Check your connection and try again.
</AlertDescription>
</Alert>
) : null}
<FieldGroup className="gap-4">
<Field name="email" disabled={submitting}>
<FieldLabel>Email</FieldLabel>
<Input
required
type="email"
autoComplete="email"
placeholder="dana@northwind.io"
/>
<FieldDescription>
We'll send a link to confirm it's you.
</FieldDescription>
<FieldError match="valueMissing">
Enter an email to subscribe.
</FieldError>
<FieldError match="typeMismatch">
Enter an address like name@company.com.
</FieldError>
</Field>
</FieldGroup>
<Button type="submit" disabled={submitting}>
{submitting ? (
<>
<Spinner aria-hidden="true" role="presentation" />
Subscribing…
</>
) : status === "error" ? (
"Try again"
) : (
"Subscribe"
)}
</Button>
<span role="status" className="sr-only">
{submitting ? "Subscribing" : ""}
</span>
</Form>
</div>
);
}
npx shadcn@latest add @sevenui/component/form-04pnpm dlx shadcn@latest add @sevenui/component/form-04yarn dlx shadcn@latest add @sevenui/component/form-04bunx --bun shadcn@latest add @sevenui/component/form-04Emergency contact
Who we call if something happens at work. Visible to HR only.
"use client";
import * as React from "react";
import { LockIcon, PencilIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Field,
FieldError,
FieldGroup,
FieldLabel,
} from "@/components/ui/field";
import { Form } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
type Contact = {
name: string;
relationship: string;
phone: string;
email: string;
};
const initialContact: Contact = {
name: "Rosa Delgado",
relationship: "Partner",
phone: "+1 415 555 0132",
email: "rosa.delgado@fastmail.com",
};
const fields: {
name: keyof Contact;
label: string;
type?: string;
autoComplete: string;
required?: boolean;
}[] = [
{ name: "name", label: "Full name", autoComplete: "off", required: true },
{ name: "relationship", label: "Relationship", autoComplete: "off" },
{
name: "phone",
label: "Phone",
type: "tel",
autoComplete: "off",
required: true,
},
{ name: "email", label: "Email", type: "email", autoComplete: "off" },
];
export default function Form05() {
const [contact, setContact] = React.useState(initialContact);
const [editing, setEditing] = React.useState(false);
const [version, setVersion] = React.useState(0);
const firstInput = React.useRef<HTMLInputElement>(null);
const editButton = React.useRef<HTMLButtonElement>(null);
const hasEdited = React.useRef(false);
// Move focus into the form when editing starts and back to the Edit button
// when it ends, so keyboard users never land on a removed element.
React.useEffect(() => {
if (editing) {
hasEdited.current = true;
firstInput.current?.focus();
} else if (hasEdited.current) {
editButton.current?.focus();
}
}, [editing]);
function cancel() {
// Remount the fields so they pick up the last saved values again.
setVersion((current) => current + 1);
setEditing(false);
}
return (
<div className="w-full max-w-lg rounded-xl border border-border bg-card">
<div className="flex items-start justify-between gap-3 border-b border-border p-5">
<div className="grid gap-1">
<h2 id="form-05-title" className="font-semibold">
Emergency contact
</h2>
<p className="text-sm text-muted-foreground">
Who we call if something happens at work. Visible to HR only.
</p>
</div>
{editing ? null : (
<Button
ref={editButton}
variant="outline"
size="sm"
onClick={() => setEditing(true)}
>
<PencilIcon aria-hidden="true" data-icon="inline-start" />
Edit
</Button>
)}
</div>
<Form
key={version}
aria-labelledby="form-05-title"
className="gap-5 p-5"
onKeyDown={(event) => {
if (event.key === "Escape" && editing) cancel();
}}
onFormSubmit={(values) => {
setContact(values as Contact);
// Remount so the read-only fields take the saved values as defaults.
setVersion((current) => current + 1);
setEditing(false);
}}
>
<FieldGroup className="grid gap-4 sm:grid-cols-2">
{fields.map((field, index) => (
<Field key={field.name} name={field.name}>
<FieldLabel>{field.label}</FieldLabel>
<Input
ref={index === 0 ? firstInput : undefined}
type={field.type}
required={field.required}
readOnly={!editing}
autoComplete={field.autoComplete}
defaultValue={contact[field.name]}
className="read-only:cursor-default read-only:border-transparent read-only:bg-muted/60 read-only:px-2.5 read-only:focus-visible:border-ring dark:read-only:bg-muted/40"
/>
<FieldError match="valueMissing">
{field.label} is required.
</FieldError>
<FieldError match="typeMismatch">
Enter an address like name@company.com.
</FieldError>
</Field>
))}
</FieldGroup>
{editing ? (
<div className="flex flex-col-reverse gap-2 sm:flex-row sm:justify-end">
<Button type="button" variant="ghost" onClick={cancel}>
Cancel
</Button>
<Button type="submit">Save contact</Button>
</div>
) : (
<p className="flex items-center gap-1.5 text-xs text-muted-foreground">
<LockIcon aria-hidden="true" className="size-3.5" />
Read-only. Select Edit to change these details.
</p>
)}
</Form>
</div>
);
}
npx shadcn@latest add @sevenui/component/form-05pnpm dlx shadcn@latest add @sevenui/component/form-05yarn dlx shadcn@latest add @sevenui/component/form-05bunx --bun shadcn@latest add @sevenui/component/form-05"use client";
import * as React from "react";
import { CircleAlertIcon, CircleCheckIcon } from "lucide-react";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import {
Field,
FieldError,
FieldGroup,
FieldLabel,
} from "@/components/ui/field";
import { Form } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
type FieldName = "fullName" | "street" | "city" | "postcode" | "phone";
const fields: {
name: FieldName;
label: string;
autoComplete: string;
type?: string;
span?: boolean;
}[] = [
{ name: "fullName", label: "Full name", autoComplete: "name", span: true },
{
name: "street",
label: "Street address",
autoComplete: "street-address",
span: true,
},
{ name: "city", label: "City", autoComplete: "address-level2" },
{ name: "postcode", label: "Postcode", autoComplete: "postal-code" },
{ name: "phone", label: "Phone", autoComplete: "tel", type: "tel", span: true },
];
function validate(values: Record<string, unknown>) {
const value = (name: FieldName) => String(values[name] ?? "").trim();
const errors: Partial<Record<FieldName, string>> = {};
if (!value("fullName")) errors.fullName = "Enter the recipient's full name.";
if (!value("street")) errors.street = "Enter a street and house number.";
if (!value("city")) errors.city = "Enter a town or city.";
if (!/^[A-Z0-9 ]{3,10}$/i.test(value("postcode"))) {
errors.postcode = "Enter a postcode, like SW1A 1AA.";
}
if (value("phone").replace(/\D/g, "").length < 7) {
errors.phone = "Enter a phone number the courier can call.";
}
return errors;
}
export default function Form06() {
const idPrefix = React.useId();
const [errors, setErrors] = React.useState<Partial<Record<FieldName, string>>>(
{},
);
const [confirmed, setConfirmed] = React.useState(false);
const entries = fields.filter((field) => errors[field.name]);
function clear(name: FieldName) {
setConfirmed(false);
if (!errors[name]) return;
setErrors((current) => {
const next = { ...current };
delete next[name];
return next;
});
}
return (
<div className="w-full max-w-md">
<Form
aria-labelledby={`${idPrefix}-title`}
errors={errors}
onFormSubmit={(values) => {
const next = validate(values);
setErrors(next);
setConfirmed(Object.keys(next).length === 0);
}}
>
<h2 id={`${idPrefix}-title`} className="font-semibold">
Shipping address
</h2>
{entries.length > 0 ? (
<Alert variant="destructive" role="alert">
<CircleAlertIcon aria-hidden="true" />
<AlertTitle>
Fix {entries.length}{" "}
{entries.length === 1 ? "field" : "fields"} to continue
</AlertTitle>
<AlertDescription>
<ul className="grid gap-1">
{entries.map((field) => (
<li key={field.name}>
<button
type="button"
className="rounded-sm text-left underline underline-offset-4 outline-none hover:no-underline focus-visible:ring-3 focus-visible:ring-destructive/30"
onClick={() =>
document
.getElementById(`${idPrefix}-${field.name}`)
?.focus()
}
>
{errors[field.name]}
</button>
</li>
))}
</ul>
</AlertDescription>
</Alert>
) : null}
<FieldGroup className="grid grid-cols-1 gap-4 sm:grid-cols-2">
{fields.map((field) => (
<Field
key={field.name}
name={field.name}
className={field.span ? "sm:col-span-2" : undefined}
>
<FieldLabel>{field.label}</FieldLabel>
<Input
id={`${idPrefix}-${field.name}`}
type={field.type}
autoComplete={field.autoComplete}
onChange={() => clear(field.name)}
/>
<FieldError />
</Field>
))}
</FieldGroup>
<Button type="submit">Continue to payment</Button>
<p
role="status"
className="flex min-h-5 items-center gap-1.5 text-sm text-muted-foreground"
>
{confirmed ? (
<>
<CircleCheckIcon
aria-hidden="true"
className="size-4 text-success"
/>
Address saved. Delivery estimate: 2–3 business days.
</>
) : null}
</p>
</Form>
</div>
);
}
npx shadcn@latest add @sevenui/component/form-06pnpm dlx shadcn@latest add @sevenui/component/form-06yarn dlx shadcn@latest add @sevenui/component/form-06bunx --bun shadcn@latest add @sevenui/component/form-06"use client";
import * as React from "react";
import { ArchiveRestoreIcon, TriangleAlertIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import {
Field,
FieldContent,
FieldDescription,
FieldError,
FieldGroup,
FieldLabel,
} from "@/components/ui/field";
import { Form } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
const project = "acme-marketing-site";
const losses = [
{ label: "Deployments", value: "214" },
{ label: "Custom domains", value: "3" },
{ label: "Env variables", value: "18" },
];
export default function Form07() {
const titleId = React.useId();
const [typed, setTyped] = React.useState("");
const [understood, setUnderstood] = React.useState(false);
const [deleted, setDeleted] = React.useState(false);
const matches = typed === project;
if (deleted) {
return (
<div
role="status"
className="flex w-full max-w-md flex-col gap-4 rounded-xl border border-border bg-card p-5"
>
<div className="flex flex-col gap-1">
<h2 className="font-semibold">{project} was deleted</h2>
<p className="text-sm text-muted-foreground">
Its domains are released now. You can restore the project and its
deployments until October 25.
</p>
</div>
<Button
variant="outline"
className="self-start"
onClick={() => {
setTyped("");
setUnderstood(false);
setDeleted(false);
}}
>
<ArchiveRestoreIcon aria-hidden="true" data-icon="inline-start" />
Restore project
</Button>
</div>
);
}
return (
<Form
aria-labelledby={titleId}
validationMode="onChange"
className="w-full max-w-md gap-5 rounded-xl border border-destructive/30 bg-card p-5"
onFormSubmit={() => {
if (matches && understood) setDeleted(true);
}}
>
<div className="flex items-start gap-3">
<span className="flex size-9 shrink-0 items-center justify-center rounded-lg bg-destructive/10">
<TriangleAlertIcon
aria-hidden="true"
className="size-4 text-destructive"
/>
</span>
<div className="flex flex-col gap-1">
<h2 id={titleId} className="font-semibold">
Delete project
</h2>
<p className="text-sm text-muted-foreground">
This removes {project} for everyone on the Acme team.
</p>
</div>
</div>
<dl className="grid grid-cols-3 divide-x divide-border rounded-lg border border-border bg-muted/40 text-center">
{losses.map((item) => (
<div key={item.label} className="flex flex-col gap-0.5 px-2 py-3">
<dt className="order-2 text-xs text-muted-foreground">
{item.label}
</dt>
<dd className="order-1 text-lg font-semibold tabular-nums">
{item.value}
</dd>
</div>
))}
</dl>
<FieldGroup className="gap-4">
<Field
name="confirmName"
validate={(value) => {
const text = String(value ?? "");
if (!text || text === project) return null;
return project.startsWith(text)
? null
: "The name doesn't match. Check for typos.";
}}
>
<FieldLabel>
<span>
Type <span className="font-mono">{project}</span> to confirm
</span>
</FieldLabel>
<Input
autoComplete="off"
spellCheck={false}
className="font-mono"
value={typed}
onChange={(event) => setTyped(event.target.value)}
/>
<FieldError />
</Field>
<Field orientation="horizontal">
<Checkbox
checked={understood}
onCheckedChange={(checked) => setUnderstood(checked)}
/>
<FieldContent>
<FieldLabel>I understand every deployment goes offline now</FieldLabel>
<FieldDescription>
You can restore it for 30 days. After that it's erased for good.
</FieldDescription>
</FieldContent>
</Field>
</FieldGroup>
<div className="flex flex-col-reverse gap-2 sm:flex-row sm:justify-end">
<Button
type="button"
variant="ghost"
onClick={() => {
setTyped("");
setUnderstood(false);
}}
>
Cancel
</Button>
<Button
type="submit"
variant="destructive"
disabled={!matches || !understood}
>
Delete project
</Button>
</div>
</Form>
);
}
npx shadcn@latest add @sevenui/component/form-07pnpm dlx shadcn@latest add @sevenui/component/form-07yarn dlx shadcn@latest add @sevenui/component/form-07bunx --bun shadcn@latest add @sevenui/component/form-07"use client";
import { useState } from "react";
import { EyeIcon, EyeOffIcon, ShieldCheckIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import {
Field,
FieldContent,
FieldDescription,
FieldError,
FieldGroup,
FieldLabel,
} from "@/components/ui/field";
import { Form } from "@/components/ui/form";
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupInput,
} from "@/components/ui/input-group";
// Stands in for the password the server has on file for this account.
const storedPassword = "correct-horse-42";
const otherSessions = 3;
function PasswordInput({
label,
autoComplete,
}: {
label: string;
autoComplete: string;
}) {
const [visible, setVisible] = useState(false);
return (
<InputGroup>
<InputGroupInput
type={visible ? "text" : "password"}
autoComplete={autoComplete}
spellCheck={false}
/>
<InputGroupAddon align="inline-end">
<InputGroupButton
size="icon-xs"
aria-label={visible ? `Hide ${label}` : `Show ${label}`}
aria-pressed={visible}
onClick={() => setVisible((current) => !current)}
>
{visible ? (
<EyeOffIcon aria-hidden="true" />
) : (
<EyeIcon aria-hidden="true" />
)}
</InputGroupButton>
</InputGroupAddon>
</InputGroup>
);
}
export default function Form08() {
const [errors, setErrors] = useState<Record<string, string>>({});
const [signOutOthers, setSignOutOthers] = useState(true);
const [updatedAt, setUpdatedAt] = useState<string | null>(null);
const [formKey, setFormKey] = useState(0);
return (
<div className="flex w-full max-w-md flex-col gap-3">
{updatedAt ? (
<div
role="status"
className="flex items-start gap-3 rounded-xl border border-success/30 bg-success/10 p-4 text-sm"
>
<ShieldCheckIcon
aria-hidden="true"
className="mt-0.5 size-4 shrink-0 text-success"
/>
<p>
Password updated at {updatedAt}.
{signOutOthers
? ` ${otherSessions} other sessions were signed out.`
: " Your other sessions stay signed in."}
</p>
</div>
) : null}
<Form
key={formKey}
className="w-full gap-5 rounded-xl border border-border bg-card p-5"
errors={errors}
onFormSubmit={(values) => {
if (values.currentPassword !== storedPassword) {
setErrors({
currentPassword:
"That's not your current password. Try again or reset it by email.",
});
return;
}
setErrors({});
setUpdatedAt(
new Date().toLocaleTimeString("en-US", {
hour: "numeric",
minute: "2-digit",
}),
);
// Remount the form to clear every field and its validation state.
setFormKey((current) => current + 1);
}}
>
<div className="flex flex-col gap-1">
<h3 className="font-semibold">Change password</h3>
<p className="text-sm text-muted-foreground">
Last changed 94 days ago. New passwords need 12 characters.
</p>
</div>
<FieldGroup className="gap-4">
<Field
name="currentPassword"
validate={(value) =>
String(value ?? "") ? null : "Enter your current password."
}
>
<FieldLabel>Current password</FieldLabel>
<PasswordInput
label="current password"
autoComplete="current-password"
/>
<FieldDescription>
The demo account uses{" "}
<span className="font-mono whitespace-nowrap">{storedPassword}</span>.
</FieldDescription>
<FieldError />
</Field>
<Field
name="newPassword"
validate={(value, formValues) => {
const next = String(value ?? "");
if (next.length < 12) return "Use at least 12 characters.";
if (next === formValues.currentPassword) {
return "Choose a password you haven't used here before.";
}
return null;
}}
>
<FieldLabel>New password</FieldLabel>
<PasswordInput label="new password" autoComplete="new-password" />
<FieldError />
</Field>
<Field
name="confirmPassword"
validate={(value, formValues) =>
value === formValues.newPassword
? null
: "Passwords don't match."
}
>
<FieldLabel>Confirm new password</FieldLabel>
<PasswordInput
label="password confirmation"
autoComplete="new-password"
/>
<FieldError />
</Field>
<Field orientation="horizontal">
<Checkbox
checked={signOutOthers}
onCheckedChange={(checked) => setSignOutOthers(checked)}
/>
<FieldContent>
<FieldLabel>Sign out of other devices</FieldLabel>
<FieldDescription>
Ends {otherSessions} active sessions on your phone, tablet, and
work laptop.
</FieldDescription>
</FieldContent>
</Field>
</FieldGroup>
<Button type="submit" className="sm:justify-self-end">
Update password
</Button>
</Form>
</div>
);
}
npx shadcn@latest add @sevenui/component/form-08pnpm dlx shadcn@latest add @sevenui/component/form-08yarn dlx shadcn@latest add @sevenui/component/form-08bunx --bun shadcn@latest add @sevenui/component/form-08"use client";
import { useId, useState } from "react";
import { PlaneIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Field,
FieldContent,
FieldDescription,
FieldError,
FieldGroup,
FieldLabel,
FieldLegend,
FieldSet,
} from "@/components/ui/field";
import { Form } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { Switch } from "@/components/ui/switch";
import { Textarea } from "@/components/ui/textarea";
const audiences = [
{ value: "everyone", label: "Everyone who emails me" },
{ value: "internal", label: "Only people at Northwind" },
];
const defaultMessage =
"Thanks for your message. I'm away until Monday, October 12 with limited access to email. For anything urgent, reach Daniel Park at daniel@northwind.co.";
function formatDay(value: string) {
return new Date(`${value}T00:00:00`).toLocaleDateString("en-US", {
weekday: "short",
month: "short",
day: "numeric",
});
}
function dayCount(start: string, end: string) {
const ms = Date.parse(`${end}T00:00:00`) - Date.parse(`${start}T00:00:00`);
return Math.round(ms / 86_400_000) + 1;
}
export default function Form09() {
const messageId = useId();
const [enabled, setEnabled] = useState(true);
const [start, setStart] = useState("2026-10-05");
const [end, setEnd] = useState("2026-10-09");
const [audience, setAudience] = useState("everyone");
const [message, setMessage] = useState(defaultMessage);
const [messageError, setMessageError] = useState(false);
const [scheduled, setScheduled] = useState<string | null>(null);
const validRange = Boolean(start && end) && end >= start;
return (
<Form
className="w-full max-w-lg gap-5 rounded-xl border border-border bg-card p-5"
onChange={() => setScheduled(null)}
onFormSubmit={() => {
if (enabled && !message.trim()) {
setMessageError(true);
return;
}
setScheduled(
enabled
? `Auto-reply scheduled for ${formatDay(start)} to ${formatDay(end)}.`
: "Auto-reply turned off.",
);
}}
>
<Field orientation="horizontal">
<FieldContent>
<FieldLabel className="text-base font-semibold">
Out-of-office reply
</FieldLabel>
<FieldDescription>
Answer incoming email automatically while you're away.
</FieldDescription>
</FieldContent>
<Switch
checked={enabled}
onCheckedChange={(checked) => {
setEnabled(checked);
setScheduled(null);
}}
/>
</Field>
{enabled ? (
<FieldGroup className="gap-4">
<div className="grid gap-4 sm:grid-cols-2">
<Field name="start">
<FieldLabel>First day away</FieldLabel>
<Input
required
type="date"
value={start}
onChange={(event) => setStart(event.target.value)}
/>
<FieldError />
</Field>
<Field
name="end"
validate={(value, formValues) =>
String(value ?? "") >= String(formValues.start ?? "")
? null
: "Pick a day on or after the first day."
}
>
<FieldLabel>Last day away</FieldLabel>
<Input
required
type="date"
min={start}
value={end}
onChange={(event) => setEnd(event.target.value)}
/>
<FieldError />
</Field>
</div>
{validRange ? (
<p className="flex items-center gap-2 rounded-lg bg-muted px-3 py-2 text-sm text-muted-foreground">
<PlaneIcon aria-hidden="true" className="size-4 shrink-0" />
<span>
Away{" "}
<span className="font-medium text-foreground">
{dayCount(start, end)}{" "}
{dayCount(start, end) === 1 ? "day" : "days"}
</span>
, back on the next working day.
</span>
</p>
) : null}
<FieldSet>
<FieldLegend variant="label">Send replies to</FieldLegend>
<RadioGroup
value={audience}
onValueChange={(value) => setAudience(value as string)}
>
{audiences.map((option) => (
<Label key={option.value} className="font-normal">
<RadioGroupItem value={option.value} />
{option.label}
</Label>
))}
</RadioGroup>
</FieldSet>
<Field invalid={messageError}>
<FieldLabel htmlFor={messageId}>Message</FieldLabel>
<Textarea
id={messageId}
rows={4}
aria-invalid={messageError || undefined}
value={message}
onChange={(event) => {
setMessage(event.target.value);
setMessageError(false);
}}
/>
{messageError ? (
<FieldError>Write a short message for people to read.</FieldError>
) : (
<FieldDescription>
Each sender gets this reply once per day.
</FieldDescription>
)}
</Field>
</FieldGroup>
) : null}
<div className="flex flex-wrap items-center justify-end gap-3">
{scheduled ? (
<p role="status" className="mr-auto text-sm text-muted-foreground">
{scheduled}
</p>
) : null}
<Button type="submit">Save</Button>
</div>
</Form>
);
}
npx shadcn@latest add @sevenui/component/form-09pnpm dlx shadcn@latest add @sevenui/component/form-09yarn dlx shadcn@latest add @sevenui/component/form-09bunx --bun shadcn@latest add @sevenui/component/form-09"use client";
import { useId, useState } from "react";
import { ReceiptTextIcon } from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Field,
FieldDescription,
FieldError,
FieldGroup,
FieldLabel,
} from "@/components/ui/field";
import { Form } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import {
NativeSelect,
NativeSelectOption,
} from "@/components/ui/native-select";
type CountryCode = "DE" | "FR" | "NL" | "US";
// Each billing country has its own tax ID name, format, and tax treatment.
const countries: Record<
CountryCode,
{
label: string;
taxLabel: string;
placeholder: string;
pattern: RegExp;
hint: string;
eu: boolean;
}
> = {
DE: {
label: "Germany",
taxLabel: "VAT number (USt-IdNr.)",
placeholder: "DE123456789",
pattern: /^DE\d{9}$/,
hint: "DE followed by 9 digits.",
eu: true,
},
FR: {
label: "France",
taxLabel: "VAT number (TVA)",
placeholder: "FR12345678901",
pattern: /^FR[A-Z0-9]{2}\d{9}$/,
hint: "FR, 2 characters, then 9 digits.",
eu: true,
},
NL: {
label: "Netherlands",
taxLabel: "VAT number (btw-id)",
placeholder: "NL123456789B01",
pattern: /^NL\d{9}B\d{2}$/,
hint: "NL, 9 digits, B, then 2 digits.",
eu: true,
},
US: {
label: "United States",
taxLabel: "EIN",
placeholder: "12-3456789",
pattern: /^\d{2}-\d{7}$/,
hint: "9 digits in the format 12-3456789.",
eu: false,
},
};
// Stands in for the VIES registry lookup a server would run.
function lookupFails(taxId: string) {
return /0{6}/.test(taxId);
}
export default function Form10() {
const countryId = useId();
const [country, setCountry] = useState<CountryCode>("DE");
const [errors, setErrors] = useState<Record<string, string>>({});
const [saved, setSaved] = useState<{
company: string;
taxId: string;
} | null>(null);
const config = countries[country];
const treatment = !saved
? null
: !config.eu
? "Sales tax is calculated from your billing address."
: saved.taxId
? "Reverse charge applies. Invoices show 0% VAT."
: "Standard VAT will be added to each invoice.";
return (
<Form
className="w-full max-w-lg gap-5 rounded-xl border border-border bg-card p-5"
errors={errors}
onChange={() => setSaved(null)}
onFormSubmit={(values) => {
const taxId = String(values.taxId ?? "")
.replace(/\s/g, "")
.toUpperCase();
if (taxId && config.eu && lookupFails(taxId)) {
setErrors({
taxId:
"The EU VAT registry couldn't verify this number. Check it, or leave it empty to be charged VAT.",
});
return;
}
setErrors({});
setSaved({ company: String(values.company ?? ""), taxId });
}}
>
<div className="flex items-start gap-3">
<span className="flex size-9 shrink-0 items-center justify-center rounded-lg bg-muted">
<ReceiptTextIcon
aria-hidden="true"
className="size-4 text-muted-foreground"
/>
</span>
<div className="flex flex-col gap-1">
<h3 className="font-semibold">Tax details</h3>
<p className="text-sm text-muted-foreground">
Printed on every invoice from your next billing date, October 1.
</p>
</div>
</div>
<FieldGroup className="gap-4">
<Field name="company">
<FieldLabel>Legal business name</FieldLabel>
<Input
required
autoComplete="organization"
defaultValue="Lindqvist Studio GmbH"
/>
<FieldError />
</Field>
<div className="grid gap-4 sm:grid-cols-[minmax(0,2fr)_minmax(0,3fr)]">
<Field>
<FieldLabel htmlFor={countryId}>Country</FieldLabel>
<NativeSelect
id={countryId}
className="w-full"
autoComplete="country"
value={country}
onChange={(event) => {
setCountry(event.target.value as CountryCode);
setErrors({});
}}
>
{(Object.keys(countries) as CountryCode[]).map((code) => (
<NativeSelectOption key={code} value={code}>
{countries[code].label}
</NativeSelectOption>
))}
</NativeSelect>
</Field>
{/* Keyed by country so a stale format error doesn't carry over. */}
<Field
key={country}
name="taxId"
validate={(value) => {
const taxId = String(value ?? "")
.replace(/\s/g, "")
.toUpperCase();
if (!taxId) return null;
return config.pattern.test(taxId) ? null : `Use ${config.hint}`;
}}
>
<FieldLabel>
{config.taxLabel}
<span className="font-normal text-muted-foreground">
Optional
</span>
</FieldLabel>
<Input
placeholder={config.placeholder}
spellCheck={false}
className="font-mono uppercase placeholder:font-sans placeholder:normal-case"
/>
<FieldError />
</Field>
</div>
<Field name="billingEmail">
<FieldLabel>Invoice email</FieldLabel>
<Input
required
type="email"
autoComplete="email"
defaultValue="finance@lindqvist.studio"
/>
<FieldDescription>
Receipts and payment failures go here, not to your login email.
</FieldDescription>
<FieldError />
</Field>
</FieldGroup>
{saved ? (
<div
role="status"
className="flex flex-col gap-2 rounded-lg border border-border bg-muted/50 p-3 text-sm"
>
<div className="flex flex-wrap items-center gap-2">
<span className="font-medium">{saved.company}</span>
{saved.taxId ? (
<Badge variant="secondary" className="font-mono">
{saved.taxId}
</Badge>
) : null}
</div>
<p className="text-muted-foreground">{treatment}</p>
</div>
) : null}
<Button type="submit" className="sm:justify-self-end">
Save tax details
</Button>
</Form>
);
}
npx shadcn@latest add @sevenui/component/form-10pnpm dlx shadcn@latest add @sevenui/component/form-10yarn dlx shadcn@latest add @sevenui/component/form-10bunx --bun shadcn@latest add @sevenui/component/form-10"use client";
import { useId, useState } from "react";
import { CalendarClockIcon, MailIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Field,
FieldDescription,
FieldError,
FieldGroup,
FieldLabel,
FieldLegend,
FieldSet,
} from "@/components/ui/field";
import { Form } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
NativeSelect,
NativeSelectOption,
} from "@/components/ui/native-select";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
type Frequency = "daily" | "weekly" | "monthly";
const frequencies: { value: Frequency; label: string }[] = [
{ value: "daily", label: "Daily" },
{ value: "weekly", label: "Weekly" },
{ value: "monthly", label: "Monthly" },
];
const weekdays = [
"Sunday",
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
];
const formats = [
{ value: "pdf", label: "PDF summary", hint: "Charts and top-line numbers" },
{ value: "csv", label: "CSV export", hint: "Raw rows for spreadsheets" },
];
// A fixed "today" keeps the preview deterministic: Friday, September 25, 2026.
const today = new Date(2026, 8, 25);
const maxRecipients = 8;
const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
function nextDelivery(frequency: Frequency, weekday: number, time: string) {
const [hours, minutes] = time.split(":").map(Number);
const next = new Date(today);
if (frequency === "daily") {
next.setDate(next.getDate() + 1);
} else if (frequency === "weekly") {
const offset = (weekday - next.getDay() + 7) % 7 || 7;
next.setDate(next.getDate() + offset);
} else {
next.setMonth(next.getMonth() + 1, 1);
}
next.setHours(hours || 0, minutes || 0);
return next.toLocaleString("en-US", {
weekday: "long",
month: "long",
day: "numeric",
hour: "numeric",
minute: "2-digit",
});
}
function parseRecipients(value: unknown) {
return String(value ?? "")
.split(",")
.map((entry) => entry.trim())
.filter(Boolean);
}
export default function Form11() {
const reportId = useId();
const weekdayId = useId();
const [frequency, setFrequency] = useState<Frequency>("weekly");
const [weekday, setWeekday] = useState(1);
const [time, setTime] = useState("09:00");
const [format, setFormat] = useState("pdf");
const [confirmation, setConfirmation] = useState<string | null>(null);
return (
<Form
className="w-full max-w-lg gap-0 overflow-hidden rounded-xl border border-border bg-card"
onChange={() => setConfirmation(null)}
onFormSubmit={(values) => {
const count = parseRecipients(values.recipients).length;
setConfirmation(
`Scheduled. ${count} ${count === 1 ? "person gets" : "people get"} the first report ${nextDelivery(frequency, weekday, time)}.`,
);
}}
>
<div className="flex flex-col gap-1 border-b border-border p-5">
<h3 className="font-semibold">Schedule email delivery</h3>
<p className="text-sm text-muted-foreground">
Send a snapshot of this dashboard to your team on a regular cadence.
</p>
</div>
<FieldGroup className="gap-5 p-5">
<Field>
<FieldLabel htmlFor={reportId}>Report</FieldLabel>
<NativeSelect
id={reportId}
className="w-full"
defaultValue="revenue"
>
<NativeSelectOption value="revenue">
Revenue overview
</NativeSelectOption>
<NativeSelectOption value="retention">
Cohort retention
</NativeSelectOption>
<NativeSelectOption value="funnel">
Signup funnel
</NativeSelectOption>
</NativeSelect>
</Field>
<FieldSet>
<FieldLegend variant="label">Frequency</FieldLegend>
<ToggleGroup
variant="outline"
spacing={0}
value={[frequency]}
onValueChange={(value) => {
const next = (value as Frequency[])[0];
if (next) setFrequency(next);
setConfirmation(null);
}}
className="w-full"
>
{frequencies.map((option) => (
<ToggleGroupItem
key={option.value}
value={option.value}
className="flex-1"
>
{option.label}
</ToggleGroupItem>
))}
</ToggleGroup>
</FieldSet>
<div className="grid gap-4 sm:grid-cols-2">
{frequency === "weekly" ? (
<Field>
<FieldLabel htmlFor={weekdayId}>Day</FieldLabel>
<NativeSelect
id={weekdayId}
className="w-full"
value={weekday}
onChange={(event) => setWeekday(Number(event.target.value))}
>
{weekdays.map((day, index) => (
<NativeSelectOption key={day} value={index}>
{day}
</NativeSelectOption>
))}
</NativeSelect>
</Field>
) : (
<Field>
<FieldLabel htmlFor={weekdayId}>Day</FieldLabel>
<Input
id={weekdayId}
readOnly
value={
frequency === "daily" ? "Every day" : "1st of the month"
}
className="text-muted-foreground"
/>
</Field>
)}
<Field name="deliverAt">
<FieldLabel>Time (CET)</FieldLabel>
<Input
required
type="time"
step={900}
value={time}
onChange={(event) => setTime(event.target.value)}
/>
<FieldError />
</Field>
</div>
<Field
name="recipients"
validate={(value) => {
const list = parseRecipients(value);
if (list.length === 0) return "Add at least one recipient.";
const invalid = list.find((entry) => !emailPattern.test(entry));
if (invalid) return `"${invalid}" isn't a valid email address.`;
if (list.length > maxRecipients) {
return `Scheduled reports go to ${maxRecipients} people at most.`;
}
return null;
}}
>
<FieldLabel>Recipients</FieldLabel>
<Input
type="text"
inputMode="email"
spellCheck={false}
defaultValue="lena@acme.io, tomas@acme.io"
/>
<FieldDescription>
Separate addresses with commas. People outside Acme get a view-only
link.
</FieldDescription>
<FieldError />
</Field>
<FieldSet>
<FieldLegend variant="label">Attachment</FieldLegend>
<RadioGroup
value={format}
onValueChange={(value) => setFormat(value as string)}
className="grid-cols-1 sm:grid-cols-2"
>
{formats.map((option) => (
<Label
key={option.value}
className="items-start rounded-lg border border-border p-3 font-normal has-data-checked:border-primary/40 has-data-checked:bg-primary/5"
>
<RadioGroupItem value={option.value} className="mt-0.5" />
<span className="flex flex-col gap-0.5">
<span className="font-medium">{option.label}</span>
<span className="text-xs text-muted-foreground">
{option.hint}
</span>
</span>
</Label>
))}
</RadioGroup>
</FieldSet>
</FieldGroup>
<div className="flex flex-col gap-3 border-t border-border bg-muted/40 px-5 py-3 sm:flex-row sm:items-center">
<p
role="status"
className="flex items-start gap-2 text-sm text-muted-foreground sm:mr-auto"
>
{confirmation ? (
<MailIcon aria-hidden="true" className="mt-0.5 size-4 shrink-0" />
) : (
<CalendarClockIcon
aria-hidden="true"
className="mt-0.5 size-4 shrink-0"
/>
)}
<span>
{confirmation ??
`Next: ${nextDelivery(frequency, weekday, time)}`}
</span>
</p>
<Button type="submit" className="shrink-0">
Schedule report
</Button>
</div>
</Form>
);
}
npx shadcn@latest add @sevenui/component/form-11pnpm dlx shadcn@latest add @sevenui/component/form-11yarn dlx shadcn@latest add @sevenui/component/form-11bunx --bun shadcn@latest add @sevenui/component/form-11"use client";
import { useId, useState } from "react";
import { PackageCheckIcon } from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import {
Field,
FieldError,
FieldGroup,
FieldLabel,
FieldLegend,
FieldSet,
} from "@/components/ui/field";
import { Form } from "@/components/ui/form";
import { Label } from "@/components/ui/label";
import {
NativeSelect,
NativeSelectOption,
} from "@/components/ui/native-select";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
const order = {
number: "SU-20418",
deliveredOn: "September 18",
returnBy: "October 18",
};
const items = [
{
id: "sweater",
name: "Merino crew sweater",
variant: "Oat, size M",
price: 98,
finalSale: false,
},
{
id: "weekender",
name: "Canvas weekender bag",
variant: "Olive",
price: 145,
finalSale: false,
},
{
id: "socks",
name: "Wool hiking socks, 3-pack",
variant: "Charcoal, size L",
price: 24,
finalSale: true,
},
];
const reasons = [
{ value: "size", label: "Doesn't fit" },
{ value: "not-as-described", label: "Not as described" },
{ value: "damaged", label: "Arrived damaged" },
{ value: "changed-mind", label: "Changed my mind" },
];
const creditBonus = 0.1;
const currency = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
});
export default function Form12() {
const baseId = useId();
const itemsErrorId = `${baseId}-items-error`;
const [selected, setSelected] = useState<Record<string, string>>({});
const [refundTo, setRefundTo] = useState("original");
const [errors, setErrors] = useState<Record<string, string>>({});
const [rma, setRma] = useState<string | null>(null);
const subtotal = items
.filter((item) => item.id in selected)
.reduce((sum, item) => sum + item.price, 0);
const refund =
refundTo === "credit" ? subtotal * (1 + creditBonus) : subtotal;
const toggleItem = (id: string, checked: boolean) => {
setErrors({});
setSelected((current) => {
const next = { ...current };
if (checked) next[id] = "";
else delete next[id];
return next;
});
};
if (rma) {
return (
<div className="flex w-full max-w-md flex-col gap-4 rounded-xl border border-border bg-card p-5">
<PackageCheckIcon aria-hidden="true" className="size-6 text-success" />
<div role="status" className="flex flex-col gap-1">
<h3 className="font-semibold">Return {rma} started</h3>
<p className="text-sm text-muted-foreground">
We emailed a prepaid label. Drop the package at any UPS location by{" "}
{order.returnBy}. Your {currency.format(refund)}{" "}
{refundTo === "credit" ? "store credit" : "refund"} is issued once
it's scanned.
</p>
</div>
<Button
variant="outline"
className="self-start"
onClick={() => {
setRma(null);
setSelected({});
}}
>
Start another return
</Button>
</div>
);
}
return (
<Form
className="w-full max-w-md gap-5 rounded-xl border border-border bg-card p-5"
errors={errors}
onFormSubmit={() => {
const ids = Object.keys(selected);
if (ids.length === 0) {
setErrors({ items: "Choose at least one item to return." });
return;
}
const missing = ids.find((id) => !selected[id]);
if (missing) {
setErrors({
[`reason-${missing}`]: "Tell us why you're returning it.",
});
return;
}
setRma("RMA-58213");
}}
>
<div className="flex flex-col gap-1">
<h3 className="font-semibold">Return items</h3>
<p className="text-sm text-muted-foreground">
Order {order.number}, delivered {order.deliveredOn}. Free returns
until {order.returnBy}.
</p>
</div>
<FieldSet aria-describedby={errors.items ? itemsErrorId : undefined}>
<FieldLegend variant="label">What are you sending back?</FieldLegend>
<FieldGroup className="gap-2">
{items.map((item) => {
const checkboxId = `${baseId}-${item.id}`;
const reasonId = `${baseId}-${item.id}-reason`;
const isSelected = item.id in selected;
return (
<div
key={item.id}
className="flex flex-col gap-3 rounded-lg border border-border p-3 has-data-checked:border-primary/40 has-data-checked:bg-primary/5 has-data-disabled:opacity-60"
>
<div className="flex items-start gap-3">
<Checkbox
id={checkboxId}
className="mt-0.5"
disabled={item.finalSale}
checked={isSelected}
onCheckedChange={(checked) => toggleItem(item.id, checked)}
/>
<img
src="/placeholder.svg"
alt=""
className="size-10 shrink-0 rounded-md bg-muted object-cover"
/>
<div className="flex min-w-0 flex-1 flex-col items-start gap-1.5 sm:flex-row sm:justify-between sm:gap-3">
<div className="flex min-w-0 flex-col gap-0.5">
<Label htmlFor={checkboxId} className="leading-snug">
{item.name}
</Label>
<span className="text-xs text-muted-foreground">
{item.variant}
</span>
</div>
{item.finalSale ? (
<Badge variant="outline">Final sale</Badge>
) : (
<span className="text-sm tabular-nums">
{currency.format(item.price)}
</span>
)}
</div>
</div>
{isSelected ? (
<Field name={`reason-${item.id}`} className="gap-1.5 pl-7">
<FieldLabel htmlFor={reasonId} className="sr-only">
Reason for returning {item.name}
</FieldLabel>
<NativeSelect
id={reasonId}
size="sm"
className="w-full"
value={selected[item.id]}
aria-invalid={
`reason-${item.id}` in errors || undefined
}
onChange={(event) => {
const value = event.target.value;
setErrors({});
setSelected((current) => ({
...current,
[item.id]: value,
}));
}}
>
<NativeSelectOption value="" disabled>
Select a reason
</NativeSelectOption>
{reasons.map((reason) => (
<NativeSelectOption
key={reason.value}
value={reason.value}
>
{reason.label}
</NativeSelectOption>
))}
</NativeSelect>
<FieldError />
</Field>
) : null}
</div>
);
})}
</FieldGroup>
{errors.items ? (
<p id={itemsErrorId} role="alert" className="text-sm text-destructive">
{errors.items}
</p>
) : null}
</FieldSet>
<FieldSet>
<FieldLegend variant="label">Refund to</FieldLegend>
<RadioGroup
value={refundTo}
onValueChange={(value) => setRefundTo(value as string)}
>
<Label className="font-normal">
<RadioGroupItem value="original" />
Original payment, Visa ending 4242
</Label>
<Label className="font-normal">
<RadioGroupItem value="credit" />
Store credit
<Badge variant="secondary">+10% bonus</Badge>
</Label>
</RadioGroup>
</FieldSet>
<div className="flex items-center justify-between gap-3 border-t border-border pt-4">
<p className="text-sm text-muted-foreground">
Refund{" "}
<span className="font-medium text-foreground tabular-nums">
{currency.format(refund)}
</span>
</p>
<Button type="submit">Request return</Button>
</div>
</Form>
);
}
npx shadcn@latest add @sevenui/component/form-12pnpm dlx shadcn@latest add @sevenui/component/form-12yarn dlx shadcn@latest add @sevenui/component/form-12bunx --bun shadcn@latest add @sevenui/component/form-12"use client";
import { useRef, useState } from "react";
import { PlusIcon, RocketIcon, Trash2Icon } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Field,
FieldError,
FieldLabel,
FieldLegend,
FieldSet,
} from "@/components/ui/field";
import { Form } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
type Row = { id: number; key: string; value: string };
const environments = [
{ value: "production", label: "Production" },
{ value: "preview", label: "Preview" },
{ value: "development", label: "Development" },
];
// Names the platform sets itself; the "server" rejects them on save.
const reservedKeys = ["NODE_ENV", "PORT", "VERCEL_URL"];
const keyPattern = /^[A-Z_][A-Z0-9_]*$/;
const initialRows: Row[] = [
{
id: 1,
key: "DATABASE_URL",
value: "postgres://app:••••@db.internal:5432/app",
},
{ id: 2, key: "STRIPE_SECRET_KEY", value: "sk_live_51Hx••••" },
];
export default function Form13() {
const nextId = useRef(initialRows.length + 1);
const [rows, setRows] = useState<Row[]>(initialRows);
const [targets, setTargets] = useState<string[]>(["production", "preview"]);
const [errors, setErrors] = useState<Record<string, string>>({});
const [saved, setSaved] = useState<string | null>(null);
const createRow = (key = "", value = ""): Row => {
const row = { id: nextId.current, key, value };
nextId.current += 1;
return row;
};
const updateRow = (id: number, patch: Partial<Row>) => {
setSaved(null);
setRows((current) =>
current.map((row) => (row.id === id ? { ...row, ...patch } : row)),
);
};
// Pasting a whole .env file into a name field expands it into rows.
const handlePaste = (id: number, text: string) => {
const pairs = text
.split(/\r?\n/)
.map((line) => line.trim())
.filter((line) => line && !line.startsWith("#") && line.includes("="))
.map((line) => {
const index = line.indexOf("=");
return {
key: line.slice(0, index).trim(),
value: line
.slice(index + 1)
.trim()
.replace(/^["']|["']$/g, ""),
};
});
if (pairs.length < 2) return false;
setSaved(null);
setRows((current) => {
const index = current.findIndex((row) => row.id === id);
const pasted = pairs.map((pair) => createRow(pair.key, pair.value));
const next = [...current];
// Replace the row being pasted into when it's still empty.
const replace = next[index] && !next[index].key && !next[index].value;
next.splice(index + (replace ? 0 : 1), replace ? 1 : 0, ...pasted);
return next;
});
return true;
};
return (
<Form
className="w-full max-w-2xl gap-0 overflow-hidden rounded-xl border border-border bg-card"
errors={errors}
onFormSubmit={() => {
const rejected = rows.find((row) => reservedKeys.includes(row.key));
if (rejected) {
setErrors({
[`key-${rejected.id}`]: `${rejected.key} is set by the platform and can't be overridden.`,
});
return;
}
setErrors({});
const names = environments
.filter((env) => targets.includes(env.value))
.map((env) => env.label);
setSaved(
`${rows.length} ${rows.length === 1 ? "variable" : "variables"} saved to ${names.join(" and ")}. Redeploy to apply them.`,
);
}}
>
<div className="flex flex-col gap-4 border-b border-border p-5">
<div className="flex flex-col gap-1">
<h3 className="font-semibold">Environment variables</h3>
<p className="text-sm text-muted-foreground">
Encrypted at rest and exposed to builds and serverless functions.
</p>
</div>
<FieldSet>
<FieldLegend variant="label">Apply to</FieldLegend>
<ToggleGroup
multiple
variant="outline"
size="sm"
value={targets}
onValueChange={(value) => {
const next = value as string[];
// Keep at least one environment selected.
if (next.length > 0) setTargets(next);
setSaved(null);
}}
className="flex-wrap"
>
{environments.map((env) => (
<ToggleGroupItem key={env.value} value={env.value}>
{env.label}
</ToggleGroupItem>
))}
</ToggleGroup>
</FieldSet>
</div>
<div className="flex flex-col gap-3 p-5">
<div
aria-hidden="true"
className="hidden grid-cols-[minmax(0,2fr)_minmax(0,3fr)_2rem] gap-2 text-xs font-medium text-muted-foreground sm:grid"
>
<span>Name</span>
<span>Value</span>
</div>
{rows.map((row, index) => (
<div
key={row.id}
className="grid grid-cols-[minmax(0,1fr)_2rem] items-start gap-2 sm:grid-cols-[minmax(0,2fr)_minmax(0,3fr)_2rem]"
>
<Field
name={`key-${row.id}`}
validate={(value, formValues) => {
const key = String(value ?? "");
if (!key) return "Enter a name.";
if (!keyPattern.test(key)) {
return "Use A–Z, 0–9, and underscores; don't start with a digit.";
}
const duplicate = Object.entries(formValues).some(
([name, other]) =>
name.startsWith("key-") &&
name !== `key-${row.id}` &&
other === key,
);
return duplicate ? `${key} is already defined.` : null;
}}
>
<FieldLabel className="sr-only">
Name of variable {index + 1}
</FieldLabel>
<Input
placeholder="API_BASE_URL"
spellCheck={false}
autoComplete="off"
className="font-mono"
value={row.key}
onChange={(event) =>
updateRow(row.id, {
key: event.target.value
.toUpperCase()
.replace(/[\s-]/g, "_"),
})
}
onPaste={(event) => {
const text = event.clipboardData.getData("text");
if (handlePaste(row.id, text)) event.preventDefault();
}}
/>
<FieldError />
</Field>
<Field
name={`value-${row.id}`}
className="col-start-1 sm:col-start-auto"
>
<FieldLabel className="sr-only">
Value of variable {index + 1}
</FieldLabel>
<Input
placeholder="Value"
spellCheck={false}
autoComplete="off"
className="font-mono"
value={row.value}
onChange={(event) =>
updateRow(row.id, { value: event.target.value })
}
/>
</Field>
<Button
type="button"
variant="ghost"
size="icon"
className="col-start-2 row-start-1 sm:col-start-3"
aria-label={`Remove ${row.key || `variable ${index + 1}`}`}
disabled={rows.length === 1}
onClick={() => {
setSaved(null);
setRows((current) => current.filter((r) => r.id !== row.id));
}}
>
<Trash2Icon aria-hidden="true" />
</Button>
</div>
))}
<Button
type="button"
variant="outline"
size="sm"
className="self-start"
onClick={() => {
setSaved(null);
setRows((current) => [...current, createRow()]);
}}
>
<PlusIcon aria-hidden="true" data-icon="inline-start" />
Add variable
</Button>
<p className="text-xs text-muted-foreground">
Paste a whole <span className="font-mono">.env</span> file into any
name field to import every line at once.
</p>
</div>
<div className="flex flex-col gap-3 border-t border-border bg-muted/40 px-5 py-3 sm:flex-row sm:items-center">
<p
role="status"
className="flex items-start gap-2 text-sm text-muted-foreground sm:mr-auto"
>
{saved ? (
<>
<RocketIcon aria-hidden="true" className="mt-0.5 size-4 shrink-0" />
{saved}
</>
) : null}
</p>
<Button type="submit" className="shrink-0">
Save variables
</Button>
</div>
</Form>
);
}
npx shadcn@latest add @sevenui/component/form-13pnpm dlx shadcn@latest add @sevenui/component/form-13yarn dlx shadcn@latest add @sevenui/component/form-13bunx --bun shadcn@latest add @sevenui/component/form-13Step 1 of 3
Create your workspace
This is where your team's projects and docs will live.
"use client";
import { useState } from "react";
import { ArrowLeftIcon, CheckIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Field,
FieldDescription,
FieldError,
FieldGroup,
FieldLabel,
FieldLegend,
FieldSet,
} from "@/components/ui/field";
import { Form } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import {
InputGroup,
InputGroupAddon,
InputGroupInput,
InputGroupText,
} from "@/components/ui/input-group";
import { Label } from "@/components/ui/label";
import { Progress } from "@/components/ui/progress";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
const steps = [
{
title: "Create your workspace",
description: "This is where your team's projects and docs will live.",
},
{
title: "Tell us about your team",
description: "We'll set up templates that match how you work.",
},
{
title: "Invite your teammates",
description: "They'll get an email with a link to join. You can skip this.",
},
];
const useCases = [
{
value: "product",
label: "Product & engineering",
hint: "Roadmaps, specs, sprint boards",
},
{
value: "marketing",
label: "Marketing",
hint: "Campaign calendars, briefs",
},
{
value: "operations",
label: "Operations",
hint: "Runbooks, vendor tracking",
},
];
const teamSizes = ["Just me", "2–10", "11–50", "51+"];
const inviteSlots = [0, 1, 2];
const takenSlugs = ["acme", "northwind", "studio", "team"];
const slugPattern = /^[a-z0-9][a-z0-9-]{1,30}[a-z0-9]$/;
const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
function toSlug(value: string) {
return value
.toLowerCase()
.trim()
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-+|-+$/g, "")
.slice(0, 32);
}
type Draft = {
workspace: string;
slug: string;
useCase: string;
teamSize: string;
invites: string[];
};
export default function Form14() {
const [step, setStep] = useState(0);
const [draft, setDraft] = useState<Draft>({
workspace: "",
slug: "",
useCase: "",
teamSize: "2–10",
invites: [],
});
const [slugEdited, setSlugEdited] = useState(false);
// Kept outside the step form so typed emails survive Back and Continue.
const [inviteInputs, setInviteInputs] = useState(inviteSlots.map(() => ""));
const [errors, setErrors] = useState<Record<string, string>>({});
const [done, setDone] = useState(false);
const goTo = (next: number) => {
setErrors({});
setStep(next);
};
if (done) {
return (
<div className="flex w-full max-w-md flex-col gap-4 rounded-xl border border-border bg-card p-6">
<span className="flex size-10 items-center justify-center rounded-full bg-success/15">
<CheckIcon aria-hidden="true" className="size-5 text-success" />
</span>
<div role="status" className="flex flex-col gap-1">
<h3 className="text-lg font-semibold">
{draft.workspace} is ready
</h3>
<p className="text-sm text-muted-foreground">
Your workspace lives at{" "}
<span className="font-medium text-foreground">
sevenui.app/{draft.slug}
</span>
.{" "}
{draft.invites.length > 0
? `We sent ${draft.invites.length} ${draft.invites.length === 1 ? "invite" : "invites"}.`
: "Invite teammates any time from Settings."}
</p>
</div>
<Button
variant="outline"
className="self-start"
onClick={() => {
setDone(false);
setStep(0);
}}
>
Review setup
</Button>
</div>
);
}
return (
<div className="flex w-full max-w-md flex-col gap-5 rounded-xl border border-border bg-card p-6">
<Progress
value={((step + 1) / steps.length) * 100}
aria-label={`Step ${step + 1} of ${steps.length}`}
className="gap-2"
>
<span className="text-xs font-medium text-muted-foreground">
Step {step + 1} of {steps.length}
</span>
</Progress>
<div className="flex flex-col gap-1">
<h3 className="text-lg font-semibold">{steps[step].title}</h3>
<p className="text-sm text-muted-foreground">
{steps[step].description}
</p>
</div>
{/* Each step is its own form, so only the visible fields validate. */}
<Form
key={step}
className="gap-6"
errors={errors}
onFormSubmit={(values) => {
if (step === 0) {
const slug = String(values.slug ?? "");
if (takenSlugs.includes(slug)) {
setErrors({
slug: `sevenui.app/${slug} is taken. Try ${slug}-hq or ${slug}-team.`,
});
return;
}
setDraft((current) => ({
...current,
workspace: String(values.workspace ?? "").trim(),
slug,
}));
goTo(1);
} else if (step === 1) {
if (!draft.useCase) {
setErrors({
useCase: "Pick what your team will mainly use it for.",
});
return;
}
goTo(2);
} else {
const invites = inviteInputs
.map((email) => email.trim())
.filter(Boolean);
setDraft((current) => ({ ...current, invites }));
setDone(true);
}
}}
>
{step === 0 ? (
<FieldGroup className="gap-4">
<Field name="workspace">
<FieldLabel>Workspace name</FieldLabel>
<Input
required
autoComplete="organization"
placeholder="Acme Robotics"
value={draft.workspace}
onChange={(event) => {
const workspace = event.target.value;
setDraft((current) => ({
...current,
workspace,
slug: slugEdited ? current.slug : toSlug(workspace),
}));
}}
/>
<FieldError />
</Field>
<Field
name="slug"
validate={(value) =>
slugPattern.test(String(value ?? ""))
? null
: "Use 3 to 32 lowercase letters, numbers, or hyphens."
}
>
<FieldLabel>Workspace URL</FieldLabel>
<InputGroup>
<InputGroupAddon>
<InputGroupText>sevenui.app/</InputGroupText>
</InputGroupAddon>
<InputGroupInput
spellCheck={false}
autoComplete="off"
placeholder="acme-robotics"
value={draft.slug}
onChange={(event) => {
setSlugEdited(true);
setDraft((current) => ({
...current,
slug: event.target.value.toLowerCase(),
}));
}}
/>
</InputGroup>
<FieldDescription>
Filled in from the name. You can change it later.
</FieldDescription>
<FieldError />
</Field>
</FieldGroup>
) : null}
{step === 1 ? (
<FieldGroup className="gap-5">
<FieldSet>
<FieldLegend variant="label">Main use</FieldLegend>
<RadioGroup
value={draft.useCase}
aria-invalid={Boolean(errors.useCase) || undefined}
onValueChange={(value) => {
setErrors({});
setDraft((current) => ({
...current,
useCase: value as string,
}));
}}
>
{useCases.map((option) => (
<Label
key={option.value}
className="items-start rounded-lg border border-border p-3 font-normal has-data-checked:border-primary/40 has-data-checked:bg-primary/5"
>
<RadioGroupItem value={option.value} className="mt-0.5" />
<span className="flex flex-col gap-0.5">
<span className="font-medium">{option.label}</span>
<span className="text-xs text-muted-foreground">
{option.hint}
</span>
</span>
</Label>
))}
</RadioGroup>
{errors.useCase ? (
<p role="alert" className="text-sm text-destructive">
{errors.useCase}
</p>
) : null}
</FieldSet>
<FieldSet>
<FieldLegend variant="label">Team size</FieldLegend>
<ToggleGroup
variant="outline"
spacing={0}
value={[draft.teamSize]}
onValueChange={(value) => {
const next = (value as string[])[0];
if (next) {
setDraft((current) => ({ ...current, teamSize: next }));
}
}}
className="w-full"
>
{teamSizes.map((size) => (
<ToggleGroupItem key={size} value={size} className="flex-1">
{size}
</ToggleGroupItem>
))}
</ToggleGroup>
</FieldSet>
</FieldGroup>
) : null}
{step === 2 ? (
<FieldSet>
<FieldLegend variant="label">Teammate emails</FieldLegend>
<FieldGroup className="gap-3">
{inviteSlots.map((slot) => (
<Field
key={slot}
name={`invite-${slot}`}
validate={(value) => {
const email = String(value ?? "").trim();
return !email || emailPattern.test(email)
? null
: "Enter a full email address, like jo@acme.io.";
}}
>
<FieldLabel className="sr-only">
Teammate email {slot + 1}
</FieldLabel>
<Input
type="text"
inputMode="email"
autoComplete="off"
spellCheck={false}
placeholder={
["jo@acme.io", "sam@acme.io", "priya@acme.io"][slot]
}
value={inviteInputs[slot]}
onChange={(event) => {
const email = event.target.value;
setInviteInputs((current) =>
current.map((entry, index) =>
index === slot ? email : entry,
),
);
}}
/>
<FieldError />
</Field>
))}
</FieldGroup>
</FieldSet>
) : null}
<div className="flex items-center gap-2">
{step > 0 ? (
<Button
type="button"
variant="ghost"
onClick={() => goTo(step - 1)}
>
<ArrowLeftIcon aria-hidden="true" data-icon="inline-start" />
Back
</Button>
) : null}
<div className="ml-auto flex items-center gap-2">
{step === 2 ? (
<Button
type="button"
variant="ghost"
onClick={() => {
setDraft((current) => ({ ...current, invites: [] }));
setDone(true);
}}
>
Skip
</Button>
) : null}
<Button type="submit">
{step === 2 ? "Send invites" : "Continue"}
</Button>
</div>
</div>
</Form>
</div>
);
}
npx shadcn@latest add @sevenui/component/form-14pnpm dlx shadcn@latest add @sevenui/component/form-14yarn dlx shadcn@latest add @sevenui/component/form-14bunx --bun shadcn@latest add @sevenui/component/form-14