diff --git a/apps/scandic-web/components/Blocks/CardsGrid.tsx b/apps/scandic-web/components/Blocks/CardsGrid.tsx index 9463665bd..675a7fd3b 100644 --- a/apps/scandic-web/components/Blocks/CardsGrid.tsx +++ b/apps/scandic-web/components/Blocks/CardsGrid.tsx @@ -1,3 +1,4 @@ +import { TeaserCard } from "@scandic-hotels/design-system/TeaserCard" import { CardsGridEnum, CardsGridLayoutEnum, @@ -9,7 +10,6 @@ import { SectionHeader } from "@/components/Section/Header" import Card from "@/components/TempDesignSystem/Card" import Grids from "@/components/TempDesignSystem/Grids" import LoyaltyCard from "@/components/TempDesignSystem/LoyaltyCard" -import TeaserCard from "@/components/TempDesignSystem/TeaserCard" import type { CardsGrid as CardsGridBlock } from "@scandic-hotels/trpc/types/blocks" import type { VariantProps } from "class-variance-authority" @@ -89,8 +89,8 @@ export default function CardsGrid({ return ( { - title: string - description: string - primaryButton?: CardProps["primaryButton"] - secondaryButton?: CardProps["secondaryButton"] - sidePeekButton?: SidePeekButton - sidePeekContent?: TeaserCard["sidepeek_content"] - image?: ImageVaultAsset - className?: string -} - -export interface TeaserCardSidepeekProps { - button: SidePeekButton - sidePeekContent: NonNullable -} diff --git a/packages/common/constants/alert.ts b/packages/common/constants/alert.ts index 87f527135..9fcff432f 100644 --- a/packages/common/constants/alert.ts +++ b/packages/common/constants/alert.ts @@ -9,13 +9,3 @@ export const AlertVisibleOnEnum = { WEB: "WEB", APP: "APP", } as const - -export type SidepeekContent = { - heading: string - content: { - json?: any - embedded_itemsConnection: { - edges: any - } - } -} diff --git a/packages/common/package.json b/packages/common/package.json index 0cdca7995..f81f9122f 100644 --- a/packages/common/package.json +++ b/packages/common/package.json @@ -30,6 +30,7 @@ "./constants/rateType": "./constants/rateType.ts", "./constants/routes/*": "./constants/routes/*.ts", "./constants/sessionKeys": "./constants/sessionKeys.ts", + "./constants/sidepeekContent": "./constants/sidepeekContent.ts", "./constants/signatureHotels": "./constants/signatureHotels.ts", "./constants/transactionType": "./constants/transactionType.ts", "./dataCache": "./dataCache/index.ts", diff --git a/packages/design-system/lib/components/Alert/Sidepeek/index.tsx b/packages/design-system/lib/components/Alert/Sidepeek/index.tsx index 182e67e1f..02b91d58f 100644 --- a/packages/design-system/lib/components/Alert/Sidepeek/index.tsx +++ b/packages/design-system/lib/components/Alert/Sidepeek/index.tsx @@ -42,8 +42,8 @@ export default function AlertSidepeek({ })} > diff --git a/packages/design-system/lib/components/Alert/Sidepeek/sidepeek.ts b/packages/design-system/lib/components/Alert/Sidepeek/sidepeek.ts index 1a6841273..50f68d64c 100644 --- a/packages/design-system/lib/components/Alert/Sidepeek/sidepeek.ts +++ b/packages/design-system/lib/components/Alert/Sidepeek/sidepeek.ts @@ -1,6 +1,6 @@ -import type { SidepeekContent } from "@scandic-hotels/common/constants/alert" +import { AlertSidepeekContent } from "../../../types/sidepeekContent" export interface AlertSidepeekProps { ctaText: string - sidePeekContent: NonNullable + sidePeekContent: NonNullable } diff --git a/packages/design-system/lib/components/Alert/alert.ts b/packages/design-system/lib/components/Alert/alert.ts index 0d2257744..2425888da 100644 --- a/packages/design-system/lib/components/Alert/alert.ts +++ b/packages/design-system/lib/components/Alert/alert.ts @@ -1,10 +1,8 @@ -import type { - AlertTypeEnum, - SidepeekContent, -} from "@scandic-hotels/common/constants/alert" +import type { AlertTypeEnum } from "@scandic-hotels/common/constants/alert" import type { VariantProps } from "class-variance-authority" import type { AriaRole, ReactNode } from "react" +import { AlertSidepeekContent } from "../../types/sidepeekContent" import type { alertVariants } from "./variants" export interface AlertProps extends VariantProps { @@ -17,7 +15,7 @@ export interface AlertProps extends VariantProps { phoneNumber?: string footnote?: string | null } | null - sidepeekContent?: SidepeekContent | null + sidepeekContent?: AlertSidepeekContent | null sidepeekCtaText?: string | null link?: { url: string diff --git a/apps/scandic-web/components/TempDesignSystem/TeaserCard/Sidepeek/index.tsx b/packages/design-system/lib/components/TeaserCard/Sidepeek/index.tsx similarity index 81% rename from apps/scandic-web/components/TempDesignSystem/TeaserCard/Sidepeek/index.tsx rename to packages/design-system/lib/components/TeaserCard/Sidepeek/index.tsx index c9acd54b7..2d344704a 100644 --- a/apps/scandic-web/components/TempDesignSystem/TeaserCard/Sidepeek/index.tsx +++ b/packages/design-system/lib/components/TeaserCard/Sidepeek/index.tsx @@ -3,15 +3,20 @@ import { useState } from "react" import { useIntl } from "react-intl" -import { Button } from "@scandic-hotels/design-system/Button" -import ButtonLink from "@scandic-hotels/design-system/ButtonLink" -import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" -import { JsonToHtml } from "@scandic-hotels/design-system/JsonToHtml" -import SidePeek from "@scandic-hotels/design-system/SidePeek" - +import { TeaserCardSidepeekContent } from "../../../types/sidepeekContent" +import { Button } from "../../Button" +import ButtonLink from "../../ButtonLink" +import { MaterialIcon } from "../../Icons/MaterialIcon" +import { JsonToHtml } from "../../JsonToHtml/JsonToHtml" +import SidePeek from "../../SidePeek" import styles from "./sidepeek.module.css" -import type { TeaserCardSidepeekProps } from "@/types/components/teaserCard" +interface TeaserCardSidepeekProps { + button: { + call_to_action_text: string + } + sidePeekContent: TeaserCardSidepeekContent +} export default function TeaserCardSidepeek({ button, diff --git a/apps/scandic-web/components/TempDesignSystem/TeaserCard/Sidepeek/sidepeek.module.css b/packages/design-system/lib/components/TeaserCard/Sidepeek/sidepeek.module.css similarity index 100% rename from apps/scandic-web/components/TempDesignSystem/TeaserCard/Sidepeek/sidepeek.module.css rename to packages/design-system/lib/components/TeaserCard/Sidepeek/sidepeek.module.css diff --git a/packages/design-system/lib/components/TeaserCard/TeaserCard.stories.tsx b/packages/design-system/lib/components/TeaserCard/TeaserCard.stories.tsx new file mode 100644 index 000000000..d53ab1803 --- /dev/null +++ b/packages/design-system/lib/components/TeaserCard/TeaserCard.stories.tsx @@ -0,0 +1,343 @@ +import type { Meta, StoryObj } from "@storybook/nextjs-vite" + +import { TeaserCard } from "./TeaserCard.tsx" +import { config } from "./variants.ts" + +const PRIMARY_BUTTON = { + title: "Primary action", + href: "#", + openInNewTab: false, +} + +const SECONDARY_BUTTON = { + ...PRIMARY_BUTTON, + title: "Secondary action", +} + +const SIDEPEEK_CONTENT = { + heading: "Sidepeek heading", + content: { + json: { + type: "doc", + attrs: {}, + uid: "8126b570ffef4090a78f8c863d73c3b8", + children: [ + { + type: "p", + attrs: {}, + uid: "ed82964e32764cf589e07a251014543b", + children: [ + { + text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur vitae neque non ipsum efficitur hendrerit at ut nulla. Cras in tellus et ligula posuere ullamcorper. Praesent pulvinar rutrum metus ut gravida.", + }, + ], + }, + { + type: "h3", + attrs: {}, + uid: "799903bc123d479d9bcf29cb4ba24b65", + children: [ + { + text: "Lorem ipsum", + }, + ], + }, + { + type: "p", + attrs: {}, + uid: "00886e5b0a5d4268930a1472b58e9170", + children: [ + { + text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur vitae neque non ipsum efficitur hendrerit at ut nulla. Cras in tellus et ligula posuere ullamcorper. Praesent pulvinar rutrum metus ut gravida.", + }, + ], + }, + { + type: "p", + attrs: {}, + uid: "6bd20356e8bd4612a99e1af6061f861c", + children: [ + { + text: "", + }, + { + uid: "28e963603b714055b948a038539bfd98", + type: "a", + attrs: { + url: "https://www.scandichotels.com/en", + target: "_blank", + }, + children: [ + { + text: "Learn more about this", + }, + ], + }, + { + text: "", + }, + ], + }, + { + type: "h3", + attrs: {}, + uid: "9db195292c5b49ac970edc8b4be19081", + children: [ + { + text: "Dolor sit amet", + }, + ], + }, + { + type: "p", + attrs: {}, + uid: "f45861b9146040ff8a6076cc2bc4d218", + children: [ + { + text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur vitae neque non ipsum efficitur hendrerit at ut nulla. Cras in tellus et ligula posuere ullamcorper. Praesent pulvinar rutrum metus ut gravida.", + }, + ], + }, + { + uid: "a40eb77a4c26401689255e0caa2f564a", + type: "ul", + children: [ + { + type: "li", + attrs: {}, + uid: "701d76e63827483cb80180056d1b396e", + children: [ + { + text: "Lorem ipsum", + id: "", + }, + ], + }, + { + type: "li", + attrs: {}, + uid: "f3f28b61c4b54162a7c723995a38a063", + children: [ + { + text: "Dolor sit amet consectetur adipiscing elit.", + id: "", + }, + ], + }, + { + type: "li", + attrs: {}, + uid: "27f51d6ad42e4dafb5037b5ee790f0b2", + children: [ + { + text: "Curabitur vitae neque non ipsum efficitur", + id: "", + }, + ], + }, + ], + attrs: {}, + }, + ], + _version: 16, + }, + embedded_itemsConnection: { + edges: [], + }, + }, + primary_button: { + title: "Sidepeek primary action", + href: "#", + openInNewTab: false, + }, +} + +const DEFAULT_ARGS = { + heading: "Lorem ipsum", + bodyText: + "Dolor sit amet, consectetur adipiscing elit. Curabitur vitae neque non ipsum efficitur hendrerit at ut nulla.", + 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 }, + }, + primaryButton: PRIMARY_BUTTON, + secondaryButton: SECONDARY_BUTTON, + alwaysStack: false, +} + +const meta: Meta = { + title: "Core Components/Cards/TeaserCard", + 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: TeaserCard, + argTypes: { + heading: { + control: "text", + table: { + type: { summary: "string" }, + }, + }, + bodyText: { + control: "text", + table: { + type: { summary: "string" }, + }, + }, + primaryButton: { + control: "object", + table: { + type: { + summary: + "{ title: string, href: string, openInNewTab?: boolean, onPress?: () => void } | undefined", + }, + }, + }, + secondaryButton: { + control: "object", + table: { + type: { + summary: + "{ title: string, href: string, openInNewTab?: boolean, onPress?: () => void } | undefined", + }, + }, + }, + sidePeekButton: { + control: "object", + table: { + type: { + summary: "{ call_to_action_text: string } | undefined", + }, + }, + }, + sidePeekContent: { + control: "object", + table: { + type: { + summary: "any | undefined", + }, + }, + }, + image: { + control: "object", + table: { + type: { + summary: "ImageVaultAsset | undefined", + detail: + "{ id: number, url: string, meta: {alt?: string | null, caption?: string | null}, focalPoint: { x: number, y: number }, dimensions: { width: number, height: number, aspectRatio: number } }", + }, + }, + }, + style: { + control: "select", + options: Object.keys(config.variants.style), + table: { + type: { summary: Object.keys(config.variants.style).join(" | ") }, + }, + defaultValue: config.defaultVariants.style, + }, + alwaysStack: { + control: "boolean", + table: { + type: { summary: "boolean" }, + }, + defaultValue: config.defaultVariants.alwaysStack.toString(), + description: + "If true, the buttons will always be stacked vertically, regardless if these would fit next to each other.", + }, + }, + args: { ...DEFAULT_ARGS }, + globals: { + backgrounds: { default: "storybookLight" }, + }, + decorators: [ + (Story, context) => { + const showMultipleStyles = [ + "with sidepeek", + "without image", + "always stack buttons", + ].some((substring) => context.name.toLowerCase().includes(substring)) + if (showMultipleStyles) { + return ( +
+ {Object.keys(config.variants.style).map((style, ix) => { + return ( + + ) + })} +
+ ) + } + + return ( +
+ +
+ ) + }, + ], +} + +export default meta + +type Story = StoryObj + +export const Default: Story = { + args: { + ...meta.args, + }, +} + +export const Featured: Story = { + args: { + ...meta.args, + style: "featured", + }, +} + +export const WithSidepeek: Story = { + args: { + ...meta.args, + sidePeekButton: { + call_to_action_text: "Side peek action", + }, + sidePeekContent: SIDEPEEK_CONTENT, + style: "featured", + }, +} + +export const WithoutImage: Story = { + args: { + ...meta.args, + image: undefined, + }, +} + +export const AlwaysStackButtons: Story = { + args: { + ...meta.args, + alwaysStack: true, + }, +} diff --git a/apps/scandic-web/components/TempDesignSystem/TeaserCard/index.tsx b/packages/design-system/lib/components/TeaserCard/TeaserCard.tsx similarity index 64% rename from apps/scandic-web/components/TempDesignSystem/TeaserCard/index.tsx rename to packages/design-system/lib/components/TeaserCard/TeaserCard.tsx index e9afa9b66..843306d95 100644 --- a/apps/scandic-web/components/TempDesignSystem/TeaserCard/index.tsx +++ b/packages/design-system/lib/components/TeaserCard/TeaserCard.tsx @@ -1,27 +1,47 @@ -import ButtonLink from "@scandic-hotels/design-system/ButtonLink" -import Image from "@scandic-hotels/design-system/Image" -import { Typography } from "@scandic-hotels/design-system/Typography" - +import { ImageVaultAsset } from "@scandic-hotels/common/utils/imageVault" +import { VariantProps } from "class-variance-authority" +import { TeaserCardSidepeekContent } from "../../types/sidepeekContent" +import { type ButtonProps } from "../Button" +import ButtonLink from "../ButtonLink" +import Image from "../Image" +import { Typography } from "../Typography" import TeaserCardSidepeek from "./Sidepeek" +import styles from "./teaserCard.module.css" import { teaserCardVariants } from "./variants" -import styles from "./teaserCard.module.css" +interface SidePeekButton { + call_to_action_text: string +} -import type { TeaserCardProps } from "@/types/components/teaserCard" +interface TeaserCardButton extends Pick { + title: string + href: string + openInNewTab?: boolean +} +interface TeaserCardProps extends VariantProps { + heading: string + bodyText: string + primaryButton?: TeaserCardButton + secondaryButton?: TeaserCardButton + sidePeekButton?: SidePeekButton + sidePeekContent?: TeaserCardSidepeekContent + image?: ImageVaultAsset + className?: string +} -export default function TeaserCard({ - title, - description, +export function TeaserCard({ + heading, + bodyText, primaryButton, secondaryButton, sidePeekButton, sidePeekContent, image, - intent, + style, alwaysStack = false, className, }: TeaserCardProps) { - const classNames = teaserCardVariants({ intent, alwaysStack, className }) + const classNames = teaserCardVariants({ style, alwaysStack, className }) return (
@@ -30,7 +50,6 @@ export default function TeaserCard({ {image.meta?.alt -

{title}

+

{heading}

-

{description}

+

{bodyText}

{sidePeekButton && sidePeekContent ? ( diff --git a/packages/design-system/lib/components/TeaserCard/index.tsx b/packages/design-system/lib/components/TeaserCard/index.tsx new file mode 100644 index 000000000..257f45d99 --- /dev/null +++ b/packages/design-system/lib/components/TeaserCard/index.tsx @@ -0,0 +1 @@ +export { TeaserCard } from "./TeaserCard" diff --git a/apps/scandic-web/components/TempDesignSystem/TeaserCard/teaserCard.module.css b/packages/design-system/lib/components/TeaserCard/teaserCard.module.css similarity index 52% rename from apps/scandic-web/components/TempDesignSystem/TeaserCard/teaserCard.module.css rename to packages/design-system/lib/components/TeaserCard/teaserCard.module.css index 14e562fa1..970e34759 100644 --- a/apps/scandic-web/components/TempDesignSystem/TeaserCard/teaserCard.module.css +++ b/packages/design-system/lib/components/TeaserCard/teaserCard.module.css @@ -1,8 +1,18 @@ -.card { +.teaserCard { border-radius: var(--Corner-radius-md); display: flex; flex-direction: column; overflow: hidden; + border: 1px solid var(--Border-Default); + color: var(--Text-Default); + + &.default { + background-color: var(--Surface-Secondary-Default); + } + + &.featured { + background-color: var(--Surface-Primary-Default); + } } .imageContainer { @@ -11,36 +21,12 @@ position: relative; } -.default { - background-color: var(--Base-Surface-Subtle-Normal); -} - -.featured { - background-color: var(--Main-Grey-White); -} - -.default, -.featured { - border: 1px solid var(--Base-Border-Subtle); -} - -.image { - width: 100%; - height: 12.5rem; /* 200px */ -} - .content { display: grid; gap: var(--Space-x15); padding: var(--Space-x2) var(--Space-x3); grid-template-rows: auto 1fr auto; flex-grow: 1; - - color: var(--Main-Grey-100); -} - -.description { - color: var(--Base-Text-Medium-contrast); } .ctaContainer { @@ -51,11 +37,11 @@ } @media (min-width: 1367px) { - .card:not(.alwaysStack) .ctaContainer { + .teaserCard:not(.alwaysStack) .ctaContainer { grid-template-columns: repeat(auto-fit, minmax(0, 1fr)); - } - .card:not(.alwaysStack) .ctaContainer:has(:only-child) { - grid-template-columns: 1fr; + &:has(:only-child) { + grid-template-columns: 1fr; + } } } diff --git a/apps/scandic-web/components/TempDesignSystem/TeaserCard/variants.ts b/packages/design-system/lib/components/TeaserCard/variants.ts similarity index 69% rename from apps/scandic-web/components/TempDesignSystem/TeaserCard/variants.ts rename to packages/design-system/lib/components/TeaserCard/variants.ts index 294f6d913..1e471a4dd 100644 --- a/apps/scandic-web/components/TempDesignSystem/TeaserCard/variants.ts +++ b/packages/design-system/lib/components/TeaserCard/variants.ts @@ -2,9 +2,9 @@ import { cva } from "class-variance-authority" import styles from "./teaserCard.module.css" -export const teaserCardVariants = cva(styles.card, { +export const config = { variants: { - intent: { + style: { default: styles.default, featured: styles.featured, }, @@ -14,7 +14,9 @@ export const teaserCardVariants = cva(styles.card, { }, }, defaultVariants: { - intent: "default", + style: "default", alwaysStack: false, }, -}) +} as const + +export const teaserCardVariants = cva(styles.teaserCard, config) diff --git a/packages/design-system/lib/normalize.css b/packages/design-system/lib/normalize.css index 85e2d9a68..d8711edde 100644 --- a/packages/design-system/lib/normalize.css +++ b/packages/design-system/lib/normalize.css @@ -24,3 +24,16 @@ ul { outline-color: var(--Border-Interactive-Focus); outline-offset: 2px; } + +/* From Tailwind */ +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border-width: 0; +} diff --git a/packages/design-system/lib/types/sidepeekContent.ts b/packages/design-system/lib/types/sidepeekContent.ts new file mode 100644 index 000000000..15acc8457 --- /dev/null +++ b/packages/design-system/lib/types/sidepeekContent.ts @@ -0,0 +1,26 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +type SidepeekContentBase = { + heading: string + content?: { + json?: any + embedded_itemsConnection: { + edges: any + } + } | null +} + +export type AlertSidepeekContent = SidepeekContentBase +export type TeaserCardSidepeekContent = SidepeekContentBase & { + primary_button?: { + href: string + title: string + openInNewTab?: boolean + isExternal?: boolean + } + secondary_button?: { + href: string + title: string + openInNewTab?: boolean + isExternal?: boolean + } +} diff --git a/packages/design-system/package.json b/packages/design-system/package.json index c7f0c0833..4599732bb 100644 --- a/packages/design-system/package.json +++ b/packages/design-system/package.json @@ -179,6 +179,7 @@ "./Switch": "./lib/components/Switch/index.tsx", "./Table": "./lib/components/Table/index.tsx", "./TermModal": "./lib/components/RateCard/TermModal/index.tsx", + "./TeaserCard": "./lib/components/TeaserCard/index.tsx", "./TextArea": "./lib/components/TextArea/index.tsx", "./TextLink": "./lib/components/TextLink/index.tsx", "./TextLinkButton": "./lib/components/TextLinkButton/index.tsx",