Skip to content

Commit ed688c5

Browse files
authored
feat(react-ui): add Storybook stories for the genui-lib Form component (#766)
Form is a genui-lib (openui-lang) component with no standalone React counterpart, so it had no Storybook coverage: the existing stories under components/*/stories exercise the plain React components, and the lang layer's Form container — validation wiring, primary-button validation, the FormControl input union — was never rendered anywhere in Storybook. Render openui-lang programs through Renderer with openuiLibrary, the same path an LLM response takes. Three stories: a contact form matching the library's prompt example, a play-function story that submits the empty form to show per-field validation errors, and a form covering every FormControl input type (DatePicker, Slider, CheckBoxGroup, RadioGroup). The stories live under a new "GenUI/" Storybook section to keep lang-layer components separate from "Components/", and to give future lang-only stories (e.g. Stack) a home.
1 parent 7901d58 commit ed688c5

1 file changed

Lines changed: 94 additions & 0 deletions

File tree

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import { Renderer } from "@openuidev/react-lang";
2+
import type { Meta, StoryObj } from "@storybook/react";
3+
import { userEvent, within } from "@storybook/test";
4+
import { openuiLibrary } from "../..";
5+
6+
/**
7+
* Form is a genui-lib (openui-lang) component with no standalone React
8+
* counterpart, so these stories render openui-lang programs through the
9+
* Renderer with the openuiLibrary — the same path an LLM response takes.
10+
*/
11+
const OpenUIProgram = ({ response }: { response: string }) => (
12+
<Renderer
13+
response={response}
14+
library={openuiLibrary}
15+
onAction={(event) => console.log("onAction:", event)}
16+
onStateUpdate={(state) => console.log("onStateUpdate:", state)}
17+
/>
18+
);
19+
20+
const meta: Meta<typeof OpenUIProgram> = {
21+
title: "GenUI/Form",
22+
component: OpenUIProgram,
23+
tags: ["!dev", "autodocs"],
24+
parameters: {
25+
layout: "centered",
26+
docs: {
27+
description: {
28+
component:
29+
"The openui-lang `Form(name, buttons, fields)` container. It wires field-level validation rules to its FormControls and validates them when a primary Button fires an action.\n\n```tsx\nimport { Renderer } from '@openuidev/react-lang';\nimport { openuiLibrary } from '@openuidev/react-ui';\n```",
30+
},
31+
},
32+
},
33+
argTypes: {
34+
response: {
35+
control: "text",
36+
description: "openui-lang source code rendered through the openuiLibrary",
37+
table: { category: "Content", type: { summary: "string" } },
38+
},
39+
},
40+
decorators: [
41+
(Story) => (
42+
<div style={{ width: "480px", margin: "2rem" }}>
43+
<Story />
44+
</div>
45+
),
46+
],
47+
};
48+
49+
export default meta;
50+
type Story = StoryObj<typeof OpenUIProgram>;
51+
52+
const contactForm = `root = Stack([title, form])
53+
title = TextContent("Contact Us", "large-heavy")
54+
form = Form("contact", btns, [nameField, emailField, countryField, msgField])
55+
nameField = FormControl("Name", Input("name", "Your name", "text", { required: true, minLength: 2 }))
56+
emailField = FormControl("Email", Input("email", "you@example.com", "email", { required: true, email: true }), "We never share your email.")
57+
countryField = FormControl("Country", Select("country", countryOpts, "Select a country...", { required: true }))
58+
msgField = FormControl("Message", TextArea("message", "Tell us more...", 4, { required: true, minLength: 10 }))
59+
countryOpts = [SelectItem("us", "United States"), SelectItem("uk", "United Kingdom"), SelectItem("de", "Germany")]
60+
btns = Buttons([Button("Submit", Action([@ToAssistant("Submit")]), "primary"), Button("Cancel", Action([@ToAssistant("Cancel")]), "secondary")])`;
61+
62+
export const Default: Story = {
63+
args: { response: contactForm },
64+
};
65+
66+
/**
67+
* Primary buttons validate the enclosing Form before firing their action.
68+
* This story submits the empty form so the per-field error hints are visible.
69+
*/
70+
export const ValidationErrors: Story = {
71+
args: { response: contactForm },
72+
play: async ({ canvasElement }) => {
73+
const canvas = within(canvasElement);
74+
const submit = await canvas.findByRole("button", { name: "Submit" });
75+
await userEvent.click(submit);
76+
},
77+
};
78+
79+
const preferencesForm = `root = Form("preferences", btns, [dateField, budgetField, interestsField, planField])
80+
dateField = FormControl("Travel date", DatePicker("date", "single", { required: true }))
81+
budgetField = FormControl("Budget", Slider("budget", "continuous", 0, 5000, 100, [1500], "USD"))
82+
interestsField = FormControl("Interests", CheckBoxGroup("interests", [beaches, culture, food]))
83+
beaches = CheckBoxItem("Beaches", "Sun, sand, and sea", "beaches")
84+
culture = CheckBoxItem("Culture", "Museums and historic sites", "culture")
85+
food = CheckBoxItem("Food", "Local cuisine and restaurants", "food")
86+
planField = FormControl("Plan", RadioGroup("plan", [basic, premium]))
87+
basic = RadioItem("Basic", "Self-guided itinerary", "basic")
88+
premium = RadioItem("Premium", "Includes a personal guide", "premium")
89+
btns = Buttons([Button("Save preferences", Action([@ToAssistant("Save preferences")]), "primary")])`;
90+
91+
/** Form accepts every FormControl input type, not just text fields. */
92+
export const AllControlTypes: Story = {
93+
args: { response: preferencesForm },
94+
};

0 commit comments

Comments
 (0)