mirror of
https://github.com/formbricks/formbricks.git
synced 2025-12-30 16:16:21 -06:00
Merge branch 'epic/survey-ui-package' of https://github.com/formbricks/formbricks into epic/survey-ui-package
This commit is contained in:
@@ -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";
|
||||
|
||||
|
||||
@@ -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:*",
|
||||
|
||||
66
packages/survey-ui/src/components/alert.stories.tsx
Normal file
66
packages/survey-ui/src/components/alert.stories.tsx
Normal 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>
|
||||
),
|
||||
};
|
||||
54
packages/survey-ui/src/components/alert.tsx
Normal file
54
packages/survey-ui/src/components/alert.tsx
Normal 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 };
|
||||
@@ -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",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
|
||||
90
packages/survey-ui/src/components/checkbox.stories.tsx
Normal file
90
packages/survey-ui/src/components/checkbox.stories.tsx
Normal 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>
|
||||
),
|
||||
};
|
||||
29
packages/survey-ui/src/components/checkbox.tsx
Normal file
29
packages/survey-ui/src/components/checkbox.tsx
Normal 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 };
|
||||
323
packages/survey-ui/src/components/input.stories.tsx
Normal file
323
packages/survey-ui/src/components/input.stories.tsx
Normal 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: "هذا الحقل مطلوب",
|
||||
},
|
||||
};
|
||||
72
packages/survey-ui/src/components/input.tsx
Normal file
72
packages/survey-ui/src/components/input.tsx
Normal 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 };
|
||||
396
packages/survey-ui/src/components/label.stories.tsx
Normal file
396
packages/survey-ui/src/components/label.stories.tsx
Normal 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],
|
||||
};
|
||||
62
packages/survey-ui/src/components/label.tsx
Normal file
62
packages/survey-ui/src/components/label.tsx
Normal 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 };
|
||||
90
packages/survey-ui/src/components/progress.stories.tsx
Normal file
90
packages/survey-ui/src/components/progress.stories.tsx
Normal 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",
|
||||
},
|
||||
},
|
||||
};
|
||||
39
packages/survey-ui/src/components/progress.tsx
Normal file
39
packages/survey-ui/src/components/progress.tsx
Normal 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 };
|
||||
312
packages/survey-ui/src/components/radio-group.stories.tsx
Normal file
312
packages/survey-ui/src/components/radio-group.stories.tsx
Normal 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>
|
||||
),
|
||||
};
|
||||
60
packages/survey-ui/src/components/radio-group.tsx
Normal file
60
packages/survey-ui/src/components/radio-group.tsx
Normal 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 };
|
||||
153
packages/survey-ui/src/components/textarea.stories.tsx
Normal file
153
packages/survey-ui/src/components/textarea.stories.tsx
Normal 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: "هذا الحقل مطلوب",
|
||||
},
|
||||
};
|
||||
37
packages/survey-ui/src/components/textarea.tsx
Normal file
37
packages/survey-ui/src/components/textarea.tsx
Normal 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 };
|
||||
@@ -1 +1,2 @@
|
||||
export { Button, buttonVariants } from "./components/button";
|
||||
export { Input, type InputProps } from "./components/input";
|
||||
|
||||
@@ -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
91
pnpm-lock.yaml
generated
@@ -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:
|
||||
|
||||
13
turbo.json
13
turbo.json
@@ -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"],
|
||||
|
||||
Reference in New Issue
Block a user