feat: add command and combobox components
This commit is contained in:
@@ -0,0 +1,163 @@
|
||||
import {
|
||||
Button,
|
||||
Combobox,
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage
|
||||
} from "@ai-ui/ui";
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { useState } from "react";
|
||||
|
||||
const teamItems = [
|
||||
{
|
||||
value: "design",
|
||||
label: "Design",
|
||||
group: "Primary teams",
|
||||
description: "Owns interface quality and review workflows.",
|
||||
keywords: ["ux", "ui", "visual"]
|
||||
},
|
||||
{
|
||||
value: "engineering",
|
||||
label: "Engineering",
|
||||
group: "Primary teams",
|
||||
description: "Implements and verifies rollout mechanics.",
|
||||
keywords: ["dev", "build", "api"]
|
||||
},
|
||||
{
|
||||
value: "legal",
|
||||
label: "Legal",
|
||||
group: "Specialist teams",
|
||||
description: "Checks policy, compliance, and contractual risk.",
|
||||
keywords: ["policy", "compliance"]
|
||||
},
|
||||
{
|
||||
value: "ops",
|
||||
label: "Operations",
|
||||
group: "Specialist teams",
|
||||
description: "Coordinates timing, communications, and monitoring.",
|
||||
keywords: ["launch", "support"]
|
||||
}
|
||||
] as const;
|
||||
|
||||
function ControlledDemo() {
|
||||
const [value, setValue] = useState("design");
|
||||
|
||||
return (
|
||||
<div className="grid w-[380px] gap-4">
|
||||
<Combobox
|
||||
aria-label="Routing team"
|
||||
items={[...teamItems]}
|
||||
onValueChange={setValue}
|
||||
searchPlaceholder="Search teams"
|
||||
value={value}
|
||||
/>
|
||||
<div className="rounded-[var(--radius-md)] border border-[var(--color-border)] bg-[var(--color-card)] px-4 py-3 text-sm text-[var(--color-muted-foreground)] shadow-[var(--shadow-xs)]">
|
||||
Current value: <span className="font-medium text-[var(--color-foreground)]">{value}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function LaunchRoutingForm() {
|
||||
const [submitted, setSubmitted] = useState<Record<string, string> | null>(null);
|
||||
const form = useForm<{ team: string }>({
|
||||
defaultValues: {
|
||||
team: ""
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form
|
||||
className="grid w-[560px] 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 routing
|
||||
</h2>
|
||||
<p className="m-0 text-sm leading-6 text-[var(--color-muted-foreground)]">
|
||||
Combobox can live inside <code>FormControl</code> and surface RHF validation state.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Controller
|
||||
control={form.control}
|
||||
name="team"
|
||||
rules={{
|
||||
required: "Choose a routing team before submitting."
|
||||
}}
|
||||
render={({ field }) => (
|
||||
<FormItem name="team">
|
||||
<FormLabel>Routing team</FormLabel>
|
||||
<FormControl>
|
||||
<Combobox
|
||||
aria-label="Routing team"
|
||||
items={[...teamItems]}
|
||||
onValueChange={field.onChange}
|
||||
searchPlaceholder="Search teams"
|
||||
value={field.value}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
The chosen team becomes the primary owner for approvals and notifications.
|
||||
</FormDescription>
|
||||
<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 routing</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/Combobox",
|
||||
component: ControlledDemo,
|
||||
parameters: {
|
||||
layout: "centered"
|
||||
},
|
||||
tags: ["autodocs"]
|
||||
} satisfies Meta<typeof ControlledDemo>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Controlled: Story = {
|
||||
render: () => <ControlledDemo />
|
||||
};
|
||||
|
||||
export const WithForm: Story = {
|
||||
render: () => <LaunchRoutingForm />
|
||||
};
|
||||
@@ -0,0 +1,109 @@
|
||||
import { useState } from "react";
|
||||
|
||||
import {
|
||||
Button,
|
||||
Command,
|
||||
CommandDialog,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
CommandSeparator,
|
||||
CommandShortcut
|
||||
} from "@ai-ui/ui";
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
|
||||
const inlineItems = [
|
||||
{
|
||||
heading: "Navigation",
|
||||
items: [
|
||||
{ label: "Open docs", shortcut: "G D", value: "open-docs" },
|
||||
{ label: "Go to releases", shortcut: "G R", value: "go-releases" }
|
||||
]
|
||||
},
|
||||
{
|
||||
heading: "Actions",
|
||||
items: [
|
||||
{ label: "Publish update", shortcut: "P U", value: "publish-update" },
|
||||
{ label: "Invite reviewer", shortcut: "I R", value: "invite-reviewer" }
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
function InlineCommandShowcase() {
|
||||
return (
|
||||
<Command className="w-[520px]">
|
||||
<CommandInput placeholder="Search across the workspace" />
|
||||
<CommandList>
|
||||
<CommandEmpty>No matching actions.</CommandEmpty>
|
||||
{inlineItems.map((group, index) => (
|
||||
<div key={group.heading}>
|
||||
<CommandGroup heading={group.heading}>
|
||||
{group.items.map((item) => (
|
||||
<CommandItem key={item.value} value={item.value}>
|
||||
{item.label}
|
||||
<CommandShortcut>{item.shortcut}</CommandShortcut>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
{index < inlineItems.length - 1 ? <CommandSeparator /> : null}
|
||||
</div>
|
||||
))}
|
||||
</CommandList>
|
||||
</Command>
|
||||
);
|
||||
}
|
||||
|
||||
function DialogCommandShowcase() {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="flex justify-center">
|
||||
<Button onClick={() => setOpen(true)}>Open command palette</Button>
|
||||
<CommandDialog onOpenChange={setOpen} open={open}>
|
||||
<CommandInput placeholder="Type a command or search" />
|
||||
<CommandList>
|
||||
<CommandEmpty>No commands available.</CommandEmpty>
|
||||
<CommandGroup heading="Suggestions">
|
||||
<CommandItem value="launch-checklist">Launch checklist</CommandItem>
|
||||
<CommandItem value="rollout-audit">Rollout audit</CommandItem>
|
||||
<CommandItem value="brand-theme">Brand theme tokens</CommandItem>
|
||||
</CommandGroup>
|
||||
<CommandSeparator />
|
||||
<CommandGroup heading="People">
|
||||
<CommandItem value="jordan-lee">
|
||||
Jordan Lee
|
||||
<CommandShortcut>@</CommandShortcut>
|
||||
</CommandItem>
|
||||
<CommandItem value="avery-carter">
|
||||
Avery Carter
|
||||
<CommandShortcut>@</CommandShortcut>
|
||||
</CommandItem>
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</CommandDialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const meta = {
|
||||
title: "Components/Command",
|
||||
component: InlineCommandShowcase,
|
||||
parameters: {
|
||||
layout: "centered"
|
||||
},
|
||||
tags: ["autodocs"]
|
||||
} satisfies Meta<typeof InlineCommandShowcase>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Playground: Story = {
|
||||
render: () => <InlineCommandShowcase />
|
||||
};
|
||||
|
||||
export const DialogPalette: Story = {
|
||||
render: () => <DialogCommandShowcase />
|
||||
};
|
||||
Reference in New Issue
Block a user