diff --git a/packages/epo-react-lib/src/index.ts b/packages/epo-react-lib/src/index.ts index c0f5d7b2..d02a1aa9 100644 --- a/packages/epo-react-lib/src/index.ts +++ b/packages/epo-react-lib/src/index.ts @@ -49,7 +49,6 @@ export { default as CarouselLayout } from "@/layout/Carousel"; export { default as Columns } from "@/layout/Columns"; export { default as Container } from "@/layout/Container"; export { default as Grid } from "@/layout/Grid"; -export { default as MasonryGrid } from "@/layout/MasonryGrid"; export { SlideoutMenu, MenuGroup, diff --git a/packages/epo-react-lib/src/layout/MasonryGrid/MasonryGrid.stories.tsx b/packages/epo-react-lib/src/layout/MasonryGrid/MasonryGrid.stories.tsx deleted file mode 100644 index dbdd0a62..00000000 --- a/packages/epo-react-lib/src/layout/MasonryGrid/MasonryGrid.stories.tsx +++ /dev/null @@ -1,114 +0,0 @@ -import { ComponentMeta, ComponentStoryObj } from "@storybook/react"; -import { getGradientImage } from "@/storybook/utilities/helpers"; - -import MasonryGrid from "."; - -const image = { - altText: "A placeholder image", - url: getGradientImage(200, 200), - width: 200, - height: 200, -}; - -const galleryItems = [ - { - galleryItemCategory: [{ id: "a1", slug: "image", title: "Gallery Item 1" }], - id: "a1", - image: [image], - title: "Gallery Item 1", - uri: "", - }, - { - galleryItemCategory: [{ id: "a2", slug: "video", title: "Gallery Item 2" }], - id: "a2", - image: [image], - title: "Gallery Item 2", - uri: "", - }, - { - galleryItemCategory: [{ id: "a3", slug: "image", title: "Gallery Item 3" }], - id: "a3", - image: [image], - title: "Gallery Item 3", - uri: "", - }, - { - galleryItemCategory: [{ id: "a4", slug: "video", title: "Gallery Item 4" }], - id: "a4", - image: [image], - title: "Gallery Item 4", - uri: "", - }, - { - galleryItemCategory: [{ id: "a5", slug: "image", title: "Gallery Item 5" }], - id: "a5", - image: [image], - title: "Gallery Item 1", - uri: "", - }, - { - galleryItemCategory: [{ id: "a6", slug: "video", title: "Gallery Item 6" }], - id: "a6", - image: [image], - title: "Gallery Item 2", - uri: "", - }, - { - galleryItemCategory: [{ id: "a7", slug: "image", title: "Gallery Item 7" }], - id: "a7", - image: [image], - title: "Gallery Item 3", - uri: "", - }, - { - galleryItemCategory: [{ id: "a8", slug: "video", title: "Gallery Item 8" }], - id: "a8", - image: [image], - title: "Gallery Item 4", - uri: "", - }, -]; - -const meta: ComponentMeta = { - component: MasonryGrid, - argTypes: { - items: { - description: "Array of gallery entries to show in masonry grid", - type: { - name: "other", - value: "GalleryEntry[]", - required: true, - }, - table: { - type: { - summary: "GalleryEntry[]", - }, - }, - }, - randomize: { - control: "boolean", - description: - "Will use randomly selected masonry grid sizes instead of following the preset order.", - table: { - type: { - summary: "boolean", - default: false, - }, - }, - }, - }, -}; -export default meta; - -export const Primary: ComponentStoryObj = { - args: { - items: galleryItems, - }, -}; - -export const Randomized: ComponentStoryObj = { - args: { - items: galleryItems, - randomize: true, - }, -}; diff --git a/packages/epo-react-lib/src/layout/MasonryGrid/MasonryGrid.tsx b/packages/epo-react-lib/src/layout/MasonryGrid/MasonryGrid.tsx deleted file mode 100644 index 20d0607d..00000000 --- a/packages/epo-react-lib/src/layout/MasonryGrid/MasonryGrid.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import * as Styled from "./styles"; -import Tile from "./MasonryGridTile"; -import { FunctionComponent } from "react"; -import { GalleryEntry, GalleryItemCategory } from "@/types/gallery"; - -interface MasonryGridProps { - items: GalleryEntry[]; - randomize?: boolean; -} - -const MasonryGrid: FunctionComponent = ({ - items, - randomize, -}) => { - const template = (i: number, width: number) => ` - a:nth-child(${i + 1}n + ${i + 1}) { - width: ${width}%; - } - `; - - const getBrickSizes = () => { - const widthMap = [20, 20, 20, 20, 30, 30, 30, 40, 40, 80]; - let str = ""; - for (let i = 0; i < 20; i++) { - const width = randomize - ? widthMap[Math.floor(Math.random() * widthMap.length)] - : widthMap[i]; - str += template(i, width); - } - return str; - }; - - const checkIfVideo = (cats: GalleryItemCategory[]) => - cats?.[0]?.slug === "video"; - - const brickSizes = getBrickSizes(); - - return ( - - {items.map(({ galleryItemCategory, id, image, title, uri }) => ( - - ))} - - ); -}; - -MasonryGrid.displayName = "Layout.MasonryGrid"; - -export default MasonryGrid; diff --git a/packages/epo-react-lib/src/layout/MasonryGrid/MasonryGridTile.tsx b/packages/epo-react-lib/src/layout/MasonryGrid/MasonryGridTile.tsx deleted file mode 100644 index 87614f8d..00000000 --- a/packages/epo-react-lib/src/layout/MasonryGrid/MasonryGridTile.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import * as Styled from "./styles"; -import Link from "next/link"; -import ResponsiveImage from "@/molecules/ResponsiveImage"; -import IconComposer from "@/svg/IconComposer"; -import { FunctionComponent } from "react"; -import { ImageShape } from "@/types/image"; - -interface TileProps { - image: ImageShape; - isVideo: boolean; - link: string; - title: string; - prefetch?: boolean; -} - -const Tile: FunctionComponent = ({ - image, - isVideo, - link, - title, - prefetch = false, -}) => { - return ( - - - - {isVideo && ( - - - - )} - - - ); -}; - -Tile.displayName = "Layout.MasonryGridTile"; - -export default Tile; diff --git a/packages/epo-react-lib/src/layout/MasonryGrid/index.ts b/packages/epo-react-lib/src/layout/MasonryGrid/index.ts deleted file mode 100644 index 78164e56..00000000 --- a/packages/epo-react-lib/src/layout/MasonryGrid/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from "./MasonryGrid"; diff --git a/packages/epo-react-lib/src/layout/MasonryGrid/styles.ts b/packages/epo-react-lib/src/layout/MasonryGrid/styles.ts deleted file mode 100644 index f66b7b08..00000000 --- a/packages/epo-react-lib/src/layout/MasonryGrid/styles.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { containerRegular } from "@/styles/utils"; -import styled from "styled-components"; - -export const TileLink = styled.a` - position: relative; - transition: filter 0.2s; - &:hover, - &.focus-visible, - &:focus-visible { - img { - filter: invert(25%) sepia(80%) saturate(102%) hue-rotate(130deg) - brightness(100%) contrast(100%); - outline: none; - opacity: 0.7; - } - } -`; - -export const PlayButton = styled.span` - position: absolute; - display: block; - width: 6%; - height: auto; - min-width: 40px; - min-height: 40px; - color: var(--white); - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - - &:hover { - color: var(--neutral15); - } - svg { - width: 100%; - height: 100%; - min-width: 40px; - min-height: 40px; - } -`; - -interface BrickRowProps { - $brickSizes: string; -} - -export const BrickRow = styled.div` - ${containerRegular()} - display: flex; - flex-wrap: wrap; - a { - max-height: 400px; - overflow: hidden; - margin: 0 0.5rem 1rem 0.5rem; - flex: 1 0 auto; - div { - height: 100%; - } - } - ${({ $brickSizes }: BrickRowProps) => $brickSizes} - - @media (max-width: 640px) { - display: block; - && a { - display: block; - width: 100%; - } - } -`; diff --git a/packages/epo-react-lib/src/molecules/MasonryGrid/MasonryGrid.stories.tsx b/packages/epo-react-lib/src/molecules/MasonryGrid/MasonryGrid.stories.tsx new file mode 100644 index 00000000..9526a6d4 --- /dev/null +++ b/packages/epo-react-lib/src/molecules/MasonryGrid/MasonryGrid.stories.tsx @@ -0,0 +1,63 @@ +import { Meta, StoryObj } from "@storybook/react"; + +import MasonryGrid from "."; +import MasonryImage from "../MasonryImage"; + +const images = [ + { + altText: "LSST Camera Arrival", + url: "https://rubin.canto.com/direct/image/otj8rcru8p0cp1kcnd623k4o3m/DGGRP7fxmsNt7UAzGOz9xFZdsiI/m800/800", + width: 7952, + height: 5304, + }, + { + altText: "Rubin's 8.4-meter primary/tertiary mirror is installed", + url: "https://rubin.canto.com/direct/image/r3ej29u8gp5pv9f1tr1po2no1c/QL-a932sO6tIOH9wDRU_gIeRBnw/m800/800", + width: 1600, + height: 900, + }, + { + altText: "Rubin Observatory May 2024", + url: "https://rubin.canto.com/direct/image/4kajmgp41h0fv9igtd4jckrq47/RgLamKnNCU3o077AyLR_Ts4lQuY/m800/800", + width: 7940, + height: 5296, + }, + { + altText: + "The 8-meter Rubin telescope structure is tipped on its side so it look like a metal ring standing vertically. The car-sized commissioning camera is being lifted and installed into the center of the ring.", + url: "https://rubin.canto.com/direct/image/tp2m2lqr0528ncjs7rnn746l7o/DJfQiF9cH9uJHvhoPtgwCypgi2M/m800/800", + width: 7952, + height: 5304, + }, + { + altText: + "The car-sized, black and teal commissioning camera sits on a yellow transport cart on Rubin Observatory's heavy lift elevator platform. The elevator lift is open to the outside and the Chilean desert mountains are visible in the background beneath a blue sky.", + url: "#", + width: 7952, + height: 5304, + }, +]; + +const galleryItems = new Array(50).fill(0).map((val, i) => { + return { + id: i, + image: images[Math.floor(Math.random() * images.length)], + title: `Gallery item ${i + 1}`, + linkProps: { href: "https://rubinobservatory.org" }, + isVideo: i % 6 === 0, + }; +}); + +const meta: Meta = { + component: MasonryGrid, + argTypes: {}, +}; +export default meta; + +const items = galleryItems.map(({ id, ...props }) => { + return { id, element: }; +}); + +export const Primary: StoryObj = { + args: { items }, +}; diff --git a/packages/epo-react-lib/src/layout/MasonryGrid/MasonryGrid.test.tsx b/packages/epo-react-lib/src/molecules/MasonryGrid/MasonryGrid.test.tsx similarity index 58% rename from packages/epo-react-lib/src/layout/MasonryGrid/MasonryGrid.test.tsx rename to packages/epo-react-lib/src/molecules/MasonryGrid/MasonryGrid.test.tsx index af6835e3..df1ae2b9 100644 --- a/packages/epo-react-lib/src/layout/MasonryGrid/MasonryGrid.test.tsx +++ b/packages/epo-react-lib/src/molecules/MasonryGrid/MasonryGrid.test.tsx @@ -1,6 +1,7 @@ import { render, screen } from "@testing-library/react"; import { getGradientImage } from "@/storybook/utilities/helpers"; import MasonryGrid from "."; +import MasonryImage from "../MasonryImage"; const image = { altText: "A placeholder image", @@ -9,17 +10,24 @@ const image = { height: 200, }; -const item = { - galleryItemCategory: [{ id: "a1", slug: "image", title: "Gallery Item 1" }], - id: "a1", - image: [image], +const imageProps = { + image, title: "Gallery Item 1", - uri: "", + linkProps: { href: "https://rubinobservatory.org" }, }; describe("MasonryGrid", () => { it("renders children", () => { - render(); + render( + , + }, + ]} + /> + ); const image = screen.getByRole("img"); const link = screen.getByRole("link"); @@ -29,8 +37,16 @@ describe("MasonryGrid", () => { expect(link).toBeDefined(); }); it("renders videos with play icon", () => { - item.galleryItemCategory[0].slug = "video"; - render(); + render( + , + }, + ]} + /> + ); const svg = screen.getByRole("presentation"); diff --git a/packages/epo-react-lib/src/molecules/MasonryGrid/index.tsx b/packages/epo-react-lib/src/molecules/MasonryGrid/index.tsx new file mode 100644 index 00000000..77b0136e --- /dev/null +++ b/packages/epo-react-lib/src/molecules/MasonryGrid/index.tsx @@ -0,0 +1,39 @@ +import { FunctionComponent, HTMLProps } from "react"; +import * as Styled from "./styles"; + +interface MasonryGridProps { + items: Array<{ id: string | number; element: JSX.Element }>; + /** An integer to start counting from for the list items. */ + start?: number; + reversed?: boolean; + rowHeight?: string; +} + +const MasonryGrid: FunctionComponent = ({ + items, + start, + reversed = false, + rowHeight = "calc(var(--size-spacing-2xl-3xl) * 2)", +}) => { + const isOrderedList = !!start || reversed; + const listProps: HTMLProps | HTMLProps = + isOrderedList + ? { + as: "ol", + start, + reversed, + } + : {}; + + return ( + + {items.map(({ id, element }) => { + return
  • {element}
  • ; + })} +
    + ); +}; + +MasonryGrid.displayName = "Molecule.Masonry.Grid"; + +export default MasonryGrid; diff --git a/packages/epo-react-lib/src/molecules/MasonryGrid/styles.ts b/packages/epo-react-lib/src/molecules/MasonryGrid/styles.ts new file mode 100644 index 00000000..f2bf3ed8 --- /dev/null +++ b/packages/epo-react-lib/src/molecules/MasonryGrid/styles.ts @@ -0,0 +1,49 @@ +import { token } from "@/styles/utils"; +import styled from "styled-components"; + +export const BrickGrid = styled.ul` + --size-spacing-masonry: var(--size-spacing-3xs-2xs, 0.5rem); + + display: flex; + flex-direction: column; + gap: var(--size-spacing-masonry); + list-style: none; + + @media screen and (min-width: ${token("BREAK_PHABLET_MIN")}) { + display: grid; + grid-auto-flow: row dense; + grid-template-columns: repeat(12, 1fr); + grid-auto-rows: var(--size-height-row); + + & > *:nth-child(20n + 1), + & > *:nth-child(20n + 2), + & > *:nth-child(20n + 3), + & > *:nth-child(20n + 8), + & > *:nth-child(20n + 9), + & > *:nth-child(20n + 10), + & > *:nth-child(20n + 16), + & > *:nth-child(20n + 17), + & > *:nth-child(20n + 18) { + grid-column: span 4; + } + & > *:nth-child(20n + 4), + & > *:nth-child(20n + 5), + & > *:nth-child(20n + 6), + & > *:nth-child(20n + 7), + & > *:nth-child(20n + 12), + & > *:nth-child(20n + 13), + & > *:nth-child(20n + 14), + & > *:nth-child(20n + 15) { + grid-column: span 3; + } + & > *:nth-child(20n + 11) { + grid-column: span 12; + grid-row: span 2; + } + & > *:nth-child(20n + 19), + & > *:nth-child(20n + 20) { + grid-column: span 6; + grid-row: span 2; + } + } +`; diff --git a/packages/epo-react-lib/src/molecules/MasonryImage/index.tsx b/packages/epo-react-lib/src/molecules/MasonryImage/index.tsx new file mode 100644 index 00000000..1b9a46c0 --- /dev/null +++ b/packages/epo-react-lib/src/molecules/MasonryImage/index.tsx @@ -0,0 +1,34 @@ +import { FunctionComponent } from "react"; +import { LinkProps } from "next/link"; +import IconComposer from "@/svg/IconComposer"; +import { ImageShape } from "@/types/image"; +import * as Styled from "./styles"; + +interface MasonryImageProps { + image: ImageShape; + linkProps: LinkProps; + isVideo?: boolean; + title: string; +} + +const MasonryImage: FunctionComponent = ({ + image, + title, + isVideo = false, + linkProps, +}) => { + return ( + + + {isVideo && ( + + + + )} + + ); +}; + +MasonryImage.displayName = "Molecule.Masonry.Image"; + +export default MasonryImage; diff --git a/packages/epo-react-lib/src/molecules/MasonryImage/styles.ts b/packages/epo-react-lib/src/molecules/MasonryImage/styles.ts new file mode 100644 index 00000000..7ad3276f --- /dev/null +++ b/packages/epo-react-lib/src/molecules/MasonryImage/styles.ts @@ -0,0 +1,51 @@ +import Image from "@/atomic/Image"; +import Link from "next/link"; +import styled from "styled-components"; + +export const TileLink = styled(Link)` + overflow: hidden; + display: flex; + justify-content: center; + align-items: center; + width: 100%; + height: 100%; + position: relative; + text-decoration: none; + + &:hover, + &.focus-visible, + &:focus-visible { + --color-play-icon: var(--neutral15); + & > img { + filter: invert(25%) sepia(80%) saturate(102%) hue-rotate(130deg) + brightness(100%) contrast(100%); + outline: none; + opacity: 0.7; + + @media (prefers-reduced-motion: no-preference) { + transform: scale(1.01); + } + } + } +`; + +export const TileImage = styled(Image)` + background-color: var(--color-background-tile-light); + inline-size: 100%; + block-size: 100%; + object-fit: cover; + object-position: center; + text-decoration: none; + transition: filter 0.2s; + + @media (prefers-reduced-motion: no-preference) { + transition: filter 0.2s, transform 0.2s; + } +`; + +export const PlayButton = styled.span` + position: absolute; + display: block; + color: var(--color-play-icon, var(--color-text-button-primary)); + transition: color 0.2s; +`; diff --git a/packages/epo-react-lib/src/molecules/ResponsiveImage/index.tsx b/packages/epo-react-lib/src/molecules/ResponsiveImage/index.tsx index 33578b57..267923d1 100644 --- a/packages/epo-react-lib/src/molecules/ResponsiveImage/index.tsx +++ b/packages/epo-react-lib/src/molecules/ResponsiveImage/index.tsx @@ -6,7 +6,7 @@ import Image from "@/atomic/Image"; interface ResponsiveImageProps extends ImageProps { /** @deprecated use `aspectRatio` instead */ ratio?: string; - aspectRatio: string; + aspectRatio?: string; position?: string; zoom?: number; }