Merged in feat/svg-instead-of-fonts (pull request #3411)
feat(SW-3695): use svg icons instead of font icons * feat(icons): use svg instead of font icons * feat(icons): use webpack/svgr for inlined svgs. Now support for isFilled again * Merge master * Remove old font icon Approved-by: Joakim Jäderberg
This commit is contained in:
@@ -3,6 +3,14 @@ import * as Sentry from "@sentry/nextjs"
|
|||||||
import type { NextConfig } from "next"
|
import type { NextConfig } from "next"
|
||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
|
turbopack: {
|
||||||
|
rules: {
|
||||||
|
"*.svg": {
|
||||||
|
loaders: ["@svgr/webpack"],
|
||||||
|
as: "*.js",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
env: {
|
env: {
|
||||||
BRANCH: process.env.BRANCH || "local",
|
BRANCH: process.env.BRANCH || "local",
|
||||||
GIT_SHA: process.env.COMMIT_REF || "",
|
GIT_SHA: process.env.COMMIT_REF || "",
|
||||||
|
|||||||
@@ -45,6 +45,7 @@
|
|||||||
"@playwright/test": "^1.53.1",
|
"@playwright/test": "^1.53.1",
|
||||||
"@scandic-hotels/common": "workspace:*",
|
"@scandic-hotels/common": "workspace:*",
|
||||||
"@scandic-hotels/typescript-config": "workspace:*",
|
"@scandic-hotels/typescript-config": "workspace:*",
|
||||||
|
"@svgr/webpack": "^8.1.0",
|
||||||
"@swc/plugin-formatjs": "^3.2.2",
|
"@swc/plugin-formatjs": "^3.2.2",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "19.2.7",
|
"@types/react": "19.2.7",
|
||||||
|
|||||||
@@ -93,7 +93,11 @@ export default async function IntroSection({
|
|||||||
>
|
>
|
||||||
<div className={styles.phoneNumber}>
|
<div className={styles.phoneNumber}>
|
||||||
<TextLink href={`tel:${phoneNumber}`} typography="Link/sm">
|
<TextLink href={`tel:${phoneNumber}`} typography="Link/sm">
|
||||||
<MaterialIcon icon="phone" color="CurrentColor" size={20} />
|
<MaterialIcon
|
||||||
|
icon="phone_enabled"
|
||||||
|
color="CurrentColor"
|
||||||
|
size={20}
|
||||||
|
/>
|
||||||
{phoneNumber}
|
{phoneNumber}
|
||||||
</TextLink>
|
</TextLink>
|
||||||
<LocalCallCharges
|
<LocalCallCharges
|
||||||
@@ -109,7 +113,7 @@ export default async function IntroSection({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<TextLink href={`mailto:${email}`} typography="Link/sm">
|
<TextLink href={`mailto:${email}`} typography="Link/sm">
|
||||||
<MaterialIcon icon="email" color="CurrentColor" size={20} />
|
<MaterialIcon icon="mail" color="CurrentColor" size={20} />
|
||||||
{email}
|
{email}
|
||||||
</TextLink>
|
</TextLink>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import { useIntl } from "react-intl"
|
|||||||
|
|
||||||
import { Button } from "@scandic-hotels/design-system/Button"
|
import { Button } from "@scandic-hotels/design-system/Button"
|
||||||
import { Divider } from "@scandic-hotels/design-system/Divider"
|
import { Divider } from "@scandic-hotels/design-system/Divider"
|
||||||
import { TextArea } from "@scandic-hotels/design-system/TextArea"
|
|
||||||
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
||||||
|
import { TextArea } from "@scandic-hotels/design-system/TextArea"
|
||||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||||
|
|
||||||
import SelectDeliveryTime from "./SelectDeliveryTime"
|
import SelectDeliveryTime from "./SelectDeliveryTime"
|
||||||
|
|||||||
@@ -1,19 +1,20 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { Button } from "@scandic-hotels/design-system/Button"
|
import { Button } from "@scandic-hotels/design-system/Button"
|
||||||
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
import {
|
||||||
|
MaterialIcon,
|
||||||
|
type MaterialIconProps,
|
||||||
|
} from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
||||||
|
|
||||||
import styles from "./actionsButton.module.css"
|
import styles from "./actionsButton.module.css"
|
||||||
|
|
||||||
import type { MaterialSymbolProps } from "@scandic-hotels/design-system/Icons/MaterialIcon/MaterialSymbol"
|
|
||||||
|
|
||||||
export default function ActionsButton({
|
export default function ActionsButton({
|
||||||
icon,
|
icon,
|
||||||
text,
|
text,
|
||||||
onPress,
|
onPress,
|
||||||
isDisabled = false,
|
isDisabled = false,
|
||||||
}: {
|
}: {
|
||||||
icon: MaterialSymbolProps["icon"]
|
icon: MaterialIconProps["icon"]
|
||||||
text: string
|
text: string
|
||||||
onPress: () => void
|
onPress: () => void
|
||||||
isDisabled?: boolean
|
isDisabled?: boolean
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ export default function ResendConfirmationEmail({
|
|||||||
<ActionsButton
|
<ActionsButton
|
||||||
onPress={resendConfirmationEmail}
|
onPress={resendConfirmationEmail}
|
||||||
isDisabled={resendEmail.isPending}
|
isDisabled={resendEmail.isPending}
|
||||||
icon="email"
|
icon="mail"
|
||||||
text={printMsg}
|
text={printMsg}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ export default async function Profile() {
|
|||||||
value: user.dateOfBirth,
|
value: user.dateOfBirth,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: "phone",
|
icon: "phone_enabled",
|
||||||
label: intl.formatMessage({
|
label: intl.formatMessage({
|
||||||
id: "common.phoneNumber",
|
id: "common.phoneNumber",
|
||||||
defaultMessage: "Phone number",
|
defaultMessage: "Phone number",
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ export default async function ContactRow({ contact }: ContactRowProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function PhoneIcon(props: MaterialIconSetIconProps) {
|
function PhoneIcon(props: MaterialIconSetIconProps) {
|
||||||
return <MaterialIcon icon="phone" {...props} />
|
return <MaterialIcon icon="phone_enabled" {...props} />
|
||||||
}
|
}
|
||||||
function MailIcon(props: MaterialIconSetIconProps) {
|
function MailIcon(props: MaterialIconSetIconProps) {
|
||||||
return <MaterialIcon icon="mail" {...props} />
|
return <MaterialIcon icon="mail" {...props} />
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { MaterialSymbolProps } from "@scandic-hotels/design-system/Icons/MaterialIcon/MaterialSymbol"
|
import type { MaterialIconProps } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
||||||
|
|
||||||
export function getBedIconName(name: string): MaterialSymbolProps["icon"] {
|
export function getBedIconName(name: string): MaterialIconProps["icon"] {
|
||||||
const iconMappings = [
|
const iconMappings = [
|
||||||
{
|
{
|
||||||
icon: "bed",
|
icon: "bed",
|
||||||
@@ -25,5 +25,5 @@ export function getBedIconName(name: string): MaterialSymbolProps["icon"] {
|
|||||||
]
|
]
|
||||||
|
|
||||||
const icon = iconMappings.find((icon) => icon.texts.includes(name))
|
const icon = iconMappings.find((icon) => icon.texts.includes(name))
|
||||||
return icon ? (icon.icon as MaterialSymbolProps["icon"]) : "single_bed"
|
return icon ? (icon.icon as MaterialIconProps["icon"]) : "single_bed"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,13 +18,21 @@ import {
|
|||||||
myStay as myStayWebview,
|
myStay as myStayWebview,
|
||||||
preliminaryReceipt as preliminaryReceiptWebview,
|
preliminaryReceipt as preliminaryReceiptWebview,
|
||||||
} from "./constants/routes/webviews"
|
} from "./constants/routes/webviews"
|
||||||
|
import { NextConfig } from "next"
|
||||||
|
|
||||||
const jiti = createJiti(fileURLToPath(import.meta.url))
|
const jiti = createJiti(fileURLToPath(import.meta.url))
|
||||||
jiti("./env/server")
|
jiti("./env/server")
|
||||||
jiti("./env/client")
|
jiti("./env/client")
|
||||||
|
|
||||||
/** @type {import('next').NextConfig} */
|
const nextConfig: NextConfig = {
|
||||||
const nextConfig = {
|
turbopack: {
|
||||||
|
rules: {
|
||||||
|
"*.svg": {
|
||||||
|
loaders: ["@svgr/webpack"],
|
||||||
|
as: "*.js",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
env: {
|
env: {
|
||||||
BRANCH: process.env.BRANCH || "local",
|
BRANCH: process.env.BRANCH || "local",
|
||||||
GIT_SHA: process.env.COMMIT_REF || "",
|
GIT_SHA: process.env.COMMIT_REF || "",
|
||||||
|
|||||||
@@ -94,6 +94,7 @@
|
|||||||
"@playwright/test": "^1.57.0",
|
"@playwright/test": "^1.57.0",
|
||||||
"@react-aria/test-utils": "1.0.0-alpha.8",
|
"@react-aria/test-utils": "1.0.0-alpha.8",
|
||||||
"@scandic-hotels/typescript-config": "workspace:*",
|
"@scandic-hotels/typescript-config": "workspace:*",
|
||||||
|
"@svgr/webpack": "^8.1.0",
|
||||||
"@testing-library/react": "^16.3.0",
|
"@testing-library/react": "^16.3.0",
|
||||||
"@testing-library/user-event": "^14.6.1",
|
"@testing-library/user-event": "^14.6.1",
|
||||||
"@types/adm-zip": "^0.5.7",
|
"@types/adm-zip": "^0.5.7",
|
||||||
|
|||||||
@@ -11,11 +11,7 @@ export default function ValidationError() {
|
|||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<Caption className={styles.title} color="red" type="bold">
|
<Caption className={styles.title} color="red" type="bold">
|
||||||
<MaterialIcon
|
<MaterialIcon icon="dangerous" color="Icon/Feedback/Error" size={20} />
|
||||||
icon="error_circle_rounded"
|
|
||||||
color="Icon/Feedback/Error"
|
|
||||||
size={20}
|
|
||||||
/>
|
|
||||||
{intl.formatMessage({
|
{intl.formatMessage({
|
||||||
id: "bookingWidget.validationError.destination",
|
id: "bookingWidget.validationError.destination",
|
||||||
defaultMessage: "Enter destination or hotel",
|
defaultMessage: "Enter destination or hotel",
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { RoomPackageCodeEnum } from "@scandic-hotels/trpc/enums/roomFilter"
|
import { RoomPackageCodeEnum } from "@scandic-hotels/trpc/enums/roomFilter"
|
||||||
|
|
||||||
import type { MaterialSymbolProps } from "@scandic-hotels/design-system/Icons/MaterialIcon/MaterialSymbol"
|
import type { MaterialIconProps } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
||||||
import type { PackageEnum } from "@scandic-hotels/trpc/types/packages"
|
import type { PackageEnum } from "@scandic-hotels/trpc/types/packages"
|
||||||
|
|
||||||
export function getIconNameByPackageCode(
|
export function getIconNameByPackageCode(
|
||||||
packageCode: PackageEnum
|
packageCode: PackageEnum
|
||||||
): MaterialSymbolProps["icon"] {
|
): MaterialIconProps["icon"] {
|
||||||
switch (packageCode) {
|
switch (packageCode) {
|
||||||
case RoomPackageCodeEnum.PET_ROOM:
|
case RoomPackageCodeEnum.PET_ROOM:
|
||||||
return "pets"
|
return "pets"
|
||||||
|
|||||||
@@ -11,11 +11,7 @@ export default function RoomNotAvailable() {
|
|||||||
return (
|
return (
|
||||||
<div className={styles.noRoomsContainer}>
|
<div className={styles.noRoomsContainer}>
|
||||||
<div className={styles.noRooms}>
|
<div className={styles.noRooms}>
|
||||||
<MaterialIcon
|
<MaterialIcon icon="dangerous" color="Icon/Feedback/Error" size={16} />
|
||||||
icon="error_circle_rounded"
|
|
||||||
color="Icon/Feedback/Error"
|
|
||||||
size={16}
|
|
||||||
/>
|
|
||||||
<Caption color="uiTextHighContrast" type="bold">
|
<Caption color="uiTextHighContrast" type="bold">
|
||||||
{intl.formatMessage({
|
{intl.formatMessage({
|
||||||
id: "selectRate.thisRoomIsNotAvailable",
|
id: "selectRate.thisRoomIsNotAvailable",
|
||||||
|
|||||||
6
packages/booking-flow/types/svg.d.ts
vendored
Normal file
6
packages/booking-flow/types/svg.d.ts
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
declare module "*.svg" {
|
||||||
|
const ReactComponent: React.FunctionComponent<
|
||||||
|
React.SVGProps<SVGSVGElement> & { title?: string }
|
||||||
|
>
|
||||||
|
export default ReactComponent
|
||||||
|
}
|
||||||
@@ -3,8 +3,8 @@ import { Button } from "react-aria-components"
|
|||||||
import type { VariantProps } from "class-variance-authority"
|
import type { VariantProps } from "class-variance-authority"
|
||||||
import type { ComponentProps } from "react"
|
import type { ComponentProps } from "react"
|
||||||
|
|
||||||
import type { SymbolCodepoints } from "../Icons/MaterialIcon/MaterialSymbol/types"
|
|
||||||
import type { variants } from "./variants"
|
import type { variants } from "./variants"
|
||||||
|
import { MaterialIconName } from "../Icons/MaterialIcon/generated"
|
||||||
|
|
||||||
export const buttonIconNames = [
|
export const buttonIconNames = [
|
||||||
"add_circle",
|
"add_circle",
|
||||||
@@ -25,13 +25,8 @@ export const buttonIconNames = [
|
|||||||
"chevron_left",
|
"chevron_left",
|
||||||
] as const
|
] as const
|
||||||
|
|
||||||
export type ButtonIconName = Extract<
|
|
||||||
SymbolCodepoints,
|
|
||||||
(typeof buttonIconNames)[number]
|
|
||||||
>
|
|
||||||
|
|
||||||
export interface ButtonProps
|
export interface ButtonProps
|
||||||
extends ComponentProps<typeof Button>, VariantProps<typeof variants> {
|
extends ComponentProps<typeof Button>, VariantProps<typeof variants> {
|
||||||
leadingIconName?: ButtonIconName | null
|
leadingIconName?: MaterialIconName | null
|
||||||
trailingIconName?: ButtonIconName | null
|
trailingIconName?: MaterialIconName | null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,16 +7,16 @@ import { variants } from "./variants"
|
|||||||
import type { VariantProps } from "class-variance-authority"
|
import type { VariantProps } from "class-variance-authority"
|
||||||
import Link from "next/link"
|
import Link from "next/link"
|
||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
import { ButtonIconName } from "../Button/types"
|
|
||||||
import { MaterialIcon } from "../Icons/MaterialIcon"
|
import { MaterialIcon } from "../Icons/MaterialIcon"
|
||||||
import { Typography } from "../Typography"
|
import { Typography } from "../Typography"
|
||||||
|
import { MaterialIconName } from "../Icons/MaterialIcon/generated"
|
||||||
|
|
||||||
export interface ButtonLinkProps
|
export interface ButtonLinkProps
|
||||||
extends
|
extends
|
||||||
Omit<ComponentProps<typeof Link>, "color">,
|
Omit<ComponentProps<typeof Link>, "color">,
|
||||||
VariantProps<typeof variants> {
|
VariantProps<typeof variants> {
|
||||||
leadingIconName?: ButtonIconName | null
|
leadingIconName?: MaterialIconName | null
|
||||||
trailingIconName?: ButtonIconName | null
|
trailingIconName?: MaterialIconName | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ButtonLink({
|
export default function ButtonLink({
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ import type { Meta, StoryObj } from "@storybook/nextjs-vite"
|
|||||||
|
|
||||||
import { fn } from "storybook/test"
|
import { fn } from "storybook/test"
|
||||||
|
|
||||||
import { MaterialIcon } from "../Icons/MaterialIcon/MaterialIcon.tsx"
|
|
||||||
import { ChipButton } from "./ChipButton.tsx"
|
import { ChipButton } from "./ChipButton.tsx"
|
||||||
import { config as chipButtonConfig } from "./variants"
|
import { config as chipButtonConfig } from "./variants"
|
||||||
|
import { MaterialIcon } from "../Icons/MaterialIcon/index.tsx"
|
||||||
|
|
||||||
const meta: Meta<typeof ChipButton> = {
|
const meta: Meta<typeof ChipButton> = {
|
||||||
title: "Core Components/ChipButton",
|
title: "Core Components/ChipButton",
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { FormProvider, useForm } from "react-hook-form"
|
|||||||
import { fn } from "storybook/test"
|
import { fn } from "storybook/test"
|
||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
|
|
||||||
import { FormTextArea } from "../FormTextArea"
|
|
||||||
import { Button } from "../../Button"
|
import { Button } from "../../Button"
|
||||||
import { MaterialIcon } from "../../Icons/MaterialIcon"
|
import { MaterialIcon } from "../../Icons/MaterialIcon"
|
||||||
import { Typography } from "../../Typography"
|
import { Typography } from "../../Typography"
|
||||||
@@ -44,9 +43,8 @@ function ExampleFormComponent({
|
|||||||
labelPosition = "floating",
|
labelPosition = "floating",
|
||||||
defaultValues,
|
defaultValues,
|
||||||
fieldPrefix = "",
|
fieldPrefix = "",
|
||||||
textAreaDescription,
|
|
||||||
textAreaError,
|
textAreaError,
|
||||||
textAreaLabel = "Message",
|
textAreaDescription,
|
||||||
}: ExampleFormProps) {
|
}: ExampleFormProps) {
|
||||||
const getFieldName = (name: string) =>
|
const getFieldName = (name: string) =>
|
||||||
fieldPrefix ? `${fieldPrefix}_${name}` : name
|
fieldPrefix ? `${fieldPrefix}_${name}` : name
|
||||||
@@ -140,9 +138,10 @@ function ExampleFormComponent({
|
|||||||
labelPosition={labelPosition}
|
labelPosition={labelPosition}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FormTextArea
|
<FormInput
|
||||||
name={getFieldName("message")}
|
name={getFieldName("message")}
|
||||||
label={textAreaLabel || undefined}
|
label="Message"
|
||||||
|
labelPosition={labelPosition}
|
||||||
registerOptions={{ required: true }}
|
registerOptions={{ required: true }}
|
||||||
descriptionIcon="info"
|
descriptionIcon="info"
|
||||||
description={textAreaDescription}
|
description={textAreaDescription}
|
||||||
@@ -201,9 +200,6 @@ export const Default: Story = {
|
|||||||
args: {
|
args: {
|
||||||
onSubmit: fn(),
|
onSubmit: fn(),
|
||||||
labelPosition: "floating",
|
labelPosition: "floating",
|
||||||
textAreaLabel: "Message",
|
|
||||||
textAreaDescription: "This is a custom description",
|
|
||||||
textAreaError: "",
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -531,7 +527,7 @@ function InputShowcase() {
|
|||||||
name="emailIcon"
|
name="emailIcon"
|
||||||
label="Email"
|
label="Email"
|
||||||
type="email"
|
type="email"
|
||||||
leftIcon={<MaterialIcon icon="email" />}
|
leftIcon={<MaterialIcon icon="mail" />}
|
||||||
/>
|
/>
|
||||||
<FormInput
|
<FormInput
|
||||||
name="searchIcon"
|
name="searchIcon"
|
||||||
@@ -562,7 +558,7 @@ function InputShowcase() {
|
|||||||
name="emailIconTop"
|
name="emailIconTop"
|
||||||
label="Email"
|
label="Email"
|
||||||
type="email"
|
type="email"
|
||||||
leftIcon={<MaterialIcon icon="email" />}
|
leftIcon={<MaterialIcon icon="mail" />}
|
||||||
labelPosition="top"
|
labelPosition="top"
|
||||||
/>
|
/>
|
||||||
<FormInput
|
<FormInput
|
||||||
@@ -615,7 +611,7 @@ function InputShowcase() {
|
|||||||
<FormInput
|
<FormInput
|
||||||
name="clearLeftRight"
|
name="clearLeftRight"
|
||||||
label="Clear with Left Icon"
|
label="Clear with Left Icon"
|
||||||
leftIcon={<MaterialIcon icon="phone" />}
|
leftIcon={<MaterialIcon icon="phone_enabled" />}
|
||||||
showClearContentIcon
|
showClearContentIcon
|
||||||
/>
|
/>
|
||||||
<FormInput
|
<FormInput
|
||||||
@@ -627,7 +623,7 @@ function InputShowcase() {
|
|||||||
<FormInput
|
<FormInput
|
||||||
name="clearBothIcons"
|
name="clearBothIcons"
|
||||||
label="Clear with Both Icon"
|
label="Clear with Both Icon"
|
||||||
leftIcon={<MaterialIcon icon="email" />}
|
leftIcon={<MaterialIcon icon="mail" />}
|
||||||
rightIcon={<MaterialIcon icon="lock" />}
|
rightIcon={<MaterialIcon icon="lock" />}
|
||||||
showClearContentIcon
|
showClearContentIcon
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import { Button as ButtonRAC } from "react-aria-components"
|
|||||||
import type { VariantProps } from "class-variance-authority"
|
import type { VariantProps } from "class-variance-authority"
|
||||||
import type { ComponentProps } from "react"
|
import type { ComponentProps } from "react"
|
||||||
|
|
||||||
import type { SymbolCodepoints } from "../Icons/MaterialIcon/MaterialSymbol/types"
|
|
||||||
import type { variants } from "./variants"
|
import type { variants } from "./variants"
|
||||||
|
import { MaterialIconName } from "../Icons/MaterialIcon/generated"
|
||||||
|
|
||||||
export const iconButtonIconNames = [
|
export const iconButtonIconNames = [
|
||||||
"arrow_forward",
|
"arrow_forward",
|
||||||
@@ -26,14 +26,9 @@ export const iconButtonIconNames = [
|
|||||||
"chevron_right",
|
"chevron_right",
|
||||||
] as const
|
] as const
|
||||||
|
|
||||||
export type IconButtonIconName = Extract<
|
|
||||||
SymbolCodepoints,
|
|
||||||
(typeof iconButtonIconNames)[number]
|
|
||||||
>
|
|
||||||
|
|
||||||
export interface IconButtonProps
|
export interface IconButtonProps
|
||||||
extends
|
extends
|
||||||
Omit<ComponentProps<typeof ButtonRAC>, "children">,
|
Omit<ComponentProps<typeof ButtonRAC>, "children">,
|
||||||
VariantProps<typeof variants> {
|
VariantProps<typeof variants> {
|
||||||
iconName: IconButtonIconName
|
iconName: MaterialIconName
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,8 +29,8 @@ import WindowNotAvailableIcon from "./Customised/Amenities_Facilities/WindowNotA
|
|||||||
import WoodFloorIcon from "./Customised/Amenities_Facilities/WoodFloor"
|
import WoodFloorIcon from "./Customised/Amenities_Facilities/WoodFloor"
|
||||||
|
|
||||||
import type { IconProps, NucleoIconProps } from "."
|
import type { IconProps, NucleoIconProps } from "."
|
||||||
import type { MaterialSymbolProps } from "./MaterialIcon/MaterialSymbol"
|
|
||||||
import type { JSX } from "react"
|
import type { JSX } from "react"
|
||||||
|
import { MaterialIconName } from "./MaterialIcon/generated"
|
||||||
|
|
||||||
interface FacilityIconProps {
|
interface FacilityIconProps {
|
||||||
name: string | undefined
|
name: string | undefined
|
||||||
@@ -67,7 +67,7 @@ export function FacilityIcon({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const MaterialIconMappings: {
|
const MaterialIconMappings: {
|
||||||
icon: MaterialSymbolProps["icon"]
|
icon: MaterialIconName
|
||||||
name: string
|
name: string
|
||||||
}[] = [
|
}[] = [
|
||||||
{ icon: "air_purifier_gen", name: "aircondition" },
|
{ icon: "air_purifier_gen", name: "aircondition" },
|
||||||
@@ -112,7 +112,7 @@ const MaterialIconMappings: {
|
|||||||
{ icon: "water_full", name: "complimentarycoldrefreshments" },
|
{ icon: "water_full", name: "complimentarycoldrefreshments" },
|
||||||
{ icon: "groups", name: "conventioncentre" },
|
{ icon: "groups", name: "conventioncentre" },
|
||||||
{ icon: "accessible", name: "disabledparking" },
|
{ icon: "accessible", name: "disabledparking" },
|
||||||
{ icon: "charging_station", name: "dockingstationforipodipad" },
|
{ icon: "mobile_charge", name: "dockingstationforipodipad" },
|
||||||
{ icon: "cool_to_dry", name: "dryingcabinet" },
|
{ icon: "cool_to_dry", name: "dryingcabinet" },
|
||||||
{ icon: "assistant_navigation", name: "easyaccess" },
|
{ icon: "assistant_navigation", name: "easyaccess" },
|
||||||
{ icon: "laundry", name: "garmentsteamer" },
|
{ icon: "laundry", name: "garmentsteamer" },
|
||||||
@@ -172,7 +172,7 @@ const MaterialIconMappings: {
|
|||||||
{ icon: "local_parking", name: "parking" },
|
{ icon: "local_parking", name: "parking" },
|
||||||
{ icon: "local_parking", name: "parkingfreeparking" },
|
{ icon: "local_parking", name: "parkingfreeparking" },
|
||||||
{ icon: "pets", name: "petfriendlyrooms" },
|
{ icon: "pets", name: "petfriendlyrooms" },
|
||||||
{ icon: "phone", name: "directdialphoneandvoicemail" },
|
{ icon: "phone_enabled", name: "directdialphoneandvoicemail" },
|
||||||
{ icon: "restaurant", name: "restaurant" },
|
{ icon: "restaurant", name: "restaurant" },
|
||||||
{ icon: "room_service", name: "roomservice" },
|
{ icon: "room_service", name: "roomservice" },
|
||||||
{ icon: "sauna", name: "sauna" },
|
{ icon: "sauna", name: "sauna" },
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import HairdryerIcon from "./Customised/Amenities_Facilities/Hairdryer"
|
|||||||
import IceMachineIcon from "./Customised/Amenities_Facilities/IceMachine"
|
import IceMachineIcon from "./Customised/Amenities_Facilities/IceMachine"
|
||||||
import InstagramIcon from "./Customised/Socials/Instagram"
|
import InstagramIcon from "./Customised/Socials/Instagram"
|
||||||
import MassageIcon from "./Customised/Amenities_Facilities/Massage"
|
import MassageIcon from "./Customised/Amenities_Facilities/Massage"
|
||||||
import { MaterialIcon, type MaterialIconSetIconProps } from "./MaterialIcon"
|
|
||||||
import PalmTreeIcon from "./Nucleo/Experiences/palm-tree-2"
|
import PalmTreeIcon from "./Nucleo/Experiences/palm-tree-2"
|
||||||
import PopcornIcon from "./Nucleo/Food/popcorn-2"
|
import PopcornIcon from "./Nucleo/Food/popcorn-2"
|
||||||
import RecordPlayerIcon from "./Nucleo/Amenities_Facilities/record-player-3"
|
import RecordPlayerIcon from "./Nucleo/Amenities_Facilities/record-player-3"
|
||||||
@@ -25,6 +25,7 @@ import { IconName } from "./iconName"
|
|||||||
|
|
||||||
import type { IconProps, NucleoIconProps } from "./icon"
|
import type { IconProps, NucleoIconProps } from "./icon"
|
||||||
import type { JSX } from "react"
|
import type { JSX } from "react"
|
||||||
|
import { MaterialIcon, MaterialIconSetIconProps } from "./MaterialIcon"
|
||||||
|
|
||||||
interface IconByIconNameProps {
|
interface IconByIconNameProps {
|
||||||
iconName: IconName | null
|
iconName: IconName | null
|
||||||
@@ -136,7 +137,7 @@ export function IconByIconName({
|
|||||||
return <MaterialIcon icon="photo_camera" {...props} />
|
return <MaterialIcon icon="photo_camera" {...props} />
|
||||||
case IconName.Cellphone:
|
case IconName.Cellphone:
|
||||||
case IconName.Phone:
|
case IconName.Phone:
|
||||||
return <MaterialIcon icon="phone" {...props} />
|
return <MaterialIcon icon="phone_enabled" {...props} />
|
||||||
case IconName.HairdryerInRoomAllScandic:
|
case IconName.HairdryerInRoomAllScandic:
|
||||||
return <HairdryerIcon {...props} />
|
return <HairdryerIcon {...props} />
|
||||||
case IconName.ComplimentaryColdRefreshments:
|
case IconName.ComplimentaryColdRefreshments:
|
||||||
|
|||||||
@@ -1,40 +0,0 @@
|
|||||||
import { MaterialSymbol, type MaterialSymbolProps } from "./MaterialSymbol"
|
|
||||||
|
|
||||||
import { iconVariants } from "../variants"
|
|
||||||
|
|
||||||
import type { VariantProps } from "class-variance-authority"
|
|
||||||
import { HTMLAttributes } from "react"
|
|
||||||
import { getIconAriaProps } from "../utils"
|
|
||||||
|
|
||||||
export interface MaterialIconProps
|
|
||||||
extends
|
|
||||||
Pick<MaterialSymbolProps, "size" | "icon" | "className" | "style">,
|
|
||||||
Omit<HTMLAttributes<HTMLSpanElement>, "color" | "id">,
|
|
||||||
VariantProps<typeof iconVariants> {
|
|
||||||
isFilled?: boolean
|
|
||||||
}
|
|
||||||
export type MaterialIconSetIconProps = Omit<MaterialIconProps, "icon">
|
|
||||||
export function MaterialIcon({
|
|
||||||
color,
|
|
||||||
size = 24,
|
|
||||||
className,
|
|
||||||
isFilled = false,
|
|
||||||
...props
|
|
||||||
}: MaterialIconProps) {
|
|
||||||
const classNames = iconVariants({ className, color })
|
|
||||||
const ariaProps = getIconAriaProps(props)
|
|
||||||
|
|
||||||
return (
|
|
||||||
// The span is used to prevent the MaterialSymbol from being underlined when used inside a link or button
|
|
||||||
<span>
|
|
||||||
<MaterialSymbol
|
|
||||||
{...props}
|
|
||||||
className={classNames}
|
|
||||||
data-testid="MaterialIcon"
|
|
||||||
size={size}
|
|
||||||
fill={isFilled}
|
|
||||||
{...ariaProps}
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
// This is adapted from https://github.com/edjonesdev/react-material-symbols
|
|
||||||
// since it doesn't support React 19 and is not maintained anymore.
|
|
||||||
// We should probably move to a different solution in the future.
|
|
||||||
|
|
||||||
import type { ElementType, CSSProperties, ReactElement, Ref } from "react"
|
|
||||||
import type {
|
|
||||||
MaterialSymbolWeight,
|
|
||||||
PolymorphicComponentProps,
|
|
||||||
SymbolCodepoints,
|
|
||||||
} from "./types"
|
|
||||||
import { cx } from "class-variance-authority"
|
|
||||||
export type { MaterialSymbolWeight, SymbolCodepoints } from "./types"
|
|
||||||
|
|
||||||
export type MaterialSymbolProps = {
|
|
||||||
/** Required. The name of the icon to render. */
|
|
||||||
icon: SymbolCodepoints
|
|
||||||
/** Default `false`.
|
|
||||||
*
|
|
||||||
* Fill gives you the ability to modify the default icon style. A single icon can render both unfilled and filled states. */
|
|
||||||
fill?: boolean
|
|
||||||
/** Weight defines the symbol’s stroke weight, with a range of weights between thin (100) and heavy (900). Weight can also affect the overall size of the symbol. */
|
|
||||||
weight?: MaterialSymbolWeight
|
|
||||||
/** Weight and grade affect a symbol’s thickness. Adjustments to grade are more granular than adjustments to weight and have a small impact on the size of the symbol. */
|
|
||||||
grade?: number
|
|
||||||
/** Default `'inherit'`.
|
|
||||||
*
|
|
||||||
* Size defines the icon width and height in pixels. For the image to look the same at different sizes, the stroke weight (thickness) changes as the icon size scales. */
|
|
||||||
size?: number
|
|
||||||
/** Default `'inherit'`
|
|
||||||
*
|
|
||||||
* Color accepts key values (`'red'`, `'blue'`, `'indigo'`, etc.), `<hex-color>`, `<rgb()>`, `<hsl()>` and `<hwb()>` values. */
|
|
||||||
color?: CSSProperties["color"]
|
|
||||||
className?: string
|
|
||||||
style?: CSSProperties
|
|
||||||
}
|
|
||||||
|
|
||||||
export type PolymorphicMaterialSymbolProps<C extends ElementType> =
|
|
||||||
PolymorphicComponentProps<C, MaterialSymbolProps>
|
|
||||||
|
|
||||||
export const MaterialSymbol = (<C extends ElementType>(
|
|
||||||
{
|
|
||||||
icon,
|
|
||||||
onClick,
|
|
||||||
as,
|
|
||||||
weight,
|
|
||||||
fill = false,
|
|
||||||
grade,
|
|
||||||
size,
|
|
||||||
style: propStyle,
|
|
||||||
color,
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: PolymorphicMaterialSymbolProps<C>,
|
|
||||||
ref: Ref<C>
|
|
||||||
): ReactElement => {
|
|
||||||
const Component =
|
|
||||||
onClick !== undefined ? "button" : ((as as ElementType) ?? "span")
|
|
||||||
const style = { color, ...propStyle }
|
|
||||||
|
|
||||||
if (fill)
|
|
||||||
style.fontVariationSettings = [style.fontVariationSettings, '"FILL" 1']
|
|
||||||
.filter(Boolean)
|
|
||||||
.join(", ")
|
|
||||||
if (weight)
|
|
||||||
style.fontVariationSettings = [
|
|
||||||
style.fontVariationSettings,
|
|
||||||
`"wght" ${weight}`,
|
|
||||||
]
|
|
||||||
.filter(Boolean)
|
|
||||||
.join(", ")
|
|
||||||
if (grade)
|
|
||||||
style.fontVariationSettings = [
|
|
||||||
style.fontVariationSettings,
|
|
||||||
`"GRAD" ${grade}`,
|
|
||||||
]
|
|
||||||
.filter(Boolean)
|
|
||||||
.join(", ")
|
|
||||||
if (size) {
|
|
||||||
style.fontVariationSettings = [
|
|
||||||
style.fontVariationSettings,
|
|
||||||
`"opsz" ${size}`,
|
|
||||||
]
|
|
||||||
.filter(Boolean)
|
|
||||||
.join(", ")
|
|
||||||
style.fontSize = size
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Component
|
|
||||||
{...props}
|
|
||||||
ref={ref}
|
|
||||||
style={style}
|
|
||||||
onClick={onClick}
|
|
||||||
className={cx("material-symbols", className)}
|
|
||||||
>
|
|
||||||
{icon}
|
|
||||||
</Component>
|
|
||||||
)
|
|
||||||
}) as <C extends ElementType>(
|
|
||||||
props: PolymorphicMaterialSymbolProps<C>
|
|
||||||
) => ReactElement
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1 +0,0 @@
|
|||||||
export { MaterialSymbol, type MaterialSymbolProps } from "./MaterialSymbol"
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
import type {
|
|
||||||
JSXElementConstructor,
|
|
||||||
ComponentPropsWithoutRef,
|
|
||||||
JSX,
|
|
||||||
} from "react"
|
|
||||||
import type { MaterialSymbolWeightArray, SymbolCodepointsArray } from "./consts"
|
|
||||||
|
|
||||||
export type SymbolCodepoints = (typeof SymbolCodepointsArray)[number]
|
|
||||||
|
|
||||||
export type MaterialSymbolWeight = (typeof MaterialSymbolWeightArray)[number]
|
|
||||||
|
|
||||||
export interface AsProperty<C extends React.ElementType> {
|
|
||||||
/**
|
|
||||||
* An override of the default HTML tag.
|
|
||||||
* Can also be another React component.
|
|
||||||
*/
|
|
||||||
as?: C
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A more sophisticated version of `InheritableElementProps` where
|
|
||||||
* the passed in `as` prop will determine which props can be included
|
|
||||||
*/
|
|
||||||
export type PolymorphicComponentProps<
|
|
||||||
C extends React.ElementType,
|
|
||||||
Properties = Record<string, unknown>,
|
|
||||||
> = InheritableElementProps<C, Properties & AsProperty<C>>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Source: https://github.com/emotion-js/emotion/blob/master/packages/styled-base/types/helper.d.ts
|
|
||||||
* A more precise version of just React.ComponentPropsWithoutRef on its own
|
|
||||||
*/
|
|
||||||
export type PropsOf<
|
|
||||||
C extends keyof JSX.IntrinsicElements | JSXElementConstructor<unknown>,
|
|
||||||
> = JSX.LibraryManagedAttributes<C, ComponentPropsWithoutRef<C>>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Allows for extending a set of props (`ExtendedProps`) by an overriding set of props
|
|
||||||
* (`OverrideProps`), ensuring that any duplicates are overridden by the overriding
|
|
||||||
* set of props.
|
|
||||||
*/
|
|
||||||
export type ExtendableProps<
|
|
||||||
ExtendedProperties = Record<string, unknown>,
|
|
||||||
OverrideProperties = Record<string, unknown>,
|
|
||||||
> = OverrideProperties & Omit<ExtendedProperties, keyof OverrideProperties>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Allows for inheriting the props from the specified element type so that
|
|
||||||
* props like children, className & style work, as well as element-specific
|
|
||||||
* attributes like aria roles. The component (`C`) must be passed in.
|
|
||||||
*/
|
|
||||||
export type InheritableElementProps<
|
|
||||||
C extends React.ElementType,
|
|
||||||
Properties = Record<string, unknown>,
|
|
||||||
> = ExtendableProps<PropsOf<C>, Properties>
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,3 @@
|
|||||||
|
.iconWrapper {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
@@ -1,5 +1,65 @@
|
|||||||
export {
|
import { HTMLAttributes } from "react"
|
||||||
MaterialIcon,
|
import { MaterialIconName, materialIcons } from "./generated"
|
||||||
type MaterialIconProps,
|
import { VariantProps } from "class-variance-authority"
|
||||||
type MaterialIconSetIconProps,
|
import { iconVariants } from "../variants"
|
||||||
} from "./MaterialIcon"
|
import styles from "./index.module.css"
|
||||||
|
|
||||||
|
export interface MaterialIconProps
|
||||||
|
extends
|
||||||
|
Omit<HTMLAttributes<HTMLOrSVGElement>, "color" | "id">,
|
||||||
|
VariantProps<typeof iconVariants> {
|
||||||
|
icon: MaterialIconName
|
||||||
|
size?: number
|
||||||
|
styleName?: "outlined" | "rounded" | "sharp"
|
||||||
|
isFilled?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MaterialIconSetIconProps = Omit<MaterialIconProps, "icon">
|
||||||
|
|
||||||
|
export function MaterialIcon({
|
||||||
|
icon,
|
||||||
|
size = 24,
|
||||||
|
styleName = "outlined",
|
||||||
|
color = "CurrentColor",
|
||||||
|
isFilled,
|
||||||
|
...props
|
||||||
|
}: MaterialIconProps) {
|
||||||
|
const iconStyles = materialIcons[icon]
|
||||||
|
|
||||||
|
if (!iconStyles) {
|
||||||
|
console.warn(`Icon "${icon}" not found in registry`)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const styleVariants = iconStyles[styleName]
|
||||||
|
|
||||||
|
if (!styleVariants) {
|
||||||
|
console.warn(`Icon "${icon}" does not have style "${styleName}"`)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const IconComponent = isFilled ? styleVariants.filled : styleVariants.outlined
|
||||||
|
|
||||||
|
if (!IconComponent) {
|
||||||
|
console.warn(
|
||||||
|
`Icon "${icon}" does not have ${
|
||||||
|
isFilled ? "filled" : "outlined"
|
||||||
|
} variant for style "${styleName}"`
|
||||||
|
)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const iconClassName = iconVariants({ color })
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className={`${styles.iconWrapper}`} {...props}>
|
||||||
|
<IconComponent
|
||||||
|
width={size}
|
||||||
|
height={size}
|
||||||
|
className={iconClassName}
|
||||||
|
aria-hidden
|
||||||
|
focusable={false}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,11 +4,9 @@ import { memo, useState } from "react"
|
|||||||
import { Button as ButtonRAC } from "react-aria-components"
|
import { Button as ButtonRAC } from "react-aria-components"
|
||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
import { MaterialIcon } from "../Icons/MaterialIcon"
|
|
||||||
import Image from "../Image"
|
import Image from "../Image"
|
||||||
import ImageFallback from "../ImageFallback"
|
import ImageFallback from "../ImageFallback"
|
||||||
import Lightbox from "../Lightbox"
|
import Lightbox from "../Lightbox"
|
||||||
import { Typography } from "../Typography"
|
|
||||||
|
|
||||||
import styles from "./imageGallery.module.css"
|
import styles from "./imageGallery.module.css"
|
||||||
import { ImageCounter } from "../ImageCounter"
|
import { ImageCounter } from "../ImageCounter"
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { expect } from "storybook/test"
|
|||||||
import { Input } from "./Input"
|
import { Input } from "./Input"
|
||||||
import { TextField } from "react-aria-components"
|
import { TextField } from "react-aria-components"
|
||||||
import { MaterialIcon } from "../Icons/MaterialIcon"
|
import { MaterialIcon } from "../Icons/MaterialIcon"
|
||||||
import type { SymbolCodepoints } from "../Icons/MaterialIcon/MaterialSymbol/types"
|
import { MaterialIconName } from "../Icons/MaterialIcon/generated"
|
||||||
|
|
||||||
const meta: Meta<typeof Input> = {
|
const meta: Meta<typeof Input> = {
|
||||||
title: "Core Components/Input",
|
title: "Core Components/Input",
|
||||||
@@ -176,12 +176,12 @@ export const Default: Story = {
|
|||||||
data-validation-state={validationState}
|
data-validation-state={validationState}
|
||||||
leftIcon={
|
leftIcon={
|
||||||
showLeftIcon && leftIconName ? (
|
showLeftIcon && leftIconName ? (
|
||||||
<MaterialIcon icon={leftIconName as SymbolCodepoints} />
|
<MaterialIcon icon={leftIconName as MaterialIconName} />
|
||||||
) : undefined
|
) : undefined
|
||||||
}
|
}
|
||||||
rightIcon={
|
rightIcon={
|
||||||
showRightIcon && rightIconName ? (
|
showRightIcon && rightIconName ? (
|
||||||
<MaterialIcon icon={rightIconName as SymbolCodepoints} />
|
<MaterialIcon icon={rightIconName as MaterialIconName} />
|
||||||
) : undefined
|
) : undefined
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ export function Gallery({
|
|||||||
})}
|
})}
|
||||||
role="button"
|
role="button"
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === 'Enter' || e.key === ' ') {
|
if (e.key === "Enter" || e.key === " ") {
|
||||||
onImageClick()
|
onImageClick()
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import { FullView } from "./FullView"
|
|||||||
import { Gallery } from "./Gallery"
|
import { Gallery } from "./Gallery"
|
||||||
|
|
||||||
import styles from "./lightbox.module.css"
|
import styles from "./lightbox.module.css"
|
||||||
import { cx } from "class-variance-authority"
|
|
||||||
|
|
||||||
export type LightboxImage = {
|
export type LightboxImage = {
|
||||||
src: string
|
src: string
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useEffect } from 'react'
|
import { useCallback, useEffect } from "react"
|
||||||
|
|
||||||
const ANIMATION_OFFSET = 300
|
const ANIMATION_OFFSET = 300
|
||||||
|
|
||||||
@@ -17,14 +17,14 @@ export const animationVariants = {
|
|||||||
export function useKeyboardNavigation(onPrev: () => void, onNext: () => void) {
|
export function useKeyboardNavigation(onPrev: () => void, onNext: () => void) {
|
||||||
const handleKeyDown = useCallback(
|
const handleKeyDown = useCallback(
|
||||||
(e: KeyboardEvent) => {
|
(e: KeyboardEvent) => {
|
||||||
if (e.key === 'ArrowLeft') onPrev()
|
if (e.key === "ArrowLeft") onPrev()
|
||||||
if (e.key === 'ArrowRight') onNext()
|
if (e.key === "ArrowRight") onNext()
|
||||||
},
|
},
|
||||||
[onPrev, onNext]
|
[onPrev, onNext]
|
||||||
)
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
window.addEventListener('keydown', handleKeyDown)
|
window.addEventListener("keydown", handleKeyDown)
|
||||||
return () => window.removeEventListener('keydown', handleKeyDown)
|
return () => window.removeEventListener("keydown", handleKeyDown)
|
||||||
}, [handleKeyDown])
|
}, [handleKeyDown])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,11 +4,12 @@ import { RatePointsOption, RateTermDetails } from "../types"
|
|||||||
import { RadioGroup } from "react-aria-components"
|
import { RadioGroup } from "react-aria-components"
|
||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
import { IconButton } from "../../IconButton"
|
import { IconButton } from "../../IconButton"
|
||||||
import { MaterialIcon } from "../../Icons/MaterialIcon"
|
|
||||||
import { Radio } from "../../Radio"
|
import { Radio } from "../../Radio"
|
||||||
import Modal from "../Modal"
|
import Modal from "../Modal"
|
||||||
import styles from "../rate-card.module.css"
|
import styles from "../rate-card.module.css"
|
||||||
import { variants } from "../variants"
|
import { variants } from "../variants"
|
||||||
|
import { MaterialIcon } from "../../Icons/MaterialIcon"
|
||||||
|
|
||||||
interface PointsRateCardProps {
|
interface PointsRateCardProps {
|
||||||
rateTitle: string
|
rateTitle: string
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import type { VariantProps } from "class-variance-authority"
|
import type { VariantProps } from "class-variance-authority"
|
||||||
|
|
||||||
import { MaterialIcon, MaterialIconSetIconProps } from "../Icons/MaterialIcon"
|
|
||||||
import { toastVariants } from "./variants"
|
import { toastVariants } from "./variants"
|
||||||
|
|
||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
import { IconButton } from "../IconButton"
|
import { IconButton } from "../IconButton"
|
||||||
import { Typography } from "../Typography"
|
import { Typography } from "../Typography"
|
||||||
import styles from "./toasts.module.css"
|
import styles from "./toasts.module.css"
|
||||||
|
import { MaterialIcon, MaterialIconSetIconProps } from "../Icons/MaterialIcon"
|
||||||
|
|
||||||
export type ToastsProps = VariantProps<typeof toastVariants> & {
|
export type ToastsProps = VariantProps<typeof toastVariants> & {
|
||||||
variant: NonNullable<VariantProps<typeof toastVariants>["variant"]>
|
variant: NonNullable<VariantProps<typeof toastVariants>["variant"]>
|
||||||
|
|||||||
@@ -2,15 +2,15 @@ import type { VariantProps } from "class-variance-authority"
|
|||||||
import { Button as ButtonRAC } from "react-aria-components"
|
import { Button as ButtonRAC } from "react-aria-components"
|
||||||
|
|
||||||
import { ComponentProps } from "react"
|
import { ComponentProps } from "react"
|
||||||
import type { SymbolCodepoints } from "../../Icons/MaterialIcon/MaterialSymbol/types"
|
|
||||||
import type { variants } from "./variants"
|
import type { variants } from "./variants"
|
||||||
|
import { MaterialIconName } from "../../Icons/MaterialIcon/generated"
|
||||||
|
|
||||||
export const videoPlayerButtonIconNames = [
|
export const videoPlayerButtonIconNames = [
|
||||||
"play_arrow",
|
"play_arrow",
|
||||||
"pause",
|
"pause",
|
||||||
"volume_up",
|
"volume_up",
|
||||||
"volume_off",
|
"volume_off",
|
||||||
] satisfies SymbolCodepoints[]
|
] satisfies MaterialIconName[]
|
||||||
|
|
||||||
type VideoPlayerButtonIconName = (typeof videoPlayerButtonIconNames)[number]
|
type VideoPlayerButtonIconName = (typeof videoPlayerButtonIconNames)[number]
|
||||||
|
|
||||||
|
|||||||
@@ -270,32 +270,3 @@
|
|||||||
src: url(/_static/shared/fonts/canela-deck/CanelaDeck-ThinItalic.otf)
|
src: url(/_static/shared/fonts/canela-deck/CanelaDeck-ThinItalic.otf)
|
||||||
format("opentype");
|
format("opentype");
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: "Material Symbols Rounded";
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 400;
|
|
||||||
font-display: block;
|
|
||||||
src: url(/_static/shared/fonts/material-symbols/rounded-3e10d67b.woff2)
|
|
||||||
format("woff2");
|
|
||||||
}
|
|
||||||
|
|
||||||
.material-symbols {
|
|
||||||
font-family: "Material Symbols Rounded";
|
|
||||||
font-weight: normal;
|
|
||||||
font-style: normal;
|
|
||||||
font-size: inherit;
|
|
||||||
line-height: 1;
|
|
||||||
letter-spacing: normal;
|
|
||||||
text-transform: none;
|
|
||||||
display: inline-block;
|
|
||||||
white-space: nowrap;
|
|
||||||
word-wrap: normal;
|
|
||||||
direction: ltr;
|
|
||||||
user-select: none;
|
|
||||||
font-feature-settings: "liga";
|
|
||||||
-webkit-font-feature-settings: "liga";
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
text-rendering: optimizeLegibility;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -224,6 +224,7 @@
|
|||||||
"include:shared": "jiti ../../scripts/copyFiles.ts ../../shared public/_static/shared"
|
"include:shared": "jiti ../../scripts/copyFiles.ts ../../shared public/_static/shared"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@material-symbols/svg-400": "0.40.2",
|
||||||
"@scandic-hotels/common": "workspace:*",
|
"@scandic-hotels/common": "workspace:*",
|
||||||
"sonner": "^2.0.3"
|
"sonner": "^2.0.3"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -10,7 +10,8 @@
|
|||||||
"lib",
|
"lib",
|
||||||
".storybook/content/**/*.mdx",
|
".storybook/content/**/*.mdx",
|
||||||
".storybook/content/**/*.tsx",
|
".storybook/content/**/*.tsx",
|
||||||
"lib/tokens/index.ts"
|
"lib/tokens/index.ts",
|
||||||
|
"types"
|
||||||
],
|
],
|
||||||
"exclude": ["node_modules", "**/*.test.ts"]
|
"exclude": ["node_modules", "**/*.test.ts"]
|
||||||
}
|
}
|
||||||
|
|||||||
6
packages/design-system/types/svg.d.ts
vendored
Normal file
6
packages/design-system/types/svg.d.ts
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
declare module "*.svg" {
|
||||||
|
const ReactComponent: React.FunctionComponent<
|
||||||
|
React.SVGProps<SVGSVGElement> & { title?: string }
|
||||||
|
>
|
||||||
|
export default ReactComponent
|
||||||
|
}
|
||||||
@@ -1,24 +1,9 @@
|
|||||||
import crypto from "node:crypto"
|
import { writeFile } from "node:fs/promises"
|
||||||
import { createWriteStream } from "node:fs"
|
import { resolve } from "node:path"
|
||||||
import { mkdir, readFile, rm, writeFile } from "node:fs/promises"
|
import { existsSync } from "node:fs"
|
||||||
import { join, resolve } from "node:path"
|
|
||||||
import { Readable } from "node:stream"
|
|
||||||
import { pipeline } from "node:stream/promises"
|
|
||||||
|
|
||||||
import stringify from "json-stable-stringify-without-jsonify"
|
// Your full list of Material Symbol icon names
|
||||||
import { fileURLToPath } from "node:url"
|
const ICONS = [
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url)
|
|
||||||
const __dirname = resolve(__filename, "..")
|
|
||||||
|
|
||||||
// Defines where the font lives
|
|
||||||
const FONT_DIR = resolve(__dirname, "../shared/fonts/material-symbols")
|
|
||||||
|
|
||||||
// Defines the settings for the font
|
|
||||||
const FONT_BASE_URL = `https://fonts.googleapis.com/css2?family=Material+Symbols+Rounded:opsz,wght,FILL,GRAD@24,400,0..1,0`
|
|
||||||
|
|
||||||
// Defines the subset of icons for the font
|
|
||||||
const icons = [
|
|
||||||
"accessibility",
|
"accessibility",
|
||||||
"accessible",
|
"accessible",
|
||||||
"acute",
|
"acute",
|
||||||
@@ -59,7 +44,7 @@ const icons = [
|
|||||||
"camera",
|
"camera",
|
||||||
"cancel",
|
"cancel",
|
||||||
"chair",
|
"chair",
|
||||||
"charging_station",
|
"mobile_charge",
|
||||||
"check_box",
|
"check_box",
|
||||||
"check_circle",
|
"check_circle",
|
||||||
"check",
|
"check",
|
||||||
@@ -84,6 +69,7 @@ const icons = [
|
|||||||
"credit_score",
|
"credit_score",
|
||||||
"curtains_closed",
|
"curtains_closed",
|
||||||
"curtains",
|
"curtains",
|
||||||
|
"dangerous",
|
||||||
"deck",
|
"deck",
|
||||||
"delete",
|
"delete",
|
||||||
"desk",
|
"desk",
|
||||||
@@ -103,7 +89,6 @@ const icons = [
|
|||||||
"electric_car",
|
"electric_car",
|
||||||
"elevator",
|
"elevator",
|
||||||
"emoji_transportation",
|
"emoji_transportation",
|
||||||
"error_circle_rounded",
|
|
||||||
"error",
|
"error",
|
||||||
"exercise",
|
"exercise",
|
||||||
"family_restroom",
|
"family_restroom",
|
||||||
@@ -174,7 +159,7 @@ const icons = [
|
|||||||
"pedal_bike",
|
"pedal_bike",
|
||||||
"person",
|
"person",
|
||||||
"pets",
|
"pets",
|
||||||
"phone",
|
"phone_enabled",
|
||||||
"photo_camera",
|
"photo_camera",
|
||||||
"play_arrow",
|
"play_arrow",
|
||||||
"pool",
|
"pool",
|
||||||
@@ -234,122 +219,105 @@ const icons = [
|
|||||||
"yard",
|
"yard",
|
||||||
].sort()
|
].sort()
|
||||||
|
|
||||||
function createHash(value: unknown) {
|
const STYLES = ["outlined", "rounded", "sharp"] as const
|
||||||
const stringified = stringify(value)
|
|
||||||
const hash = crypto.createHash("sha256")
|
|
||||||
hash.update(stringified)
|
|
||||||
return hash.digest("hex")
|
|
||||||
}
|
|
||||||
|
|
||||||
const hash = createHash(icons).substring(0, 8)
|
const OUT = resolve(
|
||||||
|
__dirname,
|
||||||
|
"../packages/design-system/lib/components/Icons/MaterialIcon/generated.tsx"
|
||||||
|
)
|
||||||
|
|
||||||
async function fetchIconUrl(url: string) {
|
const PACKAGE_BASE = resolve(
|
||||||
const response = await fetch(url, {
|
__dirname,
|
||||||
headers: {
|
"../node_modules/@material-symbols/svg-400"
|
||||||
Accept:
|
)
|
||||||
"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
|
|
||||||
"User-Agent":
|
|
||||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!response.ok) {
|
function camelCase(str: string) {
|
||||||
console.error(`Unable to fetch woff2 for ${url}`)
|
return str.replace(/[-_](\w)/g, (_, c: string) => c.toUpperCase())
|
||||||
process.exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
const text = await response.text()
|
|
||||||
|
|
||||||
const isWoff2 = /format\('woff2'\)/.test(text)
|
|
||||||
if (!isWoff2) {
|
|
||||||
console.error(`Unable to identify woff2 font in response`)
|
|
||||||
process.exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
const srcUrl = text.match(/src: url\(([^)]+)\)/)
|
|
||||||
|
|
||||||
if (srcUrl && srcUrl[1]) {
|
|
||||||
return srcUrl[1]
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
async function download(url: string, destFolder: string) {
|
|
||||||
const dest = resolve(join(destFolder, `/rounded-${hash}.woff2`))
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(url)
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
console.error(`Unable to fetch ${url}`)
|
|
||||||
process.exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!response.body) {
|
|
||||||
console.error(`Bad response from ${url}`)
|
|
||||||
process.exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
const fileStream = createWriteStream(dest)
|
|
||||||
|
|
||||||
// @ts-expect-error: type mismatch
|
|
||||||
const readableNodeStream = Readable.fromWeb(response.body)
|
|
||||||
|
|
||||||
await pipeline(readableNodeStream, fileStream)
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Error downloading file from ${url}:`, error)
|
|
||||||
process.exit(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
async function cleanFontDirs() {
|
|
||||||
await rm(FONT_DIR, { recursive: true, force: true })
|
|
||||||
await mkdir(FONT_DIR, { recursive: true })
|
|
||||||
await writeFile(
|
|
||||||
join(FONT_DIR, ".auto-generated"),
|
|
||||||
`Auto-generated, do not edit. Use scripts/material-symbols-update.mts to update.\nhash=${hash}\ncreated=${new Date().toISOString()}\n`,
|
|
||||||
{ encoding: "utf-8" }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function updateFontCSS() {
|
|
||||||
const file = resolve(__dirname, "../packages/design-system/lib/fonts.css")
|
|
||||||
|
|
||||||
const css = await readFile(file, {
|
|
||||||
encoding: "utf-8",
|
|
||||||
})
|
|
||||||
|
|
||||||
await writeFile(
|
|
||||||
file,
|
|
||||||
css.replace(
|
|
||||||
/url\(\/_static\/shared\/fonts\/material-symbols\/rounded[^)]+\)/,
|
|
||||||
`url(/_static/shared/fonts/material-symbols/rounded-${hash}.woff2)`
|
|
||||||
),
|
|
||||||
{
|
|
||||||
encoding: "utf-8",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
const fontUrl = `${FONT_BASE_URL}&icon_names=${icons.join(",")}&display=block`
|
const imports: string[] = []
|
||||||
|
const registryEntries: string[] = []
|
||||||
|
|
||||||
const iconUrl = await fetchIconUrl(fontUrl)
|
const missing: string[] = []
|
||||||
|
|
||||||
if (iconUrl) {
|
for (const icon of ICONS) {
|
||||||
await cleanFontDirs()
|
const styleEntries: string[] = []
|
||||||
|
|
||||||
await download(iconUrl, FONT_DIR)
|
for (const style of STYLES) {
|
||||||
|
const parts: string[] = []
|
||||||
|
|
||||||
await updateFontCSS()
|
const fill0Path = resolve(PACKAGE_BASE, style, `${icon}.svg`)
|
||||||
|
const fill1Path = resolve(PACKAGE_BASE, style, `${icon}-fill.svg`)
|
||||||
|
|
||||||
console.log("Successfully updated icons!")
|
let outlinedVar: string | undefined
|
||||||
process.exit(0)
|
let filledVar: string | undefined
|
||||||
} else {
|
|
||||||
console.error(
|
if (existsSync(fill0Path)) {
|
||||||
`Unable to find the icon font src URL in CSS response from Google Fonts at ${fontUrl}`
|
outlinedVar = `${camelCase(icon)}${camelCase(style)}Outlined`
|
||||||
)
|
imports.push(
|
||||||
|
`import ${outlinedVar} from "@material-symbols/svg-400/${style}/${icon}.svg"`
|
||||||
|
)
|
||||||
|
parts.push(`outlined: ${outlinedVar}`)
|
||||||
|
} else {
|
||||||
|
missing.push(`${style}/${icon}.svg`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existsSync(fill1Path)) {
|
||||||
|
filledVar = `${camelCase(icon)}${camelCase(style)}Filled`
|
||||||
|
imports.push(
|
||||||
|
`import ${filledVar} from "@material-symbols/svg-400/${style}/${icon}-fill.svg"`
|
||||||
|
)
|
||||||
|
parts.push(`filled: ${filledVar}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parts.length) {
|
||||||
|
styleEntries.push(`${style}: { ${parts.join(", ")} }`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ALWAYS emit the icon key (even if partial)
|
||||||
|
if (styleEntries.length) {
|
||||||
|
registryEntries.push(`"${icon}": { ${styleEntries.join(", ")} },`)
|
||||||
|
} else {
|
||||||
|
missing.push(`❌ no variants for "${icon}"`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const content =
|
||||||
|
`
|
||||||
|
/* AUTO-GENERATED — DO NOT EDIT */
|
||||||
|
|
||||||
|
import type { FunctionComponent, SVGProps } from "react"
|
||||||
|
|
||||||
|
${imports.join("\n")}
|
||||||
|
|
||||||
|
type SvgIcon = FunctionComponent<SVGProps<SVGSVGElement>>
|
||||||
|
|
||||||
|
export const materialIcons: Record<
|
||||||
|
string,
|
||||||
|
Partial<{
|
||||||
|
outlined: { outlined: SvgIcon; filled?: SvgIcon }
|
||||||
|
rounded: { outlined: SvgIcon; filled?: SvgIcon }
|
||||||
|
sharp: { outlined: SvgIcon; filled?: SvgIcon }
|
||||||
|
}>
|
||||||
|
> = {
|
||||||
|
${registryEntries.join("\n")}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MaterialIconName = keyof typeof materialIcons
|
||||||
|
`.trim() + "\n"
|
||||||
|
|
||||||
|
await writeFile(OUT, content, "utf8")
|
||||||
|
|
||||||
|
console.log(`✅ Generated ${registryEntries.length} icons`)
|
||||||
|
if (missing.length) {
|
||||||
|
console.warn("⚠️ Missing SVGs:")
|
||||||
|
missing.slice(0, 20).forEach((m) => console.warn(" ", m))
|
||||||
|
if (missing.length > 20) {
|
||||||
|
console.warn(` …and ${missing.length - 20} more`)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
main()
|
main().catch(console.error)
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
Auto-generated, do not edit. Use scripts/material-symbols-update.mts to update.
|
|
||||||
hash=3e10d67b
|
|
||||||
created=2025-12-09T12:38:38.912Z
|
|
||||||
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user