Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 85 additions & 0 deletions libs/ui/src/atoms/status-text.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import type { HTMLAttributes, ReactNode, Ref } from "react"
import type { VariantProps } from "tailwind-variants"
import { tv } from "../utils"
import { Icon } from "./icon"

const statusTextVariants = tv({
slots: {
base: "flex items-center",
icon: "self-start"
},
variants: {
status: {
error: "text-status-text-fg-error",
success: "text-status-text-fg-success",
warning: "text-status-text-fg-warning",
default: "text-status-text-fg-default",
},
size: {
sm: {
base: "text-status-text-sm gap-status-text-icon-gap-sm",
icon: "mt-status-text-icon-sm",
},
md: {
base: "text-status-text-md gap-status-text-icon-gap-md",
icon: "mt-status-text-icon-md",
},
lg: {
base: "text-status-text-lg gap-status-text-icon-gap-lg",
icon: "mt-status-text-icon-lg",
},
},
},
defaultVariants: {
status: "default",
size: "md",
},
})

const ICON_MAP = {
error: "token-icon-status-text-error",
success: "token-icon-status-text-success",
warning: "token-icon-status-text-warning",
default: undefined,
} as const

export interface StatusTextProps
extends HTMLAttributes<HTMLDivElement>,
VariantProps<typeof statusTextVariants> {
ref?: Ref<HTMLDivElement>
showIcon?: boolean
children: ReactNode
}

export function StatusText({
className,
showIcon = false,
status = "default",
size = "md",
children,
ref,
...props
}: StatusTextProps) {
const icon = ICON_MAP[status]

const {base, icon: iconSlot} = statusTextVariants({
status,
size,
className,
})

return (
<div
className={base({
status,
size,
className,
})}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
ref={ref}
{...props}
>
{showIcon && icon && <Icon size={size} icon={icon} className={iconSlot()} />}
<span>{children}</span>
</div>
)
}
33 changes: 33 additions & 0 deletions libs/ui/src/tokens/components/atoms/_status-text.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
@theme {
/* Status colors */
--color-status-text-fg-error: var(--color-danger);
--color-status-text-fg-success: var(--color-success);
--color-status-text-fg-warning: var(--color-warning);
--color-status-text-fg-default: var(--color-fg-secondary);

/* Text sizes - inherit from input sizing */
--text-status-text-sm: var(--text-input-sm);
--text-status-text-md: var(--text-input-md);
--text-status-text-lg: var(--text-input-lg);

/* Icon gap - spacing between icon and text */
--spacing-status-text-icon-gap-sm: var(--spacing-50);
--spacing-status-text-icon-gap-md: var(--spacing-100);
--spacing-status-text-icon-gap-lg: var(--spacing-200);

--spacing-status-text-icon-sm: var(--spacing-50);
--spacing-status-text-icon-md: var(--spacing-100);
--spacing-status-text-icon-lg: var(--spacing-200);
}

@utility token-icon-status-text-success {
@apply token-icon-success;
}

@utility token-icon-status-text-warning {
@apply token-icon-warning;
}

@utility token-icon-status-text-error {
@apply token-icon-error;
}
Comment thread
KaiUweCZE marked this conversation as resolved.
Resolved
1 change: 1 addition & 0 deletions libs/ui/src/tokens/components/components.css
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
@import "./atoms/_button.css";
@import "./atoms/_error-text.css";
@import "./atoms/_extra-text.css";
@import "./atoms/_status-text.css";
@import "./atoms/_checkbox.css";
@import "./atoms/_icon.css";
@import "./atoms/_input.css";
Expand Down
255 changes: 255 additions & 0 deletions libs/ui/stories/atoms/status-text.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
import type { Meta, StoryObj } from "@storybook/react"
import { VariantContainer, VariantGroup } from "../../.storybook/decorator"
import { StatusText } from "../../src/atoms/status-text"

const meta: Meta<typeof StatusText> = {
title: "Atoms/StatusText",
component: StatusText,
parameters: {
layout: "centered",
docs: {
description: {
component:
"StatusText component for displaying validation and status messages with appropriate icons and colors. Supports error, success, warning, and default states.",
},
},
},
tags: ["autodocs"],
argTypes: {
status: {
control: "select",
options: ["default", "error", "success", "warning"],
description: "Status type that determines color and icon",
},
size: {
control: "select",
options: ["sm", "md", "lg"],
description: "Text size",
},
showIcon: {
control: "boolean",
description: "Whether to display status icon",
},
},
}

export default meta
type Story = StoryObj<typeof StatusText>

export const Default: Story = {
args: {
children: "This is default status text",
status: "default",
showIcon: false,
},
}

export const Error: Story = {
args: {
children: "Invalid email format",
status: "error",
showIcon: true,
},
}

