feat: add form primitives and rhf integration
This commit is contained in:
@@ -0,0 +1,187 @@
|
||||
import {
|
||||
Button,
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
Input,
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
Switch,
|
||||
Textarea
|
||||
} from "@ai-ui/ui";
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { useState } from "react";
|
||||
|
||||
type LaunchFormValues = {
|
||||
email: string;
|
||||
notifications: boolean;
|
||||
role: string;
|
||||
summary: string;
|
||||
};
|
||||
|
||||
function LaunchSettingsForm() {
|
||||
const [submitted, setSubmitted] = useState<LaunchFormValues | null>(null);
|
||||
const form = useForm<LaunchFormValues>({
|
||||
defaultValues: {
|
||||
email: "",
|
||||
notifications: true,
|
||||
role: "design",
|
||||
summary: ""
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form
|
||||
className="grid w-[620px] gap-5 rounded-[var(--radius-lg)] border border-[var(--color-border)] bg-[var(--color-card)] p-6 shadow-[var(--shadow-sm)]"
|
||||
noValidate
|
||||
onSubmit={form.handleSubmit((values) => {
|
||||
setSubmitted(values);
|
||||
})}
|
||||
>
|
||||
<div className="space-y-2">
|
||||
<h2 className="m-0 text-xl font-semibold tracking-[var(--tracking-tight)]">
|
||||
Launch settings
|
||||
</h2>
|
||||
<p className="m-0 text-sm leading-6 text-[var(--color-muted-foreground)]">
|
||||
This story uses <code>react-hook-form</code> with the new form primitives.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<FormItem name="email" required>
|
||||
<FormLabel>Email address</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="team@cadence.dev"
|
||||
{...form.register("email", {
|
||||
required: "Email is required.",
|
||||
pattern: {
|
||||
value: /\S+@\S+\.\S+/,
|
||||
message: "Enter a valid email address."
|
||||
}
|
||||
})}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>Release notes and alerts will be sent here.</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
|
||||
<Controller
|
||||
control={form.control}
|
||||
name="role"
|
||||
rules={{
|
||||
required: "Choose a review lane."
|
||||
}}
|
||||
render={({ field }) => (
|
||||
<FormItem name="role">
|
||||
<FormLabel>Review lane</FormLabel>
|
||||
<FormControl>
|
||||
<Select value={field.value} onValueChange={field.onChange}>
|
||||
<SelectTrigger aria-label="Review lane">
|
||||
<SelectValue placeholder="Choose a lane" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="design">Design</SelectItem>
|
||||
<SelectItem value="engineering">Engineering</SelectItem>
|
||||
<SelectItem value="legal">Legal</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
The selected team becomes the primary reviewer for this launch.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormItem name="summary">
|
||||
<FormLabel>Launch summary</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
placeholder="Summarize the rollout, scope, and known risks."
|
||||
{...form.register("summary", {
|
||||
minLength: {
|
||||
value: 24,
|
||||
message: "Summary must be at least 24 characters."
|
||||
}
|
||||
})}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>Shown in the changelog card and Slack digest.</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
|
||||
<Controller
|
||||
control={form.control}
|
||||
name="notifications"
|
||||
render={({ field }) => (
|
||||
<FormItem name="notifications">
|
||||
<div className="flex items-start justify-between gap-4 rounded-[var(--radius-md)] border border-[var(--color-border)] bg-[var(--color-surface)] p-4">
|
||||
<div className="space-y-1">
|
||||
<FormLabel>Weekly digest</FormLabel>
|
||||
<FormDescription>
|
||||
Send a Friday summary of all rollout changes to stakeholders.
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl className="gap-0">
|
||||
<Switch
|
||||
aria-label="Weekly digest"
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</div>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="flex items-center justify-end gap-3">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
form.reset();
|
||||
setSubmitted(null);
|
||||
}}
|
||||
>
|
||||
Reset
|
||||
</Button>
|
||||
<Button type="submit">Save settings</Button>
|
||||
</div>
|
||||
|
||||
<div className="rounded-[var(--radius-md)] border border-[var(--color-border)] bg-[var(--color-background)] p-4">
|
||||
<p className="m-0 text-xs uppercase tracking-[var(--tracking-caps)] text-[var(--color-muted-foreground)]">
|
||||
Submitted payload
|
||||
</p>
|
||||
<pre className="m-0 mt-3 overflow-x-auto text-sm leading-6 text-[var(--color-foreground)]">
|
||||
<code>{submitted ? JSON.stringify(submitted, null, 2) : "Submit the form to inspect values."}</code>
|
||||
</pre>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
const meta = {
|
||||
title: "Components/Form",
|
||||
component: LaunchSettingsForm,
|
||||
parameters: {
|
||||
layout: "centered"
|
||||
},
|
||||
tags: ["autodocs"]
|
||||
} satisfies Meta<typeof LaunchSettingsForm>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const LaunchSettings: Story = {};
|
||||
Reference in New Issue
Block a user