Merged in feat/SW-3644-storybook-v10 (pull request #3240)

feat(SW-3644): Storybook v10

* Auto update to Storybook v10

* Add scandic theme and logo

* Update yarn.lock

* Update formatting of package.json

* Update vitest config and playwright plugin

* Remove vitest 4 update

* Re-added comment

* Update the Typography component to explicitly return React.ReactNode

* Add an explicit type assertion to the export

* Add an explicit type assertion to the export for Checkbox

* Explicit return type assertion

* Add an explicit type assertion to the export

* Update @types/react and fix ts warnings

* Updated typings


Approved-by: Linus Flood
Approved-by: Matilda Landström
This commit is contained in:
Rasmus Langvad
2025-11-28 08:05:40 +00:00
parent 27b3f41bff
commit c65091b36a
29 changed files with 6354 additions and 4970 deletions

View File

@@ -1,6 +1,9 @@
nodeLinker: node-modules nodeLinker: node-modules
packageExtensions: packageExtensions:
eslint-config-next@*: eslint-config-next@*:
dependencies: dependencies:
next: "*" next: "*"
storybook@*:
dependencies:
"@storybook/nextjs-vite": "*"

View File

@@ -49,8 +49,8 @@
"@scandic-hotels/typescript-config": "workspace:*", "@scandic-hotels/typescript-config": "workspace:*",
"@swc/plugin-formatjs": "^3.2.2", "@swc/plugin-formatjs": "^3.2.2",
"@types/node": "^20", "@types/node": "^20",
"@types/react": "19.1.0", "@types/react": "^19.2.3",
"@types/react-dom": "19.1.0", "@types/react-dom": "^19.2.3",
"@typescript-eslint/eslint-plugin": "^8.32.0", "@typescript-eslint/eslint-plugin": "^8.32.0",
"@typescript-eslint/parser": "^8.32.0", "@typescript-eslint/parser": "^8.32.0",
"babel-plugin-formatjs": "^10.5.39", "babel-plugin-formatjs": "^10.5.39",

View File

@@ -162,7 +162,7 @@ const variants = {
opacity: 1, opacity: 1,
transition: { duration: 0.4, ease: "easeInOut" }, transition: { duration: 0.4, ease: "easeInOut" },
}, },
}, } as const,
slideInOut: { slideInOut: {
hidden: { hidden: {
@@ -176,7 +176,7 @@ const variants = {
transition: { duration: 0.4, ease: "easeInOut" }, transition: { duration: 0.4, ease: "easeInOut" },
}, },
}, },
} } as const
function getRedeemFlow(reward: Reward, membershipNumber: string) { function getRedeemFlow(reward: Reward, membershipNumber: string) {
const { rewardType } = reward const { rewardType } = reward

View File

@@ -224,7 +224,11 @@ export default function SurprisesNotification({
animate="center" animate="center"
exit="exit" exit="exit"
transition={{ transition={{
x: { type: "ease", duration: 0.5 }, x: {
type: "tween",
ease: "easeInOut",
duration: 0.5,
},
opacity: { duration: 0.2 }, opacity: { duration: 0.2 },
}} }}
layout layout
@@ -277,4 +281,4 @@ const variants = {
opacity: 0, opacity: 0,
} }
}, },
} } as const

View File

