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:
committed by
GitHub
parent
195cf41680
commit
2e20d3e76f
369
apps/dev-tool/app/components/components/button-group-story.tsx
Normal file
369
apps/dev-tool/app/components/components/button-group-story.tsx
Normal 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;
|
||||
@@ -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 />
|
||||
|
||||
@@ -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.
|
||||
|
||||
468
apps/dev-tool/app/components/components/field-story.tsx
Normal file
468
apps/dev-tool/app/components/components/field-story.tsx
Normal 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;
|
||||
394
apps/dev-tool/app/components/components/input-group-story.tsx
Normal file
394
apps/dev-tool/app/components/components/input-group-story.tsx
Normal 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;
|
||||
412
apps/dev-tool/app/components/components/item-story.tsx
Normal file
412
apps/dev-tool/app/components/components/item-story.tsx
Normal 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;
|
||||
277
apps/dev-tool/app/components/components/kbd-story.tsx
Normal file
277
apps/dev-tool/app/components/components/kbd-story.tsx
Normal 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;
|
||||
@@ -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">
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user