Merged in feat/BOOK-61-refactor-hotel-page-css-variables (pull request #3014)

Feat/BOOK-61 refactor hotel page css variables

* feat(BOOK-61): Breadcrumbs

* feat(BOOK-61): intro section

* feat(BOOK-61): show more button

* feat(BOOK-61): rooms section

* feat(BOOK-61): sidepeeks

* feat(BOOK-61): deprecated old Link component

* feat(BOOK-61): added new TextLink component to the design-system

* feat(BOOK-61): replaced deprecated links with new TextLink component

* feat(BOOK-61): miscellaneous changes


Approved-by: Bianca Widstam
Approved-by: Christel Westerberg
This commit is contained in:
Erik Tiekstra
2025-10-29 09:15:03 +00:00
parent bfe5c5f8bb
commit 333636c81a
122 changed files with 782 additions and 498 deletions

View File

@@ -14,7 +14,7 @@
max-width: var(--max-width-page);
margin: 0 auto;
display: flex;
gap: var(--Spacing-x2);
gap: var(--Space-x2);
}
.innerContent {
@@ -22,18 +22,18 @@
flex-direction: column;
justify-content: space-between;
align-items: flex-start;
gap: var(--Spacing-x1);
padding: var(--Spacing-x2) 0;
gap: var(--Space-x1);
padding: var(--Space-x2) 0;
flex-grow: 1;
}
.textWrapper {
display: grid;
gap: var(--Spacing-x-half);
gap: var(--Space-x05);
}
.alert .closeButton {
padding: var(--Spacing-x-one-and-half);
padding: var(--Space-x15);
display: flex;
align-items: center;
}
@@ -45,10 +45,10 @@
background-color: var(--Base-Surface-Primary-light-Normal);
}
.inline .innerContent {
padding-right: var(--Spacing-x3);
padding-right: var(--Space-x3);
}
.inline .iconWrapper {
padding: var(--Spacing-x-one-and-half);
padding: var(--Space-x15);
}
.inline.alarm .iconWrapper {
background-color: var(--Surface-Feedback-Error-Accent);
@@ -110,6 +110,6 @@
.innerContent {
flex-direction: row;
align-items: center;
gap: var(--Spacing-x2);
gap: var(--Space-x2);
}
}

View File

@@ -2,7 +2,7 @@
import { Button } from '../Button'
import { MaterialIcon } from '../Icons/MaterialIcon'
import Link from '../Link'
import Link from '../OldDSLink'
import { Typography } from '../Typography'
import AlertSidepeek from './Sidepeek'

View File

@@ -39,8 +39,6 @@ export const config = {
},
} as const
export const variants = cva(styles.button, withTypography(config))
const buttonConfig = {
variants: {
...config.variants,
@@ -48,10 +46,12 @@ const buttonConfig = {
},
defaultVariants: {
...config.defaultVariants,
typography: typographyConfig.defaultVariants.variant,
typography: 'Body/Paragraph/mdBold',
},
} as const
export const variants = cva(styles.button, withTypography(buttonConfig))
export function withButton<T>(config: T) {
return deepmerge(buttonConfig, config)
}

View File

@@ -1,7 +1,7 @@
import Footnote from '../Footnote'
import { Typography } from '../Typography'
import { chipVariants } from './variants'
import { VariantProps } from 'class-variance-authority'
import { chipVariants } from './variants'
export interface ChipProps
extends React.HtmlHTMLAttributes<HTMLDivElement>,
@@ -19,8 +19,8 @@ export default function Chip({
variant,
})
return (
<Footnote asChild>
<div className={classNames}>{children}</div>
</Footnote>
<Typography variant="Tag/sm">
<span className={classNames}>{children}</span>
</Typography>
)
}

View File