@@ -6,8 +6,8 @@
"scripts": { "scripts": {
"build": "next build", "build": "next build",
"dev": "NODE_OPTIONS=--openssl-legacy-provider PORT=3000 NEXT_PUBLIC_PORT=3000 next dev --turbo", "dev": "NODE_OPTIONS=--openssl-legacy-provider PORT=3000 NEXT_PUBLIC_PORT=3000 next dev --turbo",
"lint": "yarn clean && next lint --max-warnings 0 && tsc", "lint": "next typegen && next lint --max-warnings 0 && tsc",
"lint:fix": "yarn clean && next lint --fix --max-warnings 0 && tsc", "lint:fix": "next typegen && next lint --fix --max-warnings 0 && tsc",
"start": "node .next/standalone/server.js", "start": "node .next/standalone/server.js",
"test:setup": "yarn build && yarn start", "test:setup": "yarn build && yarn start",
"preinstall": "/bin/sh -c \"export $(cat .env.local | grep -v '^#' | xargs)\"", "preinstall": "/bin/sh -c \"export $(cat .env.local | grep -v '^#' | xargs)\"",
@@ -99,8 +99,8 @@
"@types/json-stable-stringify-without-jsonify": "^1.0.2", "@types/json-stable-stringify-without-jsonify": "^1.0.2",
"@types/jsonwebtoken": "^9", "@types/jsonwebtoken": "^9",
"@types/node": "^20", "@types/node": "^20",
"@types/react": "19.1.0", "@types/react": "^19.2.3",
"@types/react-dom": "19.1.0", "@types/react-dom": "^19.2.3",
"@typescript-eslint/eslint-plugin": "^8.32.0", "@typescript-eslint/eslint-plugin": "^8.32.0",
"@typescript-eslint/parser": "^8.32.0", "@typescript-eslint/parser": "^8.32.0",
"@vitejs/plugin-react": "^5.0.0", "@vitejs/plugin-react": "^5.0.0",
@@ -122,9 +122,5 @@
}, },
"engines": { "engines": {
"node": "22" "node": "22"
},
"resolutions": {
"@types/react": "19.1.0",
"@types/react-dom": "19.1.0"
} }
} }

View File

@@ -36,8 +36,8 @@
"devDependencies": { "devDependencies": {
"@eslint/compat": "^1.2.9", "@eslint/compat": "^1.2.9",
"@formatjs/cli": "^6.7.1", "@formatjs/cli": "^6.7.1",
"@types/react": "19.1.0", "@types/react": "^19.2.3",
"@types/react-dom": "19.1.0", "@types/react-dom": "^19.2.3",
"@typescript/native-preview": "^7.0.0-dev.20251104.1", "@typescript/native-preview": "^7.0.0-dev.20251104.1",
"@yarnpkg/types": "^4.0.1", "@yarnpkg/types": "^4.0.1",
"commander": "^14.0.0", "commander": "^14.0.0",
@@ -48,7 +48,7 @@
"turbo": "^2.6.1" "turbo": "^2.6.1"
}, },
"resolutions": { "resolutions": {
"vite": "^6.3.5", "vite": "^7.2.4",
"import-in-the-middle": "^1.14.2" "import-in-the-middle": "^1.14.2"
} }
} }

View File

@@ -84,7 +84,7 @@
"@eslint/js": "^9.26.0", "@eslint/js": "^9.26.0",
"@scandic-hotels/typescript-config": "workspace:*", "@scandic-hotels/typescript-config": "workspace:*",
"@t3-oss/env-nextjs": "^0.13.4", "@t3-oss/env-nextjs": "^0.13.4",
"@types/react": "19.1.0", "@types/react": "^19.2.3",
"@typescript-eslint/eslint-plugin": "^8.32.0", "@typescript-eslint/eslint-plugin": "^8.32.0",
"@typescript-eslint/parser": "^8.32.0", "@typescript-eslint/parser": "^8.32.0",
"@vitest/coverage-v8": "^3.2.4", "@vitest/coverage-v8": "^3.2.4",

View File

