From b9cffcebd1ecc4b24da730ffd17addf9a980cf3d Mon Sep 17 00:00:00 2001 From: Theophile Sandoz Date: Fri, 22 Nov 2024 17:24:29 +0100 Subject: [PATCH] =?UTF-8?q?feat(lld):=20=F0=9F=A7=A9=20add=20new=20portfol?= =?UTF-8?q?io=20content=20card=20component=20(#8387)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(lld): allow creating button icons with new icons * feat(lld): add the PortfolioContentCard component * feat(lld): add the PortfolioContentCard stories * feat(lld): add a carousel portfolio content cards story * chore(lld): make `PortfolioContentCardProps.image` explicitly optional * chore(lld): rename shortDescription to description As it's a default field on all content cards * chore(lld): export PortfolioContentCard on `@ledgerhq/react-ui` * feat(lld): add an `onChange` prop to the Carousel to track diplayed slides * fix(lld): keep the height of the carousel slides constant * fix(lld): account for the DS being broken on LLD * fix(lld): show pointer cursor everywhere on the cards * feat(lld): force dark close button * chore: update change log * feat(lld): make card description optional * chore(lld): simplify the Carousel stories --- .changeset/loud-mangos-enjoy.md | 5 + .../react/src/components/cta/Button/index.tsx | 8 +- .../layout/Carousel/Carousel.stories.tsx | 81 +++++++++---- .../src/components/layout/Carousel/index.tsx | 10 +- .../src/components/layout/Carousel/types.tsx | 3 +- .../PortfolioContentCard.stories.tsx | 47 ++++++++ .../PortfolioContentCard/index.tsx | 106 ++++++++++++++++++ .../react/src/components/layout/index.ts | 1 + 8 files changed, 236 insertions(+), 25 deletions(-) create mode 100644 .changeset/loud-mangos-enjoy.md create mode 100644 libs/ui/packages/react/src/components/layout/ContentCard/PortfolioContentCard/PortfolioContentCard.stories.tsx create mode 100644 libs/ui/packages/react/src/components/layout/ContentCard/PortfolioContentCard/index.tsx diff --git a/.changeset/loud-mangos-enjoy.md b/.changeset/loud-mangos-enjoy.md new file mode 100644 index 000000000000..cfbb538b3ada --- /dev/null +++ b/.changeset/loud-mangos-enjoy.md @@ -0,0 +1,5 @@ +--- +"@ledgerhq/react-ui": minor +--- + +Add new portfolio content card component diff --git a/libs/ui/packages/react/src/components/cta/Button/index.tsx b/libs/ui/packages/react/src/components/cta/Button/index.tsx index 90362250c77e..52b5a9dccf3c 100644 --- a/libs/ui/packages/react/src/components/cta/Button/index.tsx +++ b/libs/ui/packages/react/src/components/cta/Button/index.tsx @@ -24,7 +24,7 @@ interface BaseProps extends BaseStyledProps, BordersProps { } export interface ButtonProps extends BaseProps, React.RefAttributes { - Icon?: React.ComponentType<{ size: number; color?: string }>; + Icon?: React.ReactElement | React.ComponentType<{ size: number; color?: string }>; children?: React.ReactNode; onClick?: (event: React.SyntheticEvent) => void; iconSize?: number; @@ -236,7 +236,11 @@ const Button = ( ref?: React.ForwardedRef, ): React.ReactElement => { const iconNodeSize = iconSize || fontSizes[props.fontSize ?? 4]; - const IconNode = useMemo(() => Icon && , [iconNodeSize, Icon]); + const IconNode = useMemo(() => { + if (!Icon) return null; + if (typeof Icon === "object") return Icon; + return ; + }, [iconNodeSize, Icon]); return ( diff --git a/libs/ui/packages/react/src/components/layout/Carousel/Carousel.stories.tsx b/libs/ui/packages/react/src/components/layout/Carousel/Carousel.stories.tsx index f5a50a0a9bc4..9b214029fda3 100644 --- a/libs/ui/packages/react/src/components/layout/Carousel/Carousel.stories.tsx +++ b/libs/ui/packages/react/src/components/layout/Carousel/Carousel.stories.tsx @@ -1,25 +1,15 @@ +import { action } from "@storybook/addon-actions"; import type { Meta, StoryObj } from "@storybook/react"; -import React from "react"; +import React, { FC, ReactElement, useContext } from "react"; + +import PortfolioContentCard from "../ContentCard/PortfolioContentCard"; import Carousel from "./"; import { Props } from "./types"; -const CarouselStory = (args: Omit & { children: number }) => { - const slides = Array.from({ length: args.children }, (_, index) => ( -
- Slide {index} -
- )); - - return ; -}; +type Args = Omit & { children: number }; +type Parameters = { Slide: FC<{ index: number }> }; +const SlideContext = React.createContext([]); export default { title: "Layout/Carousel", argTypes: { @@ -33,6 +23,9 @@ export default { defaultValue: "default", control: "inline-radio", }, + onChange: { + description: "Function called when a new slide is shown.", + }, }, args: { variant: "default", @@ -45,7 +38,55 @@ export default { }, }, }, - render: CarouselStory, -} satisfies Meta; + decorators: [ + (Story: FC, { args, parameters }: { args: Args; parameters: Parameters }) => ( + ( + + ))} + > + + + ), + ], + render: ({ children, ...props }: Args) => ( + {useContext(SlideContext)} + ), +} satisfies Meta; + +export const Default: StoryObj = { + parameters: { + Slide: (({ index }) => ( +
+ Slide {index} +
+ )) satisfies Parameters["Slide"], + }, +}; + +export const PortfolioContentCards: StoryObj = { + parameters: { + Slide: (({ index }) => ( + onSlideAction(`click on slide ${index}`)} + onClose={() => onSlideAction(`close slide ${index}`)} + /> + )) satisfies Parameters["Slide"], + }, +}; + +const onSlideAction = action("onSlideAction"); -export const Default: StoryObj = {}; +const IMAGE_SRC = + "data:image/svg+xml,"; diff --git a/libs/ui/packages/react/src/components/layout/Carousel/index.tsx b/libs/ui/packages/react/src/components/layout/Carousel/index.tsx index 1523b4b59c2c..61df29282e8a 100644 --- a/libs/ui/packages/react/src/components/layout/Carousel/index.tsx +++ b/libs/ui/packages/react/src/components/layout/Carousel/index.tsx @@ -14,8 +14,12 @@ const EmblaContainer = styled.div` `; const EmblaSlide = styled.div` + display: flex; flex: 0 0 100%; min-width: 0; + > * { + flex-basis: 100%; + } `; const CarouselContainer = styled.div>` @@ -30,7 +34,7 @@ const CarouselContainer = styled.div>` /** * This component uses the https://github.com/davidjerleke/embla-carousel library. */ -const Carousel = ({ children, variant = "default" }: Props) => { +const Carousel = ({ children, variant = "default", onChange }: Props) => { const [currentIndex, setCurrentIndex] = useState(0); const [emblaRef, emblaApi] = useEmblaCarousel({ loop: true }); @@ -40,7 +44,9 @@ const Carousel = ({ children, variant = "default" }: Props) => { const newIndex = emblaApi.selectedScrollSnap(); setCurrentIndex(newIndex); emblaApi.scrollTo(newIndex); - }, [emblaApi]); + + onChange?.(newIndex); + }, [emblaApi, onChange]); useEffect(() => { if (!emblaApi) return; diff --git a/libs/ui/packages/react/src/components/layout/Carousel/types.tsx b/libs/ui/packages/react/src/components/layout/Carousel/types.tsx index d746d306b03a..9096d9638f9c 100644 --- a/libs/ui/packages/react/src/components/layout/Carousel/types.tsx +++ b/libs/ui/packages/react/src/components/layout/Carousel/types.tsx @@ -6,12 +6,13 @@ export type Variant = "content-card" | "default"; export type Props = { children: ReactElement[]; variant?: Variant; + onChange?: (index: number) => void; }; /** * Carousel's sub props to be passed to any component used by the carousel.. */ -export type SubProps = Required & { +export type SubProps = Required> & { emblaApi: UseEmblaCarouselType[1]; currentIndex: number; }; diff --git a/libs/ui/packages/react/src/components/layout/ContentCard/PortfolioContentCard/PortfolioContentCard.stories.tsx b/libs/ui/packages/react/src/components/layout/ContentCard/PortfolioContentCard/PortfolioContentCard.stories.tsx new file mode 100644 index 000000000000..d08b10c9eb08 --- /dev/null +++ b/libs/ui/packages/react/src/components/layout/ContentCard/PortfolioContentCard/PortfolioContentCard.stories.tsx @@ -0,0 +1,47 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import PortfolioContentCard, { PortfolioContentCardProps } from "."; + +export default { + title: "Layout/ContentCard/PortfolioContentCard", + component: PortfolioContentCard, + argTypes: { + title: { + description: "Title of the card.", + }, + cta: { + description: "Call to action text.", + }, + description: { + description: "Description of the card.", + }, + tag: { + description: "Tag to be displayed on top of the card.", + }, + image: { + description: "Image to be displayed on the right of the card.", + }, + onClick: { + description: "Function to be called when the card is clicked.", + }, + onClose: { + description: "Function to be called when the close button is clicked.", + }, + }, + args: { + title: "Ledger Recover", + description: "Get peace of mind and start your free trial.", + cta: "Start my free trial", + image: + "data:image/svg+xml,", + }, +} satisfies Meta; + +export const WithCta: StoryObj = {}; + +export const WithoutCta: StoryObj = { + args: { cta: undefined }, +}; + +export const WithTag: StoryObj = { + args: { tag: "New" }, +}; diff --git a/libs/ui/packages/react/src/components/layout/ContentCard/PortfolioContentCard/index.tsx b/libs/ui/packages/react/src/components/layout/ContentCard/PortfolioContentCard/index.tsx new file mode 100644 index 000000000000..d0447785af1f --- /dev/null +++ b/libs/ui/packages/react/src/components/layout/ContentCard/PortfolioContentCard/index.tsx @@ -0,0 +1,106 @@ +import React, { type ReactEventHandler } from "react"; +import styled from "styled-components"; + +import { StyleProvider } from "../../../../styles"; +import { Icons } from "../../../../assets"; +import { Text } from "../../../asorted"; +import { Button } from "../../../cta"; +import Tag from "../../../Tag"; + +export type PortfolioContentCardProps = { + title: string; + cta?: string; + description?: string; + tag?: string; + image?: string; + + onClick: ReactEventHandler; + onClose: ReactEventHandler; +}; + +export default function PortfolioContentCard({ + title, + cta, + description, + tag, + image, + onClick, + onClose, +}: PortfolioContentCardProps) { + const handleClose: ReactEventHandler = event => { + event.stopPropagation(); + onClose(event); + }; + + return ( + + {tag && {tag}} + {title} + {description && {description}} + {cta && ( + + )} + + + + + ); +} + +const StyledTag = styled(Tag).attrs({ size: "medium", type: "plain", active: true })` + font-size: 11px; + background-color: ${p => p.theme.colors.primary.c80}; +`; + +const Title = styled(Text).attrs({ variant: "h4Inter" })` + font-family: Inter; + font-size: 24px; + font-weight: 600; +`; + +const Desc = styled(Text).attrs({ variant: "small", color: "neutral.c70" })` + font-family: Inter; + font-size: 13px; + font-style: normal; + font-weight: 500; + padding-bottom: 8px; +`; + +const Wrapper = styled.div>` + background-color: ${p => p.theme.colors.background.card}; + background-image: ${p => (p.image ? `url("${p.image}")` : "none")}; + background-position: right center; + background-repeat: no-repeat; + background-size: 50% auto; + + cursor: pointer; + padding: 16px; + padding-top: ${p => (p.tag ? "16px" : "24px")}; + padding-right: 50%; + + position: relative; + display: flex; + flex-direction: column; + justify-content: flex-end; + align-items: flex-start; + gap: 4px; +`; + +const CloseButton = styled(Button).attrs({ + Icon: , + iconButton: true, + outline: true, +})` + background-color: ${p => p.theme.colors.neutral.c30}; + position: absolute; + top: 10px; + right: 10px; + width: 24px; + height: 24px; + svg { + width: 12px; + height: 12px; + } +`; diff --git a/libs/ui/packages/react/src/components/layout/index.ts b/libs/ui/packages/react/src/components/layout/index.ts index 5fbaf23d7bbd..ddb921fa497f 100644 --- a/libs/ui/packages/react/src/components/layout/index.ts +++ b/libs/ui/packages/react/src/components/layout/index.ts @@ -7,3 +7,4 @@ export { default as Drawer } from "./Drawer"; export { default as Carousel } from "./Carousel"; export { default as VerticalTimeline } from "./List/VerticalTimeline"; export { default as NumberedList } from "./List/NumberedList"; +export { default as PortfolioContentCard } from "./ContentCard/PortfolioContentCard";