Compare commits

...

1 Commits

Author SHA1 Message Date
Dhruwang
2fdaf88572 feat: add v5 button component with Storybook integration
- Add new Button component in packages/surveys/src/components/v5/
- Implement button variants (default, destructive, outline, secondary, ghost, link)
- Add comprehensive Storybook stories with all variants and use cases
- Configure Storybook to support Preact components with React aliasing
- Add Preact-to-React aliases in Storybook vite config for compatibility
2025-11-24 14:05:42 +05:30
3 changed files with 287 additions and 1 deletions

View File

@@ -1,6 +1,7 @@
import type { StorybookConfig } from "@storybook/react-vite";
import { createRequire } from "module";
import { dirname, join } from "path";
import { mergeConfig } from "vite";
const require = createRequire(import.meta.url);
@@ -13,7 +14,11 @@ function getAbsolutePath(value: string): any {
}
const config: StorybookConfig = {
stories: ["../src/**/*.mdx", "../../web/modules/ui/**/stories.@(js|jsx|mjs|ts|tsx)"],
stories: [
"../src/**/*.mdx",
"../../web/modules/ui/**/stories.@(js|jsx|mjs|ts|tsx)",
"../../../packages/surveys/src/components/**/stories.@(js|jsx|mjs|ts|tsx)",
],
addons: [
getAbsolutePath("@storybook/addon-onboarding"),
getAbsolutePath("@storybook/addon-links"),
@@ -25,5 +30,16 @@ const config: StorybookConfig = {
name: getAbsolutePath("@storybook/react-vite"),
options: {},
},
async viteFinal(config) {
return mergeConfig(config, {
resolve: {
alias: {
preact: "react",
"preact/hooks": "react",
"preact/jsx-runtime": "react/jsx-runtime",
},
},
});
},
};
export default config;

View File

@@ -0,0 +1,60 @@
import { type VariantProps, cva } from "class-variance-authority";
import { type JSX } from "preact";
import { cn } from "../../lib/utils";
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
);
interface ButtonProps extends JSX.HTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> {
isLoading?: boolean;
disabled?: boolean;
style?: JSX.CSSProperties;
}
function Button({
className,
variant,
size,
onClick,
children,
isLoading,
disabled,
style,
...props
}: ButtonProps) {
return (
<button
data-slot="button"
className={cn(buttonVariants({ variant, size, className }))}
onClick={onClick}
disabled={isLoading || disabled}
style={style}
{...props}>
{children}
</button>
);
}
export { Button, buttonVariants, type ButtonProps };

View File

@@ -0,0 +1,210 @@
import { Meta, StoryObj } from "@storybook/react-vite";
import { ArrowLeftIcon, ArrowRightIcon } from "lucide-react";
import { type JSX } from "preact";
import { fn } from "storybook/test";
import { Button, type ButtonProps } from "./button";
const meta: Meta<ButtonProps> = {
title: "Surveys/V5/Button",
component: Button as any,
tags: ["autodocs"],
parameters: {
layout: "centered",
controls: { sort: "alpha", exclude: [] },
docs: {
description: {
component:
"The **Button** component for survey interfaces provides clickable actions with multiple variants and sizes. Built with Preact for optimal performance in embedded survey widgets.",
},
},
},
argTypes: {
onClick: {
action: "clicked",
description: "Click handler function",
table: {
category: "Behavior",
type: { summary: "function" },
},
order: 1,
},
variant: {
control: "select",
options: ["default", "destructive", "outline", "secondary", "ghost", "link"],
description: "Visual style variant of the button",
table: {
category: "Appearance",
type: { summary: "string" },
defaultValue: { summary: "default" },
},
order: 1,
},
style: {
control: "object",
description: "Inline style object for custom CSS styling",
table: {
category: "Appearance",
type: { summary: "object" },
},
order: 4,
},
},
args: { onClick: fn() },
};
export default meta;
type Story = StoryObj<ButtonProps>;
export const Default: Story = {
args: {
children: "Submit Response",
variant: "default",
},
};
export const Destructive: Story = {
args: {
children: "Skip Survey",
variant: "destructive",
},
parameters: {
docs: {
description: {
story:
"Use for actions that are destructive or exit flows, like skipping a survey or canceling progress.",
},
},
},
};
export const Outline: Story = {
args: {
children: "Back",
variant: "outline",
},
parameters: {
docs: {
description: {
story:
"Use for secondary actions like navigation or when you need a button with less visual weight than the primary action.",
},
},
},
};
export const Secondary: Story = {
args: {
children: "Save Draft",
variant: "secondary",
},
parameters: {
docs: {
description: {
story: "Use for secondary actions that are less important than the primary submit action.",
},
},
},
};
export const Ghost: Story = {
args: {
children: "Skip Question",
variant: "ghost",
},
parameters: {
docs: {
description: {
story: "Use for subtle actions or when you need minimal visual impact in the survey flow.",
},
},
},
};
export const Link: Story = {
args: {
children: "Learn more",
variant: "link",
},
parameters: {
docs: {
description: {
story:
"Use when you want button functionality but link appearance, like for help text or additional information.",
},
},
},
};
export const Icon: Story = {
args: {
children: "→",
},
parameters: {
docs: {
description: {
story: "Square button for icon-only actions. Default size for icon buttons.",
},
},
},
};
export const textWithIconOnRight: Story = {
args: {
children: (
<div className="flex items-center gap-2">
<span>Next</span>
<ArrowRightIcon className="size-4" />
</div>
),
},
};
export const textWithIconOnLeft: Story = {
args: {
children: (
<div className="flex items-center gap-2">
<ArrowLeftIcon className="size-4" />
<span>Previous</span>
</div>
),
variant: "secondary",
},
};
export const Disabled: Story = {
args: {
children: "Submit",
disabled: true,
},
parameters: {
docs: {
description: {
story: "Use when the button action is temporarily unavailable, such as when survey validation fails.",
},
},
},
};
export const InlineStyleWithGradient: Story = {
args: {
children: "Gradient Button",
style: {
background: "linear-gradient(135deg, #667eea 0%, #764ba2 100%)",
color: "white",
padding: "12px 32px",
fontSize: "16px",
fontWeight: "600",
border: "none",
cursor: "pointer",
boxShadow: "0 8px 15px rgba(102, 126, 234, 0.4)",
} as JSX.CSSProperties,
},
parameters: {
docs: {
description: {
story:
"Inline styles can include complex CSS like gradients, perfect for creating visually striking buttons with custom theming.",
},
},
},
};