diff --git a/src/atoms/box/index.tsx b/src/atoms/box/index.tsx new file mode 100644 index 0000000..4c11b9a --- /dev/null +++ b/src/atoms/box/index.tsx @@ -0,0 +1,44 @@ +import { css } from "@emotion/react"; +import styled from "@emotion/styled"; +import React, { CSSProperties, FC } from "react"; + +const StyledBox = styled.div<{ + style?: CSSProperties & { "--aspect-ratio"?: number }; + roundCorners?: boolean; +}>` + position: relative; + width: 100%; + height: 0; + padding-bottom: calc(100% / var(--aspect-ratio, 1)); + ${({ theme, roundCorners }) => + roundCorners && + css` + border-radius: ${theme.shapes.m}; + overflow: hidden; + `}; +`; + +const StyledBoxInner = styled.div` + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + width: 100%; + height: 100%; +`; + +export interface BoxProps { + aspectRatio?: number; + roundCorners?: boolean; +} + +const Box: FC = ({ aspectRatio = 1, roundCorners, children, ...props }) => { + return ( + + {children} + + ); +}; + +export default Box; diff --git a/src/molecules/masonry/column.tsx b/src/molecules/masonry/column.tsx new file mode 100644 index 0000000..29aac16 --- /dev/null +++ b/src/molecules/masonry/column.tsx @@ -0,0 +1,123 @@ +import { StyledMasonryBox } from "@/molecules/masonry/styled"; +import React, { CSSProperties, FC, RefObject, useEffect, useRef, useState } from "react"; + +export const closed = {}; + +export const getOpen = (column: number, row: number) => ({ + gridColumnStart: column, + gridRowStart: row, + gridColumnEnd: "span 2", + gridRowEnd: "span 2", +}); + +export const useMasonryColumn = ( + ref: RefObject, + isOpen: boolean, + { rowBig = "2", columnBig = "2" } +) => { + const [style, setStyle] = useState(closed); + useEffect(() => { + const handleLayout = () => { + if (isOpen && ref.current) { + const { current: element } = ref; + const { parentElement } = element; + const parentComputedStyle = window.getComputedStyle(parentElement); + const boundingClientRect = element.getBoundingClientRect(); + const parentPoundingClientRect = parentElement.getBoundingClientRect(); + + // Columns + const gridColumns = parentComputedStyle.gridTemplateColumns.split(" "); + const { length: numberColumns } = gridColumns; + + // Rows + const gridRows = parentComputedStyle.gridTemplateRows.split(" "); + const { length: numberRows } = gridRows; + // Positions + const positionX = boundingClientRect.left - parentPoundingClientRect.left; + const positionY = boundingClientRect.top - parentPoundingClientRect.top; + // Dimensions + const gridRowHeight = + Number.parseFloat(gridRows[0]) + + Number.parseFloat(parentComputedStyle.gridRowGap); + const gridColumnWidth = + Number.parseFloat(gridColumns[0]) + + Number.parseFloat(parentComputedStyle.gridColumnGap); + + // Test next position + element.style.gridColumnStart = ""; + element.style.gridRowStart = ""; + element.style.gridColumnEnd = `span ${columnBig}`; + element.style.gridRowEnd = `span ${rowBig}`; + + const computedStyle = window.getComputedStyle(element); + + const width = Number.parseFloat(computedStyle.gridColumnEnd.split(" ")[1]); + const height = Number.parseFloat(computedStyle.gridRowEnd.split(" ")[1]); + + // Get next position + let row = Math.round(positionY / gridRowHeight) + 1; + let column = Math.round(positionX / gridColumnWidth) + 1; + + if (row + height > numberRows) { + row = numberRows - height + 1; + } + + if (column + width > numberColumns) { + column = numberColumns - width + 1; + } + + setStyle({ + gridColumnStart: column, + gridRowStart: row, + gridColumnEnd: `span ${columnBig}`, + gridRowEnd: `span ${rowBig}`, + }); + } else { + setStyle(closed); + } + }; + + handleLayout(); + window.addEventListener("resize", handleLayout, { passive: true }); + return () => { + window.removeEventListener("resize", handleLayout); + }; + }, [columnBig, isOpen, ref, rowBig]); + return style; +}; + +export interface MasonryColumnProps { + colSpan?: number | string; + rowSpan?: number | string; + isOpen?: boolean; + onClick?(): void; +} + +const MasonryColumn: FC = ({ + children, + onClick, + rowSpan, + colSpan, + isOpen, + ...props +}) => { + const ref = useRef(null); + const style = useMasonryColumn(ref, isOpen, { + rowBig: `calc(${rowSpan} * 2)`, + columnBig: `calc(${colSpan} * 2)`, + }); + return ( + + {children} + + ); +}; + +export default MasonryColumn; diff --git a/src/molecules/masonry/grid.tsx b/src/molecules/masonry/grid.tsx new file mode 100644 index 0000000..0eac0f0 --- /dev/null +++ b/src/molecules/masonry/grid.tsx @@ -0,0 +1,33 @@ +import { StyledMasonryGrid } from "@/molecules/masonry/styled"; +import React, { FC } from "react"; + +export interface MasonryGridProps { + colCountXS?: number; + colCountS?: number; + colCountM?: number; + colCountL?: number; +} +const MasonryGrid: FC = ({ + colCountXS = "var(--col-span)", + colCountS = colCountXS, + colCountM = colCountS, + colCountL = colCountM, + children, + ...props +}) => { + return ( + + {children} + + ); +}; + +export default MasonryGrid; diff --git a/src/molecules/masonry/styled.ts b/src/molecules/masonry/styled.ts new file mode 100644 index 0000000..917fc61 --- /dev/null +++ b/src/molecules/masonry/styled.ts @@ -0,0 +1,54 @@ +import { css } from "@emotion/react"; +import styled from "@emotion/styled"; +import { CSSProperties } from "react"; + +export const StyledMasonryGrid = styled.div<{ + style?: CSSProperties & { + "--col-count-xs"?: number | string; + "--col-count-s"?: number | string; + "--col-count-m"?: number | string; + "--col-count-l"?: number | string; + }; +}>` + --col-count: var(--col-count-xs); + + display: grid; + grid-auto-flow: dense; + grid-auto-rows: var(--gap-x); + grid-gap: var(--gap-x); + grid-template-columns: repeat(var(--col-count), 1fr); + ${({ theme }) => css` + ${theme.mq.s} { + --col-count: var(--col-count-s); + } + ${theme.mq.m} { + --col-count: var(--col-count-m); + } + ${theme.mq.l} { + --col-count: var(--col-count-l); + } + `}; +`; + +export const StyledMasonryBox = styled.div<{ + colSpan?: number | string; + rowSpan?: number | string; +}>` + --col-span: var(--col-count); + --row-span: 1; + + ${({ theme, colSpan, rowSpan }) => css` + ${colSpan && + css` + grid-column-end: span var(--col-span, 1); + `}; + ${rowSpan && + css` + grid-row-end: span var(--row-span, 1); + `}; + ${theme.mq.s} { + --col-span: ${colSpan}; + --row-span: ${rowSpan}; + } + `}; +`; diff --git a/src/pages/design-system/masonry/index.tsx b/src/pages/design-system/masonry/index.tsx new file mode 100644 index 0000000..842aa16 --- /dev/null +++ b/src/pages/design-system/masonry/index.tsx @@ -0,0 +1,22 @@ +import { addApolloState, initializeApollo } from "@/ions/services/apollo/client"; +import Examples from "@/templates/design-system/pages/masonry"; +import { PageProps, StaticPageProps } from "@/types"; +import { GetStaticProps, NextPage } from "next"; +import { serverSideTranslations } from "next-i18next/serverSideTranslations"; +import React from "react"; + +const Page: NextPage = () => { + return ; +}; + +export const getStaticProps: GetStaticProps = async context => { + const apolloClient = initializeApollo(); + return addApolloState(apolloClient, { + props: { + ...(await serverSideTranslations(context.locale)), + locale: context.locale, + }, + }); +}; + +export default Page; diff --git a/src/templates/design-system/index.tsx b/src/templates/design-system/index.tsx index 2448db2..a519827 100644 --- a/src/templates/design-system/index.tsx +++ b/src/templates/design-system/index.tsx @@ -42,6 +42,9 @@ const DesignSystem = () => {
  • Grid
  • +
  • + Masonry +
  • Snackbar
  • diff --git a/src/templates/design-system/pages/masonry.tsx b/src/templates/design-system/pages/masonry.tsx new file mode 100644 index 0000000..d830c0c --- /dev/null +++ b/src/templates/design-system/pages/masonry.tsx @@ -0,0 +1,107 @@ +import Typography from "@/atoms/typography"; +import Layout from "@/groups/layout"; +import { RawBreadcrumb } from "@/ions/contexts/breadcrumbs/types"; +import { pxToRem } from "@/ions/utils/unit"; +import { Column, Grid } from "@/molecules/grid"; +import MasonryColumn from "@/molecules/masonry/column"; +import MasonryGrid from "@/molecules/masonry/grid"; +import Breadcrumbs from "@/organisms/breadcrumbs"; +import OverlayGrid from "@/organisms/grid-overlay"; +import { css } from "@emotion/react"; +import styled from "@emotion/styled"; +import { useTranslation } from "next-i18next"; +import process from "process"; +import React, { useMemo, useState } from "react"; + +const aspectRatios = [ + { col: 1, row: 2 }, + { col: 2, row: 4 }, + { col: 2, row: 6 }, + { col: 1, row: 5 }, + { col: 2, row: 7 }, + { col: 2, row: 5 }, +]; +const demoItems = Array.from({ length: 32 }).map((item, index) => ({ + id: index + 1, + color: `hsl(${index * 129}, 60%, 80%)`, + aspectRatio: + aspectRatios[ + (((((index + ((((index % 15) % 12) % 9) % 5)) % 17) % 15) % 14) % 11) % + aspectRatios.length + ], +})); + +const StyledColoredBox = styled.div` + display: flex; + align-content: center; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; + font-size: 2em; + font-weight: 600; + ${({ theme }) => css` + padding: ${pxToRem(theme.spaces.l)} ${pxToRem(theme.spaces.xs)}; + border-radius: ${theme.shapes.s}; + box-shadow: ${theme.shadows.s}; + `} +`; + +const MasonryExamples = () => { + const { t } = useTranslation(["navigation"]); + const [open, setOpen] = useState(-1); + const breadcrumbs: RawBreadcrumb[] = useMemo( + () => [ + { + href: "/", + title: t("navigation:home"), + }, + { + href: "/design-system", + title: "Design System", + }, + { + href: "/design-system/masonry", + title: "Masonry", + }, + ], + [t] + ); + return ( + + + + + Masonry + + + + {demoItems.map((item, index) => { + const isOpen = open === index; + return ( + { + setOpen(previousState => + previousState === index ? -1 : index + ); + }} + > + + {index + 1} + + + ); + })} + + + + {process.env.NODE_ENV === "production" && } + + ); +}; + +export default MasonryExamples;