export const Success: Story = {
args: {
children: "Username is available",
status: "success",
showIcon: true,
},
}

export const Warning: Story = {
args: {
children: "Password is weak",
status: "warning",
showIcon: true,
},
}

export const AllStatuses: Story = {
render: () => (
<VariantContainer>
<VariantGroup title="Status Variants - With Icons">
<div className="flex flex-col gap-4">
<StatusText status="error" showIcon>
Invalid email format
</StatusText>
<StatusText status="success" showIcon>
Username is available
</StatusText>
<StatusText status="warning" showIcon>
Password is weak
</StatusText>
<StatusText status="default" showIcon={false}>
This is helper text
</StatusText>
</div>
</VariantGroup>

<VariantGroup title="Status Variants - Without Icons">
<div className="flex flex-col gap-4">
<StatusText status="error" showIcon={false}>
Invalid email format
</StatusText>
<StatusText status="success" showIcon={false}>
Username is available
</StatusText>
<StatusText status="warning" showIcon={false}>
Password is weak
</StatusText>
<StatusText status="default" showIcon={false}>
This is helper text
</StatusText>
</div>
</VariantGroup>
</VariantContainer>
),
}

export const Sizes: Story = {
render: () => (
<VariantContainer>
<VariantGroup title="Small">
<div className="flex flex-col gap-4">
<StatusText status="error" showIcon size="sm">
Small error message
</StatusText>
<StatusText status="success" showIcon size="sm">
Small success message
</StatusText>
<StatusText status="warning" showIcon size="sm">
Small warning message
</StatusText>
</div>
</VariantGroup>

<VariantGroup title="Medium (Default)">
<div className="flex flex-col gap-4">
<StatusText status="error" showIcon size="md">
Medium error message
</StatusText>
<StatusText status="success" showIcon size="md">
Medium success message
</StatusText>
<StatusText status="warning" showIcon size="md">
Medium warning message
</StatusText>
</div>
</VariantGroup>

<VariantGroup title="Large">
<div className="flex flex-col gap-4">
<StatusText status="error" showIcon size="lg">
Large error message
</StatusText>
<StatusText status="success" showIcon size="lg">
Large success message
</StatusText>
<StatusText status="warning" showIcon size="lg">
Large warning message
</StatusText>
</div>
</VariantGroup>
</VariantContainer>
),
}

export const FormValidationContext: Story = {
render: () => (
<div className="flex w-full max-w-md flex-col gap-6">
<div className="flex flex-col gap-2">
<label className="font-medium text-sm" htmlFor="email">
Email
</label>
<input
className="rounded border border-red-500 px-3 py-2"
id="email"
placeholder="your@email.com"
type="email"
/>
<StatusText status="error" showIcon>
Please enter a valid email address
</StatusText>
</div>

<div className="flex flex-col gap-2">
<label className="font-medium text-sm" htmlFor="username">
Username
</label>
<input
className="rounded border border-green-500 px-3 py-2"
id="username"
placeholder="johndoe"
type="text"
/>
<StatusText status="success" showIcon>
Username is available
</StatusText>
</div>

<div className="flex flex-col gap-2">
<label className="font-medium text-sm" htmlFor="password">
Password
</label>
<input
className="rounded border border-yellow-500 px-3 py-2"
id="password"
placeholder="••••••••"
type="password"
/>
<StatusText status="warning" showIcon>
Consider using a stronger password
</StatusText>
</div>

<div className="flex flex-col gap-2">
<label className="font-medium text-sm" htmlFor="phone">
Phone (Optional)
</label>
<input
className="rounded border border-gray-300 px-3 py-2"
id="phone"
placeholder="+1 (555) 123-4567"
type="tel"
/>
<StatusText status="default" showIcon={false}>
We'll use this for account recovery
</StatusText>
</div>
</div>
),
}

export const LongText: Story = {
render: () => (
<div className="w-80">
<VariantContainer>
<VariantGroup title="Long Error Message">
<StatusText status="error" showIcon>
Your password must contain at least 8 characters, including one
uppercase letter, one lowercase letter, one number, and one special
character (!@#$%^&*).
</StatusText>
</VariantGroup>

<VariantGroup title="Long Success Message">
<StatusText status="success" showIcon>
Your account has been successfully created! We've sent a
confirmation email to your address. Please check your inbox and
click the verification link.
</StatusText>
</VariantGroup>

<VariantGroup title="Long Warning Message">
<StatusText status="warning" showIcon>
This action cannot be undone. Please make sure you have backed up
your data before proceeding. We recommend downloading a copy of your
information first.
</StatusText>
</VariantGroup>
</VariantContainer>
</div>
),
}
Loading