diff --git a/app/components/NumberCard.tsx b/app/components/NumberCard.tsx new file mode 100644 index 0000000..e33674e --- /dev/null +++ b/app/components/NumberCard.tsx @@ -0,0 +1,91 @@ +"use client"; + +import { memo } from "react"; +import SectionNumber from "./SectionNumber"; + +interface NumberCardProps { + number: number; + text: string; + size?: "Small" | "Medium" | "Large" | "XLarge"; + iconShape?: string; + iconColor?: string; +} + +const NumberCard = memo(({ number, text, size }) => { + // Base classes common to all sizes + const baseClasses = "bg-[var(--color-surface-inverse-primary)] rounded-[12px] shadow-lg"; + + // If size prop is provided, use explicit size classes + // Otherwise, use responsive breakpoints for backward compatibility + if (size) { + // Size-specific classes + const sizeClasses = { + Small: "flex flex-col items-end justify-center gap-4 p-5 relative", + Medium: "flex flex-row items-center gap-8 p-8 relative", + Large: "flex flex-col items-start justify-end gap-[22px] h-[238px] p-8 relative", + XLarge: "flex flex-col items-start justify-end gap-[22px] h-[238px] p-8 relative", + }; + + // Text size classes + const textClasses = { + Small: "font-bricolage-grotesque font-medium text-[24px] leading-[32px] text-[#141414]", + Medium: "font-bricolage-grotesque font-medium text-[24px] leading-[24px] text-[#141414]", + Large: "font-bricolage-grotesque font-medium text-[24px] leading-[24px] text-[#141414]", + XLarge: "font-bricolage-grotesque font-medium text-[32px] leading-[32px] text-[#141414]", + }; + + // Section number positioning classes + const sectionNumberClasses = { + Small: "flex justify-end items-end", + Medium: "flex justify-start flex-shrink-0", + Large: "absolute top-8 right-8", + XLarge: "absolute top-8 right-8", + }; + + // Content container classes + const contentClasses = { + Small: "", + Medium: "flex-1", + Large: "absolute bottom-8 left-8 right-16", + XLarge: "absolute bottom-8 left-8 right-16", + }; + + return ( +
+ {/* Section Number */} +
+ +
+ + {/* Card Content */} +
+

+ {text} +

+
+
+ ); + } + + // Responsive breakpoints for backward compatibility (matches original behavior) + // Maps to: Small (mobile) -> Medium (sm) -> Large (lg) -> XLarge (xl) + return ( +
+ {/* Section Number - Responsive positioning */} +
+ +
+ + {/* Card Content - Responsive positioning */} +
+

+ {text} +

