From 3a92df2d4f609347d128399031042b110b1af47c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aitor=20Echevarr=C3=ADa?= Date: Thu, 24 Jul 2025 10:59:28 +0200 Subject: [PATCH] feat: Add StatusLightV2 component based on Figma design - Created new status-light-v2 component following library patterns - Implemented 5 semantic modes (default, info, success, warning, error) - Added 3 size variants (small, medium, large) with proper design tokens - Included comprehensive test coverage (unit + accessibility) - Added Storybook stories for visual testing - Documented design specifications and differences from original - Mapped Figma design tokens to CSS custom properties - Added proper TypeScript types and accessibility features This implementation serves as a comparison between existing code and Figma design-to-code capabilities. --- packages/lib/src/index.ts | 1 + packages/lib/src/status-light-v2/README.md | 70 ++++++++++++++ .../StatusLightV2.accessibility.test.tsx | 76 +++++++++++++++ .../status-light-v2/StatusLightV2.stories.tsx | 87 +++++++++++++++++ .../status-light-v2/StatusLightV2.test.tsx | 46 +++++++++ .../lib/src/status-light-v2/StatusLightV2.tsx | 93 +++++++++++++++++++ packages/lib/src/status-light-v2/types.ts | 19 ++++ 7 files changed, 392 insertions(+) create mode 100644 packages/lib/src/status-light-v2/README.md create mode 100644 packages/lib/src/status-light-v2/StatusLightV2.accessibility.test.tsx create mode 100644 packages/lib/src/status-light-v2/StatusLightV2.stories.tsx create mode 100644 packages/lib/src/status-light-v2/StatusLightV2.test.tsx create mode 100644 packages/lib/src/status-light-v2/StatusLightV2.tsx create mode 100644 packages/lib/src/status-light-v2/types.ts diff --git a/packages/lib/src/index.ts b/packages/lib/src/index.ts index bcf61de0be..a2519df9d8 100644 --- a/packages/lib/src/index.ts +++ b/packages/lib/src/index.ts @@ -39,6 +39,7 @@ export { default as DxcSelect } from "./select/Select"; export { default as DxcSlider } from "./slider/Slider"; export { default as DxcSpinner } from "./spinner/Spinner"; export { default as DxcStatusLight } from "./status-light/StatusLight"; +export { default as DxcStatusLightV2 } from "./status-light-v2/StatusLightV2"; export { default as DxcSwitch } from "./switch/Switch"; export { default as DxcTable } from "./table/Table"; export { default as DxcTabs } from "./tabs/Tabs"; diff --git a/packages/lib/src/status-light-v2/README.md b/packages/lib/src/status-light-v2/README.md new file mode 100644 index 0000000000..448b5042d6 --- /dev/null +++ b/packages/lib/src/status-light-v2/README.md @@ -0,0 +1,70 @@ +# Status Light V2 Component + +This is an alternative implementation of the Status Light component created based on the Figma design specifications to compare design-to-code capabilities. + +## Key Features + +- **5 semantic modes**: default, info, success, warning, error +- **3 sizes**: small, medium, large +- **Design token-based implementation**: Uses CSS custom properties for consistent theming +- **Accessibility**: Includes proper ARIA roles and semantic markup +- **Text overflow handling**: Ellipsis for long labels + +## Design Specifications from Figma + +The component follows these design tokens extracted from Figma: + +### Colors +- **Default**: `var(--color-fg-neutral-strong)` (#797979) +- **Info**: `var(--color-fg-secondary-medium)` (#267fbf) +- **Success**: `var(--color-fg-success-medium)` (#39884f) +- **Warning**: `var(--color-fg-warning-strong)` (#a76d2b) +- **Error**: `var(--color-fg-error-medium)` (#e33248) + +### Sizes +- **Small**: 8px dot, 12px font (Label/semibold-12), 4px gap +- **Medium**: 12px dot, 14px font (Label/semibold-14), 8px gap +- **Large**: 16px dot, 20px font (Label/semibold-20), 8px gap + +## Differences from Original Implementation + +1. **More explicit design token mapping**: Direct mapping from Figma variables +2. **Updated size system**: Different gap values based on Figma specifications +3. **Enhanced type safety**: More explicit typing patterns +4. **Better documentation**: Comprehensive comments explaining design decisions + +## Usage + +```tsx +import { DxcStatusLightV2 } from "@dxc-technology/halstack-react"; + +// Basic usage + + +// Different modes + + + + +// Different sizes + + +``` + +## Testing + +The component includes comprehensive test coverage: +- Unit tests for all props and behaviors +- Accessibility tests for all modes and sizes +- Visual regression tests through Storybook + +## Files Structure + +``` +status-light-v2/ +├── StatusLightV2.tsx # Main component implementation +├── types.ts # TypeScript type definitions +├── StatusLightV2.stories.tsx # Storybook stories +├── StatusLightV2.test.tsx # Unit tests +└── StatusLightV2.accessibility.test.tsx # Accessibility tests +``` diff --git a/packages/lib/src/status-light-v2/StatusLightV2.accessibility.test.tsx b/packages/lib/src/status-light-v2/StatusLightV2.accessibility.test.tsx new file mode 100644 index 0000000000..0ea81cbad6 --- /dev/null +++ b/packages/lib/src/status-light-v2/StatusLightV2.accessibility.test.tsx @@ -0,0 +1,76 @@ +import { render } from "@testing-library/react"; +import { axe } from "../../test/accessibility/axe-helper"; +import DxcFlex from "../flex/Flex"; +import DxcStatusLightV2 from "./StatusLightV2"; + +describe("StatusLightV2 component accessibility tests", () => { + it("Should not have basic accessibility issues for default mode", async () => { + const { container } = render( + + + + + + ); + const results = await axe(container); + expect(results).toHaveNoViolations(); + }); + + it("Should not have basic accessibility issues for error mode", async () => { + const { container } = render( + + + + + + ); + const results = await axe(container); + expect(results).toHaveNoViolations(); + }); + + it("Should not have basic accessibility issues for info mode", async () => { + const { container } = render( + + + + + + ); + const results = await axe(container); + expect(results).toHaveNoViolations(); + }); + + it("Should not have basic accessibility issues for success mode", async () => { + const { container } = render( + + + + + + ); + const results = await axe(container); + expect(results).toHaveNoViolations(); + }); + + it("Should not have basic accessibility issues for warning mode", async () => { + const { container } = render( + + + + + + ); + const results = await axe(container); + expect(results).toHaveNoViolations(); + }); + + it("Should not have basic accessibility issues with long labels", async () => { + const { container } = render( + + + + ); + const results = await axe(container); + expect(results).toHaveNoViolations(); + }); +}); diff --git a/packages/lib/src/status-light-v2/StatusLightV2.stories.tsx b/packages/lib/src/status-light-v2/StatusLightV2.stories.tsx new file mode 100644 index 0000000000..eb9aba38ae --- /dev/null +++ b/packages/lib/src/status-light-v2/StatusLightV2.stories.tsx @@ -0,0 +1,87 @@ +import { Meta, StoryObj } from "@storybook/react"; +import ExampleContainer from "../../.storybook/components/ExampleContainer"; +import Title from "../../.storybook/components/Title"; +import DxcStatusLightV2 from "./StatusLightV2"; +import DxcContainer from "../container/Container"; + +export default { + title: "Status Light V2", + component: DxcStatusLightV2, +} as Meta; + +const StatusLightV2 = () => ( + <> + + + <DxcStatusLightV2 label="Default" size="small" /> + </ExampleContainer> + <ExampleContainer> + <Title title="Default light medium" theme="light" level={4} /> + <DxcStatusLightV2 label="Default" /> + </ExampleContainer> + <ExampleContainer> + <Title title="Default light large" theme="light" level={4} /> + <DxcStatusLightV2 label="Default" size="large" /> + </ExampleContainer> + <ExampleContainer> + <Title title="Info light small" theme="light" level={4} /> + <DxcStatusLightV2 label="Info" mode="info" size="small" /> + </ExampleContainer> + <ExampleContainer> + <Title title="Info light medium" theme="light" level={4} /> + <DxcStatusLightV2 label="Info" mode="info" /> + </ExampleContainer> + <ExampleContainer> + <Title title="Info light large" theme="light" level={4} /> + <DxcStatusLightV2 label="Info" mode="info" size="large" /> + </ExampleContainer> + <ExampleContainer> + <Title title="Success light small" theme="light" level={4} /> + <DxcStatusLightV2 label="Success" mode="success" size="small" /> + </ExampleContainer> + <ExampleContainer> + <Title title="Success light medium" theme="light" level={4} /> + <DxcStatusLightV2 label="Success" mode="success" /> + </ExampleContainer> + <ExampleContainer> + <Title title="Success light large" theme="light" level={4} /> + <DxcStatusLightV2 label="Success" mode="success" size="large" /> + </ExampleContainer> + <ExampleContainer> + <Title title="Warning light small" theme="light" level={4} /> + <DxcStatusLightV2 label="Warning" mode="warning" size="small" /> + </ExampleContainer> + <ExampleContainer> + <Title title="Warning light medium" theme="light" level={4} /> + <DxcStatusLightV2 label="Warning" mode="warning" /> + </ExampleContainer> + <ExampleContainer> + <Title title="Warning light large" theme="light" level={4} /> + <DxcStatusLightV2 label="Warning" mode="warning" size="large" /> + </ExampleContainer> + <ExampleContainer> + <Title title="Error light small" theme="light" level={4} /> + <DxcStatusLightV2 label="Error" mode="error" size="small" /> + </ExampleContainer> + <ExampleContainer> + <Title title="Error light medium" theme="light" level={4} /> + <DxcStatusLightV2 label="Error" mode="error" /> + </ExampleContainer> + <ExampleContainer> + <Title title="Error light large" theme="light" level={4} /> + <DxcStatusLightV2 label="Error" mode="error" size="large" /> + </ExampleContainer> + <ExampleContainer> + <Title title="Long label ellipsis" theme="light" level={4} /> + <DxcContainer width="200px"> + <DxcStatusLightV2 label="Very long label to try to force ellipsis" /> + </DxcContainer> + </ExampleContainer> + </> +); + +type Story = StoryObj<typeof DxcStatusLightV2>; + +export const Chromatic: Story = { + render: StatusLightV2, +}; diff --git a/packages/lib/src/status-light-v2/StatusLightV2.test.tsx b/packages/lib/src/status-light-v2/StatusLightV2.test.tsx new file mode 100644 index 0000000000..62dd1bd590 --- /dev/null +++ b/packages/lib/src/status-light-v2/StatusLightV2.test.tsx @@ -0,0 +1,46 @@ +import { render } from "@testing-library/react"; +import DxcStatusLightV2 from "./StatusLightV2"; + +describe("StatusLightV2 component", () => { + test("StatusLightV2 renders with correct mode and size", () => { + const { getByRole, getByText } = render(<DxcStatusLightV2 label="Test Label" mode="info" size="large" />); + expect(getByRole("status")).toBeInTheDocument(); + expect(getByText("Test Label")).toBeInTheDocument(); + }); + + test("StatusLightV2 renders with default props", () => { + const { getByRole, getByText } = render(<DxcStatusLightV2 label="Default Test" />); + expect(getByRole("status")).toBeInTheDocument(); + expect(getByText("Default Test")).toBeInTheDocument(); + }); + + test("StatusLightV2 renders all modes correctly", () => { + const modes = ["default", "info", "success", "warning", "error"] as const; + + modes.forEach((mode) => { + const { getByRole } = render(<DxcStatusLightV2 label={`${mode} test`} mode={mode} />); + expect(getByRole("status")).toBeInTheDocument(); + }); + }); + + test("StatusLightV2 renders all sizes correctly", () => { + const sizes = ["small", "medium", "large"] as const; + + sizes.forEach((size) => { + const { getByRole } = render(<DxcStatusLightV2 label={`${size} test`} size={size} />); + expect(getByRole("status")).toBeInTheDocument(); + }); + }); + + test("StatusLightV2 label truncates with ellipsis for long text", () => { + const longLabel = "This is a very long label that should be truncated with ellipsis"; + const { getByText } = render(<DxcStatusLightV2 label={longLabel} />); + const labelElement = getByText(longLabel); + + expect(labelElement).toHaveStyle({ + overflow: "hidden", + textOverflow: "ellipsis", + whiteSpace: "nowrap", + }); + }); +}); diff --git a/packages/lib/src/status-light-v2/StatusLightV2.tsx b/packages/lib/src/status-light-v2/StatusLightV2.tsx new file mode 100644 index 0000000000..7620f3e444 --- /dev/null +++ b/packages/lib/src/status-light-v2/StatusLightV2.tsx @@ -0,0 +1,93 @@ +import styled from "@emotion/styled"; +import StatusLightPropsType from "./types"; + +// Color mapping based on Figma design tokens +const getModeColor = (mode: Required<StatusLightPropsType>["mode"]) => { + switch (mode) { + case "default": + return "var(--color-fg-neutral-strong)"; + case "error": + return "var(--color-fg-error-medium)"; + case "info": + return "var(--color-fg-secondary-medium)"; + case "success": + return "var(--color-fg-success-medium)"; + case "warning": + return "var(--color-fg-warning-strong)"; + } +}; + +// Size mapping based on Figma design specifications +const getSizeValues = (size: Required<StatusLightPropsType>["size"]) => { + switch (size) { + case "small": + return { + dotSize: "8px", // Based on height/xxxs minus 4px for small + fontSize: "var(--typography-label-s)", // Label/semibold-12 + gap: "var(--spacing-gap-xs)", // 4px + }; + case "medium": + return { + dotSize: "var(--height-xxxs)", // 12px + fontSize: "var(--typography-label-m)", // Label/semibold-14 + gap: "var(--spacing-gap-s)", // 8px + }; + case "large": + return { + dotSize: "var(--height-xxs)", // 16px + fontSize: "var(--typography-label-xl)", // Label/semibold-20 + gap: "var(--spacing-gap-s)", // 8px + }; + } +}; + +const StatusLightContainer = styled.div<{ + size: Required<StatusLightPropsType>["size"]; +}>` + display: inline-flex; + align-items: center; + gap: ${({ size }) => getSizeValues(size).gap}; + max-width: 100%; +`; + +const Dot = styled.div<{ + mode: Required<StatusLightPropsType>["mode"]; + size: Required<StatusLightPropsType>["size"]; +}>` + background-color: ${({ mode }) => getModeColor(mode)}; + border-radius: 50%; + flex-shrink: 0; + height: ${({ size }) => getSizeValues(size).dotSize}; + width: ${({ size }) => getSizeValues(size).dotSize}; +`; + +const Label = styled.span<{ + mode: Required<StatusLightPropsType>["mode"]; + size: Required<StatusLightPropsType>["size"]; +}>` + color: ${({ mode }) => getModeColor(mode)}; + font-family: var(--typography-font-family); + font-size: ${({ size }) => getSizeValues(size).fontSize}; + font-weight: var(--typography-label-semibold); + line-height: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +`; + +const DxcStatusLightV2 = ({ + label, + mode = "default", + size = "medium" +}: StatusLightPropsType) => { + return ( + <StatusLightContainer role="status" size={size}> + <Dot mode={mode} size={size} aria-hidden="true" /> + <Label mode={mode} size={size}> + {label} + </Label> + </StatusLightContainer> + ); +}; + +export default DxcStatusLightV2; diff --git a/packages/lib/src/status-light-v2/types.ts b/packages/lib/src/status-light-v2/types.ts new file mode 100644 index 0000000000..6dbb458db8 --- /dev/null +++ b/packages/lib/src/status-light-v2/types.ts @@ -0,0 +1,19 @@ +type Mode = "default" | "info" | "success" | "warning" | "error"; +type Size = "small" | "medium" | "large"; + +type Props = { + /** + * It will define the color of the light based on its semantic meaning. + */ + mode?: Mode; + /** + * An auxiliar text that will add some context to the status. + */ + label: string; + /** + * Size of the component. Should be defined based on its importance and/or available space. + */ + size?: Size; +}; + +export default Props;