@@ -1,20 +1,19 @@
import type { StorybookConfig } from '@storybook/nextjs-vite' import type { StorybookConfig } from '@storybook/nextjs-vite'
import { dirname, join } from 'path'
import { mergeConfig } from 'vite' import { mergeConfig } from 'vite'
const config: StorybookConfig = { const config: StorybookConfig = {
framework: '@storybook/nextjs-vite',
stories: ['../lib/**/*.mdx', '../lib/**/*.stories.@(js|jsx|mjs|ts|tsx)'], stories: ['../lib/**/*.mdx', '../lib/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
addons: [ addons: [
getAbsolutePath('@storybook/addon-links'), '@storybook/addon-links',
getAbsolutePath('@storybook/addon-themes'), '@storybook/addon-themes',
getAbsolutePath('@storybook/addon-vitest'), '@storybook/addon-vitest',
getAbsolutePath('@storybook/addon-docs'), '@storybook/addon-docs',
getAbsolutePath('@storybook/addon-a11y'), '@storybook/addon-a11y',
getAbsolutePath('storybook-react-intl'), 'storybook-react-intl',
], ],
framework: { core: {
name: getAbsolutePath('@storybook/nextjs-vite'), disableTelemetry: true,
options: {},
}, },
async viteFinal(config) { async viteFinal(config) {
return mergeConfig(config, { return mergeConfig(config, {
@@ -48,7 +47,3 @@ const config: StorybookConfig = {
}, },
} }
export default config export default config
function getAbsolutePath(value: string) {
return dirname(require.resolve(join(value, 'package.json')))
}

View File

@@ -0,0 +1,6 @@
import { addons } from 'storybook/manager-api'
import scandicTheme from './scandic-theme'
addons.setConfig({
theme: scandicTheme,
})

View File

@@ -47,18 +47,23 @@ const preview: Preview = {
}, },
parameters: { parameters: {
reactIntl, reactIntl,
nextjs: { nextjs: {
appDirectory: true, appDirectory: true,
}, },
docs: { docs: {
toc: true, toc: true,
}, },
controls: { matchers: { color: /(background|color)$/i, date: /Date$/i } }, controls: { matchers: { color: /(background|color)$/i, date: /Date$/i } },
options: { options: {
storySort: { storySort: {
order: ['Introduction', 'Global', 'Components', 'Compositions', '*'], order: ['Introduction', 'Global', 'Components', 'Compositions', '*'],
}, },
}, },
backgrounds: { backgrounds: {
options: { options: {
// 👇 Scandic // 👇 Scandic
@@ -70,6 +75,13 @@ const preview: Preview = {
storybookLight: { name: 'Storybook Light', value: '#F7F9F2' }, storybookLight: { name: 'Storybook Light', value: '#F7F9F2' },
}, },
}, },
a11y: {
// 'todo' - show a11y violations in the test UI only
// 'error' - fail CI on a11y violations
// 'off' - skip a11y checks entirely
test: 'todo',
},
}, },
tags: ['autodocs'], tags: ['autodocs'],

View File

@@ -0,0 +1,8 @@
import { create } from 'storybook/theming'
export default create({
base: 'dark',
brandTitle: 'Scandic Design System',
brandUrl: 'https://www.scandichotels.com/',
brandImage: 'http://scandichotels.com/_static/img/scandic-logotype.png',
})

View File

@@ -1,4 +1,5 @@
import * as a11yAddonAnnotations from '@storybook/addon-a11y/preview'
import { setProjectAnnotations } from '@storybook/nextjs-vite' import { setProjectAnnotations } from '@storybook/nextjs-vite'
import * as previewAnnotations from './preview' import * as previewAnnotations from './preview'
setProjectAnnotations([previewAnnotations]) setProjectAnnotations([a11yAddonAnnotations, previewAnnotations])

View File

@@ -16,8 +16,10 @@ import styles from './select.module.css'
import Body from '../Body' import Body from '../Body'
import { Label } from '../Label' import { Label } from '../Label'
interface SelectProps interface SelectProps extends Omit<
extends Omit<React.SelectHTMLAttributes<HTMLSelectElement>, 'onSelect'> { React.SelectHTMLAttributes<HTMLSelectElement>,
'onSelect'
> {
defaultSelectedKey?: Key defaultSelectedKey?: Key
items: { label: string; value: Key }[] items: { label: string; value: Key }[]
label: string label: string
@@ -67,8 +69,10 @@ export default function Select({
} }
} }
function handleOnSelect(key: Key) { function handleOnSelect(key: Key | null) {
onSelect(key) if (key !== null) {
onSelect(key)
}
} }
let chevronProps = {} let chevronProps = {}
@@ -141,7 +145,7 @@ export default function Select({
key={`${item.value}_${item.label}`} key={`${item.value}_${item.label}`}
data-testid={item.label} data-testid={item.label}
> >
{optionsIcon ? optionsIcon : null} {optionsIcon}
{item.label} {item.label}
</ListBoxItem> </ListBoxItem>
))} ))}

View File

@@ -20,7 +20,7 @@ interface CheckboxProps extends React.InputHTMLAttributes<HTMLInputElement> {
errorCodeMessages?: Record<string, string> errorCodeMessages?: Record<string, string>
} }
const Checkbox = forwardRef< const CheckboxComponent = forwardRef<
HTMLInputElement, HTMLInputElement,
React.PropsWithChildren<CheckboxProps> React.PropsWithChildren<CheckboxProps>
>(function Checkbox( >(function Checkbox(
@@ -85,4 +85,8 @@ const Checkbox = forwardRef<
) )
}) })
const Checkbox = CheckboxComponent as React.ForwardRefExoticComponent<
React.PropsWithChildren<CheckboxProps> & React.RefAttributes<HTMLInputElement>
>
export default Checkbox export default Checkbox

