chore(BOOK-754): Moved ContentCard to design system and added stories
Approved-by: Bianca Widstam Approved-by: Anton Gunnarsson
This commit is contained in:
@@ -1,160 +0,0 @@
|
||||
import type { Meta, StoryObj } from "@storybook/nextjs-vite"
|
||||
import { fn } from "storybook/test"
|
||||
|
||||
import { themes } from "../../../../.storybook/preview"
|
||||
|
||||
import { Card } from "../"
|
||||
import { Button } from "../../Button"
|
||||
import { Typography } from "../../Typography"
|
||||
|
||||
type CompositionProps = React.ComponentPropsWithoutRef<typeof Card> & {
|
||||
_onPrimaryPress?: () => void
|
||||
_onSecondaryPress?: () => void
|
||||
inMainArea: boolean
|
||||
showTitle: boolean
|
||||
showPreamble: boolean
|
||||
showPrimaryButton: boolean
|
||||
showSecondaryButton: boolean
|
||||
}
|
||||
|
||||
const meta: Meta<CompositionProps> = {
|
||||
title: "Compositions/Card",
|
||||
component: Card,
|
||||
decorators: [
|
||||
(Story, context) => {
|
||||
if (context.name.toLowerCase().indexOf("all themes") >= 0) {
|
||||
return (
|
||||
<>
|
||||
<h1>{context.name}</h1>
|
||||
{Object.entries(themes.themes).map(([key, value], ix) => {
|
||||
return (
|
||||
<div key={ix} className={value} style={{ padding: "1em 0" }}>
|
||||
<h2 style={{ paddingBottom: "0.5em" }}>{key}</h2>
|
||||
<Story />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ display: "flex" }}>
|
||||
<Story />
|
||||
</div>
|
||||
)
|
||||
},
|
||||
],
|
||||
argTypes: {
|
||||
inMainArea: {
|
||||
name: "Is in main area",
|
||||
},
|
||||
showTitle: {
|
||||
name: "Composition: Show title",
|
||||
},
|
||||
showPreamble: {
|
||||
name: "Composition: Show preamble",
|
||||
},
|
||||
showPrimaryButton: {
|
||||
name: "Composition: Show primary button",
|
||||
},
|
||||
showSecondaryButton: {
|
||||
name: "Composition: Show secondary button",
|
||||
},
|
||||
_onPrimaryPress: {
|
||||
table: {
|
||||
disable: true,
|
||||
},
|
||||
},
|
||||
_onSecondaryPress: {
|
||||
table: {
|
||||
disable: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
render: ({
|
||||
inMainArea,
|
||||
showTitle,
|
||||
showPreamble,
|
||||
showPrimaryButton,
|
||||
showSecondaryButton,
|
||||
...args
|
||||
}) => (
|
||||
<Card {...args}>
|
||||
{showTitle && (
|
||||
<Typography variant="Title/md">
|
||||
<h3>Content Card</h3>
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
{showPreamble && (
|
||||
<Typography variant="Body/Paragraph/mdRegular">
|
||||
<p>
|
||||
Mattis sit duis pulvinar ultricies auctor euismod. Augue mattis
|
||||
mauris at est iaculis pulvinar pulvinar.
|
||||
</p>
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
{showPrimaryButton && inMainArea && (
|
||||
<Button size="lg" variant="Primary" onPress={args._onPrimaryPress}>
|
||||
Primary action
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{showPrimaryButton && !inMainArea && (
|
||||
<Button size="sm" variant="Tertiary" onPress={args._onPrimaryPress}>
|
||||
Primary action
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{showSecondaryButton && (
|
||||
<Button
|
||||
size={inMainArea ? "lg" : "sm"}
|
||||
variant="Secondary"
|
||||
onPress={args._onSecondaryPress}
|
||||
>
|
||||
Secondary action
|
||||
</Button>
|
||||
)}
|
||||
</Card>
|
||||
),
|
||||
}
|
||||
|
||||
export default meta
|
||||
|
||||
type Story = StoryObj<CompositionProps>
|
||||
|
||||
export const ContentCardMainArea: Story = {
|
||||
args: {
|
||||
as: "Default",
|
||||
inMainArea: true,
|
||||
showTitle: true,
|
||||
showPreamble: true,
|
||||
showPrimaryButton: true,
|
||||
showSecondaryButton: true,
|
||||
_onPrimaryPress: fn(),
|
||||
_onSecondaryPress: fn(),
|
||||
},
|
||||
}
|
||||
|
||||
export const ContentCardNonMainArea: Story = {
|
||||
args: {
|
||||
as: "Default",
|
||||
inMainArea: false,
|
||||
showTitle: true,
|
||||
showPreamble: true,
|
||||
showPrimaryButton: true,
|
||||
showSecondaryButton: true,
|
||||
_onPrimaryPress: fn(),
|
||||
_onSecondaryPress: fn(),
|
||||
},
|
||||
}
|
||||
|
||||
export const ContentCardMainAreaAllThemes: Story = {
|
||||
...ContentCardMainArea,
|
||||
}
|
||||
|
||||
export const ContentCardNonMainAreaAllThemes: Story = {
|
||||
...ContentCardNonMainArea,
|
||||
}
|
||||
@@ -2,18 +2,47 @@ import type { Meta, StoryObj } from "@storybook/nextjs-vite"
|
||||
|
||||
import { fn } from "storybook/test"
|
||||
|
||||
import { MaterialIcon } from "../Icons/MaterialIcon/index.tsx"
|
||||
import { ChipButton } from "./ChipButton.tsx"
|
||||
import { config as chipButtonConfig } from "./variants"
|
||||
import { MaterialIcon } from "../Icons/MaterialIcon/index.tsx"
|
||||
|
||||
const meta: Meta<typeof ChipButton> = {
|
||||
title: "Core Components/ChipButton",
|
||||
component: ChipButton,
|
||||
argTypes: {
|
||||
children: {
|
||||
table: {
|
||||
disable: true,
|
||||
},
|
||||
},
|
||||
variant: {
|
||||
control: "select",
|
||||
type: "string",
|
||||
options: Object.keys(chipButtonConfig.variants.variant),
|
||||
table: {
|
||||
type: {
|
||||
summary: Object.keys(chipButtonConfig.variants.variant).join(" | "),
|
||||
},
|
||||
defaultValue: { summary: chipButtonConfig.defaultVariants.variant },
|
||||
},
|
||||
},
|
||||
size: {
|
||||
control: "select",
|
||||
options: Object.keys(chipButtonConfig.variants.size),
|
||||
table: {
|
||||
type: {
|
||||
summary: Object.keys(chipButtonConfig.variants.size).join(" | "),
|
||||
},
|
||||
defaultValue: { summary: "Large" },
|
||||
},
|
||||
description:
|
||||
"Sets the size of the ChipButton component. This only affects the `FilterRounded` variant.",
|
||||
},
|
||||
selected: {
|
||||
control: "boolean",
|
||||
table: {
|
||||
type: { summary: "boolean" },
|
||||
defaultValue: { summary: "false" },
|
||||
},
|
||||
},
|
||||
onPress: {
|
||||
table: {
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
import type { Meta, StoryObj } from "@storybook/nextjs-vite"
|
||||
|
||||
import { ContentCard } from "./ContentCard.tsx"
|
||||
|
||||
const IMAGE = {
|
||||
id: 1,
|
||||
url: "./img/img2.jpg",
|
||||
title: "Placeholder image",
|
||||
meta: {
|
||||
alt: "Placeholder image",
|
||||
caption: "This is a placeholder image",
|
||||
},
|
||||
focalPoint: { x: 50, y: 50 },
|
||||
dimensions: { width: 1920, height: 1189, aspectRatio: 1.61 },
|
||||
}
|
||||
const IMAGE_2 = {
|
||||
...IMAGE,
|
||||
id: 2,
|
||||
url: "./img/img3.jpg",
|
||||
dimensions: { width: 1920, height: 2880, aspectRatio: 0.67 },
|
||||
}
|
||||
|
||||
const DEFAULT_ARGS = {
|
||||
heading: "Lorem ipsum",
|
||||
bodyText:
|
||||
"Dolor sit amet, consectetur adipiscing elit. Curabitur vitae neque non ipsum efficitur hendrerit at ut nulla.",
|
||||
link: {
|
||||
href: "#",
|
||||
text: "Learn more",
|
||||
},
|
||||
image: IMAGE,
|
||||
}
|
||||
|
||||
const meta: Meta<typeof ContentCard> = {
|
||||
title: "Core Components/Cards/ContentCard",
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component:
|
||||
"The card itself does not have a maximum width, but it will adapt to the width of its container. The card is mostly used together with other content cards. It is recommended to use the ContentCard inside a grid or a container with a set maximum width for best results.",
|
||||
},
|
||||
},
|
||||
},
|
||||
component: ContentCard,
|
||||
argTypes: {
|
||||
heading: {
|
||||
control: "text",
|
||||
table: {
|
||||
type: { summary: "string" },
|
||||
},
|
||||
},
|
||||
bodyText: {
|
||||
control: "text",
|
||||
table: {
|
||||
type: { summary: "string" },
|
||||
},
|
||||
},
|
||||
promoText: {
|
||||
control: "text",
|
||||
table: {
|
||||
type: { summary: "string | undefined" },
|
||||
},
|
||||
},
|
||||
link: {
|
||||
control: "object",
|
||||
description:
|
||||
"The link of where the card should navigate to. The whole card is clickable.",
|
||||
table: {
|
||||
type: {
|
||||
summary:
|
||||
"{href: string, openInNewTab?: boolean, isExternal?: boolean}",
|
||||
},
|
||||
},
|
||||
},
|
||||
image: {
|
||||
control: "object",
|
||||
table: {
|
||||
type: {
|
||||
summary: "ImageVaultAsset",
|
||||
detail:
|
||||
"{ id: number, url: string, meta: {alt?: string | null, caption?: string | null}, focalPoint: { x: number, y: number }, dimensions: { width: number, height: number, aspectRatio: number } }",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
args: { ...DEFAULT_ARGS },
|
||||
globals: {
|
||||
backgrounds: { default: "storybookLight" },
|
||||
},
|
||||
}
|
||||
|
||||
export default meta
|
||||
|
||||
type Story = StoryObj<typeof ContentCard>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
...meta.args,
|
||||
},
|
||||
}
|
||||
|
||||
export const WithPromoText: Story = {
|
||||
args: {
|
||||
...meta.args,
|
||||
promoText: "Popular choice",
|
||||
},
|
||||
}
|
||||
|
||||
export const MultipleCards: Story = {
|
||||
args: {
|
||||
...meta.args,
|
||||
},
|
||||
render: (args) => (
|
||||
<div
|
||||
style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "repeat(2, 1fr)",
|
||||
gap: "16px",
|
||||
width: "min(800px, 100%)",
|
||||
margin: "0 auto",
|
||||
}}
|
||||
>
|
||||
<ContentCard {...args} promoText="Popular choice" />
|
||||
<ContentCard {...args} image={IMAGE_2} />
|
||||
</div>
|
||||
),
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
"use client"
|
||||
|
||||
import { cx } from "class-variance-authority"
|
||||
import NextLink from "next/link"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import styles from "./contentCard.module.css"
|
||||
|
||||
import type { ImageVaultAsset } from "@scandic-hotels/common/utils/imageVault"
|
||||
import { ChipStatic } from "../ChipStatic"
|
||||
import Image from "../Image"
|
||||
import { Typography } from "../Typography"
|
||||
|
||||
interface ContentCardProps {
|
||||
link?: {
|
||||
href: string
|
||||
openInNewTab?: boolean
|
||||
isExternal?: boolean
|
||||
}
|
||||
heading: string
|
||||
image: ImageVaultAsset
|
||||
bodyText: string
|
||||
promoText?: string
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function ContentCard({
|
||||
heading,
|
||||
image,
|
||||
bodyText,
|
||||
promoText,
|
||||
className,
|
||||
link,
|
||||
}: ContentCardProps) {
|
||||
const intl = useIntl()
|
||||
|
||||
const card = (
|
||||
<article className={cx(styles.card, className)}>
|
||||
<div className={styles.imageContainer}>
|
||||
<Image
|
||||
src={image.url}
|
||||
alt={image.meta.alt || image.meta.caption || ""}
|
||||
className={styles.image}
|
||||
fill
|
||||
sizes="(min-width: 768px) 413px, 100vw"
|
||||
focalPoint={image.focalPoint}
|
||||
dimensions={image.dimensions}
|
||||
/>
|
||||
{promoText ? (
|
||||
<ChipStatic
|
||||
color="Neutral"
|
||||
size="sm"
|
||||
lowerCase
|
||||
className={styles.promoTag}
|
||||
>
|
||||
{promoText}
|
||||
</ChipStatic>
|
||||
) : null}
|
||||
</div>
|
||||
<div className={styles.content}>
|
||||
<Typography variant="Title/Subtitle/md">
|
||||
<h4>{heading}</h4>
|
||||
</Typography>
|
||||
<Typography variant="Body/Paragraph/mdRegular">
|
||||
<p>{bodyText}</p>
|
||||
</Typography>
|
||||
</div>
|
||||
</article>
|
||||
)
|
||||
|
||||
if (!link) return card
|
||||
|
||||
const linkProps = {
|
||||
className: styles.link,
|
||||
...(link.openInNewTab && {
|
||||
target: "_blank",
|
||||
rel: "noopener noreferrer",
|
||||
title: intl.formatMessage({
|
||||
id: "common.linkOpenInNewTab",
|
||||
defaultMessage: "Opens in a new tab/window",
|
||||
}),
|
||||
}),
|
||||
}
|
||||
|
||||
return (
|
||||
<NextLink href={link.href} {...linkProps}>
|
||||
{card}
|
||||
</NextLink>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
.card {
|
||||
display: grid;
|
||||
|
||||
&:hover {
|
||||
.imageContainer,
|
||||
.image {
|
||||
border-radius: var(--Corner-radius-lg);
|
||||
}
|
||||
.image {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.imageContainer {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 250px;
|
||||
border-radius: var(--Corner-radius-md);
|
||||
overflow: hidden;
|
||||
transition: border-radius 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.image {
|
||||
border-radius: var(--Corner-radius-md);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transition:
|
||||
transform 0.3s ease-in-out,
|
||||
border-radius 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.promoTag {
|
||||
position: absolute;
|
||||
top: 14px;
|
||||
left: 14px;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: var(--Space-x15);
|
||||
align-self: stretch;
|
||||
padding: var(--Space-x2) var(--Space-x2) var(--Space-x2) 0;
|
||||
}
|
||||
|
||||
.link {
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { ContentCard } from "./ContentCard"
|
||||
Reference in New Issue
Block a user