feat(SW-1988): Replaced current bed component with new design

Approved-by: Chuma Mcphoy (We Ahead)
This commit is contained in:
Erik Tiekstra
2025-03-27 14:47:50 +00:00
parent a28fa67195
commit a6cd7e6111
36 changed files with 687 additions and 329 deletions

View File

@@ -8,7 +8,7 @@
display: grid;
gap: var(--Spacing-x2);
grid-template-columns: repeat(auto-fill, minmax(230px, 1fr));
width: min(600px, 100%);
width: min(700px, 100%);
}
.iconContainer {

View File

@@ -10,7 +10,7 @@ import {
type ExtraBedTypeEnum,
} from "@/constants/booking"
import RadioCard from "@/components/TempDesignSystem/Form/ChoiceCard/Radio"
import RadioCard from "@/components/TempDesignSystem/Form/RadioCard"
import { useRoomContext } from "@/contexts/Details/Room"
import BedTypeInfo from "./BedTypeInfo"
@@ -114,8 +114,8 @@ function BedIconRenderer({
return (
<div className={`${props.className} ${styles.iconContainer}`}>
<MainBedIcon size={32} />
{ExtraBedIcon && <ExtraBedIcon size={32} />}
<MainBedIcon height={32} />
{ExtraBedIcon && <ExtraBedIcon height={32} />}
</div>
)
}

View File

@@ -1,7 +0,0 @@
import Card from "./_Card"
import type { RadioProps } from "./_Card/card"
export default function RadioCard(props: RadioProps) {
return <Card {...props} type="radio" />
}

View File

@@ -1,76 +0,0 @@
.label {
background-color: var(--Base-Surface-Primary-light-Normal);
border: 1px solid var(--Base-Border-Subtle);
border-radius: var(--Corner-radius-Large);
cursor: pointer;
display: grid;
grid-template-columns: 1fr auto;
padding: var(--Spacing-x-one-and-half) var(--Spacing-x2);
transition: all 200ms ease;
width: min(100%, 600px);
grid-column-gap: var(--Spacing-x2);
}
.label:hover {
background-color: var(--Base-Surface-Secondary-light-Hover);
}
.label:has(:checked) {
background-color: var(--Primary-Light-Surface-Normal);
border-color: var(--Base-Border-Hover);
}
.icon {
align-self: center;
grid-column: 2/3;
grid-row: 1/3;
justify-self: flex-end;
transition: fill 200ms ease;
}
.label:hover .icon,
.label:hover .icon *,
.label:has(:checked) .icon,
.label:has(:checked) .icon * {
fill: var(--Base-Text-Medium-contrast);
}
.label[data-declined="true"]:hover .icon,
.label[data-declined="true"]:hover .icon *,
.label[data-declined="true"]:has(:checked) .icon,
.label[data-declined="true"]:has(:checked) .icon * {
fill: var(--Base-Text-Disabled);
}
.subtitle {
grid-column: 1 / 2;
grid-row: 2;
}
.title {
grid-column: 1 / 2;
}
.label .text {
margin-top: var(--Spacing-x1);
grid-column: 1/-1;
}
.listItem {
align-items: center;
display: flex;
gap: var(--Spacing-x-quarter);
grid-column: 1/-1;
}
.listItem:first-of-type {
margin-top: var(--Spacing-x1);
}
.listItem:nth-of-type(n + 2) {
margin-top: var(--Spacing-x-quarter);
}
.highlight {
color: var(--Scandic-Brand-Scandic-Red);
}

View File

@@ -1,51 +0,0 @@
import type { IconProps } from "@scandic-hotels/design-system/Icons"
interface BaseCardProps
extends Omit<React.LabelHTMLAttributes<HTMLLabelElement>, "title"> {
Icon?: (props: IconProps) => JSX.Element
declined?: boolean
highlightSubtitle?: boolean
iconHeight?: number
iconWidth?: number
name: string
subtitle?: React.ReactNode
title: React.ReactNode
type: "checkbox" | "radio"
value?: string
}
interface ListCardProps extends BaseCardProps {
list: {
title: string
}[]
text?: never
}
interface TextCardProps extends BaseCardProps {
list?: never
text: React.ReactNode
}
interface CleanCardProps extends BaseCardProps {
list?: never
text?: never
}
export type CardProps = ListCardProps | TextCardProps | CleanCardProps
export type CheckboxProps =
| Omit<ListCardProps, "type">
| Omit<TextCardProps, "type">
export type RadioProps =
| Omit<ListCardProps, "type">
| Omit<TextCardProps, "type">
| Omit<CleanCardProps, "type">
export interface ListProps extends Pick<ListCardProps, "declined"> {
list?: ListCardProps["list"]
}
export interface SubtitleProps
extends Pick<BaseCardProps, "highlightSubtitle" | "subtitle"> {}
export interface TextProps extends Pick<TextCardProps, "text"> {}

View File

@@ -1,116 +0,0 @@
"use client"
import { useFormContext } from "react-hook-form"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons"
import Caption from "@/components/TempDesignSystem/Text/Caption"
import Footnote from "@/components/TempDesignSystem/Text/Footnote"
import styles from "./card.module.css"
import type { CardProps, ListProps, SubtitleProps, TextProps } from "./card"
export default function Card({
Icon,
iconHeight = 32,
iconWidth = 32,
declined = false,
highlightSubtitle = false,
id,
list,
name,
subtitle,
text,
title,
type,
value,
}: CardProps) {
const { register, setValue } = useFormContext()
function onLabelClick(event: React.MouseEvent) {
// Preventing click event on label elements firing twice: https://github.com/facebook/react/issues/14295
event.preventDefault()
setValue(name, value)
}
return (
<label
className={styles.label}
data-declined={declined}
onClick={onLabelClick}
tabIndex={0}
>
<Caption className={styles.title} color="burgundy" type="label" uppercase>
{title}
</Caption>
<Subtitle highlightSubtitle={highlightSubtitle} subtitle={subtitle} />
{Icon ? (
<Icon
className={styles.icon}
color="Icon/Intense"
height={iconHeight}
width={iconWidth}
/>
) : null}
<List declined={declined} list={list} />
<Text text={text} />
<input
{...register(name)}
aria-hidden
id={id || name}
hidden
type={type}
value={value}
/>
</label>
)
}
function List({ declined, list }: ListProps) {
if (!list) {
return null
}
return list.map((listItem) => (
<span key={listItem.title} className={styles.listItem}>
{declined ? (
<MaterialIcon icon="close" color="Icon/Feedback/Neutral" size={20} />
) : (
<MaterialIcon icon="check" color="Icon/Accent" size={20} />
)}
<Footnote color="uiTextMediumContrast">{listItem.title}</Footnote>
</span>
))
}
function Subtitle({ highlightSubtitle, subtitle }: SubtitleProps) {
if (!subtitle) {
return null
}
return (
<Caption
className={styles.subtitle}
color={highlightSubtitle ? "baseTextAccent" : "uiTextMediumContrast"}
type="label"
uppercase
>
{subtitle}
</Caption>
)
}
function Text({ text }: TextProps) {
if (!text) {
return null
}
return (
<Footnote className={styles.text} color="uiTextMediumContrast">
{text}
</Footnote>
)
}
export function Highlight({ children }: React.PropsWithChildren) {
return <span className={styles.highlight}>{children}</span>
}

View File

@@ -0,0 +1,69 @@
"use client"
import { cx } from "class-variance-authority"
import { useFormContext } from "react-hook-form"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons"
import { Typography } from "@scandic-hotels/design-system/Typography"
import styles from "./radioCard.module.css"
import type { RadioCardProps } from "./types"
export default function RadioCard({
Icon,
iconHeight = 32,
id,
name,
subtitle,
title,
value,
disabled = false,
}: RadioCardProps) {
const { register, setValue } = useFormContext()
function onLabelClick(event: React.MouseEvent) {
// Preventing click event on label elements firing twice: https://github.com/facebook/react/issues/14295
event.preventDefault()
if (!disabled) {
setValue(name, value)
}
}
return (
<label
className={cx(styles.label, { [styles.disabled]: disabled })}
onClick={onLabelClick}
tabIndex={0}
>
<MaterialIcon
icon="check"
className={styles.selectedIcon}
size={22}
color="Icon/Inverted"
/>
{Icon ? (
<Icon
className={styles.icon}
color="CurrentColor"
height={iconHeight}
/>
) : null}
<Typography variant="Body/Paragraph/mdBold" className={styles.title}>
<p>{title}</p>
</Typography>
<Typography variant="Body/Paragraph/mdBold" className={styles.subtitle}>
<p>{subtitle}</p>
</Typography>
<input
{...register(name)}
aria-hidden
id={id || name}
hidden
type="radio"
disabled={disabled}
value={value}
/>
</label>
)
}

View File

@@ -0,0 +1,55 @@
.label {
position: relative;
cursor: pointer;
display: grid;
grid-template-columns: 1fr auto;
grid-template-areas: "icon icon" "title subtitle";
border-radius: var(--Corner-radius-md);
border: 1px solid var(--Border-Intense);
background: var(--Surface-Primary-Default);
padding: var(--Space-x2) var(--Space-x3);
gap: var(--Space-x1);
}
.label.disabled {
background: var(--Surface-Primary-Disabled);
filter: grayscale(1);
opacity: 0.5;
cursor: not-allowed;
}
.label:has(:checked) {
border: 2px solid var(--Border-Interactive-Selected);
}
.label:not(:has(:checked)) .selectedIcon {
display: none;
}
.selectedIcon {
position: absolute;
top: calc(-1 * var(--Space-x15));
right: calc(-1 * var(--Space-x15));
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
border: 2px solid var(--Base-Border-Inverted);
border-radius: var(--Corner-radius-Rounded);
background-color: var(--Surface-Feedback-Succes-Accent);
}
.icon {
grid-area: icon;
}
.subtitle {
grid-area: subtitle;
color: var(--Text-Default);
}
.title {
grid-area: title;
color: var(--Text-Default);
}

View File

@@ -0,0 +1,12 @@
import type { IconProps } from "@scandic-hotels/design-system/Icons"
export interface RadioCardProps
extends Omit<React.LabelHTMLAttributes<HTMLLabelElement>, "title"> {
Icon?: (props: IconProps) => JSX.Element
iconHeight?: number
name: string
subtitle?: React.ReactNode
title: React.ReactNode
value?: string
disabled?: boolean
}

View File

@@ -1,9 +1,14 @@
import {
BedBunkExtraIcon,
BedGenericIcon,
BedKingIcon,
BedPullOutExtraIcon,
BedQueenIcon,
BedSingleIcon,
BedSofaExtraIcon,
BedTwinIcon,
BedWallExtraIcon,
type IconProps,
MaterialIcon,
MdiBunkBedIcon,
MovingBedsIcon,
WardIcon,
} from "@scandic-hotels/design-system/Icons"
export enum BookingStatusEnum {
@@ -142,15 +147,16 @@ export const BED_TYPE_ICONS: Record<
BedTypes,
(props: IconProps) => JSX.Element
> = {
King: () => <MaterialIcon icon="king_bed" />,
Queen: () => <MaterialIcon icon="bed" />,
Single: () => <MaterialIcon icon="single_bed" />,
Twin: () => <MaterialIcon icon="bed" />,
SofaBed: () => <MaterialIcon icon="chair" />,
WallBed: WardIcon,
BunkBed: MdiBunkBedIcon,
PullOutBed: MovingBedsIcon,
Other: () => <MaterialIcon icon="single_bed" />,
King: BedKingIcon,
Queen: BedQueenIcon,
Single: BedSingleIcon,
Twin: BedTwinIcon,
SofaBed: BedSofaExtraIcon,
WallBed: BedWallExtraIcon,
BunkBed: BedBunkExtraIcon,
PullOutBed: BedPullOutExtraIcon,
Other: BedGenericIcon,
}
export enum CancellationRuleEnum {