From 2fdaf885727b9433fc86356b9d14dc05ed2200c2 Mon Sep 17 00:00:00 2001 From: Dhruwang Date: Mon, 24 Nov 2025 14:05:42 +0530 Subject: [PATCH] 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 --- apps/storybook/.storybook/main.ts | 18 +- packages/surveys/src/components/v5/button.tsx | 60 +++++ .../surveys/src/components/v5/stories.tsx | 210 ++++++++++++++++++ 3 files changed, 287 insertions(+), 1 deletion(-) create mode 100644 packages/surveys/src/components/v5/button.tsx create mode 100644 packages/surveys/src/components/v5/stories.tsx diff --git a/apps/storybook/.storybook/main.ts b/apps/storybook/.storybook/main.ts index fa597f552f..bb85985aec 100644 --- a/apps/storybook/.storybook/main.ts +++ b/apps/storybook/.storybook/main.ts @@ -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; diff --git a/packages/surveys/src/components/v5/button.tsx b/packages/surveys/src/components/v5/button.tsx new file mode 100644 index 0000000000..dbcd7a2b10 --- /dev/null +++ b/packages/surveys/src/components/v5/button.tsx @@ -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, VariantProps { + isLoading?: boolean; + disabled?: boolean; + style?: JSX.CSSProperties; +} + +function Button({ + className, + variant, + size, + onClick, + children, + isLoading, + disabled, + style, + ...props +}: ButtonProps) { + return ( + + ); +} + +export { Button, buttonVariants, type ButtonProps }; diff --git a/packages/surveys/src/components/v5/stories.tsx b/packages/surveys/src/components/v5/stories.tsx new file mode 100644 index 0000000000..787a0dfd50 --- /dev/null +++ b/packages/surveys/src/components/v5/stories.tsx @@ -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 = { + 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; + +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: ( +
+ Next + +
+ ), + }, +}; + +export const textWithIconOnLeft: Story = { + args: { + children: ( +
+ + Previous +
+ ), + 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.", + }, + }, + }, +};