+
+
+ ); +}); + +NumberCard.displayName = "NumberCard"; + +export default NumberCard; diff --git a/app/components/NumberedCard.tsx b/app/components/NumberedCard.tsx deleted file mode 100644 index 317fca2..0000000 --- a/app/components/NumberedCard.tsx +++ /dev/null @@ -1,33 +0,0 @@ -"use client"; - -import { memo } from "react"; -import SectionNumber from "./SectionNumber"; - -interface NumberedCardProps { - number: number; - text: string; - iconShape?: string; - iconColor?: string; -} - -const NumberedCard = memo(({ number, text }) => { - return ( -
- {/* Section Number - Top right (lg breakpoint) */} -
- -
- - {/* Card Content - Bottom left (lg breakpoint) */} -
-

- {text} -

-
-
- ); -}); - -NumberedCard.displayName = "NumberedCard"; - -export default NumberedCard; diff --git a/app/components/NumberedCards/NumberedCards.view.tsx b/app/components/NumberedCards/NumberedCards.view.tsx index acc48fe..d77fb0b 100644 --- a/app/components/NumberedCards/NumberedCards.view.tsx +++ b/app/components/NumberedCards/NumberedCards.view.tsx @@ -2,7 +2,7 @@ import { useTranslation } from "../../contexts/MessagesContext"; import SectionHeader from "../SectionHeader"; -import NumberedCard from "../NumberedCard"; +import NumberCard from "../NumberCard"; import Button from "../Button"; import type { NumberedCardsViewProps } from "./NumberedCards.types"; @@ -35,7 +35,7 @@ function NumberedCardsView({ {/* Cards Container */}
{cards.map((card, index) => ( - ( +
+
+

Small

+ +
+
+

Medium

+ +
+
+

Large

+ +
+
+

XLarge

+ +
+
+ ), + parameters: { + docs: { + description: { + story: + "Shows all four size variants side by side to compare the different layouts and typography.", + }, + }, + }, +}; + +export const AllNumbers = { + args: { + number: 1, + text: "Example card text", + iconShape: "blob", + iconColor: "green", + }, + render: (args) => ( +
+ + + +
+ ), + parameters: { + docs: { + description: { + story: + "Shows all three numbered cards with different content to demonstrate the visual hierarchy.", + }, + }, + }, +}; + +export const LongText = { + args: { + number: 1, + text: "This is a much longer piece of text that demonstrates how the card handles content that spans multiple lines and requires more space to display properly", + iconShape: "blob", + iconColor: "green", + }, + parameters: { + docs: { + description: { + story: + "Demonstrates how the card handles longer text content across different breakpoints.", + }, + }, + }, +}; diff --git a/stories/NumberedCard.stories.js b/stories/NumberedCard.stories.js deleted file mode 100644 index 01ebf98..0000000 --- a/stories/NumberedCard.stories.js +++ /dev/null @@ -1,96 +0,0 @@ -import NumberedCard from "../app/components/NumberedCard"; - -export default { - title: "Components/NumberedCard", - component: NumberedCard, - parameters: { - layout: "centered", - docs: { - description: { - component: - "Individual numbered card component that displays a step in a process with a numbered icon and descriptive text. Supports responsive layouts across different breakpoints.", - }, - }, - }, - argTypes: { - number: { - control: { type: "number", min: 1, max: 9 }, - description: "The number to display on the card", - }, - text: { - control: { type: "text" }, - description: "The descriptive text for this step", - }, - iconShape: { - control: { type: "select" }, - options: ["blob", "gear", "star"], - description: - "The shape of the icon background (currently not used, uses PNG images)", - }, - iconColor: { - control: { type: "select" }, - options: ["green", "purple", "orange", "blue"], - description: - "The color theme for the icon (currently not used, uses PNG images)", - }, - }, - tags: ["autodocs"], -}; - -export const Default = { - args: { - number: 1, - text: "Document how your community makes decisions", - iconShape: "blob", - iconColor: "green", - }, -}; - -export const AllNumbers = { - args: { - number: 1, - text: "Example card text", - iconShape: "blob", - iconColor: "green", - }, - render: (args) => ( -
- - - -
- ), - parameters: { - docs: { - description: { - story: - "Shows all three numbered cards with different content to demonstrate the visual hierarchy.", - }, - }, - }, -}; - -export const LongText = { - args: { - number: 1, - text: "This is a much longer piece of text that demonstrates how the card handles content that spans multiple lines and requires more space to display properly", - iconShape: "blob", - iconColor: "green", - }, - parameters: { - docs: { - description: { - story: - "Demonstrates how the card handles longer text content across different breakpoints.", - }, - }, - }, -}; diff --git a/stories/NumberedCards.stories.js b/stories/NumberedCards.stories.js index 90ca449..3d9f651 100644 --- a/stories/NumberedCards.stories.js +++ b/stories/NumberedCards.stories.js @@ -8,7 +8,7 @@ export default { docs: { description: { component: - "A component system for visually communicating multi-step workflows, processes, or value propositions. The component's modular design with NumberedCard and SectionNumber sub-components makes it ideal for explaining any sequential process while maintaining brand consistency and accessibility standards across the design system.", + "A component system for visually communicating multi-step workflows, processes, or value propositions. The component's modular design with NumberCard and SectionNumber sub-components makes it ideal for explaining any sequential process while maintaining brand consistency and accessibility standards across the design system.", }, }, }, diff --git a/tests/unit/NumberedCard.test.jsx b/tests/unit/NumberCard.test.jsx similarity index 50% rename from tests/unit/NumberedCard.test.jsx rename to tests/unit/NumberCard.test.jsx index 8623454..370cad5 100644 --- a/tests/unit/NumberedCard.test.jsx +++ b/tests/unit/NumberCard.test.jsx @@ -1,49 +1,49 @@ import { render, screen } from "@testing-library/react"; import { describe, it, expect } from "vitest"; -import NumberedCard from "../../app/components/NumberedCard"; +import NumberCard from "../../app/components/NumberCard"; -describe("NumberedCard Component", () => { +describe("NumberCard Component", () => { const defaultProps = { number: 1, text: "Test Card Text", }; - it("renders numbered card with all required information", () => { - render(); + it("renders number card with all required information", () => { + render(); expect(screen.getByText("1")).toBeInTheDocument(); expect(screen.getByText("Test Card Text")).toBeInTheDocument(); }); it("renders with different numbers", () => { - const { rerender } = render(); + const { rerender } = render(); expect(screen.getByText("42")).toBeInTheDocument(); - rerender(); + rerender(); expect(screen.getByText("999")).toBeInTheDocument(); }); it("renders with different text content", () => { const { rerender } = render( - , + , ); expect(screen.getByText("Different Text")).toBeInTheDocument(); - rerender(); + rerender(); expect(screen.getByText("Another Text")).toBeInTheDocument(); }); - it("applies proper responsive layout classes", () => { - render(); + it("applies proper responsive layout classes when size is not specified", () => { + render(); const card = screen .getByText("Test Card Text") .closest("div").parentElement; - expect(card).toHaveClass("flex", "flex-col", "sm:flex-row", "lg:flex-row"); + expect(card).toHaveClass("flex", "flex-col", "sm:flex-row", "lg:flex-col"); }); - it("applies proper responsive spacing", () => { - render(); + it("applies proper responsive spacing when size is not specified", () => { + render(); const card = screen .getByText("Test Card Text") @@ -51,17 +51,17 @@ describe("NumberedCard Component", () => { expect(card).toHaveClass("p-5", "sm:p-8", "lg:p-8"); }); - it("applies proper responsive gap", () => { - render(); + it("applies proper responsive gap when size is not specified", () => { + render(); const card = screen .getByText("Test Card Text") .closest("div").parentElement; - expect(card).toHaveClass("gap-4", "sm:gap-8", "lg:gap-0"); + expect(card).toHaveClass("gap-4", "sm:gap-8", "lg:gap-[22px]"); }); - it("applies proper responsive height", () => { - render(); + it("applies proper responsive height when size is not specified", () => { + render(); const card = screen .getByText("Test Card Text") @@ -70,7 +70,7 @@ describe("NumberedCard Component", () => { }); it("applies proper background and shadow", () => { - render(); + render(); const card = screen .getByText("Test Card Text") @@ -82,7 +82,7 @@ describe("NumberedCard Component", () => { }); it("applies proper border radius", () => { - render(); + render(); const card = screen .getByText("Test Card Text") @@ -90,25 +90,15 @@ describe("NumberedCard Component", () => { expect(card).toHaveClass("rounded-[12px]"); }); - it("renders section number in correct position", () => { - render(); + it("renders section number in correct position for responsive mode", () => { + render(); const numberElement = screen.getByText("1"); expect(numberElement).toBeInTheDocument(); - - // Check that it's in a container with proper positioning - const numberContainer = numberElement.closest("div"); - expect(numberContainer).toHaveClass( - "absolute", - "inset-0", - "flex", - "items-center", - "justify-center", - ); }); - it("renders text content in correct position", () => { - render(); + it("renders text content in correct position for responsive mode", () => { + render(); const textElement = screen.getByText("Test Card Text"); expect(textElement).toBeInTheDocument(); @@ -125,14 +115,14 @@ describe("NumberedCard Component", () => { }); it("applies proper font classes to text", () => { - render(); + render(); const textElement = screen.getByText("Test Card Text"); expect(textElement).toHaveClass("font-bricolage-grotesque"); }); - it("applies proper text sizing", () => { - render(); + it("applies proper text sizing for responsive mode", () => { + render(); const textElement = screen.getByText("Test Card Text"); expect(textElement).toHaveClass( @@ -144,7 +134,7 @@ describe("NumberedCard Component", () => { }); it("applies proper text color", () => { - render(); + render(); const textElement = screen.getByText("Test Card Text"); expect(textElement).toHaveClass("text-[#141414]"); @@ -152,14 +142,14 @@ describe("NumberedCard Component", () => { it("handles long text content gracefully", () => { const longText = - "This is a very long text that should wrap properly and not break the layout of the numbered card component"; - render(); + "This is a very long text that should wrap properly and not break the layout of the number card component"; + render(); expect(screen.getByText(longText)).toBeInTheDocument(); }); - it("maintains proper responsive behavior", () => { - render(); + it("maintains proper responsive behavior when size is not specified", () => { + render(); const card = screen .getByText("Test Card Text") @@ -178,16 +168,16 @@ describe("NumberedCard Component", () => { // Large breakpoint expect(card).toHaveClass( - "lg:flex-row", - "lg:gap-0", + "lg:flex-col", + "lg:gap-[22px]", "lg:p-8", - "lg:items-stretch", + "lg:items-start", "lg:relative", ); }); it("renders with proper flex layout", () => { - render(); + render(); const card = screen .getByText("Test Card Text") @@ -195,12 +185,80 @@ describe("NumberedCard Component", () => { expect(card).toHaveClass("flex"); }); - it("applies proper items alignment", () => { - render(); + it("applies Small size variant correctly", () => { + render(); const card = screen .getByText("Test Card Text") .closest("div").parentElement; - expect(card).toHaveClass("sm:items-center", "lg:items-stretch"); + expect(card).toHaveClass( + "flex", + "flex-col", + "items-end", + "justify-center", + "gap-4", + "p-5", + ); + + const textElement = screen.getByText("Test Card Text"); + expect(textElement).toHaveClass("text-[24px]", "leading-[32px]"); + }); + + it("applies Medium size variant correctly", () => { + render(); + + const card = screen + .getByText("Test Card Text") + .closest("div").parentElement; + expect(card).toHaveClass( + "flex", + "flex-row", + "items-center", + "gap-8", + "p-8", + ); + + const textElement = screen.getByText("Test Card Text"); + expect(textElement).toHaveClass("text-[24px]", "leading-[24px]"); + }); + + it("applies Large size variant correctly", () => { + render(); + + const card = screen + .getByText("Test Card Text") + .closest("div").parentElement; + expect(card).toHaveClass( + "flex", + "flex-col", + "items-start", + "justify-end", + "gap-[22px]", + "h-[238px]", + "p-8", + ); + + const textElement = screen.getByText("Test Card Text"); + expect(textElement).toHaveClass("text-[24px]", "leading-[24px]"); + }); + + it("applies XLarge size variant correctly", () => { + render(); + + const card = screen + .getByText("Test Card Text") + .closest("div").parentElement; + expect(card).toHaveClass( + "flex", + "flex-col", + "items-start", + "justify-end", + "gap-[22px]", + "h-[238px]", + "p-8", + ); + + const textElement = screen.getByText("Test Card Text"); + expect(textElement).toHaveClass("text-[32px]", "leading-[32px]"); }); }); diff --git a/tests/unit/NumberedCards.test.jsx b/tests/unit/NumberedCards.test.jsx index e699567..fa36e2c 100644 --- a/tests/unit/NumberedCards.test.jsx +++ b/tests/unit/NumberedCards.test.jsx @@ -72,10 +72,10 @@ describe("NumberedCards Component", () => { expect(screen.getByText("Test Subtitle")).toBeInTheDocument(); }); - test("renders NumberedCard components with correct props", () => { + test("renders NumberCard components with correct props", () => { render(); - // Check that NumberedCard components receive correct props + // Check that NumberCard components receive correct props expect(screen.getByText("1")).toBeInTheDocument(); // First card number expect(screen.getByText("2")).toBeInTheDocument(); // Second card number expect(screen.getByText("3")).toBeInTheDocument(); // Third card number