Merged in monorepo-step-1 (pull request #1080)

Migrate to a monorepo setup - step 1

* Move web to subfolder /apps/scandic-web

* Yarn + transitive deps

- Move to yarn
- design-system package removed for now since yarn doesn't
support the parameter for token (ie project currently broken)
- Add missing transitive dependencies as Yarn otherwise
prevents these imports
- VS Code doesn't pick up TS path aliases unless you open
/apps/scandic-web instead of root (will be fixed with monorepo)

* Pin framer-motion to temporarily fix typing issue

https://github.com/adobe/react-spectrum/issues/7494

* Pin zod to avoid typ error

There seems to have been a breaking change in the types
returned by zod where error is now returned as undefined
instead of missing in the type. We should just handle this
but to avoid merge conflicts just pin the dependency for
now.

* Pin react-intl version

Pin version of react-intl to avoid tiny type issue where formatMessage
does not accept a generic any more. This will be fixed in a future
commit, but to avoid merge conflicts just pin for now.

* Pin typescript version

Temporarily pin version as newer versions as stricter and results in
a type error. Will be fixed in future commit after merge.

* Setup workspaces

* Add design-system as a monorepo package

* Remove unused env var DESIGN_SYSTEM_ACCESS_TOKEN

* Fix husky for monorepo setup

* Update netlify.toml

* Add lint script to root package.json

* Add stub readme

* Fix react-intl formatMessage types

* Test netlify.toml in root

* Remove root toml

* Update netlify.toml publish path

* Remove package-lock.json

* Update build for branch/preview builds


Approved-by: Linus Flood
This commit is contained in:
Anton Gunnarsson
2025-02-26 10:36:17 +00:00
committed by Linus Flood
parent 667cab6fb6
commit 80100e7631
2731 changed files with 30986 additions and 23708 deletions
@@ -0,0 +1,23 @@
"use client"
import { cx } from "class-variance-authority"
import { useCarousel } from "./CarouselContext"
import styles from "./carousel.module.css"
export function CarouselContent({
className,
children,
...props
}: React.HTMLAttributes<HTMLDivElement>) {
const { carouselRef } = useCarousel()
return (
<div ref={carouselRef} className={styles.viewport}>
<div className={cx(styles.container, className)} {...props}>
{children}
</div>
</div>
)
}
@@ -0,0 +1,17 @@
"use client"
import { createContext, useContext } from "react"
import type { CarouselContextProps } from "./types"
const CarouselContext = createContext<CarouselContextProps | null>(null)
export function useCarousel() {
const context = useContext(CarouselContext)
if (!context) {
throw new Error("useCarousel must be used within a <Carousel />")
}
return context
}
export { CarouselContext }
@@ -0,0 +1,45 @@
"use client"
import { cx } from "class-variance-authority"
import { useEffect, useState } from "react"
import { useCarousel } from "./CarouselContext"
import styles from "./carousel.module.css"
export function CarouselDots({ className }: { className?: string }) {
const { selectedIndex, api } = useCarousel()
const [scrollSnaps, setScrollSnaps] = useState<number[]>([])
// Update scroll snaps when the carousel is initialized or viewport changes
useEffect(() => {
if (!api) return
const onInit = () => {
setScrollSnaps(api.scrollSnapList())
}
onInit()
api.on("reInit", onInit)
return () => {
api.off("reInit", onInit)
}
}, [api])
// Don't render dots if we have 1 or fewer scroll positions
if (scrollSnaps.length <= 1) return null
return (
<div className={cx(styles.dots, className)}>
{scrollSnaps.map((_, index) => (
<div
key={index}
className={styles.dot}
data-active={index === selectedIndex}
aria-hidden="true"
/>
))}
</div>
)
}
@@ -0,0 +1,22 @@
"use client"
import { cx } from "class-variance-authority"
import styles from "./carousel.module.css"
export function CarouselItem({
className,
children,
...props
}: React.HTMLAttributes<HTMLDivElement>) {
return (
<div
role="group"
aria-roledescription="slide"
className={cx(styles.item, className)}
{...props}
>
{children}
</div>
)
}
@@ -0,0 +1,45 @@
"use client"
import { cx } from "class-variance-authority"
import { ArrowRightIcon } from "@/components/Icons"
import { useCarousel } from "./CarouselContext"
import styles from "./carousel.module.css"
import type { CarouselButtonProps } from "./types"
export function CarouselPrevious({ className, ...props }: CarouselButtonProps) {
const { scrollPrev, canScrollPrev } = useCarousel()
if (!canScrollPrev()) return null
return (
<button
className={cx(styles.button, styles.buttonPrev, className)}
onClick={scrollPrev}
aria-label="Previous slide"
{...props}
>
<ArrowRightIcon color="burgundy" className={styles.prevIcon} />
</button>
)
}
export function CarouselNext({ className, ...props }: CarouselButtonProps) {
const { scrollNext, canScrollNext } = useCarousel()
if (!canScrollNext()) return null
return (
<button
className={cx(styles.button, styles.buttonNext, className)}
onClick={scrollNext}
aria-label="Next slide"
{...props}
>
<ArrowRightIcon color="burgundy" />
</button>
)
}
@@ -0,0 +1,82 @@
.root {
position: relative;
}
.viewport {
overflow: hidden;
}
.container {
display: grid;
grid-auto-flow: column;
grid-auto-columns: 85%;
gap: var(--Spacing-x2);
}
.item {
min-width: 0;
}
.button {
display: none;
align-items: center;
justify-content: center;
position: absolute;
top: 50%;
transform: translateY(-50%);
background: var(--Base-Surface-Primary-light-Normal);
box-shadow: 0px 0px 8px 1px rgba(0, 0, 0, 0.1);
border: none;
border-radius: 50%;
padding: var(--Spacing-x1);
cursor: pointer;
z-index: 1;
}
.buttonPrev {
left: -20px;
}
.buttonNext {
right: -20px;
}
.prevIcon {
transform: rotate(180deg);
}
.dots {
display: flex;
justify-content: center;
gap: var(--Spacing-x1);
margin-top: var(--Spacing-x3);
}
.dot {
height: 8px;
width: 8px;
border-radius: var(--Corner-radius-Large);
background: var(--UI-Grey-40);
transition: width 0.3s ease;
}
.dot[data-active="true"] {
width: 22px;
background: var(--UI-Text-Medium-contrast);
}
@media (min-width: 768px) {
.container {
grid-auto-columns: calc(50% - var(--Spacing-x2) / 2);
}
.button {
display: flex;
}
}
@media (min-width: 1024px) {
.container {
grid-auto-columns: calc(33.33% - var(--Spacing-x2) * 2 / 3);
}
}
@@ -0,0 +1,110 @@
"use client"
import { cx } from "class-variance-authority"
import useEmblaCarousel from "embla-carousel-react"
import { useCallback, useEffect, useState } from "react"
import { CarouselContent } from "./CarouselContent"
import { CarouselContext } from "./CarouselContext"
import { CarouselDots } from "./CarouselDots"
import { CarouselItem } from "./CarouselItem"
import { CarouselNext, CarouselPrevious } from "./CarouselNavigation"
import styles from "./carousel.module.css"
import type { CarouselApi, CarouselProps } from "./types"
function Carousel({
opts,
setApi,
plugins,
className,
children,
}: CarouselProps) {
const [carouselRef, api] = useEmblaCarousel(
{
containScroll: "trimSnaps",
align: "start",
axis: "x",
...opts,
},
plugins
)
const [selectedIndex, setSelectedIndex] = useState(0)
const onSelect = useCallback((api: CarouselApi) => {
if (!api) return
setSelectedIndex(api.selectedScrollSnap())
}, [])
function scrollPrev() {
api?.scrollPrev()
}
function scrollNext() {
api?.scrollNext()
}
function handleKeyDown(event: React.KeyboardEvent<HTMLDivElement>) {
if (event.key === "ArrowLeft") {
event.preventDefault()
scrollPrev()
} else if (event.key === "ArrowRight") {
event.preventDefault()
scrollNext()
}
}
useEffect(() => {
if (!api || !setApi) return
setApi(api)
}, [api, setApi])
useEffect(() => {
if (!api) return
onSelect(api)
api.on("reInit", onSelect)
api.on("select", onSelect)
return () => {
api.off("select", onSelect)
}
}, [api, onSelect])
return (
<CarouselContext.Provider
value={{
carouselRef,
api,
opts,
plugins,
setApi,
children,
scrollPrev,
scrollNext,
canScrollPrev: api?.canScrollPrev.bind(api) ?? (() => false),
canScrollNext: api?.canScrollNext.bind(api) ?? (() => false),
selectedIndex,
}}
>
<div
onKeyDownCapture={handleKeyDown}
className={cx(styles.root, className)}
role="region"
aria-roledescription="carousel"
>
{children}
</div>
</CarouselContext.Provider>
)
}
Carousel.Content = CarouselContent
Carousel.Item = CarouselItem
Carousel.Next = CarouselNext
Carousel.Previous = CarouselPrevious
Carousel.Dots = CarouselDots
export { Carousel }
export type { CarouselApi }
@@ -0,0 +1,34 @@
/**
* Must be imported from the core package.
* @see https://www.embla-carousel.com/api/methods/#typescript
*/
import type { EmblaCarouselType } from "embla-carousel"
import type useEmblaCarousel from "embla-carousel-react"
import type { PropsWithChildren } from "react"
export type CarouselApi = EmblaCarouselType
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>
type CarouselOptions = UseCarouselParameters[0]
type CarouselPlugin = UseCarouselParameters[1]
export interface CarouselProps extends PropsWithChildren {
opts?: CarouselOptions
plugins?: CarouselPlugin
setApi?: (api: CarouselApi) => void
className?: string
}
export interface CarouselContextProps extends Omit<CarouselProps, "className"> {
carouselRef: ReturnType<typeof useEmblaCarousel>[0]
api: ReturnType<typeof useEmblaCarousel>[1]
scrollPrev: () => void
scrollNext: () => void
canScrollPrev: () => boolean
canScrollNext: () => boolean
selectedIndex: number
}
export interface CarouselButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
className?: string
}