@@ -16,10 +16,10 @@ import { Divider } from '../Divider'
import { FacilityToIcon } from '../FacilityToIcon'
import HotelLogoIcon from '../Icons/Logos'
import ImageGallery, { GalleryImage } from '../ImageGallery'
import Link from '../OldDSLink'
import { Typography } from '../Typography'
import { HotelPointsRow } from './HotelPointsRow'
import { NoPriceAvailableCard } from './NoPriceAvailableCard'
import Link from '../Link'
import { Typography } from '../Typography'
import HotelChequeCard from './HotelChequeCard'
import { HotelPriceCard } from './HotelPriceCard'
@@ -28,13 +28,13 @@ import { hotelCardVariants } from './variants'
import styles from './hotelCard.module.css'
import type { Lang } from '@scandic-hotels/common/constants/language'
import { CurrencyEnum } from '@scandic-hotels/common/constants/currency'
import { FacilityEnum } from '@scandic-hotels/common/constants/facilities'
import { HotelType } from '@scandic-hotels/common/constants/hotelType'
import type { Lang } from '@scandic-hotels/common/constants/language'
import { RateTypeEnum } from '@scandic-hotels/common/constants/rateType'
import { BookingCodeChip } from '../BookingCodeChip'
import { HotelType } from '@scandic-hotels/common/constants/hotelType'
import { TripAdvisorChip } from '../TripAdvisorChip'
import { CurrencyEnum } from '@scandic-hotels/common/constants/currency'
type Price = {
pricePerStay: number

View File

@@ -4,7 +4,6 @@ import { Divider } from '../Divider'
import { MaterialIcon } from '../Icons/MaterialIcon'
import Image from '../Image'
import ImageContainer from '../ImageContainer'
import Link from '../Link'
import Table from '../Table'
import { Typography } from '../Typography'
@@ -21,6 +20,7 @@ import {
mapImageVaultAssetResponseToImageVaultAsset,
mapInsertResponseToImageVaultAsset,
} from '@scandic-hotels/common/utils/imageVault'
import { TextLink } from '../TextLink'
import type { EmbedByUid } from './JsonToHtml'
import type { Attributes } from './types/rte/attrs'
import {
@@ -90,7 +90,7 @@ export const renderOptions: RenderOptions = {
const { className, ...props } = extractPossibleAttributes(node.attrs)
return (
<Link
<TextLink
key={node.uid}
className={className}
{...props}
@@ -98,7 +98,7 @@ export const renderOptions: RenderOptions = {
target={
typeof node.attrs.target === 'string' ? node.attrs.target : '_blank'
}
textDecoration="underline"
isInline
>
{next(
// Sometimes editors happen to nest a reference inside a link and vice versa.
@@ -107,7 +107,7 @@ export const renderOptions: RenderOptions = {
embeds,
fullRenderOptions
)}
</Link>
</TextLink>
)
},
@@ -400,13 +400,12 @@ export const renderOptions: RenderOptions = {
} else if (node.attrs['display-type'] === 'link' && node.attrs.href) {
const { className, ...props } = extractPossibleAttributes(node.attrs)
return (
<Link
<TextLink
key={node.uid}
className={className}
href={node.attrs.href as string}
textDecoration="underline"
target="_blank"
variant="icon"
isInline
{...props}
>
{next(
@@ -417,7 +416,7 @@ export const renderOptions: RenderOptions = {
fullRenderOptions
)}
<MaterialIcon icon="open_in_new" size={20} color="CurrentColor" />
</Link>
</TextLink>
)
}
}
@@ -462,12 +461,12 @@ export const renderOptions: RenderOptions = {
}
return (
<Link
<TextLink
key={node.uid}
className={className}
{...props}
href={entryHref ?? nodeHref}
textDecoration="underline"
isInline
>
{next(
// Sometimes editors happen to nest a reference inside a link and vice versa.
@@ -476,7 +475,7 @@ export const renderOptions: RenderOptions = {
embeds,
fullRenderOptions
)}
</Link>
</TextLink>
)
}
}

View File

@@ -1,10 +1,10 @@
'use client'
import Link, { type LinkProps } from '../Link'
import { login } from '@scandic-hotels/common/constants/routes/handleAuth'
import Link, { type LinkProps } from '../OldDSLink'
import type { PropsWithChildren } from 'react'
import type { Lang } from '@scandic-hotels/common/constants/language'
import type { PropsWithChildren } from 'react'
export function LoginButton({
lang,

View File

@@ -1,11 +1,11 @@
import type { Meta, StoryObj } from '@storybook/nextjs-vite'
import { expect } from 'storybook/test'
import Link from '.'
import OldDSLink from '.'
const meta: Meta<typeof Link> = {
title: 'Components/Link',
component: Link,
const meta: Meta<typeof OldDSLink> = {
title: 'Components/OldDSLink',
component: OldDSLink,
argTypes: {
size: {
control: 'select',
@@ -31,14 +31,14 @@ const meta: Meta<typeof Link> = {
export default meta
type Story = StoryObj<typeof Link>
type Story = StoryObj<typeof OldDSLink>
export const Default: Story = {
args: {
active: false,
href: 'https://www.scandichotels.com/en',
},
render: (args) => <Link {...args}>{args.href}</Link>,
render: (args) => <OldDSLink {...args}>{args.href}</OldDSLink>,
play: async ({ canvasElement }) => {
const link = canvasElement.querySelector('a')
if (!link) throw new Error('Link not found')

View File

@@ -9,6 +9,9 @@ import type { LinkProps } from './link'
export { LinkProps }
/**
* @deprecated Use `@scandic-hotels/design-system/TextLink` instead.
*/
export default function Link({
active,
className,

View File

@@ -4,13 +4,13 @@ import { useContext, useRef } from 'react'
import { Dialog, Modal, ModalOverlay } from 'react-aria-components'
import { MaterialIcon } from '../Icons/MaterialIcon'
import { OldDSButton as Button } from '../OldDSButton'
import { Typography } from '../Typography'
import { SidePeekContext } from './SidePeekContext'
import SidePeekSEO from './SidePeekSEO'
import { IconButton } from '../IconButton'
import styles from './sidePeek.module.css'
interface SidePeekProps {
@@ -53,24 +53,24 @@ export default function SidePeek({
>
<Modal className={styles.modal}>
<Dialog className={styles.dialog} aria-label={title}>
<aside className={styles.sidePeek}>
<aside className={styles.aside}>
<header className={styles.header}>
{title ? (
<Typography variant="Title/md">
<h2 className={styles.heading}>{title}</h2>
</Typography>
) : null}
<Button
aria-label={closeLabel}
className={styles.closeButton}
intent="text"
<IconButton
theme="Black"
style="Muted"
onPress={onClose}
aria-label={closeLabel}
>
<MaterialIcon
icon="close"
color="Icon/Interactive/Default"
/>
</Button>
</IconButton>
</header>
<div className={styles.sidePeekContent}>{children}</div>
</aside>

View File

@@ -1,18 +1,5 @@
.modal {
--sidepeek-desktop-width: 560px;
}
@keyframes slide-in {
from {
right: calc(-1 * var(--sidepeek-desktop-width));
}
to {
right: 0;
}
}
.overlay {
--sidepeek-desktop-width: 560px;
position: fixed;
top: 0;
bottom: 0;
@@ -47,7 +34,7 @@
outline: none;
}
.sidePeek {
.aside {
position: relative;
display: grid;
grid-template-rows: min-content auto;
@@ -62,14 +49,10 @@
padding: var(--Spacing-x4);
}
.header:has(> h2) {
.header:has(.heading) {
justify-content: space-between;
}
.closeButton {
padding: 0;
}
.heading {
color: var(--Text-Heading);
text-wrap: balance;
@@ -89,3 +72,13 @@
height: 100vh;
}
}
@keyframes slide-in {
from {
right: calc(-1 * var(--sidepeek-desktop-width));
}
to {
right: 0;
}
}

View File

@@ -0,0 +1,143 @@
import type { Meta, StoryObj } from '@storybook/nextjs-vite'
import { expect } from 'storybook/test'
import { TextLink } from '.'
import { MaterialIcon } from '../Icons/MaterialIcon'
import { Typography } from '../Typography'
import { config } from './variants'
const meta: Meta<typeof TextLink> = {
title: 'Components/TextLink',
component: TextLink,
argTypes: {
theme: {
control: 'select',
options: Object.keys(config.variants.theme),
default: config.defaultVariants.theme,
},
isInline: {
control: 'boolean',
default: false,
description:
'Should be used when the link is placed inside a text block, removes the padding.',
},
isDisabled: {
control: 'boolean',
default: false,
description: 'Disables the link and makes it non-interactive.',
},
},
}
export default meta
type Story = StoryObj<typeof TextLink>
export const Default: Story = {
args: {
href: 'https://www.scandichotels.com/en',
},
render: (args) => <TextLink {...args}>Default link</TextLink>,
play: async ({ canvasElement }) => {
const link = canvasElement.querySelector('a')
if (!link) throw new Error('Link not found')
expect(link).toBeInTheDocument()
},
}
export const Inverted: Story = {
args: {
...Default.args,
theme: 'Inverted',
},
render: (args) => <TextLink {...args}>Inverted link</TextLink>,
play: async ({ canvasElement }) => {
const link = canvasElement.querySelector('a')
if (!link) throw new Error('Link not found')
expect(link).toBeInTheDocument()
},
}
export const Disabled: Story = {
args: {
...Default.args,
isDisabled: true,
},
render: (args) => <TextLink {...args}>Disabled link</TextLink>,
play: async ({ canvasElement }) => {
const link = canvasElement.querySelector('a')
if (!link) throw new Error('Link not found')
expect(link).toBeInTheDocument()
},
}
export const WithIcon: Story = {
args: {
...Default.args,
},
render: (args) => (
<TextLink {...args}>
Link with icon
<MaterialIcon icon="arrow_forward" size={24} color="CurrentColor" />
</TextLink>
),
play: async ({ canvasElement }) => {
const link = canvasElement.querySelector('a')
if (!link) throw new Error('Link not found')
expect(link).toBeInTheDocument()
},
}
export const Small: Story = {
args: {
...Default.args,
typography: 'Link/sm',
},
render: (args) => <TextLink {...args}>Small link</TextLink>,
play: async ({ canvasElement }) => {
const link = canvasElement.querySelector('a')
if (!link) throw new Error('Link not found')
expect(link).toBeInTheDocument()
},
}
export const SmallWithIcon: Story = {
args: {
...Default.args,
typography: 'Link/sm',
},
render: (args) => (
<TextLink {...args}>
Small link with icon
<MaterialIcon icon="arrow_forward" size={20} color="CurrentColor" />
</TextLink>
),
play: async ({ canvasElement }) => {
const link = canvasElement.querySelector('a')
if (!link) throw new Error('Link not found')
expect(link).toBeInTheDocument()
},
}
export const Inline: Story = {
args: {
...Default.args,
isInline: true,
},
render: (args) => (
<Typography variant="Body/Paragraph/mdRegular">
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit.{' '}
<TextLink {...args}>Inline link</TextLink> Curabitur vitae neque non
ipsum efficitur hendrerit at ut nulla. Cras in tellus et ligula posuere
ullamcorper. Praesent pulvinar rutrum metus ut gravida.
</p>
</Typography>
),
play: async ({ canvasElement }) => {
const link = canvasElement.querySelector('a')
if (!link) throw new Error('Link not found')
expect(link).toBeInTheDocument()
},
}

View File

@@ -0,0 +1,34 @@
import { cx } from 'class-variance-authority'
import NextLink from 'next/link'
import { TextLinkProps } from './types'
import { variants } from './variants'
import styles from './textLink.module.css'
export function TextLink({
theme,
className,
isDisabled,
tabIndex,
isInline,
typography,
...props
}: TextLinkProps) {
const classNames = variants({
theme,
typography,
className,
})
return (
<NextLink
{...props}
tabIndex={isDisabled ? -1 : tabIndex}
aria-disabled={isDisabled}
className={cx(classNames, {
[styles.disabled]: isDisabled,
[styles.inline]: isInline,
})}
/>
)
}

View File

@@ -0,0 +1 @@
export { TextLink } from './TextLink'

View File

@@ -0,0 +1,36 @@
.textLink {
display: inline-flex;
align-items: center;
justify-content: center;
gap: var(--Space-x05);
padding: var(--Space-x025) 0;
}
.disabled {
color: var(--Text-Interactive-Disabled);
text-decoration: none;
cursor: default;
pointer-events: none;
}
.inline {
padding: 0;
}
.theme-primary:not(.disabled) {
color: var(--Text-Interactive-Secondary);
text-decoration: underline;
&:hover {
color: var(--Text-Interactive-Secondary-Hover);
}
}
.theme-inverted:not(.disabled) {
color: var(--Text-Inverted);
text-decoration: underline;
&:hover {
opacity: 0.7;
}
}

View File

@@ -0,0 +1,12 @@
import type { VariantProps } from 'class-variance-authority'
import NextLink from 'next/link'
import type { ComponentProps } from 'react'
import type { variants } from './variants'
export interface TextLinkProps
extends ComponentProps<typeof NextLink>,
VariantProps<typeof variants> {
isDisabled?: boolean
isInline?: boolean
}

View File

@@ -0,0 +1,38 @@
import { cva } from 'class-variance-authority'
import {
config as typographyConfig,
withTypography,
} from '../Typography/variants'
import { deepmerge } from 'deepmerge-ts'
import styles from './textLink.module.css'
export const config = {
variants: {
theme: {
Primary: styles['theme-primary'],
Inverted: styles['theme-inverted'],
},
},
defaultVariants: {
theme: 'Primary',
},
} as const
const textLinkConfig = {
variants: {
...config.variants,
typography: typographyConfig.variants.variant,
},
defaultVariants: {
...config.defaultVariants,
typography: 'Link/md',
},
} as const
export const variants = cva(styles.textLink, withTypography(textLinkConfig))
export function withTextLink<T>(config: T) {
return deepmerge(textLinkConfig, config)
}

View File

@@ -152,6 +152,7 @@
"./Modal/ModalContentWithActions": "./lib/components/Modal/ModalContentWithActions/index.tsx",
"./NoRateAvailableCard": "./lib/components/RateCard/NoRateAvailable/index.tsx",
"./OldDSButton": "./lib/components/OldDSButton/index.tsx",
"./OldDSLink": "./lib/components/OldDSLink/index.tsx",
"./OpeningHours": "./lib/components/OpeningHours/index.tsx",
"./ParkingInformation": "./lib/components/ParkingInformation/index.tsx",
"./Payment/PaymentMethodIcon": "./lib/components/Payment/PaymentMethodIcon.tsx",
@@ -168,6 +169,7 @@
"./Subtitle": "./lib/components/Subtitle/index.tsx",
"./Switch": "./lib/components/Switch/index.tsx",
"./Table": "./lib/components/Table/index.tsx",
"./TextLink": "./lib/components/TextLink/index.tsx",
"./Title": "./lib/components/Title/index.tsx",
"./Toast": "./lib/components/Toasts/index.tsx",
"./ToastHandler": "./lib/components/Toasts/ToastHandler.tsx",