2.18.0: New Invitation flow, refactored Database Webhooks, new ShadCN UI Components (#384)

* Streamlined invitations flow
* Removed web hooks in favor of handling logic directly in server actions
* Added new Shadcn UI Components
This commit is contained in:
Giancarlo Buomprisco
2025-10-05 17:54:16 +08:00
committed by GitHub
parent 195cf41680
commit 2e20d3e76f
60 changed files with 3760 additions and 1009 deletions

View File

@@ -0,0 +1,369 @@
'use client';
import { useMemo } from 'react';
import { Filter, Plus, Settings } from 'lucide-react';
import { Button } from '@kit/ui/button';
import {
ButtonGroup,
ButtonGroupSeparator,
ButtonGroupText,
} from '@kit/ui/button-group';
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@kit/ui/card';
import { Label } from '@kit/ui/label';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@kit/ui/select';
import { Separator } from '@kit/ui/separator';
import { Switch } from '@kit/ui/switch';
import { cn } from '@kit/ui/utils';
import {
formatCodeBlock,
generatePropsString,
useStoryControls,
} from '../lib/story-utils';
import { ComponentStoryLayout } from './story-layout';
import { SimpleStorySelect } from './story-select';
interface ButtonGroupControls {
orientation: 'horizontal' | 'vertical';
size: 'sm' | 'default' | 'lg';
withLabel: boolean;
withSeparator: boolean;
withFilter: boolean;
withPrimary: boolean;
withSelect: boolean;
fullWidth: boolean;
}
const orientationOptions = [
{
value: 'horizontal',
label: 'Horizontal',
description: 'Buttons arranged side-by-side',
},
{
value: 'vertical',
label: 'Vertical',
description: 'Stack buttons vertically',
},
] as const;
const sizeOptions = [
{
value: 'sm',
label: 'Small',
description: 'Compact 36px controls',
},
{
value: 'default',
label: 'Default',
description: 'Standard 40px controls',
},
{
value: 'lg',
label: 'Large',
description: 'Spacious 44px controls',
},
] as const;
export function ButtonGroupStory() {
const { controls, updateControl } = useStoryControls<ButtonGroupControls>({
orientation: 'horizontal',
size: 'sm',
withLabel: false,
withSeparator: false,
withFilter: false,
withPrimary: false,
withSelect: false,
fullWidth: false,
});
const buttonGroupPropsString = useMemo(
() =>
generatePropsString(
{
orientation: controls.orientation,
className: controls.fullWidth ? 'w-full' : undefined,
},
{
orientation: 'horizontal',
className: undefined,
},
),
[controls.fullWidth, controls.orientation],
);
const generatedCode = useMemo(() => {
const separatorOrientation =
controls.orientation === 'vertical' ? 'horizontal' : 'vertical';
const buttonSizeProp =
controls.size === 'default' ? '' : ` size="${controls.size}"`;
const selectTriggerClasses = [
'w-[140px] justify-between',
controls.size === 'sm' ? 'h-9 text-sm' : null,
controls.size === 'lg' ? 'h-11 text-base' : null,
]
.filter(Boolean)
.join(' ');
const labelClasses = [
'min-w-[120px] justify-between',
controls.size === 'sm' ? 'text-sm' : null,
controls.size === 'lg' ? 'text-base' : null,
'gap-2',
]
.filter(Boolean)
.join(' ');
let code = `<ButtonGroup${buttonGroupPropsString}>`;
if (controls.withLabel) {
code += `\n <ButtonGroupText className="${labelClasses}">`;
code += `\n Views`;
code += `\n <Settings className="h-4 w-4" />`;
code += `\n </ButtonGroupText>`;
}
code += `\n <Button variant="outline"${buttonSizeProp}>Overview</Button>`;
code += `\n <Button variant="outline"${buttonSizeProp}>Activity</Button>`;
code += `\n <Button variant="outline"${buttonSizeProp}>Calendar</Button>`;
if (controls.withSeparator) {
code += `\n <ButtonGroupSeparator orientation="${separatorOrientation}" />`;
}
if (controls.withFilter) {
code += `\n <Button variant="ghost" className="gap-2"${buttonSizeProp}>`;
code += `\n <Filter className="h-4 w-4" />`;
code += `\n Filters`;
code += `\n </Button>`;
}
if (controls.withSelect) {
code += `\n <Select defaultValue="all">`;
code += `\n <SelectTrigger data-slot="select-trigger" className="${selectTriggerClasses}">`;
code += `\n <SelectValue placeholder="Segment" />`;
code += `\n </SelectTrigger>`;
code += `\n <SelectContent>`;
code += `\n <SelectItem value="all">All tasks</SelectItem>`;
code += `\n <SelectItem value="mine">Assigned to me</SelectItem>`;
code += `\n <SelectItem value="review">Needs review</SelectItem>`;
code += `\n </SelectContent>`;
code += `\n </Select>`;
}
if (controls.withPrimary) {
code += `\n <Button${buttonSizeProp}>`;
code += `\n <Plus className="mr-2 h-4 w-4" />`;
code += `\n New view`;
code += `\n </Button>`;
}
code += `\n</ButtonGroup>`;
return formatCodeBlock(code, [
"import { Filter, Plus, Settings } from 'lucide-react';",
"import { Button } from '@kit/ui/button';",
"import { ButtonGroup, ButtonGroupSeparator, ButtonGroupText } from '@kit/ui/button-group';",
"import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@kit/ui/select';",
]);
}, [buttonGroupPropsString, controls]);
const separatorOrientation =
controls.orientation === 'vertical' ? 'horizontal' : 'vertical';
const preview = (
<div
className={cn(
'bg-background flex min-h-[220px] items-center justify-center rounded-xl border p-6',
controls.fullWidth && 'items-start',
)}
>
<ButtonGroup
orientation={controls.orientation}
className={cn(controls.fullWidth && 'w-full justify-between')}
>
{controls.withLabel && (
<ButtonGroupText
className={cn(
'min-w-[120px] justify-between gap-2',
controls.size === 'sm' && 'text-sm',
controls.size === 'lg' && 'text-base',
)}
>
<span>Views</span>
<Settings className="h-4 w-4" />
</ButtonGroupText>
)}
<Button variant="outline" size={controls.size}>
Overview
</Button>
<Button variant="outline" size={controls.size}>
Activity
</Button>
<Button variant="outline" size={controls.size}>
Calendar
</Button>
{controls.withSeparator && (
<ButtonGroupSeparator orientation={separatorOrientation} />
)}
{controls.withFilter && (
<Button variant="ghost" size={controls.size} className="gap-2">
<Filter className="h-4 w-4" />
Filters
</Button>
)}
{controls.withPrimary && (
<Button size={controls.size} className="gap-2">
<Plus className="h-4 w-4" />
New view
</Button>
)}
</ButtonGroup>
</div>
);
const controlsPanel = (
<>
<div className="space-y-2">
<Label htmlFor="orientation">Orientation</Label>
<SimpleStorySelect
value={controls.orientation}
onValueChange={(value) => updateControl('orientation', value)}
options={orientationOptions}
/>
</div>
<div className="space-y-2">
<Label htmlFor="size">Size</Label>
<SimpleStorySelect
value={controls.size}
onValueChange={(value) => updateControl('size', value)}
options={sizeOptions}
/>
</div>
<Separator />
<div className="flex items-center justify-between gap-3">
<Label htmlFor="withLabel" className="text-sm font-medium">
Show label
</Label>
<Switch
id="withLabel"
checked={controls.withLabel}
onCheckedChange={(checked) => updateControl('withLabel', checked)}
/>
</div>
<div className="flex items-center justify-between gap-3">
<Label htmlFor="withFilter" className="text-sm font-medium">
Show filter button
</Label>
<Switch
id="withFilter"
checked={controls.withFilter}
onCheckedChange={(checked) => updateControl('withFilter', checked)}
/>
</div>
<div className="flex items-center justify-between gap-3">
<Label htmlFor="withPrimary" className="text-sm font-medium">
Show primary action
</Label>
<Switch
id="withPrimary"
checked={controls.withPrimary}
onCheckedChange={(checked) => updateControl('withPrimary', checked)}
/>
</div>
</>
);
const examples = (
<Card>
<CardHeader>
<CardTitle>Button group sizes</CardTitle>
<CardDescription>
Mirror the documentation examples with small, default, and large
buttons.
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<ButtonGroup>
<Button variant="outline" size="sm">
Small
</Button>
<Button variant="outline" size="sm">
Button
</Button>
<Button variant="outline" size="sm">
Group
</Button>
<Button variant="outline" size="sm">
<Plus className="h-4 w-4" />
</Button>
</ButtonGroup>
<ButtonGroup>
<Button variant="outline">Default</Button>
<Button variant="outline">Button</Button>
<Button variant="outline">Group</Button>
<Button variant="outline">
<Plus className="h-4 w-4" />
</Button>
</ButtonGroup>
<ButtonGroup>
<Button variant="outline" size="lg">
Large
</Button>
<Button variant="outline" size="lg">
Button
</Button>
<Button variant="outline" size="lg">
Group
</Button>
<Button variant="outline" size="lg">
<Plus className="h-4 w-4" />
</Button>
</ButtonGroup>
</CardContent>
</Card>
);
return (
<ComponentStoryLayout
preview={preview}
controls={controlsPanel}
generatedCode={generatedCode}
examples={examples}
previewTitle="Interactive button group"
previewDescription="Coordinate related actions, filters, and dropdowns in a single control cluster."
controlsTitle="Configuration"
controlsDescription="Toggle layout options and auxiliary actions to compose the group."
codeTitle="Usage"
codeDescription="Copy the configuration that matches your toolbar requirements."
/>
);
}
export default ButtonGroupStory;

View File

@@ -19,7 +19,7 @@ export function DocsContent({ selectedComponent }: DocsContentProps) {
}
return (
<div className="flex-1 overflow-auto">
<div className="flex-1 overflow-y-auto">
<Suspense fallback={<LoadingFallback />}>
<div className="p-4">
<component.component />

View File

@@ -11,6 +11,7 @@ import {
CardTitle,
} from '@kit/ui/card';
import {
EmptyMedia,
EmptyState,
EmptyStateButton,
EmptyStateHeading,
@@ -290,7 +291,9 @@ export function EmptyStateStory() {
</CardHeader>
<CardContent className="grid gap-4 md:grid-cols-2">
<EmptyState className="min-h-[200px]">
<Package className="text-muted-foreground mb-4 h-12 w-12" />
<EmptyMedia variant="icon">
<Package className="text-muted-foreground h-8 w-8" />
</EmptyMedia>
<EmptyStateHeading>No products</EmptyStateHeading>
<EmptyStateText>
Add your first product to start selling.
@@ -299,7 +302,9 @@ export function EmptyStateStory() {
</EmptyState>
<EmptyState className="min-h-[200px]">
<FileText className="text-muted-foreground mb-4 h-12 w-12" />
<EmptyMedia variant="icon">
<FileText className="text-muted-foreground h-8 w-8" />
</EmptyMedia>
<EmptyStateHeading>No documents</EmptyStateHeading>
<EmptyStateText>
Upload or create your first document.

View File

@@ -0,0 +1,468 @@
'use client';
import { useMemo } from 'react';
import { Button } from '@kit/ui/button';
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from '@kit/ui/card';
import {
Field,
FieldContent,
FieldDescription,
FieldError,
FieldGroup,
FieldLabel,
FieldLegend,
FieldSeparator,
FieldSet,
FieldTitle,
} from '@kit/ui/field';
import { Input } from '@kit/ui/input';
import { Label } from '@kit/ui/label';
import { RadioGroup, RadioGroupItem } from '@kit/ui/radio-group';
import { Separator } from '@kit/ui/separator';
import { Switch } from '@kit/ui/switch';
import { Textarea } from '@kit/ui/textarea';
import {
formatCodeBlock,
generatePropsString,
useStoryControls,
} from '../lib/story-utils';
import { ComponentStoryLayout } from './story-layout';
import { SimpleStorySelect } from './story-select';
interface FieldControls {
orientation: 'vertical' | 'horizontal';
showDescriptions: boolean;
showErrors: boolean;
useLegend: boolean;
includeSeparator: boolean;
}
const orientationOptions = [
{
value: 'vertical',
label: 'Vertical',
description: 'Label and controls stacked',
},
{
value: 'horizontal',
label: 'Horizontal',
description: 'Label inline with controls',
},
] as const;
export function FieldStory() {
const { controls, updateControl } = useStoryControls<FieldControls>({
orientation: 'horizontal',
showDescriptions: true,
showErrors: false,
useLegend: true,
includeSeparator: true,
});
const fieldPropsString = useMemo(
() =>
generatePropsString(
{
orientation: controls.orientation,
},
{ orientation: 'vertical' },
),
[controls.orientation],
);
const generatedCode = useMemo(() => {
const separatorLine = controls.includeSeparator
? '\n <FieldSeparator className="mt-4">Preferences</FieldSeparator>'
: '';
const descriptionLine = controls.showDescriptions
? '\n <FieldDescription>The name that will appear on invoices.</FieldDescription>'
: '';
const errorLine = controls.showErrors
? '\n <FieldError errors={[{ message: "Please provide your full name." }]} />'
: '';
const code = `<FieldSet className="mx-auto w-full max-w-3xl space-y-8">
${controls.useLegend ? '<FieldLegend className="text-base font-semibold">Account Details</FieldLegend>\n ' : ''}<FieldGroup className="space-y-6">
<Field orientation="horizontal" data-invalid={${controls.showErrors}} className="items-start gap-4 sm:gap-6">
<FieldLabel htmlFor="full-name" className="text-sm font-medium sm:w-48">Full name</FieldLabel>
<FieldContent className="flex w-full max-w-xl flex-col gap-2">
<Input id="full-name" placeholder="Ada Lovelace" aria-invalid={${controls.showErrors}} />${descriptionLine}${errorLine}
</FieldContent>
</Field>${separatorLine}
<Field orientation="horizontal" className="items-start gap-4 sm:gap-6">
<FieldLabel htmlFor="email" className="text-sm font-medium sm:w-48">Email address</FieldLabel>
<FieldContent className="flex w-full max-w-xl flex-col gap-2">
<Input id="email" type="email" placeholder="ada@lovelace.dev" />${
controls.showDescriptions
? '\n <FieldDescription>Used for sign-in and notifications.</FieldDescription>'
: ''
}
</FieldContent>
</Field>
<Field orientation="horizontal" className="items-start gap-4 sm:gap-6">
<FieldLabel htmlFor="bio" className="text-sm font-medium sm:w-48">Bio</FieldLabel>
<FieldContent className="flex w-full max-w-xl flex-col gap-2">
<Textarea id="bio" rows={4} placeholder="Tell us about your work." />${
controls.showDescriptions
? '\n <FieldDescription>Supports Markdown formatting.</FieldDescription>'
: ''
}
</FieldContent>
</Field>
<Field orientation="horizontal" className="items-start gap-4 sm:gap-6">
<FieldLabel className="text-sm font-medium sm:w-48">
<FieldTitle>Notifications</FieldTitle>
</FieldLabel>
<FieldContent className="flex w-full max-w-xl flex-col gap-2">
<Switch id="notifications" defaultChecked />\n ${
controls.showDescriptions
? '\n <FieldDescription>Receive updates about comments and mentions.</FieldDescription>'
: ''
}
</FieldContent>
</Field>
<Field orientation="horizontal" className="items-start gap-4 sm:gap-6">
<FieldLabel className="text-sm font-medium sm:w-48">
<FieldTitle>Preferred contact</FieldTitle>
</FieldLabel>
<FieldContent className="flex w-full max-w-xl flex-col gap-3">
<RadioGroup defaultValue="email" className="grid gap-3 sm:grid-cols-2">
<div className="flex items-center gap-2">
<RadioGroupItem value="email" id="contact-email" />
<Label htmlFor="contact-email">Email</Label>
</div>
<div className="flex items-center gap-2">
<RadioGroupItem value="sms" id="contact-sms" />
<Label htmlFor="contact-sms">SMS</Label>
</div>
</RadioGroup>${
controls.showDescriptions
? '\n <FieldDescription>Select how we should reach out for account activity.</FieldDescription>'
: ''
}
</FieldContent>
</Field>
</FieldGroup>
</FieldSet>`;
return formatCodeBlock(code, [
"import { Button } from '@kit/ui/button';",
`import {
Field,
FieldContent,
FieldDescription,
FieldError,
FieldGroup,
FieldLabel,
FieldLegend,
FieldSeparator,
FieldSet,
FieldTitle
} from '@kit/ui/field';`,
"import { Input } from '@kit/ui/input';",
"import { Label } from '@kit/ui/label';",
"import { RadioGroup, RadioGroupItem } from '@kit/ui/radio-group';",
"import { Switch } from '@kit/ui/switch';",
"import { Textarea } from '@kit/ui/textarea';",
]);
}, [
controls.includeSeparator,
controls.orientation,
controls.showDescriptions,
controls.showErrors,
controls.useLegend,
fieldPropsString,
]);
const preview = (
<Card>
<CardHeader>
<CardTitle>Profile settings</CardTitle>
<CardDescription>
Organize related form controls with consistent spacing and error
handling.
</CardDescription>
</CardHeader>
<CardContent>
<FieldSet className="mx-auto max-w-3xl space-y-8">
{controls.useLegend && (
<FieldLegend className="text-base font-semibold">
Account details
</FieldLegend>
)}
<FieldGroup className="space-y-6">
<Field
orientation={controls.orientation}
data-invalid={controls.showErrors || undefined}
className="items-start"
>
<FieldLabel
htmlFor="story-full-name"
className="text-sm font-medium sm:w-48"
>
Full name
</FieldLabel>
<FieldContent className="space-y-2 sm:max-w-xl">
<Input
id="story-full-name"
placeholder="Ada Lovelace"
aria-invalid={controls.showErrors}
/>
{controls.showDescriptions && (
<FieldDescription>
The name that will appear on invoices.
</FieldDescription>
)}
{controls.showErrors && (
<FieldError
errors={[{ message: 'Please provide your full name.' }]}
/>
)}
</FieldContent>
</Field>
{controls.includeSeparator && (
<FieldSeparator className="mt-4">Preferences</FieldSeparator>
)}
<Field orientation={controls.orientation} className="items-start">
<FieldLabel
htmlFor="story-email"
className="text-sm font-medium sm:w-48"
>
Email address
</FieldLabel>
<FieldContent className="space-y-2 sm:max-w-xl">
<Input
id="story-email"
type="email"
placeholder="ada@lovelace.dev"
/>
{controls.showDescriptions && (
<FieldDescription>
Used for sign-in and important notifications.
</FieldDescription>
)}
</FieldContent>
</Field>
<Field orientation={controls.orientation} className="items-start">
<FieldLabel
htmlFor="story-bio"
className="text-sm font-medium sm:w-48"
>
Bio
</FieldLabel>
<FieldContent className="space-y-2 sm:max-w-xl">
<Textarea
id="story-bio"
rows={4}
placeholder="Tell us about your work."
/>
{controls.showDescriptions && (
<FieldDescription>
Share a short summary. Markdown is supported.
</FieldDescription>
)}
</FieldContent>
</Field>
<Field orientation={controls.orientation} className="items-start">
<FieldLabel className="text-sm font-medium sm:w-48">
<FieldTitle>Notifications</FieldTitle>
</FieldLabel>
<FieldContent className="space-y-2 sm:max-w-xl">
<Switch id="story-notifications" defaultChecked />
{controls.showDescriptions && (
<FieldDescription>
Receive updates about comments, mentions, and reminders.
</FieldDescription>
)}
</FieldContent>
</Field>
<Field orientation={controls.orientation} className="items-start">
<FieldLabel className="text-sm font-medium sm:w-48">
<FieldTitle>Preferred contact</FieldTitle>
</FieldLabel>
<FieldContent className="space-y-3 sm:max-w-xl">
<RadioGroup
defaultValue="email"
className="grid grid-cols-1 gap-3 sm:grid-cols-2"
>
<Label
htmlFor="story-contact-email"
className="flex items-center gap-2 rounded-md border p-3"
>
<RadioGroupItem value="email" id="story-contact-email" />
Email
</Label>
<Label
htmlFor="story-contact-sms"
className="flex items-center gap-2 rounded-md border p-3"
>
<RadioGroupItem value="sms" id="story-contact-sms" />
SMS
</Label>
</RadioGroup>
{controls.showDescriptions && (
<FieldDescription>
We will use this channel for account and security updates.
</FieldDescription>
)}
</FieldContent>
</Field>
</FieldGroup>
</FieldSet>
</CardContent>
<CardFooter className="flex justify-end gap-2">
<Button variant="outline">Cancel</Button>
<Button>Save changes</Button>
</CardFooter>
</Card>
);
const controlsPanel = (
<>
<div className="space-y-2">
<Label htmlFor="field-orientation">Orientation</Label>
<SimpleStorySelect
value={controls.orientation}
onValueChange={(value) => updateControl('orientation', value)}
options={orientationOptions}
/>
</div>
<Separator />
<div className="flex items-center justify-between gap-3">
<Label htmlFor="legend-toggle" className="text-sm font-medium">
Show legend
</Label>
<Switch
id="legend-toggle"
checked={controls.useLegend}
onCheckedChange={(checked) => updateControl('useLegend', checked)}
/>
</div>
<div className="flex items-center justify-between gap-3">
<Label htmlFor="descriptions-toggle" className="text-sm font-medium">
Show descriptions
</Label>
<Switch
id="descriptions-toggle"
checked={controls.showDescriptions}
onCheckedChange={(checked) =>
updateControl('showDescriptions', checked)
}
/>
</div>
<div className="flex items-center justify-between gap-3">
<Label htmlFor="errors-toggle" className="text-sm font-medium">
Show validation errors
</Label>
<Switch
id="errors-toggle"
checked={controls.showErrors}
onCheckedChange={(checked) => updateControl('showErrors', checked)}
/>
</div>
<div className="flex items-center justify-between gap-3">
<Label htmlFor="separator-toggle" className="text-sm font-medium">
Include separator
</Label>
<Switch
id="separator-toggle"
checked={controls.includeSeparator}
onCheckedChange={(checked) =>
updateControl('includeSeparator', checked)
}
/>
</div>
</>
);
const examples = (
<div className="grid gap-4 md:grid-cols-2">
<Card>
<CardHeader>
<CardTitle>Inline layout</CardTitle>
<CardDescription>
Use the horizontal orientation when labels and inputs share a row.
</CardDescription>
</CardHeader>
<CardContent>
<Field orientation="horizontal">
<FieldLabel htmlFor="inline-name">Full name</FieldLabel>
<FieldContent>
<Input id="inline-name" placeholder="Grace Hopper" />
</FieldContent>
</Field>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Stacked layout</CardTitle>
<CardDescription>
Vertical orientation keeps dense forms readable on small screens.
</CardDescription>
</CardHeader>
<CardContent>
<Field orientation="vertical">
<FieldLabel htmlFor="stacked-bio">Bio</FieldLabel>
<FieldContent>
<Textarea id="stacked-bio" rows={3} />
</FieldContent>
</Field>
</CardContent>
</Card>
</div>
);
return (
<ComponentStoryLayout
preview={preview}
controls={controlsPanel}
generatedCode={generatedCode}
examples={examples}
previewTitle="Field primitives"
previewDescription="Compose accessible field layouts with consistent spacing, descriptions, and error messaging."
controlsTitle="Configuration"
controlsDescription="Switch between orientations and auxiliary helpers to match your form design."
codeTitle="Usage"
codeDescription="Combine the primitives to build structured forms."
/>
);
}
export default FieldStory;

View File

@@ -0,0 +1,394 @@
'use client';
import { useMemo } from 'react';
import { Calendar, Clock, Mail, Search } from 'lucide-react';
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@kit/ui/card';
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupInput,
InputGroupText,
InputGroupTextarea,
} from '@kit/ui/input-group';
import { Kbd, KbdGroup } from '@kit/ui/kbd';
import { Label } from '@kit/ui/label';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@kit/ui/select';
import { Separator } from '@kit/ui/separator';
import { Switch } from '@kit/ui/switch';
import {
formatCodeBlock,
generatePropsString,
useStoryControls,
} from '../lib/story-utils';
import { ComponentStoryLayout } from './story-layout';
import { SimpleStorySelect } from './story-select';
interface InputGroupControls {
prefixAlign: 'inline-start' | 'inline-end' | 'block-start' | 'block-end';
showPrefix: boolean;
showSuffix: boolean;
showKeyboardHint: boolean;
showPrimaryAction: boolean;
useTextarea: boolean;
}
const alignmentOptions = [
{
value: 'inline-start',
label: 'Inline Start',
description: 'Display prefix before the input',
},
{
value: 'inline-end',
label: 'Inline End',
description: 'Display prefix after the input',
},
{
value: 'block-start',
label: 'Block Start',
description: 'Stack prefix above the control',
},
{
value: 'block-end',
label: 'Block End',
description: 'Stack prefix below the control',
},
] as const;
export function InputGroupStory() {
const { controls, updateControl } = useStoryControls<InputGroupControls>({
prefixAlign: 'inline-start',
showPrefix: true,
showSuffix: true,
showKeyboardHint: true,
showPrimaryAction: true,
useTextarea: false,
});
const inputGroupPropsString = useMemo(() => generatePropsString({}, {}), []);
const generatedCode = useMemo(() => {
const lines: string[] = [];
lines.push(`<InputGroup${inputGroupPropsString}>`);
if (controls.showPrefix) {
lines.push(
` <InputGroupAddon align="${controls.prefixAlign}">`,
` <InputGroupText className="gap-2">`,
` <Search className="h-4 w-4" />`,
` Search`,
' </InputGroupText>',
' </InputGroupAddon>',
);
}
if (controls.useTextarea) {
lines.push(
' <InputGroupTextarea rows={3} placeholder="Leave a message" />',
);
} else {
lines.push(
' <InputGroupInput type="search" placeholder="Find tasks, docs, people..." />',
);
}
if (controls.showKeyboardHint) {
lines.push(
' <InputGroupAddon align="inline-end">',
' <KbdGroup>',
' <Kbd>⌘</Kbd>',
' <Kbd>K</Kbd>',
' </KbdGroup>',
' </InputGroupAddon>',
);
}
if (controls.showSuffix) {
lines.push(
' <InputGroupAddon align="inline-end">',
' <InputGroupText>',
' <Clock className="h-4 w-4" />',
' Recent',
' </InputGroupText>',
' </InputGroupAddon>',
);
}
if (controls.showPrimaryAction) {
lines.push(
' <InputGroupAddon align="inline-end">',
' <InputGroupButton size="sm">Search</InputGroupButton>',
' </InputGroupAddon>',
);
}
lines.push('</InputGroup>');
return formatCodeBlock(lines.join('\n'), [
"import { Calendar, Clock, Mail, Search } from 'lucide-react';",
`import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupInput,
InputGroupText,
InputGroupTextarea
} from '@kit/ui/input-group';`,
"import { Kbd, KbdGroup } from '@kit/ui/kbd';",
"import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@kit/ui/select';",
]);
}, [controls, inputGroupPropsString]);
const preview = (
<div className="flex flex-col gap-6">
<InputGroup>
{controls.showPrefix && (
<InputGroupAddon align={controls.prefixAlign}>
<InputGroupText className="gap-2">
<Search className="h-4 w-4" />
Search
</InputGroupText>
</InputGroupAddon>
)}
{controls.useTextarea ? (
<InputGroupTextarea rows={3} placeholder="Leave a message" />
) : (
<InputGroupInput
type="search"
placeholder="Find tasks, docs, people..."
/>
)}
{controls.showKeyboardHint && (
<InputGroupAddon align="inline-end">
<KbdGroup>
<Kbd></Kbd>
<Kbd>K</Kbd>
</KbdGroup>
</InputGroupAddon>
)}
{controls.showSuffix && (
<InputGroupAddon align="inline-end">
<InputGroupText className="gap-2">
<Clock className="h-4 w-4" />
Recent
</InputGroupText>
</InputGroupAddon>
)}
{controls.showPrimaryAction && (
<InputGroupAddon align="inline-end">
<InputGroupButton size="sm">Search</InputGroupButton>
</InputGroupAddon>
)}
</InputGroup>
<InputGroup>
<InputGroupAddon align="inline-start">
<InputGroupText className="gap-2">
<Mail className="h-4 w-4" />
Invite
</InputGroupText>
</InputGroupAddon>
<InputGroupInput type="email" placeholder="Enter teammate email" />
<InputGroupAddon align="inline-end">
<Select defaultValue="editor">
<SelectTrigger
data-slot="input-group-control"
className="hover:bg-muted h-8 border-transparent shadow-none"
>
<SelectValue placeholder="Role" />
</SelectTrigger>
<SelectContent>
<SelectItem value="viewer">Viewer</SelectItem>
<SelectItem value="editor">Editor</SelectItem>
<SelectItem value="admin">Admin</SelectItem>
</SelectContent>
</Select>
</InputGroupAddon>
<InputGroupAddon align="inline-end">
<InputGroupButton size="sm">Send invite</InputGroupButton>
</InputGroupAddon>
</InputGroup>
<InputGroup>
<InputGroupAddon align="block-start">
<InputGroupText className="gap-2">
<Calendar className="h-4 w-4" />
Availability window
</InputGroupText>
</InputGroupAddon>
<InputGroupTextarea
placeholder="Share a short update for the team."
rows={3}
/>
</InputGroup>
</div>
);
const controlsPanel = (
<>
<div className="space-y-2">
<Label htmlFor="prefix-align">Prefix alignment</Label>
<SimpleStorySelect
value={controls.prefixAlign}
onValueChange={(value) => updateControl('prefixAlign', value)}
options={alignmentOptions}
/>
</div>
<Separator />
<div className="flex items-center justify-between gap-3">
<Label htmlFor="showPrefix" className="text-sm font-medium">
Show prefix
</Label>
<Switch
id="showPrefix"
checked={controls.showPrefix}
onCheckedChange={(checked) => updateControl('showPrefix', checked)}
/>
</div>
<div className="flex items-center justify-between gap-3">
<Label htmlFor="showSuffix" className="text-sm font-medium">
Show suffix label
</Label>
<Switch
id="showSuffix"
checked={controls.showSuffix}
onCheckedChange={(checked) => updateControl('showSuffix', checked)}
/>
</div>
<div className="flex items-center justify-between gap-3">
<Label htmlFor="showShortcut" className="text-sm font-medium">
Show keyboard hint
</Label>
<Switch
id="showShortcut"
checked={controls.showKeyboardHint}
onCheckedChange={(checked) =>
updateControl('showKeyboardHint', checked)
}
/>
</div>
<div className="flex items-center justify-between gap-3">
<Label htmlFor="showPrimary" className="text-sm font-medium">
Show primary action
</Label>
<Switch
id="showPrimary"
checked={controls.showPrimaryAction}
onCheckedChange={(checked) =>
updateControl('showPrimaryAction', checked)
}
/>
</div>
<div className="flex items-center justify-between gap-3">
<Label htmlFor="useTextarea" className="text-sm font-medium">
Use textarea control
</Label>
<Switch
id="useTextarea"
checked={controls.useTextarea}
onCheckedChange={(checked) => updateControl('useTextarea', checked)}
/>
</div>
</>
);
const examples = (
<div className="grid gap-4 md:grid-cols-2">
<Card>
<CardHeader>
<CardTitle>Search with hotkey</CardTitle>
<CardDescription>
Combine keyboard shortcuts and actions within the group wrapper.
</CardDescription>
</CardHeader>
<CardContent>
<InputGroup>
<InputGroupAddon align="inline-start">
<InputGroupText className="gap-2">
<Search className="h-4 w-4" />
Quick search
</InputGroupText>
</InputGroupAddon>
<InputGroupInput placeholder="Search knowledge base" />
<InputGroupAddon align="inline-end">
<KbdGroup>
<Kbd></Kbd>
<Kbd>K</Kbd>
</KbdGroup>
</InputGroupAddon>
</InputGroup>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Comment composer</CardTitle>
<CardDescription>
Switch to a textarea while keeping prefixes and suffixes aligned.
</CardDescription>
</CardHeader>
<CardContent>
<InputGroup>
<InputGroupAddon align="block-start">
<InputGroupText>Comment</InputGroupText>
</InputGroupAddon>
<InputGroupTextarea
rows={3}
placeholder="Share an update with the team"
/>
<InputGroupAddon align="inline-end">
<InputGroupButton size="sm">Post</InputGroupButton>
</InputGroupAddon>
</InputGroup>
</CardContent>
</Card>
</div>
);
return (
<ComponentStoryLayout
preview={preview}
controls={controlsPanel}
generatedCode={generatedCode}
examples={examples}
previewTitle="Flexible input groups"
previewDescription="Compose inputs with inline buttons, keyboard hints, and stacked addons."
controlsTitle="Configuration"
controlsDescription="Toggle prefixes, suffixes, and alignment options for the group."
codeTitle="Usage"
codeDescription="Copy the layout that matches your form control requirements."
/>
);
}
export default InputGroupStory;

View File

@@ -0,0 +1,412 @@
'use client';
import { useMemo } from 'react';
import { ArrowUpRight, Calendar, CheckCircle2, Clock3 } from 'lucide-react';
import { Avatar, AvatarFallback, AvatarImage } from '@kit/ui/avatar';
import { Badge } from '@kit/ui/badge';
import { Button } from '@kit/ui/button';
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@kit/ui/card';
import {
Item,
ItemActions,
ItemContent,
ItemDescription,
ItemFooter,
ItemGroup,
ItemHeader,
ItemMedia,
ItemSeparator,
ItemTitle,
} from '@kit/ui/item';
import { Label } from '@kit/ui/label';
import { Separator } from '@kit/ui/separator';
import { Switch } from '@kit/ui/switch';
import { cn } from '@kit/ui/utils';
import {
formatCodeBlock,
generatePropsString,
useStoryControls,
} from '../lib/story-utils';
import { ComponentStoryLayout } from './story-layout';
import { SimpleStorySelect } from './story-select';
interface ItemControls {
variant: 'default' | 'outline' | 'muted';
size: 'default' | 'sm';
showMedia: boolean;
showDescription: boolean;
showActions: boolean;
showFooter: boolean;
showSeparator: boolean;
}
const variantOptions = [
{ value: 'default', label: 'Default', description: 'Unstyled surface' },
{ value: 'outline', label: 'Outline', description: 'Bordered list item' },
{ value: 'muted', label: 'Muted', description: 'Soft background highlight' },
] as const;
const sizeOptions = [
{ value: 'default', label: 'Default', description: 'Spacious item' },
{ value: 'sm', label: 'Small', description: 'Compact height' },
] as const;
export function ItemStory() {
const { controls, updateControl } = useStoryControls<ItemControls>({
variant: 'default',
size: 'default',
showMedia: true,
showDescription: true,
showActions: true,
showFooter: true,
showSeparator: true,
});
const itemPropsString = useMemo(
() =>
generatePropsString(
{
variant: controls.variant,
size: controls.size,
},
{
variant: 'default',
size: 'default',
},
),
[controls.variant, controls.size],
);
const generatedCode = useMemo(() => {
const lines: string[] = [];
lines.push(`<Item${itemPropsString}>`);
if (controls.showMedia) {
lines.push(
' <ItemMedia variant="icon">',
' <Calendar className="h-4 w-4" />',
' </ItemMedia>',
);
}
lines.push(' <ItemContent>');
lines.push(' <ItemHeader>');
lines.push(' <ItemTitle>Weekly planning</ItemTitle>');
if (controls.showActions) {
lines.push(
' <ItemActions>',
' <Badge variant="secondary">12 tasks</Badge>',
' <Button variant="ghost" size="sm" className="gap-1">',
' View',
' <ArrowUpRight className="h-4 w-4" />',
' </Button>',
' </ItemActions>',
);
}
lines.push(' </ItemHeader>');
if (controls.showDescription) {
lines.push(
' <ItemDescription>Plan upcoming sprints and capture blockers from the team sync.</ItemDescription>',
);
}
if (controls.showFooter) {
lines.push(
' <ItemFooter>',
' <div className="flex items-center gap-2 text-xs text-muted-foreground">',
' <Clock3 className="h-3.5 w-3.5" />',
' Updated 2 hours ago',
' </div>',
' <Badge variant="outline">In progress</Badge>',
' </ItemFooter>',
);
}
lines.push(' </ItemContent>');
lines.push('</Item>');
return formatCodeBlock(lines.join('\n'), [
"import { ArrowUpRight, Calendar, Clock3 } from 'lucide-react';",
"import { Badge } from '@kit/ui/badge';",
"import { Button } from '@kit/ui/button';",
"import { Item, ItemActions, ItemContent, ItemDescription, ItemFooter, ItemMedia, ItemTitle } from '@kit/ui/item';",
]);
}, [controls, itemPropsString]);
const preview = (
<ItemGroup className="gap-3">
<Item
variant={controls.variant}
size={controls.size}
className={cn('w-full')}
>
{controls.showMedia && (
<ItemMedia variant="icon">
<Calendar className="h-4 w-4" />
</ItemMedia>
)}
<ItemContent>
<ItemHeader>
<ItemTitle className="gap-2">
Weekly planning
<Badge variant="secondary">Sprint</Badge>
</ItemTitle>
{controls.showActions && (
<ItemActions className="gap-2">
<Badge variant="outline">12 tasks</Badge>
<Button variant="ghost" size="sm" className="gap-1">
View
<ArrowUpRight className="h-4 w-4" />
</Button>
</ItemActions>
)}
</ItemHeader>
{controls.showDescription && (
<ItemDescription>
Plan upcoming sprints and capture blockers from the team sync.
</ItemDescription>
)}
{controls.showFooter && (
<ItemFooter>
<div className="text-muted-foreground flex items-center gap-2 text-xs">
<Clock3 className="h-3.5 w-3.5" />
Updated 2 hours ago
</div>
<Badge variant="outline">In progress</Badge>
</ItemFooter>
)}
</ItemContent>
</Item>
{controls.showSeparator && <ItemSeparator />}
<Item variant="muted" size="sm">
<ItemMedia>
<Avatar className="h-9 w-9">
<AvatarImage src="https://daily.jstor.org/wp-content/uploads/2019/10/ada_lovelace_pioneer_1050x700.jpg" />
<AvatarFallback>AL</AvatarFallback>
</Avatar>
</ItemMedia>
<ItemContent>
<ItemHeader>
<ItemTitle className="gap-2">
Ada Lovelace
<Badge variant="outline">Owner</Badge>
</ItemTitle>
{controls.showActions && (
<ItemActions className="gap-2">
<Button size="sm" variant="ghost" className="gap-1">
Message
<ArrowUpRight className="h-4 w-4" />
</Button>
</ItemActions>
)}
</ItemHeader>
{controls.showDescription && (
<ItemDescription>
Building the analytics module. Next milestone due Friday.
</ItemDescription>
)}
{controls.showFooter && (
<ItemFooter>
<Badge variant="secondary" className="gap-1">
<CheckCircle2 className="h-3.5 w-3.5" />
Active
</Badge>
<span className="text-muted-foreground text-xs">Joined 2023</span>
</ItemFooter>
)}
</ItemContent>
</Item>
</ItemGroup>
);
const controlsPanel = (
<>
<div className="space-y-2">
<Label htmlFor="item-variant">Variant</Label>
<SimpleStorySelect
value={controls.variant}
onValueChange={(value) => updateControl('variant', value)}
options={variantOptions}
/>
</div>
<div className="space-y-2">
<Label htmlFor="item-size">Size</Label>
<SimpleStorySelect
value={controls.size}
onValueChange={(value) => updateControl('size', value)}
options={sizeOptions}
/>
</div>
<Separator />
<div className="flex items-center justify-between gap-3">
<Label htmlFor="show-media" className="text-sm font-medium">
Show media
</Label>
<Switch
id="show-media"
checked={controls.showMedia}
onCheckedChange={(checked) => updateControl('showMedia', checked)}
/>
</div>
<div className="flex items-center justify-between gap-3">
<Label htmlFor="show-description" className="text-sm font-medium">
Show description
</Label>
<Switch
id="show-description"
checked={controls.showDescription}
onCheckedChange={(checked) =>
updateControl('showDescription', checked)
}
/>
</div>
<div className="flex items-center justify-between gap-3">
<Label htmlFor="show-actions" className="text-sm font-medium">
Show actions
</Label>
<Switch
id="show-actions"
checked={controls.showActions}
onCheckedChange={(checked) => updateControl('showActions', checked)}
/>
</div>
<div className="flex items-center justify-between gap-3">
<Label htmlFor="show-footer" className="text-sm font-medium">
Show footer
</Label>
<Switch
id="show-footer"
checked={controls.showFooter}
onCheckedChange={(checked) => updateControl('showFooter', checked)}
/>
</div>
<div className="flex items-center justify-between gap-3">
<Label htmlFor="show-separator" className="text-sm font-medium">
Show separator
</Label>
<Switch
id="show-separator"
checked={controls.showSeparator}
onCheckedChange={(checked) => updateControl('showSeparator', checked)}
/>
</div>
</>
);
const examples = (
<div className="grid gap-4 md:grid-cols-2">
<Card>
<CardHeader>
<CardTitle>Checklist</CardTitle>
<CardDescription>
Stack compact items for task summaries or changelog entries.
</CardDescription>
</CardHeader>
<CardContent className="space-y-3">
<ItemGroup className="gap-y-4">
<Item size="sm" variant="outline">
<ItemContent>
<ItemHeader>
<ItemTitle className="gap-2">
Deployment checklist
<Badge variant="secondary">Today</Badge>
</ItemTitle>
</ItemHeader>
<ItemDescription>
Review release notes, QA smoke tests, and notify support.
</ItemDescription>
</ItemContent>
</Item>
<ItemSeparator />
<Item size="sm" variant="outline">
<ItemContent>
<ItemTitle>QA sign-off</ItemTitle>
<ItemFooter>
<Badge variant="outline">Pending</Badge>
<span className="text-muted-foreground text-xs">Due 5pm</span>
</ItemFooter>
</ItemContent>
</Item>
</ItemGroup>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Directory</CardTitle>
<CardDescription>
Combine avatar media with actions to build list views.
</CardDescription>
</CardHeader>
<CardContent>
<ItemGroup className="gap-3">
<Item variant="muted" size="sm">
<ItemMedia>
<Avatar className="h-9 w-9">
<AvatarFallback>GH</AvatarFallback>
</Avatar>
</ItemMedia>
<ItemContent>
<ItemHeader>
<ItemTitle>Grace Hopper</ItemTitle>
<ItemActions>
<Button variant="ghost" size="sm" className="gap-1">
Profile
<ArrowUpRight className="h-4 w-4" />
</Button>
</ItemActions>
</ItemHeader>
<ItemDescription>Staff engineer · Platform</ItemDescription>
</ItemContent>
</Item>
</ItemGroup>
</CardContent>
</Card>
</div>
);
return (
<ComponentStoryLayout
preview={preview}
controls={controlsPanel}
generatedCode={generatedCode}
examples={examples}
previewTitle="Composable list items"
previewDescription="Mix media, headers, and actions to create rich list presentations."
controlsTitle="Configuration"
controlsDescription="Toggle structural primitives to match your layout requirements."
codeTitle="Usage"
codeDescription="Start from a base item and add media, actions, or metadata as needed."
/>
);
}
export default ItemStory;

View File

@@ -0,0 +1,277 @@
'use client';
import { useMemo } from 'react';
import { Command, Search } from 'lucide-react';
import { Button } from '@kit/ui/button';
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@kit/ui/card';
import { Input } from '@kit/ui/input';
import { Kbd, KbdGroup } from '@kit/ui/kbd';
import { Label } from '@kit/ui/label';
import { Separator } from '@kit/ui/separator';
import { Switch } from '@kit/ui/switch';
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from '@kit/ui/tooltip';
import { formatCodeBlock, useStoryControls } from '../lib/story-utils';
import { ComponentStoryLayout } from './story-layout';
import { SimpleStorySelect } from './story-select';
interface KbdControls {
preset: 'command-k' | 'shift-option-s' | 'control-shift-p' | 'custom';
showTooltip: boolean;
customShortcut: string;
}
const presetOptions = [
{
value: 'command-k',
label: 'Command + K',
description: 'Open global command palette',
},
{
value: 'shift-option-s',
label: 'Shift + Option + S',
description: 'Capture screenshot or share',
},
{
value: 'control-shift-p',
label: 'Ctrl + Shift + P',
description: 'Trigger quick action menu',
},
{
value: 'custom',
label: 'Custom',
description: 'Provide your own keys',
},
] as const;
function resolveKeys(preset: KbdControls['preset'], custom: string) {
if (preset === 'custom') {
return custom
.split(/\+|\s+/)
.map((key) => key.trim())
.filter(Boolean);
}
if (preset === 'command-k') {
return ['⌘', 'K'];
}
if (preset === 'shift-option-s') {
return ['⇧ Shift', '⌥ Option', 'S'];
}
return ['Ctrl', 'Shift', 'P'];
}
export function KbdStory() {
const { controls, updateControl } = useStoryControls<KbdControls>({
preset: 'command-k',
showTooltip: true,
customShortcut: 'Ctrl+Shift+P',
});
const keys = useMemo(
() => resolveKeys(controls.preset, controls.customShortcut),
[controls.customShortcut, controls.preset],
);
const generatedCode = useMemo(() => {
const groupLines: string[] = [];
groupLines.push('<KbdGroup>');
keys.forEach((key) => {
groupLines.push(` <Kbd>${key}</Kbd>`);
});
groupLines.push('</KbdGroup>');
let snippet = groupLines.join('\n');
if (controls.showTooltip) {
snippet = `<TooltipProvider>\n <Tooltip>\n <TooltipTrigger asChild>\n <Button variant="outline">Command palette</Button>\n </TooltipTrigger>\n <TooltipContent className="flex items-center gap-2">\n <span>Press</span>\n ${groupLines.join('\n ')}\n </TooltipContent>\n </Tooltip>\n</TooltipProvider>`;
}
return formatCodeBlock(snippet, [
"import { Button } from '@kit/ui/button';",
"import { Kbd, KbdGroup } from '@kit/ui/kbd';",
"import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@kit/ui/tooltip';",
]);
}, [controls.showTooltip, keys]);
const preview = (
<div className="flex flex-col gap-6">
<div className="flex items-center justify-center gap-4">
{controls.showTooltip ? (
<TooltipProvider delayDuration={200}>
<Tooltip>
<TooltipTrigger asChild>
<Button variant="outline" className="gap-2">
<Command className="h-4 w-4" />
Command palette
</Button>
</TooltipTrigger>
<TooltipContent className="flex items-center gap-2">
<span>Press</span>
<KbdGroup>
{keys.map((key) => (
<Kbd key={key}>{key}</Kbd>
))}
</KbdGroup>
</TooltipContent>
</Tooltip>
</TooltipProvider>
) : (
<KbdGroup>
{keys.map((key) => (
<Kbd key={key}>{key}</Kbd>
))}
</KbdGroup>
)}
</div>
<div className="bg-muted/30 rounded-xl border p-6">
<div className="flex flex-col gap-3">
<div className="text-sm font-medium">Keyboard shortcut hint</div>
<div className="text-muted-foreground text-sm">
Use the keyboard primitives to surface power-user workflows in
menus, tooltips, or helper text.
</div>
<div className="flex flex-wrap items-center gap-2">
<SpanShortcut keys={keys} />
</div>
</div>
</div>
</div>
);
const controlsPanel = (
<>
<div className="space-y-2">
<Label htmlFor="preset">Preset</Label>
<SimpleStorySelect
value={controls.preset}
onValueChange={(value) => updateControl('preset', value)}
options={presetOptions}
/>
</div>
{controls.preset === 'custom' && (
<div className="space-y-2">
<Label htmlFor="custom-shortcut">Custom keys</Label>
<Input
id="custom-shortcut"
value={controls.customShortcut}
onChange={(event) =>
updateControl('customShortcut', event.target.value)
}
placeholder="Ctrl+Alt+Delete"
/>
</div>
)}
<Separator />
<div className="flex items-center justify-between gap-3">
<Label htmlFor="show-tooltip" className="text-sm font-medium">
Show tooltip usage
</Label>
<Switch
id="show-tooltip"
checked={controls.showTooltip}
onCheckedChange={(checked) => updateControl('showTooltip', checked)}
/>
</div>
</>
);
const examples = (
<div className="grid gap-4 md:grid-cols-2">
<Card>
<CardHeader>
<CardTitle>Shortcut legend</CardTitle>
<CardDescription>
Combine keyboard hints with descriptions for quick reference.
</CardDescription>
</CardHeader>
<CardContent className="space-y-3">
<div className="flex items-center justify-between rounded-md border p-3">
<span>Search workspace</span>
<KbdGroup>
<Kbd></Kbd>
<Kbd>K</Kbd>
</KbdGroup>
</div>
<div className="flex items-center justify-between rounded-md border p-3">
<span>Toggle spotlight</span>
<KbdGroup>
<Kbd>Ctrl</Kbd>
<Kbd>Space</Kbd>
</KbdGroup>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Empty state helper</CardTitle>
<CardDescription>
Incorporate shortcuts into product education moments.
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="bg-background rounded-md border p-4 text-center">
<Search className="text-muted-foreground mx-auto mb-2 h-5 w-5" />
<div className="text-sm font-medium">No results yet</div>
<div className="text-muted-foreground text-sm">
Try launching the command palette with
</div>
<div className="mt-2 flex justify-center">
<KbdGroup>
<Kbd></Kbd>
<Kbd>K</Kbd>
</KbdGroup>
</div>
</div>
</CardContent>
</Card>
</div>
);
return (
<ComponentStoryLayout
preview={preview}
controls={controlsPanel}
generatedCode={generatedCode}
examples={examples}
previewTitle="Keyboard input primitives"
previewDescription="Display keyboard shortcuts inline, in tooltips, or within helper content."
controlsTitle="Configuration"
controlsDescription="Select a preset or provide a custom shortcut combination."
codeTitle="Usage"
codeDescription="Wrap shortcut keys in the keyboard primitives wherever hints are required."
/>
);
}
function SpanShortcut({ keys }: { keys: string[] }) {
return (
<KbdGroup>
{keys.map((key) => (
<Kbd key={key}>{key}</Kbd>
))}
</KbdGroup>
);
}
export default KbdStory;

View File

@@ -1,5 +1,6 @@
'use client';
import { Button } from '@kit/ui/button';
import {
Card,
CardContent,
@@ -10,7 +11,11 @@ import {
import { Label } from '@kit/ui/label';
import { Spinner } from '@kit/ui/spinner';
import { generatePropsString, useStoryControls } from '../lib/story-utils';
import {
formatCodeBlock,
generatePropsString,
useStoryControls,
} from '../lib/story-utils';
import { ComponentStoryLayout } from './story-layout';
import { SimpleStorySelect } from './story-select';
@@ -44,7 +49,9 @@ export function SpinnerStory() {
{ className: undefined },
);
return `<Spinner${propsString} />`;
return formatCodeBlock(`<Spinner${propsString} />`, [
"import { Spinner } from '@kit/ui/spinner';",
]);
};
const renderPreview = () => (
@@ -105,10 +112,10 @@ export function SpinnerStory() {
<CardContent className="space-y-4">
<div className="space-y-2">
<h4 className="text-sm font-semibold">Button Loading</h4>
<button className="bg-primary text-primary-foreground inline-flex items-center rounded-md px-4 py-2">
<Spinner className="mr-2 h-4 w-4" />
<Button className="gap-2" disabled>
<Spinner className="h-4 w-4" />
Loading...
</button>
</Button>
</div>
<div className="space-y-2">

View File

@@ -77,6 +77,16 @@ const ButtonStory = dynamic(
},
);
const ButtonGroupStory = dynamic(
() =>
import('../components/button-group-story').then((mod) => ({
default: mod.ButtonGroupStory,
})),
{
loading: () => <LoadingFallback />,
},
);
const CardStory = dynamic(
() =>
import('../components/card-story').then((mod) => ({
@@ -287,6 +297,16 @@ const FormStory = dynamic(
},
);
const FieldStory = dynamic(
() =>
import('../components/field-story').then((mod) => ({
default: mod.FieldStory,
})),
{
loading: () => <LoadingFallback />,
},
);
const HeadingStory = dynamic(
() =>
import('../components/heading-story').then((mod) => ({
@@ -297,6 +317,16 @@ const HeadingStory = dynamic(
},
);
const KbdStory = dynamic(
() =>
import('../components/kbd-story').then((mod) => ({
default: mod.KbdStory,
})),
{
loading: () => <LoadingFallback />,
},
);
const InputOTPStory = dynamic(
() =>
import('../components/input-otp-story').then((mod) => ({
@@ -307,6 +337,16 @@ const InputOTPStory = dynamic(
},
);
const InputGroupStory = dynamic(
() =>
import('../components/input-group-story').then((mod) => ({
default: mod.InputGroupStory,
})),
{
loading: () => <LoadingFallback />,
},
);
const RadioGroupStory = dynamic(
() =>
import('../components/radio-group-story').then((mod) => ({
@@ -367,6 +407,16 @@ const SimpleTableStory = dynamic(
},
);
const ItemStory = dynamic(
() =>
import('../components/item-story').then((mod) => ({
default: mod.ItemStory,
})),
{
loading: () => <LoadingFallback />,
},
);
// Component type definition
export interface ComponentInfo {
id: string;
@@ -405,6 +455,34 @@ export const COMPONENTS_REGISTRY: ComponentInfo[] = [
icon: Type,
},
{
id: 'field',
name: 'Field',
category: 'Forms',
subcategory: 'Structure',
description:
'Primitive set for arranging labels, descriptions, and validation messaging.',
status: 'stable',
component: FieldStory,
sourceFile: '@kit/ui/field',
props: ['orientation', 'className', 'data-invalid'],
icon: FileText,
},
{
id: 'input-group',
name: 'Input Group',
category: 'Forms',
subcategory: 'Fields',
description:
'Wrap inputs with inline addons, keyboard hints, and primary actions.',
status: 'stable',
component: InputGroupStory,
sourceFile: '@kit/ui/input-group',
props: ['className', 'role', 'children'],
icon: ToggleLeft,
},
{
id: 'card-button',
name: 'Card Button',
@@ -806,6 +884,20 @@ export const COMPONENTS_REGISTRY: ComponentInfo[] = [
icon: HeadingIcon,
},
{
id: 'kbd',
name: 'Keyboard Key',
category: 'Display',
subcategory: 'Helpers',
description:
'Display keyboard shortcuts inline, in tooltips, or within helper text.',
status: 'stable',
component: KbdStory,
sourceFile: '@kit/ui/kbd',
props: ['className', 'children'],
icon: Command,
},
// Interaction Components
{
id: 'button',
@@ -820,6 +912,20 @@ export const COMPONENTS_REGISTRY: ComponentInfo[] = [
icon: MousePointer,
},
{
id: 'button-group',
name: 'Button Group',
category: 'Interaction',
subcategory: 'Actions',
description:
'Coordinate related buttons, dropdowns, and inputs within a shared toolbar.',
status: 'stable',
component: ButtonGroupStory,
sourceFile: '@kit/ui/button-group',
props: ['orientation', 'className', 'children'],
icon: CircleDot,
},
// Layout Components
{
id: 'card',
@@ -834,6 +940,20 @@ export const COMPONENTS_REGISTRY: ComponentInfo[] = [
icon: Layout,
},
{
id: 'item',
name: 'Item',
category: 'Layout',
subcategory: 'Lists',
description:
'Composable list item primitive with media, actions, and metadata slots.',
status: 'stable',
component: ItemStory,
sourceFile: '@kit/ui/item',
props: ['variant', 'size', 'asChild', 'className'],
icon: Layers,
},
{
id: 'badge',
name: 'Badge',

View File

@@ -18,10 +18,10 @@ async function ComponentDocsPage(props: ComponentDocsPageProps) {
}
return (
<div className="bg-background flex h-screen">
<div className="bg-background flex h-screen overflow-x-hidden">
<DocsSidebar selectedComponent={component} selectedCategory={category} />
<div className="flex flex-1 flex-col">
<div className="flex flex-1 flex-col overflow-x-hidden">
<DocsHeader selectedComponent={component} />
<DocsContent selectedComponent={component} />
</div>