Merge branch 'develop' into feature/tracking

This commit is contained in:
Linus Flood
2024-10-15 07:46:54 +02:00
209 changed files with 4413 additions and 1046 deletions

View File

@@ -53,6 +53,7 @@ a.inverted {
a.text {
background: none;
border: none;
outline: none;
}
/* VARIANTS */

View File

@@ -0,0 +1,40 @@
.container {
display: flex;
flex-direction: column;
color: var(--text-color);
}
.container[data-selected] .checkbox {
border: none;
background: var(--UI-Input-Controls-Fill-Selected);
}
.checkboxContainer {
display: flex;
align-items: flex-start;
gap: var(--Spacing-x-one-and-half);
}
.checkbox {
width: 24px;
height: 24px;
min-width: 24px;
background-color: var(--UI-Input-Controls-Surface-Normal);
border: 2px solid var(--UI-Input-Controls-Border-Normal);
border-radius: var(--Corner-radius-Small);
transition: all 200ms;
display: flex;
align-items: center;
justify-content: center;
transition: all 200ms;
forced-color-adjust: none;
cursor: pointer;
}
.error {
align-items: center;
color: var(--Scandic-Red-60);
display: flex;
gap: var(--Spacing-x-half);
margin-top: var(--Spacing-x1);
}

View File

@@ -0,0 +1,7 @@
import { RegisterOptions } from "react-hook-form"
export interface CheckboxProps
extends React.InputHTMLAttributes<HTMLInputElement> {
name: string
registerOptions?: RegisterOptions
}

View File

@@ -0,0 +1,49 @@
import { Checkbox as AriaCheckbox } from "react-aria-components"
import { useController, useFormContext } from "react-hook-form"
import { InfoCircleIcon } from "@/components/Icons"
import CheckIcon from "@/components/Icons/Check"
import Caption from "@/components/TempDesignSystem/Text/Caption"
import { CheckboxProps } from "./checkbox"
import styles from "./checkbox.module.css"
export default function Checkbox({
name,
children,
registerOptions,
}: React.PropsWithChildren<CheckboxProps>) {
const { control } = useFormContext()
const { field, fieldState } = useController({
control,
name,
rules: registerOptions,
})
return (
<AriaCheckbox
className={styles.container}
isSelected={field.value}
onChange={field.onChange}
data-testid={name}
>
{({ isSelected }) => (
<>
<div className={styles.checkboxContainer}>
<div className={styles.checkbox}>
{isSelected && <CheckIcon color="white" />}
</div>
{children}
</div>
{children && fieldState.error ? (
<Caption className={styles.error}>
<InfoCircleIcon color="red" />
{fieldState.error.message}
</Caption>
) : null}
</>
)}
</AriaCheckbox>
)
}

View File

@@ -0,0 +1,39 @@
.container {
display: flex;
flex-direction: column;
color: var(--text-color);
}
.container[data-selected] .checkbox {
border: none;
background: var(--UI-Input-Controls-Fill-Selected);
}
.checkboxContainer {
display: flex;
align-items: flex-start;
gap: var(--Spacing-x-one-and-half);
}
.checkbox {
flex-grow: 1;
width: 24px;
height: 24px;
min-width: 24px;
border: 2px solid var(--UI-Input-Controls-Border-Normal);
border-radius: 4px;
transition: all 200ms;
display: flex;
align-items: center;
justify-content: center;
transition: all 200ms;
forced-color-adjust: none;
}
.error {
align-items: center;
color: var(--Scandic-Red-60);
display: flex;
gap: var(--Spacing-x-half);
margin: var(--Spacing-x1) 0 0;
}

View File

@@ -0,0 +1,7 @@
import { RegisterOptions } from "react-hook-form"
export interface CheckboxProps
extends React.InputHTMLAttributes<HTMLInputElement> {
name: string
registerOptions?: RegisterOptions
}

View File

@@ -0,0 +1,51 @@
"use client"
import { Checkbox as AriaCheckbox } from "react-aria-components"
import { useController, useFormContext } from "react-hook-form"
import { InfoCircleIcon } from "@/components/Icons"
import CheckIcon from "@/components/Icons/Check"
import Caption from "@/components/TempDesignSystem/Text/Caption"
import { CheckboxProps } from "./checkbox"
import styles from "./checkbox.module.css"
export default function Checkbox({
name,
children,
registerOptions,
}: React.PropsWithChildren<CheckboxProps>) {
const { control } = useFormContext()
const { field, fieldState } = useController({
control,
name,
rules: registerOptions,
})
return (
<AriaCheckbox
className={styles.container}
isSelected={field.value}
onChange={field.onChange}
data-testid={name}
>
{({ isSelected }) => (
<>
<div className={styles.checkboxContainer}>
<div className={styles.checkbox}>
{isSelected && <CheckIcon color="white" />}
</div>
{children}
</div>
{fieldState.error ? (
<Caption className={styles.error} fontOnly>
<InfoCircleIcon color="red" />
{fieldState.error.message}
</Caption>
) : null}
</>
)}
</AriaCheckbox>
)
}

View File

@@ -1,5 +1,7 @@
"use client"
import { useFormContext } from "react-hook-form"
import { CheckIcon, CloseIcon, HeartIcon } from "@/components/Icons"
import Caption from "@/components/TempDesignSystem/Text/Caption"
import Footnote from "@/components/TempDesignSystem/Text/Footnote"
@@ -23,6 +25,8 @@ export default function Card({
type,
value,
}: CardProps) {
const { register } = useFormContext()
return (
<label className={styles.label} data-declined={declined}>
<Caption className={styles.title} textTransform="bold" uppercase>
@@ -68,9 +72,9 @@ export default function Card({
aria-hidden
id={id || name}
hidden
name={name}
type={type}
value={value}
{...register(name)}
/>
</label>
)

View File

@@ -68,6 +68,7 @@ export default function CountrySelect({
onSelectionChange={handleChange}
ref={field.ref}
selectedKey={field.value}
data-testid={name}
>
<div className={styles.comboBoxContainer}>
<Label

View File

@@ -1,5 +1,6 @@
"use client"
import { parseDate } from "@internationalized/date"
import { useState } from "react"
import { DateInput, DatePicker, Group } from "react-aria-components"
import { useController, useFormContext, useWatch } from "react-hook-form"
import { useIntl } from "react-intl"
@@ -18,7 +19,7 @@ import type { Key } from "react-aria-components"
import type { DateProps } from "./date"
export default function DateSelect({ name, registerOptions = {} }: DateProps) {
const { formatMessage } = useIntl()
const intl = useIntl()
const d = useWatch({ name })
const { control, setValue } = useFormContext()
const { field } = useController({
@@ -26,6 +27,19 @@ export default function DateSelect({ name, registerOptions = {} }: DateProps) {
name,
rules: registerOptions,
})
const [dateSegments, setDateSegment] = useState<{
year: number | null
month: number | null
date: number | null
daysInMonth: number
}>({
year: null,
month: null,
date: null,
daysInMonth: 31,
})
const currentYear = new Date().getFullYear()
const months = rangeArray(1, 12).map((month) => ({
value: month,
@@ -41,17 +55,38 @@ export default function DateSelect({ name, registerOptions = {} }: DateProps) {
* must subtract by 1 to get the selected month
*/
return (select: Key) => {
if (selector === DateName.month) {
select = Number(select) - 1
const value =
selector === DateName.month ? Number(select) - 1 : Number(select)
const newSegments = { ...dateSegments, [selector]: value }
/**
* Update daysInMonth when year or month changes
* to ensure the user can't select a date that doesn't exist.
*/
if (selector === DateName.year || selector === DateName.month) {
const year = selector === DateName.year ? value : newSegments.year
const month = selector === DateName.month ? value : newSegments.month
if (year !== null && month !== null) {
newSegments.daysInMonth = dt().year(year).month(month).daysInMonth()
}
}
const newDate = dt(d).set(selector, Number(select))
setValue(name, newDate.format("YYYY-MM-DD"))
if (Object.values(newSegments).every((val) => val !== null)) {
const newDate = dt()
.utc()
.set("year", newSegments.year!)
.set("month", newSegments.month!)
.set("date", Math.min(newSegments.date!, newSegments.daysInMonth))
setValue(name, newDate.format("YYYY-MM-DD"))
}
setDateSegment(newSegments)
}
}
const dayLabel = formatMessage({ id: "Day" })
const monthLabel = formatMessage({ id: "Month" })
const yearLabel = formatMessage({ id: "Year" })
const dayLabel = intl.formatMessage({ id: "Day" })
const monthLabel = intl.formatMessage({ id: "Month" })
const yearLabel = intl.formatMessage({ id: "Year" })
let dateValue = null
try {
@@ -60,35 +95,30 @@ export default function DateSelect({ name, registerOptions = {} }: DateProps) {
* date, but we can't check isNan since
* we recieve the date as "1999-01-01"
*/
dateValue = parseDate(d)
dateValue = dt(d).isValid() ? parseDate(d) : null
} catch (error) {
console.error(error)
}
return (
<DatePicker
aria-label={formatMessage({ id: "Select date of birth" })}
granularity="day"
aria-label={intl.formatMessage({ id: "Select date of birth" })}
isRequired={!!registerOptions.required}
name={name}
ref={field.ref}
value={dateValue}
data-testid={name}
>
<Group>
<DateInput className={styles.container}>
{(segment) => {
switch (segment.type) {
case "day":
let days = []
if (segment.maxValue && segment.minValue) {
days = rangeArray(segment.minValue, segment.maxValue).map(
(day) => ({ value: day, label: `${day}` })
)
} else {
days = Array.from(Array(segment.maxValue).keys()).map(
(i) => ({ value: i + 1, label: `${i + 1}` })
)
}
const maxDays = dateSegments.daysInMonth
const days = rangeArray(1, maxDays).map((day) => ({
value: day,
label: `${day}`,
}))
return (
<div className={styles.day}>
<Select
@@ -100,7 +130,9 @@ export default function DateSelect({ name, registerOptions = {} }: DateProps) {
placeholder="DD"
required
tabIndex={3}
value={segment.value}
defaultValue={
segment.isPlaceholder ? undefined : segment.value
}
/>
</div>
)
@@ -116,7 +148,9 @@ export default function DateSelect({ name, registerOptions = {} }: DateProps) {
placeholder="MM"
required
tabIndex={2}
value={segment.value}
defaultValue={
segment.isPlaceholder ? undefined : segment.value
}
/>
</div>
)
@@ -132,7 +166,9 @@ export default function DateSelect({ name, registerOptions = {} }: DateProps) {
placeholder="YYYY"
required
tabIndex={1}
value={segment.value}
defaultValue={
segment.isPlaceholder ? undefined : segment.value
}
/>
</div>
)

View File

@@ -1,33 +1,63 @@
"use client"
import { useState } from "react"
import { Text, TextField } from "react-aria-components"
import { Controller, useFormContext } from "react-hook-form"
import { useIntl } from "react-intl"
import { CheckIcon, CloseIcon } from "@/components/Icons"
import Error from "@/components/TempDesignSystem/Form/ErrorMessage/Error"
import {
CheckIcon,
CloseIcon,
EyeHideIcon,
EyeShowIcon,
InfoCircleIcon,
} from "@/components/Icons"
import AriaInputWithLabel from "@/components/TempDesignSystem/Form/Input/AriaInputWithLabel"
import Caption from "@/components/TempDesignSystem/Text/Caption"
import { passwordValidators } from "@/utils/passwordValidator"
import { type IconProps, Key, type NewPasswordProps } from "./newPassword"
import Button from "../../Button"
import { IconProps, type NewPasswordProps } from "./newPassword"
import styles from "./newPassword.module.css"
import { PasswordValidatorKey } from "@/types/components/form/newPassword"
export default function NewPassword({
name = "newPassword",
"aria-label": ariaLabel,
disabled = false,
placeholder = "",
registerOptions = {},
label,
}: NewPasswordProps) {
const { control } = useFormContext()
const { formatMessage } = useIntl()
const intl = useIntl()
const [isPasswordVisible, setPasswordVisible] = useState(false)
function getErrorMessage(key: PasswordValidatorKey) {
switch (key) {
case "length":
return `10 ${intl.formatMessage({ id: "to" })} 40 ${intl.formatMessage({ id: "characters" })}`
case "hasUppercase":
return `1 ${intl.formatMessage({ id: "uppercase letter" })}`
case "hasLowercase":
return `1 ${intl.formatMessage({ id: "lowercase letter" })}`
case "hasNumber":
return `1 ${intl.formatMessage({ id: "number" })}`
case "hasSpecialChar":
return `1 ${intl.formatMessage({ id: "special character" })}`
}
}
return (
<Controller
disabled={disabled}
control={control}
name="newPassword"
name={name}
rules={registerOptions}
render={({ field, fieldState }) => {
const messages = (fieldState.error?.message?.split(",") ?? []) as Key[]
render={({ field, fieldState, formState }) => {
const errors = Object.values(formState.errors[name]?.types ?? []).flat()
return (
<TextField
aria-label={ariaLabel}
@@ -39,51 +69,47 @@ export default function NewPassword({
onChange={field.onChange}
validationBehavior="aria"
value={field.value}
type="password"
type={isPasswordVisible ? "text" : "password"}
>
<AriaInputWithLabel
{...field}
aria-labelledby={field.name}
id={field.name}
label={formatMessage({ id: "New password" })}
placeholder={placeholder}
type="password"
/>
<div className={styles.inputWrapper}>
<AriaInputWithLabel
{...field}
aria-labelledby={field.name}
id={field.name}
label={intl.formatMessage({ id: "New password" })}
placeholder={placeholder}
type={isPasswordVisible ? "text" : "password"}
/>
<Button
className={styles.eyeIcon}
type="button"
variant="icon"
size="small"
intent="tertiary"
onClick={() => setPasswordVisible(!isPasswordVisible)}
>
{isPasswordVisible ? <EyeHideIcon /> : <EyeShowIcon />}
</Button>
</div>
{field.value ? (
<div className={styles.errors}>
<Caption asChild color="black">
<Text className={styles.helpText} slot="description">
<Icon matcher={Key.CHAR_LENGTH} messages={messages} />
10 {formatMessage({ id: "to" })} 40{" "}
{formatMessage({ id: "characters" })}
</Text>
</Caption>
<Caption asChild color="black">
<Text className={styles.helpText} slot="description">
<Icon matcher={Key.UPPERCASE} messages={messages} />1{" "}
{formatMessage({ id: "uppercase letter" })}
</Text>
</Caption>
<Caption asChild color="black">
<Text className={styles.helpText} slot="description">
<Icon matcher={Key.NUM} messages={messages} />1{" "}
{formatMessage({ id: "number" })}
</Text>
</Caption>
<Caption asChild color="black">
<Text className={styles.helpText} slot="description">
<Icon matcher={Key.SPECIAL_CHAR} messages={messages} />1{" "}
{formatMessage({ id: "special character" })}
</Text>
</Caption>
{Object.entries(passwordValidators).map(
([key, { message }]) => (
<Caption asChild color="black" key={key}>
<Text className={styles.helpText} slot="description">
<Icon errorMessage={message} errors={errors} />
{getErrorMessage(key as PasswordValidatorKey)}
</Text>
</Caption>
)
)}
</div>
) : null}
{!field.value && fieldState.error ? (
<Error>
<Text className={styles.helpText} slot="description">
{fieldState.error.message}
</Text>
</Error>
<Caption className={styles.error} fontOnly>
<InfoCircleIcon color="red" />
{fieldState.error.message}
</Caption>
) : null}
</TextField>
)
@@ -92,8 +118,8 @@ export default function NewPassword({
)
}
function Icon({ matcher, messages }: IconProps) {
return messages.includes(matcher) ? (
function Icon({ errorMessage, errors }: IconProps) {
return errors.includes(errorMessage) ? (
<CloseIcon color="red" height={20} width={20} />
) : (
<CheckIcon color="green" height={20} width={20} />

View File

@@ -1,12 +1,88 @@
.container {
align-content: center;
background-color: var(--Main-Grey-White);
border-color: var(--Scandic-Beige-40);
border-style: solid;
border-width: 1px;
border-radius: var(--Corner-radius-Medium);
display: grid;
height: 60px;
padding: var(--Spacing-x1) var(--Spacing-x2);
transition: border-color 200ms ease;
position: relative;
}
.container:has(.input:active, .input:focus) {
border-color: var(--Scandic-Blue-90);
}
.container:has(.input:disabled) {
background-color: var(--Main-Grey-10);
border: none;
color: var(--Main-Grey-40);
}
.container:has(.input[data-invalid="true"], .input[aria-invalid="true"]) {
border-color: var(--Scandic-Red-60);
}
.input {
background: none;
border: none;
color: var(--Main-Grey-100);
height: 18px;
margin: 0;
order: 2;
overflow: visible;
padding: 0;
}
.input:not(:active, :focus):placeholder-shown {
height: 0px;
transition: height 150ms ease;
}
.input:focus,
.input:focus:placeholder-shown,
.input:active,
.input:active:placeholder-shown {
height: 18px;
transition: height 150ms ease;
outline: none;
}
.input:disabled {
color: var(--Main-Grey-40);
}
.helpText {
align-items: flex-start;
display: flex;
gap: var(--Spacing-x-half);
}
.error {
align-items: center;
color: var(--Scandic-Red-60);
display: flex;
gap: var(--Spacing-x-half);
margin: var(--Spacing-x1) 0 0;
}
.errors {
display: flex;
flex-wrap: wrap;
gap: var(--Spacing-x-one-and-half) var(--Spacing-x1);
padding-top: var(--Spacing-x1);
}
.eyeIcon {
position: absolute;
right: var(--Spacing-x2);
top: 50%;
transform: translateY(-50%);
}
.inputWrapper {
position: relative;
}

View File

@@ -1,12 +1,5 @@
import type { RegisterOptions } from "react-hook-form"
export enum Key {
CHAR_LENGTH = "CHAR_LENGTH",
NUM = "NUM",
SPECIAL_CHAR = "SPECIAL_CHAR",
UPPERCASE = "UPPERCASE",
}
export interface NewPasswordProps
extends React.InputHTMLAttributes<HTMLInputElement> {
label?: string
@@ -14,6 +7,6 @@ export interface NewPasswordProps
}
export interface IconProps {
matcher: Key
messages: Key[]
errorMessage: string
errors: string[]
}

View File

@@ -85,6 +85,7 @@ export default function Phone({
className={styles.select}
tabIndex={0}
type="button"
data-testid="country-selector"
>
<Label required={!!registerOptions.required} size="small">
{formatMessage({ id: "Country code" })}

View File

@@ -25,11 +25,13 @@ export default function Select({
disabled={field.disabled}
items={items}
label={label}
aria-label={label}
name={field.name}
onBlur={field.onBlur}
onSelect={field.onChange}
placeholder={placeholder}
value={field.value}
data-testid={name}
/>
)
}

View File

@@ -57,7 +57,7 @@
.shortcut {
align-items: center;
border-bottom: 0.5px solid var(--Scandic-Beige-20);
border-bottom: 1px solid var(--Base-Border-Subtle);
display: grid;
font-family: var(--typography-Body-Regular-fontFamily);
font-size: var(--typography-Body-Regular-fontSize);
@@ -107,6 +107,10 @@
font-weight: 600;
}
.baseButtonTextOnFillNormal {
color: var(--Base-Button-Text-On-Fill-Normal);
}
.black {
color: #000;
}
@@ -218,3 +222,15 @@
color: var(--Base-Text-High-contrast);
background-color: var(--Base-Surface-Primary-light-Hover-alt);
}
.signupVerification {
background-color: var(--Base-Button-Primary-Fill-Normal);
color: var(--Base-Button-Inverted-Fill-Normal);
cursor: pointer;
padding: var(--Spacing-x-one-and-half) var(--Spacing-x2);
border-radius: var(--Corner-radius-Rounded);
text-decoration: none;
display: inline-block;
text-align: center;
transition: background-color 0.3s ease;
}

View File

@@ -8,6 +8,7 @@ export const linkVariants = cva(styles.link, {
true: styles.active,
},
color: {
baseButtonTextOnFillNormal: styles.baseButtonTextOnFillNormal,
black: styles.black,
burgundy: styles.burgundy,
none: "",
@@ -35,6 +36,7 @@ export const linkVariants = cva(styles.link, {
shortcut: styles.shortcut,
sidebar: styles.sidebar,
tab: styles.tab,
signupVerification: styles.signupVerification,
},
},
defaultVariants: {

View File

@@ -59,7 +59,7 @@ export default function Select({
selectedKey={value as Key}
>
<Body asChild fontOnly>
<Button className={styles.input}>
<Button className={styles.input} data-testid={name}>
<div className={styles.inputContentWrapper} tabIndex={tabIndex}>
<Label required={required} size="small">
{label}
@@ -89,6 +89,7 @@ export default function Select({
className={styles.listBoxItem}
id={item.value}
key={item.label}
data-testid={item.label}
>
{item.label}
</ListBoxItem>

View File

@@ -10,7 +10,7 @@ import {
} from "react-aria-components"
import { useIntl } from "react-intl"
import { CloseIcon } from "@/components/Icons"
import { CloseLargeIcon } from "@/components/Icons"
import { SidePeekContext } from "@/components/SidePeekProvider"
import Button from "../Button"
@@ -52,13 +52,16 @@ function SidePeek({
</div>
)
}
return (
<div ref={setRef}>
<DialogTrigger>
<ModalOverlay
UNSTABLE_portalContainer={rootDiv}
className={styles.overlay}
isOpen={isOpen || contentKey === context?.activeSidePeek}
isOpen={
isOpen || (!!contentKey && contentKey === context?.activeSidePeek)
}
onOpenChange={onClose}
isDismissable
>
@@ -82,7 +85,7 @@ function SidePeek({
intent="text"
onPress={onClose}
>
<CloseIcon color="burgundy" height={32} width={32} />
<CloseLargeIcon color="burgundy" />
</Button>
</header>
<div className={styles.sidePeekContent}>{children}</div>

View File

@@ -22,12 +22,13 @@
}
.overlay {
position: absolute;
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
z-index: 99;
z-index: var(--sidepeek-z-index);
background-color: var(--UI-Opacity-Almost-Black-30);
}
.modal {

View File

@@ -1,5 +1,5 @@
export interface SidePeekProps {
contentKey: string
contentKey?: string
title: string
isOpen?: boolean
handleClose?: (isOpen: boolean) => void

View File

@@ -0,0 +1,82 @@
"use client"
import { useState } from "react"
import { ChevronRightIcon } from "@/components/Icons"
import JsonToHtml from "@/components/JsonToHtml"
import Button from "@/components/TempDesignSystem/Button"
import Link from "../../Link"
import SidePeek from "../../SidePeek"
import styles from "./sidepeek.module.css"
import type { TeaserCardSidepeekProps } from "@/types/components/teaserCard"
export default function TeaserCardSidepeek({
button,
sidePeekContent,
}: TeaserCardSidepeekProps) {
const [sidePeekIsOpen, setSidePeekIsOpen] = useState(false)
const { heading, content, primary_button, secondary_button } = sidePeekContent
return (
<>
<Button
onPress={() => setSidePeekIsOpen(true)}
theme="base"
variant="icon"
intent="text"
size="small"
wrapping
>
{button.call_to_action_text}
<ChevronRightIcon />
</Button>
<SidePeek
title={heading}
isOpen={sidePeekIsOpen}
handleClose={() => setSidePeekIsOpen(false)}
>
<JsonToHtml
nodes={content.json.children}
embeds={content.embedded_itemsConnection.edges}
/>
<div className={styles.ctaContainer}>
{primary_button && (
<Button
asChild
theme="base"
intent="primary"
size="small"
className={styles.ctaButton}
>
<Link
href={primary_button.href}
target={primary_button.openInNewTab ? "_blank" : undefined}
color="none"
>
{primary_button.title}
</Link>
</Button>
)}
{secondary_button && (
<Button
asChild
intent="secondary"
size="small"
className={styles.ctaButton}
>
<Link
href={secondary_button.href}
target={secondary_button.openInNewTab ? "_blank" : undefined}
>
{secondary_button.title}
</Link>
</Button>
)}
</div>
</SidePeek>
</>
)
}

View File

@@ -0,0 +1,4 @@
.ctaContainer {
display: grid;
gap: var(--Spacing-x2);
}

View File

@@ -1,10 +1,10 @@
import { ChevronRightIcon } from "@/components/Icons"
import Image from "@/components/Image"
import Button from "@/components/TempDesignSystem/Button"
import Link from "@/components/TempDesignSystem/Link"
import Body from "@/components/TempDesignSystem/Text/Body"
import Subtitle from "../Text/Subtitle"
import TeaserCardSidepeek from "./Sidepeek"
import { teaserCardVariants } from "./variants"
import styles from "./teaserCard.module.css"
@@ -17,6 +17,7 @@ export default function TeaserCard({
primaryButton,
secondaryButton,
sidePeekButton,
sidePeekContent,
image,
style = "default",
alwaysStack = false,
@@ -41,21 +42,14 @@ export default function TeaserCard({
<Subtitle textAlign="left" type="two" color="black">
{title}
</Subtitle>
<Body color="black">{description}</Body>
{!!sidePeekButton ? (
<Button
// onClick={() => {
// // TODO: Implement sidePeek functionality once SW-341 is merged.
// }}
theme="base"
variant="icon"
intent="text"
size="small"
className={styles.sidePeekCTA}
>
{sidePeekButton.title}
<ChevronRightIcon />
</Button>
<Body color="black" className={styles.body}>
{description}
</Body>
{sidePeekButton && sidePeekContent ? (
<TeaserCardSidepeek
button={sidePeekButton}
sidePeekContent={sidePeekContent}
/>
) : (
<div className={styles.ctaContainer}>
{primaryButton && (

View File

@@ -54,6 +54,17 @@
width: 100%;
}
.body {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3;
line-clamp: 3;
overflow: hidden;
text-overflow: ellipsis;
/* line-height variables are in %, so using the value in rem instead */
max-height: calc(3 * 1.5rem);
}
@media (min-width: 1367px) {
.card:not(.alwaysStack) .ctaContainer {
grid-template-columns: repeat(auto-fit, minmax(0, 1fr));
@@ -63,9 +74,3 @@
grid-template-columns: 1fr;
}
}
.sidePeekCTA {
/* TODO: Create ticket to remove padding on "link" buttons,
align w. design on this. */
padding: 0 !important;
}

View File

@@ -99,3 +99,7 @@
.uiTextPlaceholder {
color: var(--UI-Text-Placeholder);
}
.disabled {
color: var(--Base-Text-Disabled);
}

View File

@@ -7,6 +7,7 @@ const config = {
color: {
black: styles.black,
burgundy: styles.burgundy,
disabled: styles.disabled,
grey: styles.grey,
pale: styles.pale,
red: styles.red,

View File

@@ -75,6 +75,10 @@ p.caption {
color: var(--UI-Text-High-contrast);
}
.uiTextPlaceholder {
color: var(--UI-Text-Placeholder);
}
.disabled {
color: var(--Base-Text-Disabled);
}

View File

@@ -15,6 +15,7 @@ const config = {
uiTextHighContrast: styles.uiTextHighContrast,
uiTextActive: styles.uiTextActive,
uiTextMediumContrast: styles.uiTextMediumContrast,
uiTextPlaceholder: styles.uiTextPlaceholder,
disabled: styles.disabled,
},
textTransform: {

View File

@@ -66,3 +66,7 @@
.uiTextPlaceholder {
color: var(--UI-Text-Placeholder);
}
.white {
color: var(--Main-Grey-White);
}

View File

@@ -11,6 +11,7 @@ const config = {
peach50: styles.peach50,
uiTextMediumContrast: styles.uiTextMediumContrast,
uiTextPlaceholder: styles.uiTextPlaceholder,
white: styles.white,
},
textAlign: {
center: styles.center,

View File

@@ -66,3 +66,11 @@
.uiTextMediumContrast {
color: var(--UI-Text-Medium-contrast);
}
.red {
color: var(--Scandic-Brand-Scandic-Red);
}
.disabled {
color: var(--Base-Text-Disabled);
}

View File

@@ -7,9 +7,11 @@ const config = {
color: {
black: styles.black,
burgundy: styles.burgundy,
disabled: styles.disabled,
pale: styles.pale,
uiTextHighContrast: styles.uiTextHighContrast,
uiTextMediumContrast: styles.uiTextMediumContrast,
red: styles.red,
},
textAlign: {
center: styles.center,

View File

@@ -2,7 +2,7 @@ import { ExternalToast, toast as sonnerToast, Toaster } from "sonner"
import {
CheckCircleIcon,
CloseLarge,
CloseLargeIcon,
CrossCircle,
InfoCircleIcon,
WarningTriangle,
@@ -42,7 +42,7 @@ export function Toast({ message, onClose, variant }: ToastsProps) {
</div>
<Body className={styles.message}>{message}</Body>
<Button onClick={onClose} variant="icon" intent="text">
<CloseLarge />
<CloseLargeIcon />
</Button>
</div>
)