From 7a4124434dbcd826da4789e6a7123faa3c152f4c Mon Sep 17 00:00:00 2001 From: KaiUweCZE Date: Fri, 26 Dec 2025 18:17:01 +0100 Subject: [PATCH 1/5] feat(ui): add StatusText component with styles and stories Introduce a new StatusText atomic to display contextual validation/status messages with optional icons. Add Tailwind-variants configuration for status and size variants, an ICON_MAP for status->icon mapping, and a simple component API supporting showIcon, status, and size props. Expose variant classes via the tv utility and forward props/ref. Add theme tokens and component CSS for status text: - reference tokens for error/success/warning/default colors - derived tokens for text sizes and icon gap Import the new CSS into the components bundle. Add comprehensive Storybook stories demonstrating usage and variants (default, error, success, warning) and controls for status, size, and showIcon. This change provides standardized status messaging UI and theming so callers can consistently render validation and status feedback. --- libs/ui/src/atoms/status-text.tsx | 67 +++++ .../tokens/components/atoms/_status-text.css | 21 ++ libs/ui/src/tokens/components/components.css | 1 + libs/ui/stories/atoms/status-text.stories.tsx | 260 ++++++++++++++++++ 4 files changed, 349 insertions(+) create mode 100644 libs/ui/src/atoms/status-text.tsx create mode 100644 libs/ui/src/tokens/components/atoms/_status-text.css create mode 100644 libs/ui/stories/atoms/status-text.stories.tsx diff --git a/libs/ui/src/atoms/status-text.tsx b/libs/ui/src/atoms/status-text.tsx new file mode 100644 index 0000000000..18c701d241 --- /dev/null +++ b/libs/ui/src/atoms/status-text.tsx @@ -0,0 +1,67 @@ +import type { HTMLAttributes, ReactNode, Ref } from "react" +import type { VariantProps } from "tailwind-variants" +import { tv } from "../utils" +import { Icon } from "./icon" + +const statusTextVariants = tv({ + base: ["flex items-center gap-status-text-icon-gap"], + 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: "text-status-text-sm", + md: "text-status-text-md", + lg: "text-status-text-lg", + }, + }, + defaultVariants: { + status: "default", + size: "md", + }, +}) + +const ICON_MAP = { + error: "token-icon-error", + success: "token-icon-success", + warning: "token-icon-warning", + default: undefined, +} as const + +export interface StatusTextProps + extends HTMLAttributes, + VariantProps { + ref?: Ref + showIcon?: boolean + children: ReactNode +} + +export function StatusText({ + className, + showIcon = false, + status = "default", + size = "md", + children, + ref, + ...props +}: StatusTextProps) { + const icon = ICON_MAP[status ?? "default"] + + return ( +
+ {showIcon && icon && } + {children} +
+ ) +} diff --git a/libs/ui/src/tokens/components/atoms/_status-text.css b/libs/ui/src/tokens/components/atoms/_status-text.css new file mode 100644 index 0000000000..365a18d603 --- /dev/null +++ b/libs/ui/src/tokens/components/atoms/_status-text.css @@ -0,0 +1,21 @@ +@theme { + /* === Reference Layer === */ + /* Maps to semantic tokens for themeable properties */ + + /* 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); + + /* === Derived Tokens === */ + /* Component-specific tokens derived from reference layer */ + + /* 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: var(--spacing-50); +} diff --git a/libs/ui/src/tokens/components/components.css b/libs/ui/src/tokens/components/components.css index 661ecf84c9..b4c7cf5c77 100644 --- a/libs/ui/src/tokens/components/components.css +++ b/libs/ui/src/tokens/components/components.css @@ -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"; diff --git a/libs/ui/stories/atoms/status-text.stories.tsx b/libs/ui/stories/atoms/status-text.stories.tsx new file mode 100644 index 0000000000..0c00461615 --- /dev/null +++ b/libs/ui/stories/atoms/status-text.stories.tsx @@ -0,0 +1,260 @@ +import type { Meta, StoryObj } from "@storybook/react" +import { VariantContainer, VariantGroup } from "../../.storybook/decorator" +import { StatusText } from "../../src/atoms/status-text" + +const meta: Meta = { + 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 + +// Basic usage examples +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, + }, +} + +// All status variants +export const AllStatuses: Story = { + render: () => ( + + +
+ + Invalid email format + + + Username is available + + + Password is weak + + + This is helper text + +
+
+ + +
+ + Invalid email format + + + Username is available + + + Password is weak + + + This is helper text + +
+
+
+ ), +} + +// Size variants +export const Sizes: Story = { + render: () => ( + + +
+ + Small error message + + + Small success message + + + Small warning message + +
+
+ + +
+ + Medium error message + + + Medium success message + + + Medium warning message + +
+
+ + +
+ + Large error message + + + Large success message + + + Large warning message + +
+
+
+ ), +} + +// Form validation context +export const FormValidationContext: Story = { + render: () => ( +
+
+ + + + Please enter a valid email address + +
+ +
+ + + + Username is available + +
+ +
+ + + + Consider using a stronger password + +
+ +
+ + + + We'll use this for account recovery + +
+
+ ), +} + +// Long text wrapping +export const LongText: Story = { + render: () => ( +
+ + + + Your password must contain at least 8 characters, including one + uppercase letter, one lowercase letter, one number, and one special + character (!@#$%^&*). + + + + + + Your account has been successfully created! We've sent a + confirmation email to your address. Please check your inbox and + click the verification link. + + + + + + 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. + + + +
+ ), +} From 7c213220bf06183fa1fa735d8bbc3696ef80cfeb Mon Sep 17 00:00:00 2001 From: KaiUweCZE Date: Fri, 26 Dec 2025 18:27:11 +0100 Subject: [PATCH 2/5] refactor(ui): unify status-text spacing, tokens, and story cleanup --- libs/ui/src/atoms/status-text.tsx | 14 +++++------ .../tokens/components/atoms/_status-text.css | 24 ++++++++++++------- libs/ui/stories/atoms/status-text.stories.tsx | 5 ---- 3 files changed, 23 insertions(+), 20 deletions(-) diff --git a/libs/ui/src/atoms/status-text.tsx b/libs/ui/src/atoms/status-text.tsx index 18c701d241..3506929445 100644 --- a/libs/ui/src/atoms/status-text.tsx +++ b/libs/ui/src/atoms/status-text.tsx @@ -4,7 +4,7 @@ import { tv } from "../utils" import { Icon } from "./icon" const statusTextVariants = tv({ - base: ["flex items-center gap-status-text-icon-gap"], + base: ["flex items-center"], variants: { status: { error: "text-status-text-fg-error", @@ -13,9 +13,9 @@ const statusTextVariants = tv({ default: "text-status-text-fg-default", }, size: { - sm: "text-status-text-sm", - md: "text-status-text-md", - lg: "text-status-text-lg", + sm: "text-status-text-sm gap-status-text-icon-gap-sm", + md: "text-status-text-md gap-status-text-icon-gap-md", + lg: "text-status-text-lg gap-status-text-icon-gap-lg", }, }, defaultVariants: { @@ -25,9 +25,9 @@ const statusTextVariants = tv({ }) const ICON_MAP = { - error: "token-icon-error", - success: "token-icon-success", - warning: "token-icon-warning", + error: "token-icon-status-text-error", + success: "token-icon-status-text-success", + warning: "token-icon-status-text-warning", default: undefined, } as const diff --git a/libs/ui/src/tokens/components/atoms/_status-text.css b/libs/ui/src/tokens/components/atoms/_status-text.css index 365a18d603..ad291cf2ee 100644 --- a/libs/ui/src/tokens/components/atoms/_status-text.css +++ b/libs/ui/src/tokens/components/atoms/_status-text.css @@ -1,21 +1,29 @@ @theme { - /* === Reference Layer === */ - /* Maps to semantic tokens for themeable properties */ - /* 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); - - /* === Derived Tokens === */ - /* Component-specific tokens derived from reference layer */ - + /* 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: var(--spacing-50); + --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); +} + +@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; +} \ No newline at end of file diff --git a/libs/ui/stories/atoms/status-text.stories.tsx b/libs/ui/stories/atoms/status-text.stories.tsx index 0c00461615..6e35f9dffa 100644 --- a/libs/ui/stories/atoms/status-text.stories.tsx +++ b/libs/ui/stories/atoms/status-text.stories.tsx @@ -36,7 +36,6 @@ const meta: Meta = { export default meta type Story = StoryObj -// Basic usage examples export const Default: Story = { args: { children: "This is default status text", @@ -69,7 +68,6 @@ export const Warning: Story = { }, } -// All status variants export const AllStatuses: Story = { render: () => ( @@ -110,7 +108,6 @@ export const AllStatuses: Story = { ), } -// Size variants export const Sizes: Story = { render: () => ( @@ -159,7 +156,6 @@ export const Sizes: Story = { ), } -// Form validation context export const FormValidationContext: Story = { render: () => (
@@ -226,7 +222,6 @@ export const FormValidationContext: Story = { ), } -// Long text wrapping export const LongText: Story = { render: () => (
From b019cdbf441cbc0bd23955c545c8ca9473203d39 Mon Sep 17 00:00:00 2001 From: KaiUweCZE Date: Sat, 27 Dec 2025 17:38:14 +0100 Subject: [PATCH 3/5] fix(ui): add icon slot and spacing token for status text component For long texts, align the icon with the first line of text. --- libs/ui/src/atoms/status-text.tsx | 15 ++++++++++++--- .../src/tokens/components/atoms/_status-text.css | 2 ++ 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/libs/ui/src/atoms/status-text.tsx b/libs/ui/src/atoms/status-text.tsx index 3506929445..0021d39999 100644 --- a/libs/ui/src/atoms/status-text.tsx +++ b/libs/ui/src/atoms/status-text.tsx @@ -4,7 +4,10 @@ import { tv } from "../utils" import { Icon } from "./icon" const statusTextVariants = tv({ - base: ["flex items-center"], + slots: { + base: "flex items-center", + icon: "self-start mt-status-text-icon-margin" + }, variants: { status: { error: "text-status-text-fg-error", @@ -50,9 +53,15 @@ export function StatusText({ }: StatusTextProps) { const icon = ICON_MAP[status ?? "default"] + const {base, icon: iconSlot} = statusTextVariants({ + status, + size, + className, + }) + return (
- {showIcon && icon && } + {showIcon && icon && } {children}
) diff --git a/libs/ui/src/tokens/components/atoms/_status-text.css b/libs/ui/src/tokens/components/atoms/_status-text.css index ad291cf2ee..24a9753a53 100644 --- a/libs/ui/src/tokens/components/atoms/_status-text.css +++ b/libs/ui/src/tokens/components/atoms/_status-text.css @@ -14,6 +14,8 @@ --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-margin: var(--spacing-100); } @utility token-icon-status-text-success { From 90cd415f7a1c3ce181821737cc49bf3556751275 Mon Sep 17 00:00:00 2001 From: KaiUweCZE Date: Sat, 27 Dec 2025 18:00:04 +0100 Subject: [PATCH 4/5] refactor(ui): separate status-text icon spacing tokens and apply size-specific margins --- libs/ui/src/atoms/status-text.tsx | 17 +++++++++++++---- .../tokens/components/atoms/_status-text.css | 4 +++- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/libs/ui/src/atoms/status-text.tsx b/libs/ui/src/atoms/status-text.tsx index 0021d39999..f8f3078e13 100644 --- a/libs/ui/src/atoms/status-text.tsx +++ b/libs/ui/src/atoms/status-text.tsx @@ -6,7 +6,7 @@ import { Icon } from "./icon" const statusTextVariants = tv({ slots: { base: "flex items-center", - icon: "self-start mt-status-text-icon-margin" + icon: "self-start" }, variants: { status: { @@ -16,9 +16,18 @@ const statusTextVariants = tv({ default: "text-status-text-fg-default", }, size: { - sm: "text-status-text-sm gap-status-text-icon-gap-sm", - md: "text-status-text-md gap-status-text-icon-gap-md", - lg: "text-status-text-lg gap-status-text-icon-gap-lg", + 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: { diff --git a/libs/ui/src/tokens/components/atoms/_status-text.css b/libs/ui/src/tokens/components/atoms/_status-text.css index 24a9753a53..5a097764f6 100644 --- a/libs/ui/src/tokens/components/atoms/_status-text.css +++ b/libs/ui/src/tokens/components/atoms/_status-text.css @@ -15,7 +15,9 @@ --spacing-status-text-icon-gap-md: var(--spacing-100); --spacing-status-text-icon-gap-lg: var(--spacing-200); - --spacing-status-text-icon-margin: var(--spacing-100); + --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 { From df1eaf119a564eaebeb133246daf4fc3e9d95337 Mon Sep 17 00:00:00 2001 From: KaiUweCZE Date: Mon, 29 Dec 2025 14:19:26 +0100 Subject: [PATCH 5/5] remove redundant ?? operator --- libs/ui/src/atoms/status-text.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/ui/src/atoms/status-text.tsx b/libs/ui/src/atoms/status-text.tsx index f8f3078e13..db51c33af7 100644 --- a/libs/ui/src/atoms/status-text.tsx +++ b/libs/ui/src/atoms/status-text.tsx @@ -60,7 +60,7 @@ export function StatusText({ ref, ...props }: StatusTextProps) { - const icon = ICON_MAP[status ?? "default"] + const icon = ICON_MAP[status] const {base, icon: iconSlot} = statusTextVariants({ status,