View File

@@ -122,7 +122,7 @@ export type HotelCardProps = {
onAddressClick: () => void onAddressClick: () => void
} }
export const HotelCard = memo( export const HotelCardComponent = memo(
({ ({
prices, prices,
hotel, hotel,
@@ -385,6 +385,10 @@ export const HotelCard = memo(
} }
) )
export const HotelCard = HotelCardComponent as React.MemoExoticComponent<
(props: HotelCardProps) => React.ReactElement
>
interface PricesWrapperProps { interface PricesWrapperProps {
children: React.ReactNode children: React.ReactNode
isClickable?: boolean isClickable?: boolean

View File

@@ -97,4 +97,8 @@ function ImageGallery({
) )
} }
export default memo(ImageGallery) const ImageGalleryComponent = memo(ImageGallery)
export default ImageGalleryComponent as React.MemoExoticComponent<
(props: ImageGalleryProps) => React.ReactElement
>

View File

@@ -15,7 +15,7 @@ import styles from './input.module.css'
import type { InputProps } from './types' import type { InputProps } from './types'
import { Typography } from '../Typography' import { Typography } from '../Typography'
export const Input = forwardRef(function AriaInputWithLabelComponent( const InputComponent = forwardRef(function AriaInputWithLabelComponent(
{ label, ...props }: InputProps, { label, ...props }: InputProps,
forwardedRef: ForwardedRef<HTMLInputElement> forwardedRef: ForwardedRef<HTMLInputElement>
) { ) {
@@ -44,3 +44,7 @@ export const Input = forwardRef(function AriaInputWithLabelComponent(
</AriaLabel> </AriaLabel>
) )
}) })
export const Input = InputComponent as React.ForwardRefExoticComponent<
InputProps & React.RefAttributes<HTMLInputElement>
>

View File

@@ -82,7 +82,7 @@ export default function FullView({
opacity: 0, opacity: 0,
x: animateLeft ? -300 : 300, x: animateLeft ? -300 : 300,
}), }),
} } as const
return ( return (
<div className={styles.fullView}> <div className={styles.fullView}>

View File

@@ -82,7 +82,7 @@ export default function Gallery({
opacity: 0, opacity: 0,
x: animateLeft ? -300 : 300, x: animateLeft ? -300 : 300,
}), }),
} } as const
return ( return (
<div className={styles.gallery}> <div className={styles.gallery}>

View File

@@ -1,23 +1,23 @@
export const fade = { export const fade = {
hidden: { hidden: {
opacity: 0, opacity: 0,
transition: { duration: 0.4, ease: 'easeInOut' }, transition: { duration: 0.4, ease: 'easeInOut' as const },
}, },
visible: { visible: {
opacity: 1, opacity: 1,
transition: { duration: 0.4, ease: 'easeInOut' }, transition: { duration: 0.4, ease: 'easeInOut' as const },
}, },
} } as const
export const slideInOut = { export const slideInOut = {
hidden: { hidden: {
opacity: 0, opacity: 0,
y: 32, y: 32,
transition: { duration: 0.4, ease: 'easeInOut' }, transition: { duration: 0.4, ease: 'easeInOut' as const },
}, },
visible: { visible: {
opacity: 1, opacity: 1,
y: 0, y: 0,
transition: { duration: 0.4, ease: 'easeInOut' }, transition: { duration: 0.4, ease: 'easeInOut' as const },
}, },
} } as const

View File

@@ -7,7 +7,7 @@ export const fade = {
opacity: 1, opacity: 1,
transition: { duration: 0.4, ease: 'easeInOut' }, transition: { duration: 0.4, ease: 'easeInOut' },
}, },
} } as const
export const slideInOut = { export const slideInOut = {
hidden: { hidden: {
@@ -20,7 +20,7 @@ export const slideInOut = {
y: 0, y: 0,
transition: { duration: 0.4, ease: 'easeInOut' }, transition: { duration: 0.4, ease: 'easeInOut' },
}, },
} } as const
export const slideFromTop = { export const slideFromTop = {
hidden: { hidden: {
@@ -33,4 +33,4 @@ export const slideFromTop = {
y: 0, y: 0,
transition: { duration: 0.4, ease: 'easeInOut' }, transition: { duration: 0.4, ease: 'easeInOut' },
}, },
} } as const

View File

@@ -4,7 +4,11 @@ import { variants } from './variants'
import type { TypographyProps } from './types' import type { TypographyProps } from './types'
export function Typography({ variant, className, children }: TypographyProps) { export function Typography({
variant,
className,
children,
}: TypographyProps): React.ReactNode {
if (!isValidElement(children)) return null if (!isValidElement(children)) return null
const classNames = variants({ const classNames = variants({

View File

@@ -246,19 +246,19 @@
"devDependencies": { "devDependencies": {
"@eslint/eslintrc": "^3.3.1", "@eslint/eslintrc": "^3.3.1",
"@eslint/js": "^9.26.0", "@eslint/js": "^9.26.0",
"@storybook/addon-a11y": "^9.1.2", "@storybook/addon-a11y": "^10.0.8",
"@storybook/addon-docs": "^9.1.2", "@storybook/addon-docs": "^10.0.8",
"@storybook/addon-links": "^9.1.2", "@storybook/addon-links": "^10.0.8",
"@storybook/addon-themes": "^9.1.2", "@storybook/addon-themes": "^10.0.8",
"@storybook/addon-vitest": "^9.1.2", "@storybook/addon-vitest": "^10.0.8",
"@storybook/nextjs-vite": "^9.1.2", "@storybook/nextjs-vite": "^10.0.8",
"@types/css-modules": "^1.0.5", "@types/css-modules": "^1.0.5",
"@types/node": "^20.17.17", "@types/node": "^20.17.17",
"@types/react": "^19", "@types/react": "^19.2.3",
"@types/react-dom": "^19", "@types/react-dom": "^19.2.3",
"@typescript-eslint/eslint-plugin": "^8.32.0", "@typescript-eslint/eslint-plugin": "^8.32.0",
"@typescript-eslint/parser": "^8.32.0", "@typescript-eslint/parser": "^8.32.0",
"@vitejs/plugin-react": "^5.0.0", "@vitejs/plugin-react": "^5.1.1",
"@vitest/browser": "^3.2.4", "@vitest/browser": "^3.2.4",
"babel-plugin-formatjs": "^10.5.10", "babel-plugin-formatjs": "^10.5.10",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
@@ -270,7 +270,7 @@
"eslint-plugin-import": "^2.31.0", "eslint-plugin-import": "^2.31.0",
"eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.20", "eslint-plugin-react-refresh": "^0.4.20",
"eslint-plugin-storybook": "^9.1.2", "eslint-plugin-storybook": "^10.0.8",
"glob": "^11.0.2", "glob": "^11.0.2",
"globals": "^16.1.0", "globals": "^16.1.0",
"husky": "^9.1.7", "husky": "^9.1.7",
@@ -282,11 +282,11 @@
"react-dom": "^19.1.0", "react-dom": "^19.1.0",
"rollup": "^4.40.2", "rollup": "^4.40.2",
"rollup-preserve-directives": "^1.1.3", "rollup-preserve-directives": "^1.1.3",
"storybook": "^9.1.2", "storybook": "^10.0.8",
"storybook-react-intl": "^4.0.7", "storybook-react-intl": "^10.0.1",
"typescript": "^5.8.3", "typescript": "^5.8.3",
"vite": "^6.3.5", "vite": "^7.2.4",
"vite-plugin-dts": "^4.5.3", "vite-plugin-dts": "^4.5.4",
"vite-plugin-lib-inject-css": "^2.2.2", "vite-plugin-lib-inject-css": "^2.2.2",
"vitest": "^3.2.4", "vitest": "^3.2.4",
"vitest-browser-react": "^1.0.1" "vitest-browser-react": "^1.0.1"

View File

@@ -5,7 +5,7 @@ import {
type Control, type Control,
type FieldValues, type FieldValues,
useFormState, useFormState,
type UseFromSubscribe, type UseFormSubscribe,
} from "react-hook-form" } from "react-hook-form"
import { import {
@@ -17,7 +17,7 @@ import {
export function useFormTracking<T extends FieldValues>( export function useFormTracking<T extends FieldValues>(
formType: FormType, formType: FormType,
subscribe: UseFromSubscribe<T>, subscribe: UseFormSubscribe<T>,
control: Control<T>, control: Control<T>,
nameSuffix: string = "" nameSuffix: string = ""
) { ) {

View File

@@ -3,6 +3,8 @@ import "server-only"
import deepmerge from "deepmerge" import deepmerge from "deepmerge"
import merge from "deepmerge" import merge from "deepmerge"
import type { DocumentNode } from "graphql"
import { createLogger } from "@scandic-hotels/common/logger/createLogger" import { createLogger } from "@scandic-hotels/common/logger/createLogger"
import { request } from "./request" import { request } from "./request"
@@ -24,7 +26,11 @@ export async function batchRequest<T>(
try { try {
const response = await Promise.allSettled( const response = await Promise.allSettled(
queries.map((query) => queries.map((query) =>
request<T>(query.document, query.variables, query.cacheOptions) request<T>(
query.document as string | DocumentNode,
query.variables,
query.cacheOptions
)
) )
) )

View File

@@ -80,7 +80,7 @@ function internalRequest<T>(
...params, ...params,
signal: AbortSignal.timeout(15_000), signal: AbortSignal.timeout(15_000),
}) })
}), }) as unknown as typeof fetch,
}) })
const mergedParams = const mergedParams =

View File

@@ -77,7 +77,7 @@
"@eslint/eslintrc": "^3.3.1", "@eslint/eslintrc": "^3.3.1",
"@eslint/js": "^9.26.0", "@eslint/js": "^9.26.0",
"@scandic-hotels/typescript-config": "workspace:*", "@scandic-hotels/typescript-config": "workspace:*",
"@types/react": "19.1.0", "@types/react": "^19.2.3",
"@typescript-eslint/eslint-plugin": "^8.32.0", "@typescript-eslint/eslint-plugin": "^8.32.0",
"@typescript-eslint/parser": "^8.32.0", "@typescript-eslint/parser": "^8.32.0",
"dotenv": "^16.5.0", "dotenv": "^16.5.0",

View File

@@ -4,13 +4,13 @@
"esModuleInterop": true, "esModuleInterop": true,
"incremental": false, "incremental": false,
"isolatedModules": true, "isolatedModules": true,
"lib": ["es2022", "DOM", "DOM.Iterable"], "lib": ["es2023", "DOM", "DOM.Iterable"],
"module": "ESNext", "module": "ESNext",
"moduleDetection": "force", "moduleDetection": "force",
"moduleResolution": "bundler", "moduleResolution": "bundler",
"resolveJsonModule": true, "resolveJsonModule": true,
"skipLibCheck": true, "skipLibCheck": true,
"strict": true, "strict": true,
"target": "ES2022" "target": "ES2023"
} }
} }

11113
yarn.lock

File diff suppressed because it is too large Load Diff