Merge branch 'epic/survey-ui-package' of https://github.com/formbricks/formbricks into epic/survey-ui-package

This commit is contained in:
Dhruwang
2025-12-05 17:49:52 +05:30
22 changed files with 2093 additions and 116 deletions

View File

@@ -1,5 +1,6 @@
import type { Preview } from "@storybook/react-vite";
import React from "react";
import "../../../packages/survey-ui/src/styles/globals.css";
import { I18nProvider } from "../../web/lingodotdev/client";
import "../../web/modules/ui/globals.css";

View File

@@ -40,12 +40,14 @@
},
"dependencies": {
"@radix-ui/react-checkbox": "1.3.1",
"@radix-ui/react-label": "2.1.1",
"@radix-ui/react-progress": "1.1.8",
"@radix-ui/react-radio-group": "1.3.6",
"@radix-ui/react-slot": "1.2.2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"tailwind-merge": "^2.5.5"
"tailwind-merge": "^2.5.5",
"lucide-react": "0.555.0"
},
"devDependencies": {
"@formbricks/config-typescript": "workspace:*",

View File

@@ -0,0 +1,66 @@
import { type Meta, type StoryObj } from "@storybook/react";
import { TriangleAlertIcon } from "lucide-react";
import { Alert, AlertDescription, AlertTitle } from "./alert";
const meta: Meta<typeof Alert> = {
title: "UI-package/Alert",
component: Alert,
tags: ["autodocs"],
parameters: {
layout: "centered",
},
argTypes: {
variant: {
control: "select",
options: ["default", "destructive"],
description: "Style variant of the alert",
},
},
};
export default meta;
type Story = StoryObj<typeof Alert>;
export const Default: Story = {
render: () => (
<Alert>
<AlertTitle>Alert Title</AlertTitle>
<AlertDescription>This is a default alert message.</AlertDescription>
</Alert>
),
};
export const Destructive: Story = {
render: () => (
<Alert variant="destructive">
<AlertTitle>Error</AlertTitle>
<AlertDescription>Something went wrong. Please try again.</AlertDescription>
</Alert>
),
};
export const DestructiveWithIcon: Story = {
render: () => (
<Alert variant="destructive">
<TriangleAlertIcon />
<AlertTitle>Error</AlertTitle>
<AlertDescription>Something went wrong. Please try again.</AlertDescription>
</Alert>
),
};
export const WithTitleOnly: Story = {
render: () => (
<Alert>
<AlertTitle>Important Notice</AlertTitle>
</Alert>
),
};
export const WithDescriptionOnly: Story = {
render: () => (
<Alert>
<AlertDescription>This alert only has a description.</AlertDescription>
</Alert>
),
};

View File

@@ -0,0 +1,54 @@
import { type VariantProps, cva } from "class-variance-authority";
import * as React from "react";
import { cn } from "../lib/utils";
const alertVariants = cva(
"relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
{
variants: {
variant: {
default: "bg-card text-card-foreground",
destructive:
"text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90",
},
},
defaultVariants: {
variant: "default",
},
}
);
function Alert({
className,
variant,
...props
}: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>): React.JSX.Element {
return (
<div data-slot="alert" role="alert" className={cn(alertVariants({ variant }), className)} {...props} />
);
}
function AlertTitle({ className, ...props }: React.ComponentProps<"div">): React.JSX.Element {
return (
<div
data-slot="alert-title"
className={cn("col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight", className)}
{...props}
/>
);
}
function AlertDescription({ className, ...props }: React.ComponentProps<"div">): React.JSX.Element {
return (
<div
data-slot="alert-description"
className={cn(
"text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed",
className
)}
{...props}
/>
);
}
export { Alert, AlertTitle, AlertDescription };

View File

@@ -1,7 +1,23 @@
import { type Meta, type StoryObj } from "@storybook/react";
import type { Decorator, Meta, StoryObj } from "@storybook/react";
import React from "react";
import { Button } from "./button";
const meta: Meta<typeof Button> = {
// Styling options for the StylingPlayground story
interface StylingOptions {
buttonHeight: string;
buttonWidth: string;
buttonFontSize: string;
buttonBorderRadius: string;
buttonBgColor: string;
buttonTextColor: string;
buttonPaddingX: string;
buttonPaddingY: string;
}
type ButtonProps = React.ComponentProps<typeof Button>;
type StoryProps = ButtonProps & StylingOptions;
const meta: Meta<StoryProps> = {
title: "UI-package/Button",
component: Button,
tags: ["autodocs"],
@@ -11,17 +27,19 @@ const meta: Meta<typeof Button> = {
argTypes: {
variant: {
control: "select",
options: ["default", "destructive", "outline", "secondary", "ghost", "link", "custom"],
options: ["default", "destructive", "outline", "secondary", "ghost", "link"],
description: "Visual style variant of the button",
table: { category: "Component Props" },
},
size: {
control: "select",
options: ["default", "sm", "lg", "icon", "icon-sm", "icon-lg"],
options: ["default", "sm", "lg", "icon"],
description: "Size of the button",
table: { category: "Component Props" },
},
style: {
control: "object",
description: "Custom style for the button (only works with variant 'custom')",
disabled: {
control: "boolean",
table: { category: "Component Props" },
},
asChild: {
table: { disable: true },
@@ -33,7 +51,115 @@ const meta: Meta<typeof Button> = {
};
export default meta;
type Story = StoryObj<typeof Button>;
type Story = StoryObj<StoryProps>;
// Decorator to apply CSS variables from story args
const withCSSVariables: Decorator<StoryProps> = (Story, context) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access -- Storybook's Decorator type doesn't properly infer args type
const args = context.args as StoryProps;
const {
buttonHeight,
buttonWidth,
buttonFontSize,
buttonBorderRadius,
buttonBgColor,
buttonTextColor,
buttonPaddingX,
buttonPaddingY,
} = args;
const cssVarStyle = {
"--fb-button-height": buttonHeight,
"--fb-button-width": buttonWidth,
"--fb-button-font-size": buttonFontSize,
"--fb-button-border-radius": buttonBorderRadius,
"--fb-button-bg-color": buttonBgColor,
"--fb-button-text-color": buttonTextColor,
"--fb-button-padding-x": buttonPaddingX,
"--fb-button-padding-y": buttonPaddingY,
} as React.CSSProperties;
return (
<div style={cssVarStyle}>
<Story />
</div>
);
};
export const StylingPlayground: Story = {
args: {
children: "Custom Button",
// Default styling values
buttonHeight: "40px",
buttonWidth: "auto",
buttonFontSize: "14px",
buttonBorderRadius: "0.5rem",
buttonBgColor: "#3b82f6",
buttonTextColor: "#ffffff",
buttonPaddingX: "16px",
buttonPaddingY: "8px",
},
argTypes: {
// Button Styling (CSS Variables) - Only for this story
buttonHeight: {
control: "text",
table: {
category: "Button Styling",
defaultValue: { summary: "40px" },
},
},
buttonWidth: {
control: "text",
table: {
category: "Button Styling",
defaultValue: { summary: "auto" },
},
},
buttonFontSize: {
control: "text",
table: {
category: "Button Styling",
defaultValue: { summary: "14px" },
},
},
buttonBorderRadius: {
control: "text",
table: {
category: "Button Styling",
defaultValue: { summary: "var(--fb-border-radius)" },
},
},
buttonBgColor: {
control: "color",
table: {
category: "Button Styling",
defaultValue: { summary: "var(--fb-brand-color)" },
},
},
buttonTextColor: {
control: "color",
table: {
category: "Button Styling",
defaultValue: { summary: "var(--fb-brand-text-color)" },
},
},
buttonPaddingX: {
control: "text",
table: {
category: "Button Styling",
defaultValue: { summary: "16px" },
},
},
buttonPaddingY: {
control: "text",
table: {
category: "Button Styling",
defaultValue: { summary: "8px" },
},
},
},
decorators: [withCSSVariables],
};
export const Default: Story = {
args: {
@@ -103,14 +229,3 @@ export const Disabled: Story = {
children: "Disabled Button",
},
};
export const WithCustomStyle: Story = {
args: {
style: {
fontWeight: "bold",
backgroundColor: "red",
},
variant: "custom",
children: "Custom Style Button",
},
};

View File

@@ -16,7 +16,6 @@ const buttonVariants = cva(
secondary: "bg-secondary text-secondary-foreground shadow-xs 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",
custom: "",
},
size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3",
@@ -32,17 +31,29 @@ const buttonVariants = cva(
}
);
// Default styles driven by CSS variables
export const cssVarStyles: React.CSSProperties = {
height: "var(--fb-button-height)",
width: "var(--fb-button-width)",
fontSize: "var(--fb-button-font-size)",
borderRadius: "var(--fb-button-border-radius)",
backgroundColor: "var(--fb-button-bg-color)",
color: "var(--fb-button-text-color)",
paddingLeft: "var(--fb-button-padding-x)",
paddingRight: "var(--fb-button-padding-x)",
paddingTop: "var(--fb-button-padding-y)",
paddingBottom: "var(--fb-button-padding-y)",
};
function Button({
className,
variant,
size,
asChild = false,
style,
...props
}: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean;
style?: React.CSSProperties;
}): React.JSX.Element {
const Comp = asChild ? Slot : "button";
@@ -50,8 +61,8 @@ function Button({
<Comp
data-slot="button"
aria-label={props["aria-label"]}
className={cn(buttonVariants({ variant, size, className }))}
style={variant === "custom" ? style : undefined}
className={cn(buttonVariants({ variant, size }), className)}
style={cssVarStyles}
{...props}
/>
);

View File

@@ -0,0 +1,90 @@
import type { Meta, StoryObj } from "@storybook/react";
import { Checkbox } from "./checkbox";
import { Label } from "./label";
const meta: Meta<typeof Checkbox> = {
title: "UI-package/Checkbox",
component: Checkbox,
parameters: {
layout: "centered",
docs: {
description: {
component:
"A checkbox component built with Radix UI primitives. Supports checked, unchecked, and indeterminate states with full accessibility support.",
},
},
},
tags: ["autodocs"],
argTypes: {
checked: {
control: { type: "boolean" },
description: "The controlled checked state of the checkbox",
},
disabled: {
control: { type: "boolean" },
description: "Whether the checkbox is disabled",
},
required: {
control: { type: "boolean" },
description: "Whether the checkbox is required",
},
},
};
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: {
"aria-label": "Checkbox",
},
};
export const Checked: Story = {
args: {
checked: true,
"aria-label": "Checked checkbox",
},
};
export const Disabled: Story = {
args: {
disabled: true,
"aria-label": "Disabled checkbox",
},
};
export const DisabledChecked: Story = {
args: {
disabled: true,
checked: true,
"aria-label": "Disabled checked checkbox",
},
};
export const WithLabel: Story = {
render: () => (
<div className="flex items-center space-x-2">
<Checkbox id="terms" />
<Label htmlFor="terms">Accept terms and conditions</Label>
</div>
),
};
export const WithLabelChecked: Story = {
render: () => (
<div className="flex items-center space-x-2">
<Checkbox id="terms-checked" checked />
<Label htmlFor="terms-checked">Accept terms and conditions</Label>
</div>
),
};
export const WithLabelDisabled: Story = {
render: () => (
<div className="flex items-center space-x-2">
<Checkbox id="terms-disabled" disabled />
<Label htmlFor="terms-disabled">Accept terms and conditions</Label>
</div>
),
};

View File

@@ -0,0 +1,29 @@
"use client";
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
import { CheckIcon } from "lucide-react";
import * as React from "react";
import { cn } from "../lib/utils";
function Checkbox({
className,
...props
}: React.ComponentProps<typeof CheckboxPrimitive.Root>): React.JSX.Element {
return (
<CheckboxPrimitive.Root
data-slot="checkbox"
className={cn(
"border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive shadow-xs peer size-4 shrink-0 rounded-[4px] border outline-none transition-shadow focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}>
<CheckboxPrimitive.Indicator
data-slot="checkbox-indicator"
className="flex items-center justify-center text-current transition-none">
<CheckIcon className="size-3.5" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
);
}
export { Checkbox };

View File

@@ -0,0 +1,323 @@
import type { Decorator, Meta, StoryObj } from "@storybook/react";
import React from "react";
import { Input, type InputProps } from "./input";
// Styling options for the StylingPlayground story
interface StylingOptions {
inputWidth: string;
inputHeight: string;
inputBgColor: string;
inputBorderColor: string;
inputBorderRadius: string;
inputFontFamily: string;
inputFontSize: string;
inputFontWeight: string;
inputColor: string;
inputPlaceholderColor: string;
inputPaddingX: string;
inputPaddingY: string;
inputShadow: string;
}
type StoryProps = InputProps & Partial<StylingOptions>;
const meta: Meta<StoryProps> = {
title: "UI-package/Input",
component: Input,
parameters: {
layout: "centered",
},
tags: ["autodocs"],
argTypes: {
type: {
control: { type: "select" },
options: ["text", "email", "password", "number", "tel", "url", "search", "file"],
table: { category: "Component Props" },
},
placeholder: {
control: "text",
table: { category: "Component Props" },
},
disabled: {
control: "boolean",
table: { category: "Component Props" },
},
errorMessage: {
control: "text",
table: { category: "Component Props" },
},
dir: {
control: { type: "select" },
options: ["ltr", "rtl"],
table: { category: "Component Props" },
},
},
};
export default meta;
type Story = StoryObj<StoryProps>;
// Decorator to apply CSS variables from story args
const withCSSVariables: Decorator<StoryProps> = (Story, context) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access -- Storybook's Decorator type doesn't properly infer args type
const args = context.args as StoryProps;
const {
inputWidth,
inputHeight,
inputBgColor,
inputBorderColor,
inputBorderRadius,
inputFontFamily,
inputFontSize,
inputFontWeight,
inputColor,
inputPlaceholderColor,
inputPaddingX,
inputPaddingY,
inputShadow,
} = args;
const cssVarStyle: React.CSSProperties & Record<string, string | undefined> = {
"--fb-input-width": inputWidth,
"--fb-input-height": inputHeight,
"--fb-input-bg-color": inputBgColor,
"--fb-input-border-color": inputBorderColor,
"--fb-input-border-radius": inputBorderRadius,
"--fb-input-font-family": inputFontFamily,
"--fb-input-font-size": inputFontSize,
"--fb-input-font-weight": inputFontWeight,
"--fb-input-color": inputColor,
"--fb-input-placeholder-color": inputPlaceholderColor,
"--fb-input-padding-x": inputPaddingX,
"--fb-input-padding-y": inputPaddingY,
"--fb-input-shadow": inputShadow,
};
return (
<div style={cssVarStyle}>
<Story />
</div>
);
};
export const StylingPlayground: Story = {
args: {
placeholder: "Enter text...",
// Default styling values
inputWidth: "400px",
inputHeight: "2.5rem",
inputBgColor: "#ffffff",
inputBorderColor: "#e2e8f0",
inputBorderRadius: "0.5rem",
inputFontFamily: "system-ui, sans-serif",
inputFontSize: "0.875rem",
inputFontWeight: "400",
inputColor: "#1e293b",
inputPlaceholderColor: "#94a3b8",
inputPaddingX: "0.75rem",
inputPaddingY: "0.5rem",
inputShadow: "0 1px 2px 0 rgb(0 0 0 / 0.05)",
},
argTypes: {
// Input Styling (CSS Variables) - Only for this story
inputWidth: {
control: "text",
table: {
category: "Input Styling",
defaultValue: { summary: "100%" },
},
},
inputHeight: {
control: "text",
table: {
category: "Input Styling",
defaultValue: { summary: "2.25rem" },
},
},
inputBgColor: {
control: "color",
table: {
category: "Input Styling",
defaultValue: { summary: "transparent" },
},
},
inputBorderColor: {
control: "color",
table: {
category: "Input Styling",
defaultValue: { summary: "var(--input)" },
},
},
inputBorderRadius: {
control: "text",
table: {
category: "Input Styling",
defaultValue: { summary: "0.5rem" },
},
},
inputFontFamily: {
control: "text",
table: {
category: "Input Styling",
defaultValue: { summary: "inherit" },
},
},
inputFontSize: {
control: "text",
table: {
category: "Input Styling",
defaultValue: { summary: "0.875rem" },
},
},
inputFontWeight: {
control: "text",
table: {
category: "Input Styling",
defaultValue: { summary: "400" },
},
},
inputColor: {
control: "color",
table: {
category: "Input Styling",
defaultValue: { summary: "var(--foreground)" },
},
},
inputPlaceholderColor: {
control: "color",
table: {
category: "Input Styling",
defaultValue: { summary: "var(--muted-foreground)" },
},
},
inputPaddingX: {
control: "text",
table: {
category: "Input Styling",
defaultValue: { summary: "0.75rem" },
},
},
inputPaddingY: {
control: "text",
table: {
category: "Input Styling",
defaultValue: { summary: "0.25rem" },
},
},
inputShadow: {
control: "text",
table: {
category: "Input Styling",
defaultValue: { summary: "0 1px 2px 0 rgb(0 0 0 / 0.05)" },
},
},
},
decorators: [withCSSVariables],
};
export const Default: Story = {
args: {
placeholder: "Enter text...",
},
};
export const WithValue: Story = {
args: {
defaultValue: "Sample text",
placeholder: "Enter text...",
},
};
export const Email: Story = {
args: {
type: "email",
placeholder: "email@example.com",
},
};
export const Password: Story = {
args: {
type: "password",
placeholder: "Enter password",
},
};
export const NumberInput: Story = {
args: {
type: "number",
placeholder: "0",
},
};
export const WithError: Story = {
args: {
placeholder: "Enter your email",
defaultValue: "invalid-email",
errorMessage: "Please enter a valid email address",
},
};
export const Disabled: Story = {
args: {
placeholder: "Disabled input",
disabled: true,
},
};
export const DisabledWithValue: Story = {
args: {
defaultValue: "Cannot edit this",
disabled: true,
},
};
export const FileUpload: Story = {
args: {
type: "file",
},
};
export const FileUploadWithRTL: Story = {
args: {
type: "file",
dir: "rtl",
},
};
export const FileUploadWithError: Story = {
args: {
type: "file",
errorMessage: "Please upload a valid file",
},
};
export const FileUploadWithErrorAndRTL: Story = {
args: {
type: "file",
errorMessage: "Please upload a valid file",
dir: "rtl",
},
};
export const RTL: Story = {
args: {
dir: "rtl",
placeholder: "أدخل النص هنا",
defaultValue: "نص تجريبي",
},
};
export const FullWidth: Story = {
args: {
placeholder: "Full width input",
className: "w-96",
},
};
export const WithErrorAndRTL: Story = {
args: {
dir: "rtl",
placeholder: "أدخل بريدك الإلكتروني",
errorMessage: "هذا الحقل مطلوب",
},
};

View File

@@ -0,0 +1,72 @@
import { AlertCircle } from "lucide-react";
import * as React from "react";
import { cn } from "../lib/utils";
interface InputProps extends React.ComponentProps<"input"> {
/** Text direction for RTL language support */
dir?: "ltr" | "rtl";
/** Error message to display above the input */
errorMessage?: string;
}
function Input({ className, type, errorMessage, dir, ...props }: InputProps): React.JSX.Element {
const hasError = Boolean(errorMessage);
// Default styles driven by CSS variables
const cssVarStyles: React.CSSProperties = {
width: "var(--fb-input-width)",
height: "var(--fb-input-height)",
backgroundColor: "var(--fb-input-bg-color)",
borderColor: "var(--fb-input-border-color)",
borderRadius: "var(--fb-input-border-radius)",
fontFamily: "var(--fb-input-font-family)",
fontSize: "var(--fb-input-font-size)",
fontWeight: "var(--fb-input-font-weight)" as React.CSSProperties["fontWeight"],
color: "var(--fb-input-color)",
paddingLeft: "var(--fb-input-padding-x)",
paddingRight: "var(--fb-input-padding-x)",
paddingTop: "var(--fb-input-padding-y)",
paddingBottom: "var(--fb-input-padding-y)",
boxShadow: "var(--fb-input-shadow)",
};
return (
<div className="space-y-1">
{errorMessage ? (
<div className="text-destructive flex items-center gap-1 text-sm" dir={dir}>
<AlertCircle className="size-4" />
<span>{errorMessage}</span>
</div>
) : null}
<input
type={type}
dir={dir}
style={cssVarStyles}
data-slot="input"
aria-invalid={hasError || undefined}
className={cn(
// Layout and behavior (Tailwind)
"flex min-w-0 border outline-none transition-[color,box-shadow]",
// Placeholder styling via CSS variable
"[&::placeholder]:opacity-[var(--fb-input-placeholder-opacity)]",
"placeholder:[color:var(--fb-input-placeholder-color)]",
// Selection styling
"selection:bg-primary selection:text-primary-foreground",
// File input specifics
"file:text-foreground file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium",
// Focus ring
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
// Error state ring
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
// Disabled state
"disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
/>
</div>
);
}
export { Input };
export type { InputProps };

View File

@@ -0,0 +1,396 @@
import type { Decorator, Meta, StoryObj } from "@storybook/react";
import React from "react";
import { Checkbox } from "./checkbox";
import { Input } from "./input";
import { Label, type LabelProps } from "./label";
import { Textarea } from "./textarea";
// Styling options for the StylingPlayground stories
interface HeadlineStylingOptions {
headlineFontFamily: string;
headlineFontWeight: string;
headlineFontSize: string;
headlineColor: string;
headlineOpacity: string;
}
interface DescriptionStylingOptions {
descriptionFontFamily: string;
descriptionFontWeight: string;
descriptionFontSize: string;
descriptionColor: string;
descriptionOpacity: string;
}
interface DefaultStylingOptions {
labelFontFamily: string;
labelFontWeight: string;
labelFontSize: string;
labelColor: string;
labelOpacity: string;
}
type StoryProps = LabelProps &
Partial<HeadlineStylingOptions & DescriptionStylingOptions & DefaultStylingOptions>;
const meta: Meta<StoryProps> = {
title: "UI-package/Label",
component: Label,
parameters: {
layout: "centered",
docs: {
description: {
component:
"A label component built with Radix UI primitives. Provides accessible labeling for form controls with proper association and styling.",
},
},
},
tags: ["autodocs"],
argTypes: {
variant: {
control: "select",
options: ["default", "headline", "description"],
description: "Visual style variant of the label",
table: { category: "Component Props" },
},
htmlFor: {
control: { type: "text" },
description: "The id of the form control this label is associated with",
table: { category: "Component Props" },
},
style: {
control: "object",
table: { category: "Component Props" },
},
},
args: {
children: "Label text",
},
};
export default meta;
type Story = StoryObj<StoryProps>;
// Decorator to apply CSS variables for headline variant
const withHeadlineCSSVariables: Decorator<StoryProps> = (Story, context) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access -- Storybook's Decorator type doesn't properly infer args type
const args = context.args as StoryProps;
const { headlineFontFamily, headlineFontWeight, headlineFontSize, headlineColor, headlineOpacity } = args;
const cssVarStyle: React.CSSProperties & Record<string, string | undefined> = {
"--fb-question-headline-font-family": headlineFontFamily ?? undefined,
"--fb-question-headline-font-weight": headlineFontWeight ?? undefined,
"--fb-question-headline-font-size": headlineFontSize ?? undefined,
"--fb-question-headline-color": headlineColor ?? undefined,
"--fb-question-headline-opacity": headlineOpacity ?? undefined,
};
return (
<div style={cssVarStyle}>
<Story />
</div>
);
};
// Decorator to apply CSS variables for description variant
const withDescriptionCSSVariables: Decorator<StoryProps> = (Story, context) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access -- Storybook's Decorator type doesn't properly infer args type
const args = context.args as StoryProps;
const {
descriptionFontFamily,
descriptionFontWeight,
descriptionFontSize,
descriptionColor,
descriptionOpacity,
} = args;
const cssVarStyle: React.CSSProperties & Record<string, string | undefined> = {
"--fb-question-description-font-family": descriptionFontFamily ?? undefined,
"--fb-question-description-font-weight": descriptionFontWeight ?? undefined,
"--fb-question-description-font-size": descriptionFontSize ?? undefined,
"--fb-question-description-color": descriptionColor ?? undefined,
"--fb-question-description-opacity": descriptionOpacity ?? undefined,
};
return (
<div style={cssVarStyle}>
<Story />
</div>
);
};
const withCustomCSSVariables: Decorator<StoryProps> = (Story, context) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access -- Storybook's Decorator type doesn't properly infer args type
const args = context.args as StoryProps;
const { labelFontFamily, labelFontWeight, labelFontSize, labelColor, labelOpacity } = args;
const cssVarStyle: React.CSSProperties & Record<string, string | undefined> = {
"--fb-label-font-family": labelFontFamily ?? undefined,
"--fb-label-font-weight": labelFontWeight ?? undefined,
"--fb-label-font-size": labelFontSize ?? undefined,
"--fb-label-color": labelColor ?? undefined,
"--fb-label-opacity": labelOpacity ?? undefined,
};
return (
<div style={cssVarStyle}>
<Story />
</div>
);
};
export const Default: Story = {
args: {},
};
export const WithInput: Story = {
render: () => (
<div className="space-y-2">
<Label htmlFor="username">Username</Label>
<Input id="username" placeholder="Enter your username..." />
</div>
),
};
export const WithCheckbox: Story = {
render: () => (
<div className="flex items-center space-x-2">
<Checkbox id="terms" />
<Label htmlFor="terms">I agree to the terms and conditions</Label>
</div>
),
};
export const WithTextarea: Story = {
render: () => (
<div className="space-y-2">
<Label htmlFor="message">Message</Label>
<Textarea id="message" placeholder="Enter your message..." />
</div>
),
};
export const Required: Story = {
render: () => (
<div className="space-y-2">
<Label htmlFor="email">
Email address <span className="text-red-500">*</span>
</Label>
<Input id="email" type="email" placeholder="Enter your email..." required />
</div>
),
};
export const Optional: Story = {
render: () => (
<div className="space-y-2">
<Label htmlFor="website">
Website <span className="text-muted-foreground">(optional)</span>
</Label>
<Input id="website" type="url" placeholder="https://..." />
</div>
),
};
export const WithHelpText: Story = {
render: () => (
<div className="space-y-2">
<Label htmlFor="password">Password</Label>
<Input id="password" type="password" placeholder="Enter your password..." />
<p className="text-muted-foreground text-sm">
Must be at least 8 characters with a mix of letters and numbers
</p>
</div>
),
};
export const ErrorState: Story = {
render: () => (
<div className="space-y-2">
<Label htmlFor="invalid-email">
Email address <span className="text-red-500">*</span>
</Label>
<Input
id="invalid-email"
type="email"
aria-invalid
value="invalid-email"
placeholder="Enter your email..."
/>
<p className="text-destructive text-sm">Please enter a valid email address</p>
</div>
),
};
export const FormSection: Story = {
render: () => (
<div className="w-[300px] space-y-6">
<div>
<h3 className="mb-4 text-lg font-semibold">Personal Information</h3>
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="first-name">First name</Label>
<Input id="first-name" placeholder="Enter your first name..." />
</div>
<div className="space-y-2">
<Label htmlFor="last-name">Last name</Label>
<Input id="last-name" placeholder="Enter your last name..." />
</div>
<div className="space-y-2">
<Label htmlFor="birth-date">Date of birth</Label>
<Input id="birth-date" type="date" />
</div>
</div>
</div>
<div>
<h3 className="mb-4 text-lg font-semibold">Contact Information</h3>
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="contact-email">
Email address <span className="text-red-500">*</span>
</Label>
<Input id="contact-email" type="email" placeholder="Enter your email..." />
</div>
<div className="space-y-2">
<Label htmlFor="phone">
Phone number <span className="text-muted-foreground">(optional)</span>
</Label>
<Input id="phone" type="tel" placeholder="Enter your phone number..." />
</div>
</div>
</div>
<div>
<h3 className="mb-4 text-lg font-semibold">Preferences</h3>
<div className="space-y-3">
<div className="flex items-center space-x-2">
<Checkbox id="newsletter" />
<Label htmlFor="newsletter">Subscribe to newsletter</Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox id="notifications" />
<Label htmlFor="notifications">Enable email notifications</Label>
</div>
</div>
</div>
</div>
),
};
export const LongLabel: Story = {
render: () => (
<div className="w-[350px] space-y-2">
<Label htmlFor="long-label">
This is a very long label that demonstrates how labels wrap when they contain a lot of text and need
to span multiple lines
</Label>
<Input id="long-label" placeholder="Enter value..." />
</div>
),
};
export const HeadlineVariant: Story = {
args: {
variant: "headline",
children: "Headline Label",
headlineFontFamily: "system-ui, sans-serif",
headlineFontWeight: "600",
headlineFontSize: "1.25rem",
headlineColor: "#1e293b",
headlineOpacity: "1",
},
argTypes: {
headlineFontFamily: {
control: "text",
table: { category: "Headline Styling" },
},
headlineFontWeight: {
control: "text",
table: { category: "Headline Styling" },
},
headlineFontSize: {
control: "text",
table: { category: "Headline Styling" },
},
headlineColor: {
control: "color",
table: { category: "Headline Styling" },
},
headlineOpacity: {
control: "text",
table: { category: "Headline Styling" },
},
},
decorators: [withHeadlineCSSVariables],
};
export const DescriptionVariant: Story = {
args: {
variant: "description",
children: "Description Label",
descriptionFontFamily: "system-ui, sans-serif",
descriptionFontWeight: "400",
descriptionFontSize: "0.875rem",
descriptionColor: "#64748b",
descriptionOpacity: "1",
},
argTypes: {
descriptionFontFamily: {
control: "text",
table: { category: "Description Styling" },
},
descriptionFontWeight: {
control: "text",
table: { category: "Description Styling" },
},
descriptionFontSize: {
control: "text",
table: { category: "Description Styling" },
},
descriptionColor: {
control: "color",
table: { category: "Description Styling" },
},
descriptionOpacity: {
control: "text",
table: { category: "Description Styling" },
},
},
decorators: [withDescriptionCSSVariables],
};
export const DefaultVariant: Story = {
args: {
variant: "default",
children: "Default Label",
labelFontFamily: "system-ui, sans-serif",
labelFontWeight: "500",
labelFontSize: "0.875rem",
labelColor: "#1e293b",
labelOpacity: "1",
},
argTypes: {
labelFontFamily: {
control: "text",
table: { category: "Default Label Styling" },
},
labelFontWeight: {
control: "text",
table: { category: "Default Label Styling" },
},
labelFontSize: {
control: "text",
table: { category: "Default Label Styling" },
},
labelColor: {
control: "color",
table: { category: "Default Label Styling" },
},
labelOpacity: {
control: "text",
table: { category: "Default Label Styling" },
},
},
decorators: [withCustomCSSVariables],
};

View File

@@ -0,0 +1,62 @@
"use client";
import * as LabelPrimitive from "@radix-ui/react-label";
import * as React from "react";
import { cn } from "../lib/utils";
interface LabelProps extends React.ComponentProps<typeof LabelPrimitive.Root> {
/** Label variant for different styling contexts */
variant?: "default" | "headline" | "description";
}
function Label({ className, variant = "default", ...props }: LabelProps): React.JSX.Element {
// Default styles driven by CSS variables based on variant
const getCssVarStyles = (): React.CSSProperties => {
if (variant === "headline") {
return {
fontFamily: "var(--fb-question-headline-font-family)",
fontWeight: "var(--fb-question-headline-font-weight)" as React.CSSProperties["fontWeight"],
fontSize: "var(--fb-question-headline-font-size)",
color: "var(--fb-question-headline-color)",
opacity: "var(--fb-question-headline-opacity)",
};
}
if (variant === "description") {
return {
fontFamily: "var(--fb-question-description-font-family)",
fontWeight: "var(--fb-question-description-font-weight)" as React.CSSProperties["fontWeight"],
fontSize: "var(--fb-question-description-font-size)",
color: "var(--fb-question-description-color)",
opacity: "var(--fb-question-description-opacity)",
};
}
// Default variant styles
return {
fontFamily: "var(--fb-label-font-family)",
fontWeight: "var(--fb-label-font-weight)" as React.CSSProperties["fontWeight"],
fontSize: "var(--fb-label-font-size)",
color: "var(--fb-label-color)",
opacity: "var(--fb-label-opacity)",
};
};
const cssVarStyles = getCssVarStyles();
return (
<LabelPrimitive.Root
data-slot="label"
data-variant={variant}
className={cn(
"flex select-none items-center gap-2 text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-50 group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50",
className
)}
style={cssVarStyles}
{...props}
/>
);
}
export { Label };
export type { LabelProps };

View File

@@ -0,0 +1,90 @@
import { type Meta, type StoryObj } from "@storybook/react";
import { Progress } from "./progress";
const meta: Meta<typeof Progress> = {
title: "UI-package/Progress",
component: Progress,
tags: ["autodocs"],
parameters: {
layout: "centered",
},
argTypes: {
value: {
control: { type: "range", min: 0, max: 100, step: 1 },
description: "Progress value (0-100)",
},
indicatorStyle: {
control: { type: "object" },
description: "Style for the progress indicator",
},
trackStyle: {
control: { type: "object" },
description: "Style for the progress track",
},
},
};
export default meta;
type Story = StoryObj<typeof Progress>;
export const Default: Story = {
render: (args: React.ComponentProps<typeof Progress>) => (
<div className="w-64">
<Progress {...args} />
</div>
),
args: {
value: 50,
},
};
export const Zero: Story = {
render: (args: React.ComponentProps<typeof Progress>) => (
<div className="w-64">
<Progress {...args} />
</div>
),
args: {
value: 0,
},
};
export const Half: Story = {
render: (args: React.ComponentProps<typeof Progress>) => (
<div className="w-64">
<Progress {...args} />
</div>
),
args: {
value: 50,
},
};
export const Complete: Story = {
render: (args: React.ComponentProps<typeof Progress>) => (
<div className="w-64">
<Progress {...args} />
</div>
),
args: {
value: 100,
},
};
export const CustomStyles: Story = {
render: (args: React.ComponentProps<typeof Progress>) => (
<div className="w-64">
<Progress {...args} />
</div>
),
args: {
value: 75,
indicatorStyle: {
backgroundColor: "green",
},
trackStyle: {
backgroundColor: "black",
height: "20px",
},
},
};

View File

@@ -0,0 +1,39 @@
"use client";
import * as ProgressPrimitive from "@radix-ui/react-progress";
import * as React from "react";
import { cn } from "../lib/utils";
interface ProgressProps extends Omit<React.ComponentProps<"div">, "children"> {
/** Progress value (0-100) */
value?: number;
/** Custom inline styles for the progress indicator */
indicatorStyle?: React.CSSProperties;
/** Custom inline styles for the progress track */
trackStyle?: React.CSSProperties;
}
function Progress({
className,
value,
indicatorStyle,
trackStyle,
...props
}: ProgressProps): React.JSX.Element {
const progressValue: number = typeof value === "number" ? value : 0;
return (
<ProgressPrimitive.Root
data-slot="progress"
className={cn("bg-primary/20 relative h-2 w-full overflow-hidden rounded-full", className)}
style={trackStyle}
{...props}>
<ProgressPrimitive.Indicator
data-slot="progress-indicator"
className="bg-primary h-full w-full flex-1 transition-all"
style={{ transform: `translateX(-${String(100 - progressValue)}%)`, ...indicatorStyle }}
/>
</ProgressPrimitive.Root>
);
}
export { Progress };

View File

@@ -0,0 +1,312 @@
import type { Meta, StoryObj } from "@storybook/react";
import { Label } from "./label";
import { RadioGroup, RadioGroupItem } from "./radio-group";
const meta: Meta<typeof RadioGroup> = {
title: "UI-package/RadioGroup",
component: RadioGroup,
parameters: {
layout: "centered",
docs: {
description: {
component:
"A radio group component built with Radix UI primitives. Allows users to select one option from a set of mutually exclusive choices.",
},
},
},
tags: ["autodocs"],
argTypes: {
defaultValue: {
control: { type: "text" },
description: "The default selected value",
},
disabled: {
control: { type: "boolean" },
description: "Whether the entire radio group is disabled",
},
required: {
control: { type: "boolean" },
description: "Whether a selection is required",
},
dir: {
control: { type: "select" },
options: ["ltr", "rtl"],
description: "Text direction",
},
},
};
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
render: (args: React.ComponentProps<typeof RadioGroup>) => (
<RadioGroup defaultValue="option1" {...args}>
<div className="flex items-center gap-2">
<RadioGroupItem value="option1" id="option1" />
<Label htmlFor="option1">Option 1</Label>
</div>
<div className="flex items-center gap-2">
<RadioGroupItem value="option2" id="option2" />
<Label htmlFor="option2">Option 2</Label>
</div>
<div className="flex items-center gap-2">
<RadioGroupItem value="option3" id="option3" />
<Label htmlFor="option3">Option 3</Label>
</div>
</RadioGroup>
),
args: {
defaultValue: "option1",
},
};
export const WithoutDefault: Story = {
render: () => (
<RadioGroup>
<div className="flex items-center gap-2">
<RadioGroupItem value="option1" id="no-default-1" />
<Label htmlFor="no-default-1">Option 1</Label>
</div>
<div className="flex items-center gap-2">
<RadioGroupItem value="option2" id="no-default-2" />
<Label htmlFor="no-default-2">Option 2</Label>
</div>
<div className="flex items-center gap-2">
<RadioGroupItem value="option3" id="no-default-3" />
<Label htmlFor="no-default-3">Option 3</Label>
</div>
</RadioGroup>
),
};
export const Disabled: Story = {
render: () => (
<RadioGroup defaultValue="option1" disabled>
<div className="flex items-center gap-2">
<RadioGroupItem value="option1" id="disabled-1" />
<Label htmlFor="disabled-1">Option 1 (Selected)</Label>
</div>
<div className="flex items-center gap-2">
<RadioGroupItem value="option2" id="disabled-2" />
<Label htmlFor="disabled-2">Option 2</Label>
</div>
<div className="flex items-center gap-2">
<RadioGroupItem value="option3" id="disabled-3" />
<Label htmlFor="disabled-3">Option 3</Label>
</div>
</RadioGroup>
),
};
export const SingleDisabledOption: Story = {
render: () => (
<RadioGroup defaultValue="option1">
<div className="flex items-center gap-2">
<RadioGroupItem value="option1" id="single-disabled-1" />
<Label htmlFor="single-disabled-1">Option 1</Label>
</div>
<div className="flex items-center gap-2">
<RadioGroupItem value="option2" id="single-disabled-2" disabled />
<Label htmlFor="single-disabled-2">Option 2 (Disabled)</Label>
</div>
<div className="flex items-center gap-2">
<RadioGroupItem value="option3" id="single-disabled-3" />
<Label htmlFor="single-disabled-3">Option 3</Label>
</div>
</RadioGroup>
),
};
export const PaymentMethod: Story = {
render: () => (
<div className="space-y-4">
<h3 className="text-lg font-medium">Payment Method</h3>
<RadioGroup defaultValue="credit-card">
<div className="flex items-center gap-2">
<RadioGroupItem value="credit-card" id="credit-card" />
<Label htmlFor="credit-card">Credit Card</Label>
</div>
<div className="flex items-center gap-2">
<RadioGroupItem value="paypal" id="paypal" />
<Label htmlFor="paypal">PayPal</Label>
</div>
<div className="flex items-center gap-2">
<RadioGroupItem value="bank-transfer" id="bank-transfer" />
<Label htmlFor="bank-transfer">Bank Transfer</Label>
</div>
<div className="flex items-center gap-2">
<RadioGroupItem value="crypto" id="crypto" disabled />
<Label htmlFor="crypto">Cryptocurrency (Coming Soon)</Label>
</div>
</RadioGroup>
</div>
),
};
export const SurveyQuestion: Story = {
render: () => (
<div className="w-[400px] space-y-4">
<div>
<h3 className="text-lg font-medium">How satisfied are you with our service?</h3>
<p className="text-muted-foreground mt-1 text-sm">
Please select one option that best describes your experience.
</p>
</div>
<RadioGroup>
<div className="flex items-center gap-2">
<RadioGroupItem value="very-satisfied" id="very-satisfied" />
<Label htmlFor="very-satisfied">Very satisfied</Label>
</div>
<div className="flex items-center gap-2">
<RadioGroupItem value="satisfied" id="satisfied" />
<Label htmlFor="satisfied">Satisfied</Label>
</div>
<div className="flex items-center gap-2">
<RadioGroupItem value="neutral" id="neutral" />
<Label htmlFor="neutral">Neutral</Label>
</div>
<div className="flex items-center gap-2">
<RadioGroupItem value="dissatisfied" id="dissatisfied" />
<Label htmlFor="dissatisfied">Dissatisfied</Label>
</div>
<div className="flex items-center gap-2">
<RadioGroupItem value="very-dissatisfied" id="very-dissatisfied" />
<Label htmlFor="very-dissatisfied">Very dissatisfied</Label>
</div>
</RadioGroup>
</div>
),
};
export const WithDescriptions: Story = {
render: () => (
<div className="space-y-4">
<h3 className="text-lg font-medium">Choose your plan</h3>
<RadioGroup defaultValue="basic">
<div className="space-y-2 rounded-lg border p-4">
<div className="flex items-center gap-2">
<RadioGroupItem value="basic" id="plan-basic" />
<Label htmlFor="plan-basic" className="font-medium">
Basic Plan
</Label>
</div>
<p className="text-muted-foreground ml-6 text-sm">
Perfect for individuals. Includes basic features and 5GB storage.
</p>
<p className="ml-6 text-sm font-medium">$9/month</p>
</div>
<div className="space-y-2 rounded-lg border p-4">
<div className="flex items-center gap-2">
<RadioGroupItem value="pro" id="plan-pro" />
<Label htmlFor="plan-pro" className="font-medium">
Pro Plan
</Label>
</div>
<p className="text-muted-foreground ml-6 text-sm">
Great for small teams. Advanced features and 50GB storage.
</p>
<p className="ml-6 text-sm font-medium">$29/month</p>
</div>
<div className="space-y-2 rounded-lg border p-4">
<div className="flex items-center gap-2">
<RadioGroupItem value="enterprise" id="plan-enterprise" />
<Label htmlFor="plan-enterprise" className="font-medium">
Enterprise Plan
</Label>
</div>
<p className="text-muted-foreground ml-6 text-sm">
For large organizations. Custom features and unlimited storage.
</p>
<p className="ml-6 text-sm font-medium">Contact sales</p>
</div>
</RadioGroup>
</div>
),
};
export const Required: Story = {
render: () => (
<div className="space-y-4">
<div>
<h3 className="text-lg font-medium">
Gender <span className="text-red-500">*</span>
</h3>
<p className="text-muted-foreground text-sm">This field is required</p>
</div>
<RadioGroup required>
<div className="flex items-center gap-2">
<RadioGroupItem value="male" id="gender-male" />
<Label htmlFor="gender-male">Male</Label>
</div>
<div className="flex items-center gap-2">
<RadioGroupItem value="female" id="gender-female" />
<Label htmlFor="gender-female">Female</Label>
</div>
<div className="flex items-center gap-2">
<RadioGroupItem value="other" id="gender-other" />
<Label htmlFor="gender-other">Other</Label>
</div>
<div className="flex items-center gap-2">
<RadioGroupItem value="prefer-not-to-say" id="gender-prefer-not" />
<Label htmlFor="gender-prefer-not">Prefer not to say</Label>
</div>
</RadioGroup>
</div>
),
};
export const WithRTL: Story = {
render: () => (
<div className="space-y-4">
<RadioGroup dir="rtl" required>
<div className="flex items-center gap-2">
<RadioGroupItem value="male" id="gender-male" />
<Label htmlFor="gender-male">Male</Label>
</div>
<div className="flex items-center gap-2">
<RadioGroupItem value="female" id="gender-female" />
<Label htmlFor="gender-female">Female</Label>
</div>
</RadioGroup>
</div>
),
};
export const WithErrorMessage: Story = {
render: () => (
<RadioGroup errorMessage="Please select an option">
<div className="flex items-center gap-2">
<RadioGroupItem value="option1" id="option1" />
<Label htmlFor="option1">Option 1</Label>
</div>
<div className="flex items-center gap-2">
<RadioGroupItem value="option2" id="option2" />
<Label htmlFor="option2">Option 2</Label>
</div>
<div className="flex items-center gap-2">
<RadioGroupItem value="option3" id="option3" />
<Label htmlFor="option3">Option 3</Label>
</div>
</RadioGroup>
),
};
export const WithErrorMessageAndRTL: Story = {
render: () => (
<RadioGroup errorMessage="يرجى اختيار الخيار" dir="rtl">
<div className="flex items-center gap-2">
<RadioGroupItem value="option1" id="option1" />
<Label htmlFor="option1">اختر الخيار 1</Label>
</div>
<div className="flex items-center gap-2">
<RadioGroupItem value="option2" id="option2" />
<Label htmlFor="option2">اختر الخيار 2</Label>
</div>
<div className="flex items-center gap-2">
<RadioGroupItem value="option3" id="option3" />
<Label htmlFor="option3">اختر الخيار 3</Label>
</div>
</RadioGroup>
),
};

View File

@@ -0,0 +1,60 @@
"use client";
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group";
import { AlertCircle, CircleIcon } from "lucide-react";
import * as React from "react";
import { cn } from "../lib/utils";
function RadioGroup({
className,
errorMessage,
dir,
...props
}: React.ComponentProps<typeof RadioGroupPrimitive.Root> & {
errorMessage?: string;
dir?: "ltr" | "rtl";
}): React.JSX.Element {
return (
<div className="flex gap-2" dir={dir}>
{errorMessage ? <div className="bg-destructive min-h-full w-1" /> : null}
<div className="space-y-2">
{errorMessage ? (
<div className="text-destructive flex items-center gap-1 text-sm">
<AlertCircle className="size-4" />
<span>{errorMessage}</span>
</div>
) : null}
<RadioGroupPrimitive.Root
aria-invalid={Boolean(errorMessage)}
data-slot="radio-group"
dir={dir}
className={cn("grid gap-3", className)}
{...props}
/>
</div>
</div>
);
}
function RadioGroupItem({
className,
...props
}: React.ComponentProps<typeof RadioGroupPrimitive.Item>): React.JSX.Element {
return (
<RadioGroupPrimitive.Item
data-slot="radio-group-item"
className={cn(
"border-primary text-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 shadow-xs aspect-square size-4 shrink-0 rounded-full border outline-none transition-[color,box-shadow] focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}>
<RadioGroupPrimitive.Indicator
data-slot="radio-group-indicator"
className="relative flex items-center justify-center">
<CircleIcon className="fill-primary absolute left-1/2 top-1/2 size-3 -translate-x-1/2 -translate-y-1/2" />
</RadioGroupPrimitive.Indicator>
</RadioGroupPrimitive.Item>
);
}
export { RadioGroup, RadioGroupItem };

View File

@@ -0,0 +1,153 @@
import type { Meta, StoryObj } from "@storybook/react";
import { Label } from "./label";
import { Textarea } from "./textarea";
const meta: Meta<typeof Textarea> = {
title: "UI-package/Textarea",
component: Textarea,
parameters: {
layout: "centered",
docs: {
description: {
component:
"A flexible textarea component with error handling, custom styling, and RTL support. Built with accessibility in mind using proper ARIA attributes and automatic resizing.",
},
},
},
tags: ["autodocs"],
argTypes: {
placeholder: {
control: { type: "text" },
description: "Placeholder text for the textarea",
},
disabled: {
control: { type: "boolean" },
description: "Whether the textarea is disabled",
},
required: {
control: { type: "boolean" },
description: "Whether the textarea is required",
},
rows: {
control: { type: "number" },
description: "Number of visible text lines",
},
errorMessage: {
control: "text",
description: "Error message to display below the textarea",
},
dir: {
control: { type: "select" },
options: ["ltr", "rtl"],
description: "Text direction",
},
style: {
control: { type: "object" },
description: "Custom styling for the textarea",
},
},
args: {
placeholder: "Enter your text...",
},
};
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: {},
};
export const WithValue: Story = {
args: {
value:
"This textarea has some predefined content that spans multiple lines.\n\nIt demonstrates how the component handles existing text.",
},
};
export const Disabled: Story = {
args: {
disabled: true,
value: "This textarea is disabled and cannot be edited.",
},
};
export const WithRows: Story = {
args: {
rows: 5,
placeholder: "This textarea has 5 visible rows...",
},
};
export const WithLabel: Story = {
render: () => (
<div className="w-[300px] space-y-2">
<Label htmlFor="message">Message</Label>
<Textarea id="message" placeholder="Enter your message..." />
</div>
),
};
export const LongContent: Story = {
render: () => (
<div className="w-[500px] space-y-2">
<Label htmlFor="long-content">Terms and Conditions</Label>
<Textarea
id="long-content"
rows={8}
readOnly
value={`Terms of Service
1. Acceptance of Terms
By accessing and using this service, you accept and agree to be bound by the terms and provision of this agreement.
2. Use License
Permission is granted to temporarily download one copy of the materials on this website for personal, non-commercial transitory viewing only.
3. Disclaimer
The materials on this website are provided on an 'as is' basis. We make no warranties, expressed or implied, and hereby disclaim and negate all other warranties including without limitation, implied warranties or conditions of merchantability, fitness for a particular purpose, or non-infringement of intellectual property or other violation of rights.
4. Limitations
In no event shall our company or its suppliers be liable for any damages (including, without limitation, damages for loss of data or profit, or due to business interruption) arising out of the use or inability to use the materials on this website.`}
className="font-mono text-sm"
/>
</div>
),
};
export const WithError: Story = {
args: {
placeholder: "Enter your message",
defaultValue: "Too short",
errorMessage: "Message must be at least 10 characters long",
},
};
export const RTL: Story = {
args: {
dir: "rtl",
placeholder: "أدخل رسالتك هنا",
defaultValue: "نص تجريبي طويل",
},
};
export const CustomStyling: Story = {
args: {
placeholder: "Custom styled textarea",
style: {
height: "120px",
borderRadius: "12px",
padding: "16px",
backgroundColor: "#f8f9fa",
border: "2px solid #e9ecef",
},
},
};
export const WithErrorAndRTL: Story = {
args: {
dir: "rtl",
placeholder: "أدخل رسالتك",
errorMessage: "هذا الحقل مطلوب",
},
};

View File

@@ -0,0 +1,37 @@
import { AlertCircle } from "lucide-react";
import * as React from "react";
import { cn } from "../lib/utils";
type TextareaProps = React.ComponentProps<"textarea"> & {
dir?: "ltr" | "rtl";
errorMessage?: string;
style?: React.CSSProperties;
};
function Textarea({ className, errorMessage, dir, style, ...props }: TextareaProps): React.JSX.Element {
const hasError = Boolean(errorMessage);
return (
<div className="space-y-1">
{errorMessage ? (
<div className="text-destructive flex items-center gap-1 text-sm" dir={dir}>
<AlertCircle className="size-4" />
<span>{errorMessage}</span>
</div>
) : null}
<textarea
data-slot="textarea"
dir={dir}
style={style}
aria-invalid={hasError || undefined}
className={cn(
"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 field-sizing-content shadow-xs flex min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base outline-none transition-[color,box-shadow] focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className
)}
{...props}
/>
</div>
);
}
export { Textarea };

View File

@@ -1 +1,2 @@
export { Button, buttonVariants } from "./components/button";
export { Input, type InputProps } from "./components/input";

View File

@@ -1,97 +1,57 @@
@import "tailwindcss";
@import "tw-animate-css";
:root {
--background: hsl(0 0% 100%);
/* Base variables used by other variables */
--foreground: hsl(222.2 84% 4.9%);
--card: hsl(0 0% 100%);
--card-foreground: hsl(222.2 84% 4.9%);
--popover: hsl(0 0% 100%);
--popover-foreground: hsl(222.2 84% 4.9%);
--primary: hsl(222.2 47.4% 11.2%);
--primary-foreground: hsl(210 40% 98%);
--secondary: hsl(210 40% 96.1%);
--secondary-foreground: hsl(222.2 47.4% 11.2%);
--muted: hsl(210 40% 96.1%);
--muted-foreground: hsl(215.4 16.3% 46.9%);
--accent: hsl(210 40% 96.1%);
--accent-foreground: hsl(222.2 47.4% 11.2%);
--destructive: hsl(0 84.2% 60.2%);
--destructive-foreground: hsl(210 40% 98%);
--border: hsl(214.3 31.8% 91.4%);
--input: hsl(214.3 31.8% 91.4%);
--ring: hsl(222.2 84% 4.9%);
--radius: 0.5rem;
--chart-1: hsl(12 76% 61%);
--chart-2: hsl(173 58% 39%);
--chart-3: hsl(197 37% 24%);
--chart-4: hsl(43 74% 66%);
--chart-5: hsl(27 87% 67%);
/* Question Headline CSS variables */
--fb-question-headline-font-family: inherit;
--fb-question-headline-font-weight: 600;
--fb-question-headline-font-size: 1.125rem;
--fb-question-headline-color: hsl(222.2 84% 4.9%);
--fb-question-headline-opacity: 1;
/* Question Description CSS variables */
--fb-question-description-font-family: inherit;
--fb-question-description-font-weight: 400;
--fb-question-description-font-size: 0.875rem;
--fb-question-description-color: hsl(215.4 16.3% 46.9%);
--fb-question-description-opacity: 1;
/* Label CSS variables */
--fb-label-font-family: inherit;
--fb-label-font-weight: 500;
--fb-label-font-size: 0.875rem;
--fb-label-color: var(--foreground);
--fb-label-opacity: 1;
/* Button CSS variables */
--fb-button-height: 2.25rem;
--fb-button-width: auto;
--fb-button-font-size: 0.875rem;
--fb-button-border-radius: var(--radius);
--fb-button-bg-color: hsl(222.2 47.4% 11.2%);
--fb-button-text-color: hsl(210 40% 98%);
--fb-button-padding-x: 1rem;
--fb-button-padding-y: 0.5rem;
/* Input CSS variables */
--fb-input-bg-color: transparent;
--fb-input-bg-opacity: 1;
--fb-input-hover-bg-color: var(--input);
--fb-input-hover-bg-opacity: 0.3;
--fb-input-border-color: var(--input);
--fb-input-border-radius: var(--radius);
--fb-input-font-family: inherit;
--fb-input-font-size: 0.875rem;
--fb-input-font-weight: 400;
--fb-input-color: var(--foreground);
--fb-input-placeholder-color: var(--muted-foreground);
--fb-input-placeholder-opacity: 1;
--fb-input-width: 100%;
--fb-input-height: 2.25rem;
--fb-input-padding-x: 0.75rem;
--fb-input-padding-y: 0.25rem;
--fb-input-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
}
.dark {
--background: hsl(222.2 84% 4.9%);
--foreground: hsl(210 40% 98%);
--card: hsl(222.2 84% 4.9%);
--card-foreground: hsl(210 40% 98%);
--popover: hsl(222.2 84% 4.9%);
--popover-foreground: hsl(210 40% 98%);
--primary: hsl(210 40% 98%);
--primary-foreground: hsl(222.2 47.4% 11.2%);
--secondary: hsl(217.2 32.6% 17.5%);
--secondary-foreground: hsl(210 40% 98%);
--muted: hsl(217.2 32.6% 17.5%);
--muted-foreground: hsl(215 20.2% 65.1%);
--accent: hsl(217.2 32.6% 17.5%);
--accent-foreground: hsl(210 40% 98%);
--destructive: hsl(0 62.8% 30.6%);
--destructive-foreground: hsl(210 40% 98%);
--border: hsl(217.2 32.6% 17.5%);
--input: hsl(217.2 32.6% 17.5%);
--ring: hsl(212.7 26.8% 83.9%);
--chart-1: hsl(220 70% 50%);
--chart-2: hsl(160 60% 45%);
--chart-3: hsl(30 80% 55%);
--chart-4: hsl(280 65% 60%);
--chart-5: hsl(340 75% 55%);
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--radius-lg: var(--radius);
--radius-md: calc(var(--radius) - 2px);
--radius-sm: calc(var(--radius) - 4px);
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}

91
pnpm-lock.yaml generated
View File

@@ -800,6 +800,9 @@ importers:
'@radix-ui/react-checkbox':
specifier: 1.3.1
version: 1.3.1(@types/react-dom@19.1.0(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-label':
specifier: 2.1.1
version: 2.1.1(@types/react-dom@19.1.0(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-progress':
specifier: 1.1.8
version: 1.1.8(@types/react-dom@19.1.0(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
@@ -815,6 +818,9 @@ importers:
clsx:
specifier: ^2.1.1
version: 2.1.1
lucide-react:
specifier: 0.555.0
version: 0.555.0(react@19.1.0)
tailwind-merge:
specifier: ^2.5.5
version: 2.6.0
@@ -3141,6 +3147,15 @@ packages:
'@types/react-dom':
optional: true
'@radix-ui/react-compose-refs@1.1.1':
resolution: {integrity: sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@radix-ui/react-compose-refs@1.1.2':
resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==}
peerDependencies:
@@ -3247,6 +3262,19 @@ packages:
'@types/react':
optional: true
'@radix-ui/react-label@2.1.1':
resolution: {integrity: sha512-UUw5E4e/2+4kFMH7+YxORXGWggtY6sM8WIwh5RZchhLuUg2H1hc98Py+pr8HMz6rdaYrK2t296ZEjYLOCO5uUw==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-label@2.1.6':
resolution: {integrity: sha512-S/hv1mTlgcPX2gCTJrWuTjSXf7ER3Zf7zWGtOprxhIIY93Qin3n5VgNA0Ez9AgrK/lEtlYgzLd4f5x6AVar4Yw==}
peerDependencies:
@@ -3325,6 +3353,19 @@ packages:
'@types/react-dom':
optional: true
'@radix-ui/react-primitive@2.0.1':
resolution: {integrity: sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-primitive@2.1.2':
resolution: {integrity: sha512-uHa+l/lKfxuDD2zjN/0peM/RhhSmRjr5YWdk/37EnSv1nJ88uvG85DPexSm8HdFQROd2VdERJ6ynXbkCFi+APw==}
peerDependencies:
@@ -3442,6 +3483,15 @@ packages:
'@types/react-dom':
optional: true
'@radix-ui/react-slot@1.1.1':
resolution: {integrity: sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@radix-ui/react-slot@1.2.2':
resolution: {integrity: sha512-y7TBO4xN4Y94FvcWIOIh18fM4R1A8S4q1jhoz4PNzOoHsFcN8pogcFmZrTYAm4F9VRUrWP/Mw7xSKybIeRI+CQ==}
peerDependencies:
@@ -7654,6 +7704,11 @@ packages:
peerDependencies:
react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
lucide-react@0.555.0:
resolution: {integrity: sha512-D8FvHUGbxWBRQM90NZeIyhAvkFfsh3u9ekrMvJ30Z6gnpBHS6HC6ldLg7tL45hwiIz/u66eKDtdA23gwwGsAHA==}
peerDependencies:
react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
lz-string@1.5.0:
resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==}
hasBin: true
@@ -7983,6 +8038,7 @@ packages:
next@15.5.7:
resolution: {integrity: sha512-+t2/0jIJ48kUpGKkdlhgkv+zPTEOoXyr60qXe68eB/pl3CMJaLeIGjzp5D6Oqt25hCBiBTt8wEeeAzfJvUKnPQ==}
engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0}
deprecated: This version has a security vulnerability. Please upgrade to a patched version. See https://nextjs.org/blog/CVE-2025-66478 for more details.
hasBin: true
peerDependencies:
'@opentelemetry/api': ^1.1.0
@@ -13597,6 +13653,12 @@ snapshots:
'@types/react': 19.1.4
'@types/react-dom': 19.1.5(@types/react@19.1.4)
'@radix-ui/react-compose-refs@1.1.1(@types/react@19.1.4)(react@19.1.0)':
dependencies:
react: 19.1.0
optionalDependencies:
'@types/react': 19.1.4
'@radix-ui/react-compose-refs@1.1.2(@types/react@19.1.4)(react@19.1.0)':
dependencies:
react: 19.1.0
@@ -13720,6 +13782,15 @@ snapshots:
optionalDependencies:
'@types/react': 19.1.4
'@radix-ui/react-label@2.1.1(@types/react-dom@19.1.0(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
dependencies:
'@radix-ui/react-primitive': 2.0.1(@types/react-dom@19.1.0(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
react: 19.1.0
react-dom: 19.1.0(react@19.1.0)
optionalDependencies:
'@types/react': 19.1.4
'@types/react-dom': 19.1.0(@types/react@19.1.4)
'@radix-ui/react-label@2.1.6(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)':
dependencies:
'@radix-ui/react-primitive': 2.1.2(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)
@@ -13826,6 +13897,15 @@ snapshots:
'@types/react': 19.1.4
'@types/react-dom': 19.1.5(@types/react@19.1.4)
'@radix-ui/react-primitive@2.0.1(@types/react-dom@19.1.0(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
dependencies:
'@radix-ui/react-slot': 1.1.1(@types/react@19.1.4)(react@19.1.0)
react: 19.1.0
react-dom: 19.1.0(react@19.1.0)
optionalDependencies:
'@types/react': 19.1.4
'@types/react-dom': 19.1.0(@types/react@19.1.4)
'@radix-ui/react-primitive@2.1.2(@types/react-dom@19.1.0(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
dependencies:
'@radix-ui/react-slot': 1.2.2(@types/react@19.1.4)(react@19.1.0)
@@ -13999,6 +14079,13 @@ snapshots:
'@types/react': 19.1.4
'@types/react-dom': 19.1.5(@types/react@19.1.4)
'@radix-ui/react-slot@1.1.1(@types/react@19.1.4)(react@19.1.0)':
dependencies:
'@radix-ui/react-compose-refs': 1.1.1(@types/react@19.1.4)(react@19.1.0)
react: 19.1.0
optionalDependencies:
'@types/react': 19.1.4
'@radix-ui/react-slot@1.2.2(@types/react@19.1.4)(react@19.1.0)':
dependencies:
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.4)(react@19.1.0)
@@ -18860,6 +18947,10 @@ snapshots:
dependencies:
react: 19.1.2
lucide-react@0.555.0(react@19.1.0):
dependencies:
react: 19.1.0
lz-string@1.5.0: {}
magic-string@0.27.0:

View File

@@ -100,6 +100,19 @@
"dependsOn": ["@formbricks/surveys#build"],
"persistent": true
},
"@formbricks/ui#build": {
"dependsOn": ["^build"],
"outputs": ["dist/**"]
},
"@formbricks/ui#build:dev": {
"dependsOn": ["^build:dev"],
"outputs": ["dist/**"]
},
"@formbricks/ui#go": {
"cache": false,
"dependsOn": ["@formbricks/ui#build"],
"persistent": true
},
"@formbricks/web#go": {
"cache": false,
"dependsOn": ["@formbricks/database#db:setup"],