diff --git a/README.md b/README.md index dfefa8ff..e43af93f 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@ -# [Giving Campaign (Stanford On Purpose)](https://github.com/SU-SWS/ood-giving-campaign) +# [Stanford Momentum](https://github.com/SU-SWS/ood-giving-campaign) [![Netlify Status](https://api.netlify.com/api/v1/badges/738e5599-7329-41a1-8429-82f8540636d9/deploy-status?branch=dev)](https://app.netlify.com/sites/giving-campaign/deploys) Description --- -Netlify hosted, Next.js built, Storyblok headless CMS site for the Stanford On Purpose website. +Netlify hosted, Next.js built, Storyblok headless CMS site for the Stanford Momentum website. Environment variable set up and installation --- diff --git a/app/(editor)/editor/page.tsx b/app/(editor)/editor/page.tsx index 42ec3154..caf305fa 100644 --- a/app/(editor)/editor/page.tsx +++ b/app/(editor)/editor/page.tsx @@ -38,7 +38,7 @@ const bridgeOptions = { * Init on the server. */ storyblokInit({ - accessToken: process.env.STORYBLOK_ACCESS_TOKEN, // Preview token because this is in server side. + accessToken: process.env.STORYBLOK_PREVIEW_EDITOR_TOKEN, // Preview token because this is in server side. use: [apiPlugin], apiOptions: { region: 'us', @@ -89,17 +89,14 @@ async function getStoryData({ path }: PageProps['searchParams']): Promise { - const validationString = searchParams['_storyblok_tk[space_id]'] + ':' + process.env.STORYBLOK_ACCESS_TOKEN + ':' + searchParams['_storyblok_tk[timestamp]']; + const validationString = searchParams['_storyblok_tk[space_id]'] + ':' + process.env.STORYBLOK_PREVIEW_EDITOR_TOKEN + ':' + searchParams['_storyblok_tk[timestamp]']; const validationToken = crypto.createHash('sha1').update(validationString).digest('hex'); - if (searchParams['_storyblok_tk[token]'] == validationToken && - Number(searchParams['_storyblok_tk[timestamp]']) > Math.floor(Date.now()/1000)-3600) { - // you're in the edit mode. + if (searchParams['_storyblok_tk[token]'] == validationToken) { + //You're in the edit mode. return true; } // Something didn't work out. diff --git a/app/robots.ts b/app/robots.ts index 9a01b816..55b669ec 100644 --- a/app/robots.ts +++ b/app/robots.ts @@ -1,7 +1,7 @@ import { MetadataRoute } from 'next'; export default function robots(): MetadataRoute.Robots { - const CurrentURL = process.env.DEPLOY_PRIME_URL || 'http://localhost:3000'; + const CurrentURL = process.env.URL || process.env.DEPLOY_PRIME_URL || 'https://momentum.stanford.edu'; return { rules: { userAgent: '*', @@ -9,4 +9,4 @@ export default function robots(): MetadataRoute.Robots { }, sitemap: CurrentURL + '/sitemap.xml', }; -} \ No newline at end of file +} diff --git a/components/BlurryPoster/BlurryPoster.styles.tsx b/components/BlurryPoster/BlurryPoster.styles.tsx index cc17dabb..5510998e 100644 --- a/components/BlurryPoster/BlurryPoster.styles.tsx +++ b/components/BlurryPoster/BlurryPoster.styles.tsx @@ -2,7 +2,7 @@ import { cnb } from 'cnbuilder'; export const root = 'relative bg-no-repeat bg-cover overflow-hidden break-words'; -export const bgImage = 'absolute top-0 left-0 w-full h-full object-cover'; +export const bgImage = 'absolute top-0 left-0 size-full object-cover'; export const blurWrapper = ( addBgBlur?: boolean, @@ -10,7 +10,7 @@ export const blurWrapper = ( type?: 'hero' | 'poster', bgColor?: 'black' | 'white', ) => cnb( - 'relative w-full h-full z-10', { + 'relative size-full z-10', { 'backdrop-blur-md' : addBgBlur, 'lg:from-black-true/20 lg:to-black-true/70': type === 'poster' && addDarkOverlay && bgColor === 'black', 'lg:bg-none': type === 'poster' && bgColor === 'black' && !addDarkOverlay, @@ -34,7 +34,7 @@ export const contentWrapper = ( 'lg:order-first': !imageOnLeft && isTwoCol, }); -export const superhead = (imageOnLeft?: boolean, isTwoCol?: boolean) => cnb('cc rs-mb-1 w-full', { +export const superhead = (imageOnLeft?: boolean, isTwoCol?: boolean) => cnb('cc rs-mb-1 w-full text-shadow-sm', { 'lg:pl-40 2xl:pl-60 3xl:pr-[calc(100%-750px)]': imageOnLeft && isTwoCol, 'lg:pr-40 2xl:pr-60 3xl:pl-[calc(100%-750px)]': !imageOnLeft && isTwoCol, }); @@ -92,7 +92,7 @@ export const bodyWrapper = (imageOnLeft?: boolean, isTwoCol?: boolean) => cnb('c '3xl:pl-[calc(100%-750px)] lg:pr-40 2xl:pr-60': !imageOnLeft && isTwoCol, '*:max-w-[50ch]': !isTwoCol, }); - +export const dek = 'text-shadow-sm'; export const cta = 'rs-mt-4'; export const imageWrapper = (imageOnLeft?: boolean, isTwoCol?: boolean, hasImage?: boolean) => cnb('w-full cc', { diff --git a/components/BlurryPoster/BlurryPoster.tsx b/components/BlurryPoster/BlurryPoster.tsx index f856e1b5..39137274 100644 --- a/components/BlurryPoster/BlurryPoster.tsx +++ b/components/BlurryPoster/BlurryPoster.tsx @@ -93,42 +93,44 @@ export const BlurryPoster = ({ return ( - - - - - - {bgImageAlt - + {bgImageSrc && ( + + + + + + {bgImageAlt + + )}
{body} )} - {/* No authors and published dates for MVP */} - {/* {byline && ( - {byline} - )} - {date && ( - - )} */} {cta && (
{cta} diff --git a/components/Bookshelf/Book.tsx b/components/Bookshelf/Book.tsx index 828474d8..10ebccdf 100644 --- a/components/Bookshelf/Book.tsx +++ b/components/Bookshelf/Book.tsx @@ -38,7 +38,7 @@ export const Book = ({ // animate={{ backgroundColor: isOpen ? '#FF0088' : '#0055FF' }} className={cnb('group relative transition-colors mr-4 w-120 flex justify-center align-start shrink-0 rounded hocus-visible:underline decoration-white', buttonClassName)} > -
+
cnb( export const imageAspectRatio = 'aspect-w-6 aspect-h-5'; export const image = 'object-cover group-hocus-within:scale-105 transition-transform'; export const imageOverlay = (textOnLeft: boolean) => cnb( - 'hidden sm:block from-black-true/50 via-black-true/20 to-transparent via-20% sm:absolute w-full h-full sm:top-0 sm:left-0', + 'hidden sm:block from-black-true/50 via-black-true/20 to-transparent via-20% sm:absolute size-full sm:top-0 sm:left-0', textOnLeft ? 'bg-gradient-to-r' : 'bg-gradient-to-l', ); diff --git a/components/ChangemakerCard/ChangemakerCard.styles.ts b/components/ChangemakerCard/ChangemakerCard.styles.ts index a457fbda..11e74307 100644 --- a/components/ChangemakerCard/ChangemakerCard.styles.ts +++ b/components/ChangemakerCard/ChangemakerCard.styles.ts @@ -1,12 +1,12 @@ export const root = 'relative w-full max-w-[29rem] sm:max-w-300 lg:max-w-[35rem] mx-auto break-words'; -export const cardInner = 'relative w-full h-full aspect-w-1 aspect-h-2'; +export const cardInner = 'relative size-full aspect-w-1 aspect-h-2'; -export const cardFront = 'absolute w-full h-full top-0 left-0'; +export const cardFront = 'absolute size-full top-0 left-0'; export const imageWrapper = 'overflow-hidden aspect-w-1 aspect-h-2'; -export const info = 'rs-px-1 pb-150 absolute w-full h-full bottom-0 left-0 mb-0'; +export const info = 'rs-px-1 pb-150 absolute size-full bottom-0 left-0 mb-0'; export const heading = 'mb-02em mt-auto'; -export const cardContent = 'absolute w-full h-full top-0 left-0 px-20 py-30 3xl:py-48 3xl:px-36 aria-hidden:opacity-0 opacity-100 backdrop-blur-sm transition-opacity duration-500 bg-gradient-to-b from-gc-black/60 to-gc-black/90 gc-changemaker *:*:*:!mb-1em'; +export const cardContent = 'absolute size-full top-0 left-0 px-20 py-30 3xl:py-48 3xl:px-36 aria-hidden:opacity-0 opacity-100 backdrop-blur-sm transition-opacity duration-500 bg-gradient-to-b from-gc-black/60 to-gc-black/90 gc-changemaker *:*:*:!mb-1em'; -export const button = 'group absolute w-full h-full top-0 left-0'; +export const button = 'group absolute size-full top-0 left-0'; export const icon = 'absolute bottom-40 right-36 text-white w-65 h-65 border-2 border-white rounded-full p-16 group-hocus-visible:border-dashed group-aria-expanded:rotate-45 transition-transform'; diff --git a/components/ChangemakerCard/ChangemakerCard.tsx b/components/ChangemakerCard/ChangemakerCard.tsx index d4ad81c7..5138ca98 100644 --- a/components/ChangemakerCard/ChangemakerCard.tsx +++ b/components/ChangemakerCard/ChangemakerCard.tsx @@ -97,13 +97,14 @@ export const ChangemakerCard = ({ size={2} leading="tight" align="center" + color="white" className={styles.heading} > {heading} )} {subheading && ( - {subheading} + {subheading} )}
diff --git a/components/Cta/Cta.styles.ts b/components/Cta/Cta.styles.ts index 700ee1a0..f2bbb8ab 100644 --- a/components/Cta/Cta.styles.ts +++ b/components/Cta/Cta.styles.ts @@ -131,6 +131,7 @@ export const ctaIconMap: CtaIconMapType = { }; export const iconAnimation = { + none: '', 'top-right': 'group-hocus:translate-x-01em group-hocus:-translate-y-01em', down: 'group-hocus:translate-y-02em', up: 'group-hocus:-translate-y-02em', @@ -152,6 +153,9 @@ export const iconLeftMarginDefault = 'ml-03em'; export const iconLeftMargin: CtaIconLeftMarginType = { 'arrow-right': 'ml-04em', back: 'ml-04em', + email: 'ml-05em', + external: 'ml-04em', + link: 'ml-05em', 'triangle-right': 'ml-04em', 'triangle-down': 'ml-04em', 'triangle-up': 'ml-04em', diff --git a/components/Cta/Cta.types.ts b/components/Cta/Cta.types.ts index ecdf98eb..ab3162ea 100644 --- a/components/Cta/Cta.types.ts +++ b/components/Cta/Cta.types.ts @@ -40,6 +40,6 @@ export interface CtaCommonProps { icon?: IconType; iconPosition?: 'left' | 'right'; animate?: IconAnimationType; - iconProps?: HeroIconProps & React.ComponentProps<'svg'>; + iconProps?: Omit & React.ComponentProps<'svg'>; children?: React.ReactNode; } diff --git a/components/DataCard/DataCard.styles.ts b/components/DataCard/DataCard.styles.ts new file mode 100644 index 00000000..168d3002 --- /dev/null +++ b/components/DataCard/DataCard.styles.ts @@ -0,0 +1,16 @@ +import { cnb } from 'cnbuilder'; + +export const animateWrapper = 'h-full'; +// Use border-black-50/50 which works well on both light and dark backgrounds +export const root = 'relative overflow-hidden size-full break-words border-l-2 border-black-50/50'; + +export const flex = 'h-full'; +export const content = ( + hasBarColor?: boolean, +) => cnb('rs-pl-2', { + 'border-l-[1.4rem] md:border-l-[2rem]': hasBarColor, +}); + +export const heading = 'rs-mb-3 ml-22 whitespace-pre-line mt-auto'; +export const body = '*:*:leading-snug'; +export const cta = 'rs-mt-2'; diff --git a/components/DataCard/DataCard.tsx b/components/DataCard/DataCard.tsx new file mode 100644 index 00000000..2cd842ad --- /dev/null +++ b/components/DataCard/DataCard.tsx @@ -0,0 +1,93 @@ +import { cnb } from 'cnbuilder'; +import { AnimateInView, type AnimationType } from '@/components/Animate'; +import { NumberCounter } from '@/components/NumberCounter'; +import { Container } from '@/components/Container'; +import { FlexBox } from '@/components/FlexBox'; +import { Heading, type HeadingType } from '../Typography'; +import { accentBorderColors, type AccentBorderColorType, type PaddingType } from '@/utilities/datasource'; +import { splitNumberString } from '@/utilities/splitNumberString'; +import * as styles from './DataCard.styles'; + +export type DataCardProps = React.HTMLAttributes & { + heading?: string; + headingLevel?: HeadingType; + isDarkTheme?: boolean; + barColor?: AccentBorderColorType; + body?: React.ReactNode; + paddingTop?: PaddingType; + cta?: React.ReactNode; + isCounter?: boolean; + // In number of seconds + counterDuration?: number; + animation?: AnimationType; + delay?: number; +}; + +export const DataCard = ({ + heading, + headingLevel = 'h3', + barColor, + body, + cta, + paddingTop, + isDarkTheme, + isCounter, + counterDuration, + animation = 'slideUp', + delay, + children, + className, + ...props +}: DataCardProps) => { + const headingProcessed = isCounter ? splitNumberString(heading) : undefined; + + return ( + + + + {/* If number counter is enabled, aria-hidden the animated heading and add a SR only heading */} + {isCounter && heading && ( + {heading} + )} + {heading && ( + + {isCounter ? ( + <> + {headingProcessed?.beforeNumber} + + {headingProcessed?.afterNumber} + + ) : ( + heading + )} + + )} +
+
+ {body} +
+ {!!cta && ( +
+ {cta} +
+ )} +
+
+
+
+ ); +}; diff --git a/components/DataCard/index.ts b/components/DataCard/index.ts new file mode 100644 index 00000000..e8082810 --- /dev/null +++ b/components/DataCard/index.ts @@ -0,0 +1 @@ +export * from './DataCard'; diff --git a/components/Embed/Embed.tsx b/components/Embed/Embed.tsx new file mode 100644 index 00000000..a3518e21 --- /dev/null +++ b/components/Embed/Embed.tsx @@ -0,0 +1,89 @@ +/** + * A NextJS Embed Component + * + * Credit where credit is deserved. + * @see: https://github.com/christo-pr/dangerously-set-html-content + * + * Use this widget with caution. There are no safeguards on what it can do. It + * is also not good practice to inject and manipulate the page outside of + * REACT as that can lead to irregularities and troubles. + */ +import React, { useRef, useEffect } from 'react'; +import { WidthBox, type WidthType } from '../WidthBox'; +import { type PaddingType } from '@/utilities/datasource'; + +export interface EmbedProps extends React.HTMLAttributes { + id?: string; + src?: string; + content?: string; + boundingWidth?: 'site' | 'full'; + width?: WidthType; + spacingTop?: PaddingType; + spacingBottom?: PaddingType; + className?: string; +} + +const Embed = ({ +id, src, content, boundingWidth, spacingTop, spacingBottom, className, width, ...props +}:EmbedProps) => { + + const myEmbed = useRef(null); + + useEffect(() => { + // If there is no content or src, return. + if (!content && !src) return; + const domElement = myEmbed.current; + // Clear the container. + domElement.innerHTML = ''; + + if (src.length > 1) { + // Create a script tag. + const script = document.createElement('script'); + // Set the src to the src provided. + script.src = src; + domElement.appendChild(script); + } + + if (content.length > 1) { + // Create a 'tiny' document and parse the html string. + // https://developer.mozilla.org/en-US/docs/Web/API/DocumentFragment + const miniDom = document.createRange().createContextualFragment(content); + // Force the scripts in the embed script field to load sync. + const scripts = miniDom.querySelectorAll('script'); + if (scripts.length >= 1) { + for (const item of scripts) { + if (item.src && item.src.length > 1) { + item.async = false; + item.defer = false; + } + } + } + // Append the new content. + domElement.appendChild(miniDom); + } + + return () => { + // Clean up the script tag. + domElement.innerHTML = ''; + }; + }, [content, src]); + + if (content) { + return ( + +
+ + ); + } + + return (

You must provide either an src or content to the embed

); +}; + +export default Embed; diff --git a/components/Embed/index.ts b/components/Embed/index.ts new file mode 100644 index 00000000..0effca01 --- /dev/null +++ b/components/Embed/index.ts @@ -0,0 +1,2 @@ +import Embed from './Embed'; +export { Embed }; diff --git a/components/FeaturedStories/FeatureMasonry.tsx b/components/FeaturedStories/FeatureMasonry.tsx index 7d195be0..3b244910 100644 --- a/components/FeaturedStories/FeatureMasonry.tsx +++ b/components/FeaturedStories/FeatureMasonry.tsx @@ -53,7 +53,7 @@ export const FeatureMasonry = ({ )} overlay="black-10" /> -
+
-
+
{imageAlt1
cnb('absolute top-0 left-0 size-full z-10', hasBgGradient ? 'bg-gradient-to-b via-50%' : ''); + +export const contentWrapper = '*:mx-auto'; +export const superhead = 'relative z-10 xl:max-w-900 mx-auto rs-mb-0 text-shadow-sm'; +export const heading = (isDrukHeading: boolean, isSmallHeading: boolean) => cnb('relative z-10 max-w-1200 mx-auto mb-0 text-balance', { + 'fluid-type-7 md:fluid-type-8': isDrukHeading && isSmallHeading, + 'fluid-type-7 md:gc-splash': isDrukHeading && !isSmallHeading, + 'fluid-type-6 md:fluid-type-7': !isDrukHeading && isSmallHeading, + 'fluid-type-6 md:fluid-type-8': !isDrukHeading && !isSmallHeading, +}); +export const subhead = 'relative z-10 xl:max-w-900 mx-auto rs-mt-1 text-shadow-sm'; +export const content = 'rs-mt-4 w-fit'; diff --git a/components/Hero/BasicHero.tsx b/components/Hero/BasicHero.tsx new file mode 100644 index 00000000..24e11a79 --- /dev/null +++ b/components/Hero/BasicHero.tsx @@ -0,0 +1,154 @@ +import { cnb } from 'cnbuilder'; +import { Container } from '@/components/Container'; +import { Heading, SrOnlyText, Text } from '@/components/Typography'; +import { ImageOverlay } from '@/components/ImageOverlay'; +import { getProcessedImage } from '@/utilities/getProcessedImage'; +import { + gradientFroms, + type GradientFromType, + gradientTos, + type GradientToType, + gradientVias, + type GradientViaType, + bgBlurs, + type BgBlurType, +} from '@/utilities/datasource'; +import * as styles from './BasicHero.styles'; + +/** + * Basic page hero that allows for different hero styles (e.g., initiative landing and detailed pages) + */ +type BasicHeroProps = { + title: string; + isDrukHeading?: boolean; + isSmallHeading?: boolean; + superhead?: string; + subheading?: string; + imageSrc?: string; + imageFocus?: string; + gradientTop?: GradientToType; + gradientBottom?: GradientFromType; + gradientVia?: GradientViaType; + bgBlur?: BgBlurType; + heroContent?: React.ReactNode; + paddingType?: styles.HeroPaddingType; +}; + +export const BasicHero = ({ + title, + isDrukHeading, + isSmallHeading, + superhead, + subheading, + imageSrc, + imageFocus, + gradientTop, + gradientBottom, + gradientVia, + bgBlur, + heroContent, + paddingType, +}: BasicHeroProps) => { + // To render a dark overlay, both a top and bottom gradient color must be selected + const hasBgGradient = !!gradientTop && !!gradientBottom; + const hasBgBlur = !!bgBlur && bgBlur !== 'none'; + + return ( + + {!!imageSrc && ( + + + + + + + + )} + {!!imageSrc && (hasBgBlur || hasBgGradient) && ( +
+ )} + + {superhead && ( + + {superhead} + + )} + + {superhead && {`${superhead}: `}}{title} + + {subheading && ( + + {subheading} + + )} + {!!heroContent && ( +
+ {heroContent} +
+ )} +
+ + ); +}; diff --git a/components/Hero/BasicHeroMvp.tsx b/components/Hero/BasicHeroMvp.tsx deleted file mode 100644 index f51ac484..00000000 --- a/components/Hero/BasicHeroMvp.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import { Container } from '@/components/Container'; -import { Heading, Text } from '@/components/Typography'; -import { ImageOverlay } from '@/components/ImageOverlay'; -import { getProcessedImage } from '@/utilities/getProcessedImage'; -import { subhead } from '../Scrollytelling'; - -/** - * Temporary hero for MVP basic page - */ -type BasicHeroMvpProps = { - title: string; - subheading?: string; - imageSrc?: string; - imageFocus?: string; - heroContent?: React.ReactNode; -}; - -export const BasicHeroMvp = ({ - title, - subheading, - imageSrc, - imageFocus, - heroContent, -}: BasicHeroMvpProps) => ( - - {imageSrc && ( - <> - - - - )} - - - {title} - - {subheading && ( - - {subheading} - - )} - {!!heroContent && ( -
- {heroContent} -
- )} -
-
-); diff --git a/components/Hero/StoryHero.styles.ts b/components/Hero/StoryHero.styles.ts index deb5175a..a43e88a3 100644 --- a/components/Hero/StoryHero.styles.ts +++ b/components/Hero/StoryHero.styles.ts @@ -85,10 +85,10 @@ export const imageWrapper = (isVerticalHero: boolean, isLeftImage: boolean) => c }); export const image = (renderTwoImages: boolean) => cnb( - 'w-full h-full', + 'size-full', renderTwoImages ? 'hidden lg:block' : '', ); -export const mobileImage = 'w-full h-full lg:hidden'; +export const mobileImage = 'size-full lg:hidden'; export const caption = (isVerticalHero: boolean, isLeftImage: boolean) => cnb('text-current rs-mt-0 cc type-0', { 'lg:pr-0': isVerticalHero && isLeftImage, diff --git a/components/Hero/StoryHeroMvp.styles.ts b/components/Hero/StoryHeroMvp.styles.ts index 8f778611..147c2407 100644 --- a/components/Hero/StoryHeroMvp.styles.ts +++ b/components/Hero/StoryHeroMvp.styles.ts @@ -1,30 +1,3 @@ -import { cnb } from 'cnbuilder'; - export const root = 'relative'; - -export const imageCrops = { - '1x1': '1200x1200', - '2x1': '2000x1000', - '3x2': '2100x1400', - '5x8': '1000x1600', - '16x9': '2000x1125', - 'free': '2000x0', -}; -export type ImageCropType = keyof typeof imageCrops; - -export const mobileImageCrops = { - '1x1': '1000x1000', - '2x1': '1000x500', - '3x2': '1200x800', - '5x8': '1000x1600', - '16x9': '1600x900', - 'free': '1000x0', -}; - -export const image = (renderTwoImages: boolean) => cnb( - 'w-full h-full', - renderTwoImages ? 'hidden lg:block' : '', -); -export const mobileImage = 'w-full h-full lg:hidden'; export const captionWrapper = 'mt-06em'; export const caption = 'caption *:leading-display mt-08em max-w-prose-wide'; diff --git a/components/Hero/StoryHeroMvp.tsx b/components/Hero/StoryHeroMvp.tsx index e41b5d6a..fdb593d7 100644 --- a/components/Hero/StoryHeroMvp.tsx +++ b/components/Hero/StoryHeroMvp.tsx @@ -3,12 +3,13 @@ import { Container } from '@/components/Container'; import { BlurryPoster } from '@/components/BlurryPoster'; import { CreateBloks } from '@/components/CreateBloks'; import { RichText } from '@/components/RichText'; -import { type SbImageType, type SbTypographyProps } from '@/components/Storyblok/Storyblok.types'; +import { StoryHeroStacked } from '@/components/Hero/StoryHeroStacked'; +import { type SbImageType, type SbTypographyProps, type SbColorPickerType } from '@/components/Storyblok/Storyblok.types'; import { type SbBlokData } from '@storyblok/react/rsc'; import { paletteAccentColors, type PaletteAccentHexColorType } from '@/utilities/colorPalettePlugin'; import { getNumBloks } from '@/utilities/getNumBloks'; import { hasRichText } from '@/utilities/hasRichText'; -import{ type HeroOverlayType } from '@/utilities/datasource'; +import { type HeroOverlayType } from '@/utilities/datasource'; import * as styles from './StoryHeroMvp.styles'; export type StoryHeroMvpProps = { @@ -20,6 +21,8 @@ export type StoryHeroMvpProps = { byline?: string; publishedDate?: string; dek?: string; + heroVariant?: 'default' | 'stacked'; + heroBgColor?: SbColorPickerType; heroImage?: SbImageType; bgImage?: SbImageType; bgImageAlt?: string; @@ -46,6 +49,8 @@ export const StoryHeroMvp = ({ byline, dek, publishedDate, + heroVariant, + heroBgColor: { color: bgHexColor } = {}, heroImage: { filename, focus } = {}, bgImage: { filename: bgImageSrc, focus: bgImageFocus } = {}, bgImageAlt, @@ -67,10 +72,8 @@ export const StoryHeroMvp = ({ day: 'numeric', year: 'numeric', }); - - const Caption = hasRichText(caption) - ? - : undefined; + const hasCaption = hasRichText(caption); + const Caption = hasCaption ? : undefined; return ( - + {heroVariant === 'stacked' ? ( + + ) : ( + + )} {!!getNumBloks(heroTexturedBar) && ( )} diff --git a/components/Hero/StoryHeroStacked.styles.ts b/components/Hero/StoryHeroStacked.styles.ts new file mode 100644 index 00000000..9e9d3a96 --- /dev/null +++ b/components/Hero/StoryHeroStacked.styles.ts @@ -0,0 +1,21 @@ +import { cnb } from 'cnbuilder'; + +export const root = 'relative'; + +export const contentWrapper = 'mt-40 md:-mt-60 xl:mt-0'; +export const superhead = (isLightHero: boolean) => cnb('cc mb-04em w-full', !isLightHero && 'text-shadow-sm'); +export const heading = ( + isSmallHeading?: boolean, + headingFont?: 'druk' | 'serif', +) => cnb('mb-0 text-balance mx-auto whitespace-pre-line', { + 'fluid-type-7 max-w-1400': headingFont === 'druk', + 'md:fluid-type-8': isSmallHeading && headingFont === 'druk', + 'md:fluid-type-9': !isSmallHeading && headingFont === 'druk', + 'fluid-type-5 md:fluid-type-7 max-w-1200': headingFont === 'serif', + 'xl:fluid-type-8 ': headingFont === 'serif' && !isSmallHeading, +}); +export const dek = 'max-w-900 text-balance mx-auto rs-mt-3'; +export const image = 'rs-mt-4 w-full'; +export const mobileImage = 'size-full lg:hidden'; +export const captionWrapper = 'mt-06em'; +export const caption = 'caption *:leading-display mt-08em max-w-prose-wide'; diff --git a/components/Hero/StoryHeroStacked.tsx b/components/Hero/StoryHeroStacked.tsx new file mode 100644 index 00000000..4831df2c --- /dev/null +++ b/components/Hero/StoryHeroStacked.tsx @@ -0,0 +1,130 @@ +import { AnimateInView } from '@/components/Animate'; +import { Container } from '@/components/Container'; +import { + Heading, Paragraph, Text, SrOnlyText, +} from '@/components/Typography'; +import { getProcessedImage } from '@/utilities/getProcessedImage'; +import { getSbImageSize } from '@/utilities/getSbImageSize'; +import * as styles from './StoryHeroStacked.styles'; + +export type StoryHeroStackedProps = { + title: string; + superhead?: string; + headingFont?: 'serif' | 'druk'; + isSmallHeading?: boolean; + dek?: string; + heroBgColor?: string; // Hex color value from Storyblok native color picker + imageSrc?: string; + imageFocus?: string; + alt?: string; + hasCaption?: boolean; + isLightHero?: boolean; +}; + +export const StoryHeroStacked = ({ + title, + superhead, + headingFont, + isSmallHeading, + dek, + heroBgColor, + imageSrc, + imageFocus, + alt, + hasCaption, + isLightHero = false, +}: StoryHeroStackedProps) => { + // We keep the original image aspect ratio + const imageSize = getSbImageSize(imageSrc) || { width: 0, height: 0 }; + const { width: imageWidth, height: imageHeight } = imageSize; + + return ( + + + {superhead && ( + + + {superhead} + + + )} + {title && ( + + + {superhead && {`${superhead}:`}}{title} + + + )} + {dek && ( + + + {dek} + + + )} + + {imageSrc && ( + + + + + + + + {alt + + + )} + + ); +}; diff --git a/components/Hero/index.ts b/components/Hero/index.ts index 80f34ee4..6475fd2d 100644 --- a/components/Hero/index.ts +++ b/components/Hero/index.ts @@ -1,4 +1,5 @@ export * from './Hero'; -export * from './BasicHeroMvp'; +export * from './BasicHero'; export * from './StoryHero'; export * from './StoryHeroMvp'; +export * from './StoryHeroStacked'; diff --git a/components/HeroIcon/HeroIcon.styles.tsx b/components/HeroIcon/HeroIcon.styles.tsx index 7d6d1386..b8056cb3 100644 --- a/components/HeroIcon/HeroIcon.styles.tsx +++ b/components/HeroIcon/HeroIcon.styles.tsx @@ -2,6 +2,7 @@ import { ArrowPathIcon, ArrowUpIcon, ArrowDownIcon, + ArrowDownTrayIcon, ArrowLeftIcon, ArrowRightIcon, ArrowUpRightIcon, @@ -34,6 +35,7 @@ export const iconMap = { 'chevron-down': ChevronDownIcon, 'chevron-right': ChevronRightIcon, 'chevron-up': ChevronUpIcon, + download: ArrowDownTrayIcon, 'triangle-down': PlayIcon, 'triangle-right': PlayIcon, 'triangle-up': PlayIcon, @@ -71,13 +73,14 @@ export const iconBaseStyleDefault = 'w-1em'; export const iconBaseStyle: IconBaseStyleType = { 'arrow-left': 'w-09em -mt-01em', 'arrow-right': 'w-09em -mt-01em', - 'triangle-right': 'w-09em scale-x-[0.9] mt-01em', - 'triangle-down': 'w-09em scale-x-[0.9] rotate-90 mt-01em', - 'triangle-up': 'w-09em scale-x-[0.9] -rotate-90 mt-02em', - email: 'w-[1.2em]', + 'triangle-right': 'w-09em scale-x-90 mt-01em', + 'triangle-down': 'w-09em scale-x-90 rotate-90 mt-01em', + 'triangle-up': 'w-09em scale-x-90 -rotate-90 mt-02em', + download: 'w-09em', + email: 'w-1em', external: 'w-08em', left: 'w-08em', - link: 'w-1em -mt-01em', + link: 'w-09em -mt-01em', more: 'w-08em', plus: 'w-08em', right: 'w-08em', diff --git a/components/Homepage/BelowBlockBanner.tsx b/components/Homepage/BelowBlockBanner.tsx index 32cfe273..4eec04d5 100644 --- a/components/Homepage/BelowBlockBanner.tsx +++ b/components/Homepage/BelowBlockBanner.tsx @@ -3,7 +3,7 @@ import { Grid } from '../Grid'; export const BelowBlockBanner = ({ children }: { children: React.ReactNode }) => ( -
+
{children} diff --git a/components/Homepage/Changemaker.tsx b/components/Homepage/Changemaker.tsx index fad965e9..048d44d8 100644 --- a/components/Homepage/Changemaker.tsx +++ b/components/Homepage/Changemaker.tsx @@ -20,7 +20,7 @@ export const Changemaker = ({ style={{ backgroundImage: `url('${bgImage}')` }} py={10} > -
+
diff --git a/components/Homepage/HomepageSplitHero.styles.ts b/components/Homepage/HomepageSplitHero.styles.ts index 71b37a8c..35e56abb 100644 --- a/components/Homepage/HomepageSplitHero.styles.ts +++ b/components/Homepage/HomepageSplitHero.styles.ts @@ -1,17 +1,15 @@ -import { cnb } from 'cnbuilder'; - export const root = 'relative overflow-hidden'; export const imageGridWrapper = 'relative max-h-[180rem] bg-black-true pt-170 sm:pt-[24vw] 2xl:pt-[16vw] 4xl:pt-[32rem] pb-[50vw] sm:pb-[40vw] 2xl:pb-[36vw] 4xl:pb-[64rem]'; export const mobileBg = 'sm:hidden'; export const bg = 'hidden sm:block'; -export const gradientOverlay = 'absolute top-0 left-0 w-full h-full bg-gradient-to-bl from-[#001c36ab] via-transparent via-60%'; +export const gradientOverlay = 'absolute top-0 left-0 size-full bg-gradient-to-bl from-[#001c36ab] via-transparent via-60%'; export const imageGrid = 'relative w-11/12 sm:w-[70vw] mx-auto 4xl:max-w-[140rem]'; export const imageWrapper = 'relative w-full aspect-w-2 aspect-h-3 sm:aspect-w-1 sm:aspect-h-1'; -export const imageTopLayerCommon = 'absolute top-0 right-0 w-full h-full object-cover mix-blend-lighten -scale-x-100'; -export const imageBottomLayerCommon = 'w-full h-full object-cover'; +export const imageTopLayerCommon = 'absolute top-0 right-0 size-full object-cover mix-blend-lighten -scale-x-100'; +export const imageBottomLayerCommon = 'size-full object-cover'; -export const textFlexbox = 'absolute w-full h-full top-0 left-0 cc 3xl:px-100 4xl:px-[calc((100%-1800px)/2)]'; +export const textFlexbox = 'absolute size-full top-0 left-0 cc 3xl:px-100 4xl:px-[calc((100%-1800px)/2)]'; export const textWrapperTop = 'relative -top-60 sm:-top-[10vw] xl:-top-[8vw] 4xl:-top-[15rem] right-0'; export const textWrapperBottom = 'relative top-[12%]'; export const serifText = 'text-[clamp(2.5rem,2.74vw+1.51rem,7rem)]'; diff --git a/components/Homepage/IdealFellow.tsx b/components/Homepage/IdealFellow.tsx index 66800b7c..2721d32e 100644 --- a/components/Homepage/IdealFellow.tsx +++ b/components/Homepage/IdealFellow.tsx @@ -23,8 +23,8 @@ export const IdealFellow = () => {
Shape - what’s - next + what’s + next
Preparing citizens diff --git a/components/ImageOverlay.tsx b/components/ImageOverlay.tsx index 106b79c2..c2004723 100644 --- a/components/ImageOverlay.tsx +++ b/components/ImageOverlay.tsx @@ -43,13 +43,13 @@ export const ImageOverlay = ({ loading={loading} width={width} height={height} - className={cnb('absolute w-full h-full object-cover top-0 left-0', className)} + className={cnb('absolute size-full object-cover top-0 left-0', className)} {...props} /> {overlay && (
cnb('group relative size-full max-w-600 mx-auto', { + 'xl:flex-row xl:max-w-[124.2rem]': isHorizontal, // 1242px is the width of 10 of 12 columns at 2XL + '2xl:max-w-700': !isHorizontal, +}); -export const imageWrapper = 'bg-gc-black transition-all aspect-w-1 aspect-h-1 sm:aspect-w-3 sm:aspect-h-4 overflow-hidden'; +export const topWrapper = (isHorizontal: boolean) => cnb('relative @320:text-18 @sm:text-21 @md:text-23', { + 'xl:basis-1/2': isHorizontal, +}); -export const image = 'object-cover backface-hidden w-full h-full group-hocus-within:scale-105 transition-transform will-change-transform'; +export const imageWrapper = (imageAspectRatio: InitiativeCardImageAspectRatio) => cnb('bg-gc-black aspect-w-1 aspect-h-1 size-full overflow-hidden', { + 'sm:aspect-w-3 sm:aspect-h-4': imageAspectRatio === '3x4', +}); -export const heading = 'absolute bottom-0 w-full bg-black-true/60 text-white rs-p-1 mb-0 group-hocus-within:bg-black-true/70 group-hocus-within:border-y-4 border-transparent group-hocus-within:border-white transition-all text-shadow-sm group-hocus-within:rs-py-2'; +export const image = 'object-cover backface-hidden size-full group-hocus-within:scale-105 transition-transform will-change-transform'; -export const bodyWrapper = 'grow bg-gc-black text-black-10 rs-pt-2 rs-pr-1 @320:text-18 @sm:text-21 @md:text-23'; +export const heading = (isHorizontal: boolean) => cnb('absolute bottom-0 w-full bg-black-true/60 text-white mb-0 group-hocus-within:bg-black-true/60 border-transparent group-hocus-within:border-white transition-all text-shadow-sm rs-p-1 group-hocus-within:rs-py-2', { + 'group-hocus-within:border-y-4': !isHorizontal, + 'xl:rs-px-3 xl:rs-py-2 xl:group-hocus-within:rs-py-3 group-hocus-within:border-y-4 xl:group-hocus-within:border-b-0': isHorizontal, +}); + +export const bodyWrapper = (isHorizontal: boolean) => cnb('@container grow bg-gc-black text-black-10 rs-pt-2 rs-pr-1 @320:text-18 @sm:text-21 @md:text-23', { + 'xl:basis-1/2 flex flex-col justify-center': isHorizontal, +}); export const body = (hasTabColor: boolean) => cnb('rs-pl-1 text-current', { 'border-l-[1.4rem] md:border-l-[2rem]': hasTabColor, }); -export const cta = 'inline-block bg-gc-black text-white hocus:text-white stretched-link no-underline rs-py-1 rs-pr-1'; +export const cta = (isHorizontal: boolean) => cnb('group/cta inline-block bg-gc-black text-white hocus:text-white stretched-link no-underline hocus:no-underline rs-py-1 rs-pr-1', { + 'xl:hidden': isHorizontal, +}); +export const linkText = 'text-19 md:text-25 mr-0 ml-auto last:children:underline'; +export const lastword = 'underline decoration-digital-red-light underline-offset-4 group-hocus/cta:decoration-white'; +export const icon = (hasLinkText?: boolean) => cnb('inline-block mb-0 mt-auto w-20 md:w-24 mr-0', + hasLinkText? 'ml-10' : 'ml-auto', +); -export const arrowIcon = 'inline-block mb-0 mt-auto w-34 ml-auto mr-0 group-hover:translate-x-03em'; +export const horizontalCta = 'stretched-link mx-auto rs-mt-2 hidden xl:block'; diff --git a/components/InitiativeCard/InitiativeCard.tsx b/components/InitiativeCard/InitiativeCard.tsx index d10e9a60..918e3210 100644 --- a/components/InitiativeCard/InitiativeCard.tsx +++ b/components/InitiativeCard/InitiativeCard.tsx @@ -1,23 +1,28 @@ import { HTMLAttributes } from 'react'; import { cnb } from 'cnbuilder'; import { AnimateInView, type AnimationType } from '../Animate'; -import { CtaLink } from '../Cta/CtaLink'; -import { Heading, type HeadingType, Paragraph } from '../Typography'; -import { HeroIcon } from '../HeroIcon'; +import { CtaLink, type IconAnimationType } from '../Cta'; +import { + Heading, type HeadingType, Paragraph, Text, SrOnlyText, +} from '../Typography'; import { FlexBox } from '../FlexBox'; import { type SbLinkType } from '../Storyblok/Storyblok.types'; import { getProcessedImage } from '@/utilities/getProcessedImage'; import { accentBorderColors, type AccentBorderColorType } from '@/utilities/datasource'; import * as styles from './InitiativeCard.styles'; +import { IconType } from '../HeroIcon'; +import { image } from '../Banner'; export type InitiativeCardProps = HTMLAttributes & { heading?: string; headingLevel?: HeadingType; - isSmallHeading?: boolean; body?: string; imageSrc?: string; imageFocus?: string; + imageAspectRatio?: styles.InitiativeCardImageAspectRatio; tabColor?: AccentBorderColorType; + isHorizontal?: boolean; + linkText?: string; link?: SbLinkType; animation?: AnimationType; delay?: number; @@ -26,64 +31,118 @@ export type InitiativeCardProps = HTMLAttributes & { export const InitiativeCard = ({ heading, headingLevel = 'h2', - isSmallHeading, body, imageSrc = '', imageFocus, + imageAspectRatio = '3x4', tabColor, + isHorizontal, + linkText, link, animation = 'none', delay, className, ...props -}: InitiativeCardProps) => ( - - -
-
- +}: InitiativeCardProps) => { + // Split the linkText into an array of words + const words = linkText?.trim().split(/\s+/); + + // Extract the last word + const lastWord = words?.pop(); + + // Join the remaining words to form the first part of the link text + const firstPart = words?.join(' '); + + let cardIcon: IconType; + let iconAnimation: IconAnimationType; + + const imageSize = imageAspectRatio === '3x4' ? '510x680' : '700x700'; + + switch (link?.linktype) { + case 'asset': + cardIcon = 'download'; + iconAnimation = 'down'; + break; + case 'story': + cardIcon = 'arrow-right'; + iconAnimation = 'right'; + break; + default: + cardIcon = 'external'; + iconAnimation = 'top-right'; + } + + return ( + + +
+
+ +
+ + {heading} +
- - {heading} - -
-
- + + {body} + + {/* Only show the button styled CTA for XL breakpoint and up */} + {isHorizontal && linkText && ( + + {linkText} + + )} +
+ - {body} - -
- - - -
-
-); + {linkText ? ( + + {firstPart} {lastWord} + + ) : ( + {`Go to the ${heading} page`} + )} + + + + ); +}; diff --git a/components/LocalFooter/LocalFooterMvp.styles.tsx b/components/LocalFooter/LocalFooterMvp.styles.tsx index caab0b66..2c6fc64c 100644 --- a/components/LocalFooter/LocalFooterMvp.styles.tsx +++ b/components/LocalFooter/LocalFooterMvp.styles.tsx @@ -1,5 +1,5 @@ export const root = 'relative overflow-hidden bg-no-repeat bg-bottom bg-cover md:bg-contain'; -export const overlay = 'absolute top-0 left-0 w-full h-full bg-gradient-to-b from-gc-black via-gc-black/80 to-gc-black/40'; +export const overlay = 'absolute top-0 left-0 size-full bg-gradient-to-b from-gc-black via-gc-black/80 to-gc-black/40'; export const flexWrapper = 'items-start lg:items-center relative z-10 w-full'; export const logo = 'scale-125 origin-left sm:scale-100 text-[1.67em] md:text-[1.72em] lg:text-[1.9em]'; export const ul = 'list-unstyled divide-x divide-white *:inline-block *:mb-0 *:px-16 md:*:px-30 xl:*:px-48 *:leading-display first:*:pl-0 last:*:pr-0 lg:mx-auto w-fit rs-mt-2 gap-y-10'; diff --git a/components/MomentPoster/MomentPoster.styles.ts b/components/MomentPoster/MomentPoster.styles.ts new file mode 100644 index 00000000..ec84695e --- /dev/null +++ b/components/MomentPoster/MomentPoster.styles.ts @@ -0,0 +1,21 @@ +import { cnb } from 'cnbuilder'; + +export const root = 'relative overflow-hidden'; +export const wrapper = 'relative w-full z-10'; + +export const bgImage = 'absolute top-0 left-0 size-full object-cover'; +export const overlay = (hasBgGradient: boolean) => cnb('absolute top-0 left-0 size-full z-10', hasBgGradient ? 'bg-gradient-to-b via-50%' : ''); + +export const contentWrapper = 'lg:rs-pr-9 ml-0'; + +export const heading = 'max-w-1000 mx-auto rs-mb-1'; +export const headingWrapper = 'mx-auto w-fit gap-02em'; + +export const thumbnailWrapper = 'inline-block'; +export const thumbnail = 'size-[0.75em]'; + +export const body = (isDarktheme: boolean) => cnb('max-w-prose mx-auto rs-mt-3 *:*:text-center *:*:leading-snug text-balance mb-0', isDarktheme && 'text-shadow-sm'); + +export const cta = (isStackedCtas: boolean) => cnb( + 'gap-27 mx-auto w-fit *:mx-auto rs-mt-4', !isStackedCtas && 'md:flex-row', +); diff --git a/components/MomentPoster/MomentPoster.tsx b/components/MomentPoster/MomentPoster.tsx new file mode 100644 index 00000000..eceb17b3 --- /dev/null +++ b/components/MomentPoster/MomentPoster.tsx @@ -0,0 +1,170 @@ +import { HTMLAttributes } from 'react'; +import { cnb } from 'cnbuilder'; +import { AnimateInView } from '../Animate'; +import { Container } from '../Container'; +import { Heading, Text } from '../Typography'; +import { FlexBox } from '../FlexBox'; +import { getProcessedImage } from '@/utilities/getProcessedImage'; +import * as styles from './MomentPoster.styles'; +import { + gradientFroms, + type GradientFromType, + gradientTos, + type GradientToType, + gradientVias, + type GradientViaType, + bgBlurs, + type BgBlurType, +} from '@/utilities/datasource'; + +type MomentPosterProps = HTMLAttributes & { + textBefore?: string; + textAfter?: string; + textNewRow?: string; + subhead?: string; + body?: React.ReactNode; + cta?: React.ReactNode; + bgImageSrc?: string; + bgImageFocus?: string; + thumbnailSrc?: string; + thumbnailFocus?: string; + isDarkTheme?: boolean; + gradientTop?: GradientToType; + gradientBottom?: GradientFromType; + gradientVia?: GradientViaType; + bgBlur?: BgBlurType; + isStackedCtas?: boolean; +}; + +export const MomentPoster = ({ + textBefore, + textAfter, + textNewRow, + subhead, + body, + cta, + bgImageSrc, + bgImageFocus, + thumbnailSrc, + thumbnailFocus, + isDarkTheme, + gradientTop, + gradientBottom, + gradientVia, + bgBlur, + isStackedCtas, + ...props +}: MomentPosterProps) => { + // To render a dark overlay, both a top and bottom gradient color must be selected + const hasBgGradient = !!gradientTop && !!gradientBottom; + + return ( + + {!!bgImageSrc && ( + + + + + + + + )} + {/* Render the overlay if there's a background image, and if background blur or/and gradient is selected */} + {!!bgImageSrc && (bgBlur !== 'none' || hasBgGradient) && ( +
+ )} + + + + {textBefore && ( + + {textBefore} + + )} + {thumbnailSrc && + + + + } + {textAfter && ( + + {textAfter} + + )} + + {textNewRow && ( + + {textNewRow} + + )} + + {subhead && ( + + + {subhead} + + + )} + {body && ( + + + {body} + + + )} + {cta && ( + + + {cta} + + + )} + + + ); +}; diff --git a/components/MomentPoster/index.ts b/components/MomentPoster/index.ts new file mode 100644 index 00000000..3f4701b2 --- /dev/null +++ b/components/MomentPoster/index.ts @@ -0,0 +1 @@ +export * from './MomentPoster'; diff --git a/components/NumberCounter.tsx b/components/NumberCounter.tsx index 21d215c5..0eb4a9ef 100644 --- a/components/NumberCounter.tsx +++ b/components/NumberCounter.tsx @@ -1,22 +1,18 @@ 'use client'; import React, { useState, useEffect, useRef } from 'react'; import { m, useInView } from 'framer-motion'; -import { Text } from './Typography'; -/** - * TODO: This is a POC. Will add types and props if client decides we need this. - */ type NumberCounterProps = { number: number; duration?: number; - afterText?: string; }; -export const NumberCounter = ({ number, duration = 2500, afterText = '' }: NumberCounterProps) => { +export const NumberCounter = ({ number, duration = 2.5 }: NumberCounterProps) => { const [count, setCount] = useState(1); const ref = useRef(null); const isInView = useInView(ref); - const interval = duration / number; + // Multiply duration by 1000 to convert to milliseconds + const interval = Math.floor(duration * 1000 / number); useEffect(() => { if (isInView && count < number) { @@ -28,7 +24,7 @@ export const NumberCounter = ({ number, duration = 2500, afterText = '' }: Numbe return ( 0 ? 1 : 0 }} ref={ref}> - {count}{afterText} + {count} ); }; diff --git a/components/Scrollytelling/Scrollytelling.styles.ts b/components/Scrollytelling/Scrollytelling.styles.ts index 51fdc863..8e976e81 100644 --- a/components/Scrollytelling/Scrollytelling.styles.ts +++ b/components/Scrollytelling/Scrollytelling.styles.ts @@ -23,8 +23,8 @@ export type OverlayType = keyof typeof overlays; export const wrapper = 'relative'; export const imageWrapper = 'sticky top-0 h-screen w-full z-0'; -export const image = 'absolute w-full h-full object-cover top-0 left-0 z-0'; -export const imageOverlay = (overlay?: OverlayType) => cnb('absolute w-full h-full top-0 left-0 z-0 bg-black-true/50', overlays[overlay]); +export const image = 'absolute size-full object-cover top-0 left-0 z-0'; +export const imageOverlay = (overlay?: OverlayType) => cnb('absolute size-full top-0 left-0 z-0 bg-black-true/50', overlays[overlay]); export const content = 'relative z-10 cc text-white rs-py-10'; export const contentWrapper = (contentAlign: ContentAlignType) => cnb('w-full mx-auto md:w-2/3 xl:w-1/2', { diff --git a/components/Scrollytelling/ScrollytellingDemo.tsx b/components/Scrollytelling/ScrollytellingDemo.tsx index 1b675878..654975ad 100644 --- a/components/Scrollytelling/ScrollytellingDemo.tsx +++ b/components/Scrollytelling/ScrollytellingDemo.tsx @@ -95,14 +95,14 @@ export const ScrollytellingDemo = () => { Chapter 2
-
+
{section4InView && ( <>
{ @@ -147,7 +147,7 @@ export const ScrollytellingDemo = () => { muted loop aria-label="video of milky way rotating" - className="block w-full h-full object-cover" + className="block size-full object-cover" > diff --git a/components/StoryCard/StoryCard.styles.tsx b/components/StoryCard/StoryCard.styles.tsx index cb5e468b..5734ce77 100644 --- a/components/StoryCard/StoryCard.styles.tsx +++ b/components/StoryCard/StoryCard.styles.tsx @@ -1,15 +1,35 @@ import { cnb } from 'cnbuilder'; -export const root = '@container relative z-10 max-w-[32rem] sm:max-w-400 md:max-w-full mx-auto'; - -export const cardWrapper = 'relative group @200:text-15 @250:text-17 @280:!type-0 @md:!text-26'; +export const root = (isHorizontal: boolean) => cnb( + '@container relative z-10 mx-auto', { + 'max-w-300 sm:max-w-400 md:max-w-full': !isHorizontal, + 'max-w-700 lg:max-w-none': isHorizontal, + }, +); + +export const cardWrapper = (isHorizontal: boolean) => cnb( + 'relative group', { + 'grid lg:grid-cols-2 bg-black-true/50': isHorizontal, + '@200:text-15 @250:text-17 @280:!type-0 @md:!text-26': !isHorizontal, + }, +); export const imageWrapper = 'transition-all aspect-w-1 aspect-h-1 overflow-hidden'; -export const image = 'object-cover w-full h-full group-hocus-within:scale-105 transition-transform'; +export const image = 'object-cover size-full group-hocus-within:scale-105 transition-transform'; -export const heading = (hasTabColor: boolean) => cnb('mt-06em rs-mb-neg1 text-current pr-08em xl:pr-1em pl-12 xl:pl-21 border-l-[1.2rem] xl:border-l-[1.8rem]', { - '@200:border-l-[1.2rem] @xs:border-l-[1.8rem] @200:pl-12 @xs:pl-21 @200:pr-08em @320:pr-1em': hasTabColor, +export const contentWrapper = (isHorizontal: boolean) => cnb({ + 'rs-pr-4 rs-py-4': isHorizontal, +}); +export const heading = (hasTabColor: boolean, isHorizontal: boolean, isSmallHeading: boolean) => cnb('text-current', { + 'border-l-[1.2rem] xl:border-l-[1.8rem] @200:border-l-[1.2rem] @xs:border-l-[1.8rem]': hasTabColor, + '@200:pl-12 @xs:pl-21 @200:pr-08em @320:pr-1em': hasTabColor && !isHorizontal, + 'mt-06em rs-mb-neg1 pr-08em xl:pr-1em pl-12 xl:pl-21': !isHorizontal, + 'rs-pb-2 mb-0 rs-pl-2': isHorizontal, + 'type-3': !isHorizontal && !isSmallHeading, + 'type-2': !isHorizontal && isSmallHeading, + 'fluid-type-4 xl:fluid-type-5': isHorizontal && !isSmallHeading, + 'fluid-type-3 xl:fluid-type-4': isHorizontal && isSmallHeading, }); export const headingLink = 'stretched-link no-underline !font-bold !leading-tight'; @@ -20,4 +40,7 @@ export const taxonomy = (hasTabColor: boolean) => cnb('list-unstyled leading-dis export const taxonomyItem = 'inline-block mb-0'; -export const body = 'pl-12 xl:pl-21 @200:pl-12 @xs:pl-21 pr-08em xl:pr-1em @200:pr-08em @320:pr-1em ml-12 xl:ml-18 @200:ml-12 @xs:ml-18'; +export const body = (isHorizontal: boolean) => cnb('', { + 'border-l-[1.2rem] xl:border-l-[1.8rem] @200:border-l-[1.2rem] @xs:border-l-[1.8rem] rs-pl-2' : isHorizontal, + 'pl-12 xl:pl-21 @200:pl-12 @xs:pl-21 pr-08em xl:pr-1em @200:pr-08em @320:pr-1em ml-12 xl:ml-18 @200:ml-12 @xs:ml-18': !isHorizontal, +}); diff --git a/components/StoryCard/StoryCard.tsx b/components/StoryCard/StoryCard.tsx index e27fb2fe..96ba28d9 100644 --- a/components/StoryCard/StoryCard.tsx +++ b/components/StoryCard/StoryCard.tsx @@ -1,12 +1,14 @@ import { cnb } from 'cnbuilder'; import { AnimateInView, type AnimationType } from '../Animate'; import { CtaLink } from '../Cta/CtaLink'; -import { Heading, type HeadingType, Paragraph } from '../Typography'; +import { + Heading, type HeadingType, Paragraph, type FontSizeType, +} from '../Typography'; import { SbLinkType } from '../Storyblok/Storyblok.types'; import { getProcessedImage } from '@/utilities/getProcessedImage'; -import { slugify } from '@/utilities/slugify'; import { accentBorderColors, type AccentBorderColorType } from '@/utilities/datasource'; import * as styles from './StoryCard.styles'; +import { FlexBox } from '../FlexBox'; export type StoryCardProps = React.HTMLAttributes & { heading?: string; @@ -21,6 +23,7 @@ export type StoryCardProps = React.HTMLAttributes & { taxonomy?: string[]; animation?: AnimationType; delay?: number; + isHorizontal?: boolean; }; export const StoryCard = ({ @@ -36,67 +39,81 @@ export const StoryCard = ({ taxonomy, animation = 'none', delay, + isHorizontal, className, ...props -}: StoryCardProps) => ( - -
-
- {imageSrc && ( -
- - - - - - -
- )} - {heading && ( - { + let headingSize: FontSizeType = 3; + if (isHorizontal) { + headingSize = 'f5'; + } else if (isSmallHeading && !isHorizontal) { + headingSize = 2; + }; + + return ( + +
+
+ {imageSrc && ( +
+ + {!isHorizontal && ( + + )} + + + + +
+ )} + - - {heading} - - - )} - {body && ( - {body} - )} -
- {/* No taxonomy for MVP; display max 3 topic tags */} - {/* {!!taxonomy?.length && ( -
    - {taxonomy.slice(0, 3).map((item) => ( -
  • - {item} -
  • - ))} -
- )} */} -
-
-); + {heading && ( + + + {heading} + + + )} + {body && ( + + {body} + + )} + +
+
+
+ ); +}; diff --git a/components/StoryImage/StoryImage.styles.ts b/components/StoryImage/StoryImage.styles.ts index 4521d600..afa14045 100644 --- a/components/StoryImage/StoryImage.styles.ts +++ b/components/StoryImage/StoryImage.styles.ts @@ -20,5 +20,5 @@ export const root = (isFullHeight?: boolean) => cnb(isFullHeight ? 'h-full' : '' export const animateWrapper = (isFullHeight?: boolean) => cnb(isFullHeight ? 'h-full' : ''); export const figure = (isFullHeight?: boolean) => cnb(isFullHeight ? 'h-full' : ''); export const imageWrapper = (isFullHeight?: boolean) => cnb(isFullHeight ? 'h-full' : ''); -export const image = 'w-full h-full object-cover'; +export const image = 'size-full object-cover'; export const caption = '*:*:leading-display caption mt-1em max-w-prose-wide'; diff --git a/components/StoryImage/StoryImage.tsx b/components/StoryImage/StoryImage.tsx index 5497421d..53f2da98 100644 --- a/components/StoryImage/StoryImage.tsx +++ b/components/StoryImage/StoryImage.tsx @@ -67,14 +67,16 @@ export const StoryImage = ({
- {alt + {!!imageSrc && ( + {alt + )}
{caption && ( diff --git a/components/StoryPoC/BrochureChapter2.tsx b/components/StoryPoC/BrochureChapter2.tsx index b6a4cb59..fbed5ad3 100644 --- a/components/StoryPoC/BrochureChapter2.tsx +++ b/components/StoryPoC/BrochureChapter2.tsx @@ -24,7 +24,7 @@ export const BrochureChapter2 = () => {
@@ -80,7 +80,7 @@ export const BrochureChapter2 = () => {
@@ -91,7 +91,7 @@ export const BrochureChapter2 = () => {
@@ -124,7 +124,7 @@ export const BrochureChapter2 = () => {
diff --git a/components/StoryPoC/BrochureStory.tsx b/components/StoryPoC/BrochureStory.tsx index 1db8feb0..6e7aa316 100644 --- a/components/StoryPoC/BrochureStory.tsx +++ b/components/StoryPoC/BrochureStory.tsx @@ -59,7 +59,7 @@ export const BrochureStory = () => {
@@ -116,7 +116,7 @@ export const BrochureStory = () => {
diff --git a/components/StoryPoC/ProgressStory.tsx b/components/StoryPoC/ProgressStory.tsx index 00446790..975d6881 100644 --- a/components/StoryPoC/ProgressStory.tsx +++ b/components/StoryPoC/ProgressStory.tsx @@ -14,7 +14,7 @@ export const ProgressStory = () => { src={getProcessedImage('https://a-us.storyblok.com/f/1005200/1901x1643/e36a942af8/progress-dish-cropped.jpg', '2000x0')} alt="" loading="eager" - className="absolute w-full h-full object-cover object-top top-0 left-0" + className="absolute size-full object-cover object-top top-0 left-0" /> @@ -37,10 +37,10 @@ export const ProgressStory = () => { src={getProcessedImage('https://a-us.storyblok.com/f/1005200/4000x2250/0c54166208/vlad-hilitanu-pt7qzb4zlww-unsplash.jpg', '2000x2000')} alt="" loading="eager" - className="relative w-full h-full object-cover object-top" + className="relative size-full object-cover object-top" />
@@ -87,10 +87,10 @@ export const ProgressStory = () => { src={getProcessedImage('https://a-us.storyblok.com/f/1005200/2100x1350/02b8df40d3/21664-12-0011_cmyk-1.jpg', '2000x1200')} alt="" loading="eager" - className="relative w-full h-full object-cover object-top" + className="relative size-full object-cover object-top" />
diff --git a/components/StoryPoC/VideoScrollStory.tsx b/components/StoryPoC/VideoScrollStory.tsx index 8b1f362a..68efe6f8 100644 --- a/components/StoryPoC/VideoScrollStory.tsx +++ b/components/StoryPoC/VideoScrollStory.tsx @@ -28,7 +28,7 @@ export const VideoScrollStory = () => { muted loop aria-label="Background Video" - className="block w-full h-full object-cover" + className="block size-full object-cover" > @@ -36,9 +36,9 @@ export const VideoScrollStory = () => { -
+
diff --git a/components/Storyblok/SbBasicPage.tsx b/components/Storyblok/SbBasicPage.tsx index 9fa377d0..a5ddc63b 100644 --- a/components/Storyblok/SbBasicPage.tsx +++ b/components/Storyblok/SbBasicPage.tsx @@ -1,16 +1,31 @@ import { storyblokEditable, type SbBlokData } from '@storyblok/react/rsc'; import { CreateBloks } from '@/components/CreateBloks'; -import { BasicHeroMvp } from '@/components/Hero'; +import { BasicHero } from '@/components/Hero'; import { Masthead } from '@/components/Masthead'; import { getNumBloks } from '@/utilities/getNumBloks'; import { type SbImageType } from '@/components/Storyblok/Storyblok.types'; +import { type HeroPaddingType } from '@/components/Hero/BasicHero.styles'; +import { + type GradientFromType, + type GradientToType, + type GradientViaType, + BgBlurType, +} from '@/utilities/datasource'; type SbBasicPageProps = { blok: { _uid: string; title?: string; + isDrukHeading?: boolean; + isSmallHeading?: boolean; hero?: SbBlokData[]; heroImage?: SbImageType; + gradientTop?: GradientToType; + gradientBottom?: GradientFromType; + gradientVia?: GradientViaType; + bgBlur?: BgBlurType; + paddingType?: HeroPaddingType; + superhead?: string; subheading?: string; heroContent?: SbBlokData[]; content?: SbBlokData[]; @@ -21,8 +36,16 @@ type SbBasicPageProps = { export const SbBasicPage = ({ blok: { title, + isDrukHeading, + isSmallHeading, hero, heroImage: { filename, focus } = {}, + gradientTop, + gradientBottom, + gradientVia, + bgBlur, + paddingType, + superhead, subheading, heroContent, content, @@ -36,21 +59,27 @@ export const SbBasicPage = ({
-
- {!!getNumBloks(hero) ? ( - - ) : ( - - )} - - -
+ {!!getNumBloks(hero) ? ( + + ) : ( + + )} + +
); diff --git a/components/Storyblok/SbCta.tsx b/components/Storyblok/SbCta.tsx index 2ec75d2e..967e4920 100644 --- a/components/Storyblok/SbCta.tsx +++ b/components/Storyblok/SbCta.tsx @@ -1,8 +1,13 @@ import { storyblokEditable } from '@storyblok/react/rsc'; -import { CtaLink, type CtaColorType, type CtaVariantType } from '../Cta'; +import { + CtaLink, + type CtaColorType, + type CtaVariantType, + type CtaCurveType, + type IconAnimationType, +} from '../Cta'; import { type IconType } from '../HeroIcon'; import { type SbLinkType } from './Storyblok.types'; -import { type CtaCurveType } from '../Cta'; type SbCtaType = { blok: { @@ -14,6 +19,7 @@ type SbCtaType = { curve?: CtaCurveType; isLarge?: boolean; icon?: IconType; + animation?: IconAnimationType; }; }; @@ -26,6 +32,7 @@ export const SbCta = ({ curve, isLarge, icon, + animation, }, blok, }: SbCtaType) => { @@ -43,6 +50,7 @@ export const SbCta = ({ color={color} srText={srText} icon={icon} + animate={animation} > {label} diff --git a/components/Storyblok/SbDataCard.tsx b/components/Storyblok/SbDataCard.tsx new file mode 100644 index 00000000..fd02611f --- /dev/null +++ b/components/Storyblok/SbDataCard.tsx @@ -0,0 +1,69 @@ +import { storyblokEditable, type SbBlokData } from '@storyblok/react/rsc'; +import { type StoryblokRichtext } from 'storyblok-rich-text-react-renderer-ts'; +import { CreateBloks } from '@/components/CreateBloks'; +import { DataCard } from '@/components/DataCard'; +import { RichText } from '@/components/RichText'; +import { type AnimationType } from '@/components/Animate'; +import { type HeadingType } from '@/components/Typography'; +import { paletteAccentColors, type PaletteAccentHexColorType } from '@/utilities/colorPalettePlugin'; +import { type PaddingType } from '@/utilities/datasource'; +import { getNumBloks } from '@/utilities/getNumBloks'; +import { hasRichText } from '@/utilities/hasRichText'; + +export type SbDataCardProps = { + blok: { + _uid: string; + heading?: string; + headingLevel?: HeadingType; + isSmallHeading?: boolean; + superhead?: string; + body: StoryblokRichtext; + cta?: SbBlokData[]; + paddingTop?: PaddingType; + isDarkTheme?: boolean; + isCounter?: boolean; + counterDuration?: number; + barColor?: { + value?: PaletteAccentHexColorType; + } + animation?: AnimationType; + delay?: number; + }; +}; + +export const SbDataCard = ({ + blok: { + heading, + headingLevel, + body, + cta, + paddingTop, + isDarkTheme, + isCounter, + counterDuration, + barColor: { value } = {}, + animation, + delay, + }, + blok, +}: SbDataCardProps) => { + const Body = hasRichText(body) ? : undefined; + const Cta = !!getNumBloks(cta) ? : undefined; + + return ( + + ); +}; diff --git a/components/Storyblok/SbEmbed.tsx b/components/Storyblok/SbEmbed.tsx new file mode 100644 index 00000000..9c135e47 --- /dev/null +++ b/components/Storyblok/SbEmbed.tsx @@ -0,0 +1,27 @@ +import { storyblokEditable} from '@storyblok/react/rsc'; +import { Embed } from '@/components/Embed'; +import { type WidthType } from '../WidthBox'; +import { type PaddingType } from '@/utilities/datasource'; + +type SbEmbedProps = { + blok: { + _uid: string; + src?: string; + content?: string; + boundingWidth?: 'site' | 'full'; + width?: WidthType; + spacingTop?: PaddingType; + spacingBottom?: PaddingType; + }; +}; + +export const SbEmbed = ({ blok, blok: { _uid } }:SbEmbedProps) => { + + return ( + + ); +}; diff --git a/components/Storyblok/SbHomepageThemeSection/SbHomepageThemeSection.styles.ts b/components/Storyblok/SbHomepageThemeSection/SbHomepageThemeSection.styles.ts new file mode 100644 index 00000000..15907e97 --- /dev/null +++ b/components/Storyblok/SbHomepageThemeSection/SbHomepageThemeSection.styles.ts @@ -0,0 +1,12 @@ +import { cnb } from 'cnbuilder'; + +export const root = 'relative overflow-hidden'; +export const bgImage = 'absolute top-0 left-0 size-full object-cover'; +export const overlay = (hasBgGradient: boolean) => cnb('absolute top-0 left-0 size-full z-10', hasBgGradient ? 'bg-gradient-to-b' : ''); +export const header = 'relative overflow-hidden cc 3xl:px-100 4xl:px-[calc((100%-1800px)/2)] z-20'; +export const superhead = (isDarkTheme: boolean) => isDarkTheme && 'text-shadow-sm'; +export const heading = 'fluid-type-7 md:gc-splash mb-0 whitespace-pre-line'; +export const introWrapper = 'cc relative z-20'; +export const intro = (isDarkTheme: boolean) => cnb('intro-text *:leading-display *:md:leading-cozy rs-mt-7 max-w-1000', isDarkTheme && 'text-shadow-sm'); +export const contentWrapper = 'relative z-20'; +export const cta = 'relative cc md:flex-row *:mx-auto rs-mt-6 gap-20 lg:gap-27 w-fit'; diff --git a/components/Storyblok/SbHomepageThemeSection/SbHomepageThemeSection.tsx b/components/Storyblok/SbHomepageThemeSection/SbHomepageThemeSection.tsx new file mode 100644 index 00000000..ec63c0b4 --- /dev/null +++ b/components/Storyblok/SbHomepageThemeSection/SbHomepageThemeSection.tsx @@ -0,0 +1,165 @@ +import { cnb } from 'cnbuilder'; +import { AnimateInView } from '@/components/Animate'; +import { storyblokEditable, type SbBlokData } from '@storyblok/react/rsc'; +import { type StoryblokRichtext } from 'storyblok-rich-text-react-renderer-ts'; +import { CreateBloks } from '@/components/CreateBloks'; +import { FlexBox } from '@/components/FlexBox'; +import { Heading, SrOnlyText, Text } from '@/components/Typography'; +import { Container } from '@/components/Container'; +import { RichText } from '@/components/RichText'; +import { type SbImageType } from '../Storyblok.types'; +import { getProcessedImage } from '@/utilities/getProcessedImage'; +import { hasRichText } from '@/utilities/hasRichText'; +import * as styles from './SbHomepageThemeSection.styles'; +import { + gradientFroms, + type GradientFromType, + gradientTos, + type GradientToType, + gradientVias, + type GradientViaType, + bgBlurs, + type BgBlurType, +} from '@/utilities/datasource'; +import { getNumBloks } from '@/utilities/getNumBloks'; + +type SbHomepageThemeSectionProps = { + blok: { + _uid: string; + superhead?: string; + heading?: string; + intro?: StoryblokRichtext; + content?: SbBlokData[]; + cta?: SbBlokData[]; + isDarkTheme?: boolean; + bgImage?: SbImageType; + bgBlur?: BgBlurType; + gradientTop?: GradientToType; + gradientBottom?: GradientFromType; + gradientVia?: GradientViaType; + }; +}; + +export const SbHomepageThemeSection = ({ + blok: { + superhead, + heading, + intro, + content, + cta, + isDarkTheme, + bgImage: { filename, focus } = {}, + bgBlur, + gradientTop, + gradientBottom, + gradientVia, + }, + blok, +}: SbHomepageThemeSectionProps) => { + const hasBgGradient = !!gradientTop && !!gradientBottom; + + return ( + + {filename && ( + <> + + + + + + + + {(bgBlur !== 'none' || hasBgGradient) && ( +
+ )} + + )} + + {superhead && ( + + {superhead} + + )} + {heading && ( + + {superhead && {`${superhead}:`}}{heading} + + )} + + {hasRichText(intro) && ( + + + + )} + + + + {!!getNumBloks(cta) && ( + + + + )} + + ); +}; diff --git a/components/Storyblok/SbHomepageThemeSection/index.ts b/components/Storyblok/SbHomepageThemeSection/index.ts new file mode 100644 index 00000000..e002ac36 --- /dev/null +++ b/components/Storyblok/SbHomepageThemeSection/index.ts @@ -0,0 +1 @@ +export * from './SbHomepageThemeSection'; diff --git a/components/Storyblok/SbInitiativeCard.tsx b/components/Storyblok/SbInitiativeCard.tsx index c34a4d5f..6ed5bc38 100644 --- a/components/Storyblok/SbInitiativeCard.tsx +++ b/components/Storyblok/SbInitiativeCard.tsx @@ -1,6 +1,6 @@ import { storyblokEditable } from '@storyblok/react/rsc'; import { type AnimationType } from '../Animate'; -import { InitiativeCard } from '../InitiativeCard'; +import { InitiativeCard, type InitiativeCardImageAspectRatio } from '../InitiativeCard'; import { type HeadingType } from '../Typography'; import { type SbImageType, type SbLinkType } from './Storyblok.types'; import { paletteAccentColors, type PaletteAccentHexColorType } from '@/utilities/colorPalettePlugin'; @@ -11,11 +11,13 @@ type SbInitiativeCardProps = { heading?: string; headingLevel?: HeadingType; body?: string; - isSmallHeading?: boolean; image?: SbImageType; + imageAspectRatio?: InitiativeCardImageAspectRatio; + isHorizontal?: boolean; tabColor?: { value?: PaletteAccentHexColorType; } + linkText?: string; link?: SbLinkType; animation?: AnimationType; delay?: number; @@ -28,7 +30,10 @@ export const SbInitiativeCard = ({ headingLevel, body, image: { filename, focus } = {}, + imageAspectRatio, tabColor: { value } = {}, + isHorizontal, + linkText, link, animation, delay, @@ -42,7 +47,10 @@ export const SbInitiativeCard = ({ body={body} imageSrc={filename} imageFocus={focus} + imageAspectRatio={imageAspectRatio} + isHorizontal={isHorizontal} tabColor={paletteAccentColors[value]} + linkText={linkText} link={link} animation={animation} delay={delay} diff --git a/components/Storyblok/SbMomentPoster.tsx b/components/Storyblok/SbMomentPoster.tsx new file mode 100644 index 00000000..39048c7a --- /dev/null +++ b/components/Storyblok/SbMomentPoster.tsx @@ -0,0 +1,79 @@ +import { storyblokEditable, type SbBlokData } from '@storyblok/react/rsc'; +import { type StoryblokRichtext } from 'storyblok-rich-text-react-renderer-ts'; +import { MomentPoster } from '../MomentPoster'; +import { CreateBloks } from '../CreateBloks'; +import { RichText } from '../RichText'; +import { type SbImageType } from './Storyblok.types'; +import { hasRichText } from '@/utilities/hasRichText'; +import { getNumBloks } from '@/utilities/getNumBloks'; +import { + type GradientFromType, + type GradientToType, + type GradientViaType, + BgBlurType, +} from '@/utilities/datasource'; + +type SbMomentPosterProps = { + blok: { + _uid: string; + textBefore?: string; + textAfter?: string; + textNewRow?: string; + subhead?: string; + body: StoryblokRichtext; + cta?: SbBlokData[]; + bgImage?: SbImageType; + thumbnail?: SbImageType; + isDarkTheme?: boolean; + gradientTop?: GradientToType; + gradientBottom?: GradientFromType; + gradientVia?: GradientViaType; + bgBlur?: BgBlurType; + isStackedCtas?: boolean; + }; +}; + +export const SbMomentPoster = ({ + blok: { + textBefore, + textAfter, + textNewRow, + subhead, + body, + cta, + bgImage: { filename, focus } = {}, + thumbnail: { filename: thumbnailFilename, focus: thumbnailFocus } = {}, + isDarkTheme, + gradientTop, + gradientBottom, + gradientVia, + bgBlur, + isStackedCtas, + }, + blok, +}: SbMomentPosterProps) => { + const Cta = !!getNumBloks(cta) ? : undefined; + const Body = hasRichText(body) ? : undefined; + + return ( + + ); +}; diff --git a/components/Storyblok/SbSection/SbSection.styles.ts b/components/Storyblok/SbSection/SbSection.styles.ts new file mode 100644 index 00000000..56528295 --- /dev/null +++ b/components/Storyblok/SbSection/SbSection.styles.ts @@ -0,0 +1,41 @@ +import { cnb } from 'cnbuilder'; + +export type AlignType = 'left' | 'center' | 'right'; + +export const root = 'relative overflow-hidden'; +export const wrapper = 'relative overflow-hidden'; + +export const headerWrapper = (headerAlign?: AlignType) => cnb('relative z-10', headerAlign === 'right' ? 'mr-0 ml-auto' : 'ml-0'); +export const bar = (headerAlign?: AlignType) => cnb( + 'block w-10 sm:w-14 md:w-20 lg:w-30 xl:w-40', { + 'order-first': headerAlign === 'left', + 'order-last': headerAlign === 'right', + }, +); +export const headerContent = (hasBarColor: boolean, hasSuperhead: boolean, headerAlign?: AlignType) => cnb( + 'cc whitespace-pre-line w-full 3xl:max-w-[90%]', { + '-ml-10 sm:-ml-14 md:-ml-20 lg:-ml-30 xl:-ml-40': hasBarColor && headerAlign !== 'right' && headerAlign !== 'center', + '-mr-10 sm:-mr-14 md:-mr-20 lg:-mr-30 xl:-mr-40': hasBarColor && headerAlign === 'right', + 'ml-0': !hasBarColor && headerAlign === 'left', + 'mr-0': !hasBarColor && headerAlign === 'right', + '-mt-05em' : !hasSuperhead, +}); +export const heading = (isSerifHeader: boolean, isSmallHeading: boolean, headerAlign?: AlignType ) => cnb( + 'mb-0', { + 'text-balance mx-auto max-w-1000': headerAlign === 'center', + 'fluid-type-6': isSerifHeader, + 'md:fluid-type-7': isSerifHeader && !isSmallHeading, + 'fluid-type-7': !isSerifHeader, + 'md:gc-splash': !isSerifHeader && !isSmallHeading, +}); +export const subhead = (headerAlign?: AlignType) => cnb('relative z-10 rs-mt-3 ', { + 'mr-0 ml-auto': headerAlign === 'right', + 'mx-auto max-w-800': headerAlign === 'center', + 'max-w-prose': headerAlign !== 'center', +}); +export const contentWrapper = 'relative z-10'; +export const cta = 'cc md:flex-row *:mx-auto rs-mt-3 gap-20 lg:gap-27 w-fit'; +export const caption = 'caption *:leading-display mt-08em max-w-prose-wide'; + +export const bgImage = 'absolute top-0 left-0 size-full object-cover'; +export const overlay = (hasBgGradient?: boolean) => cnb('absolute top-0 left-0 size-full z-10', hasBgGradient ? 'bg-gradient-to-b via-50%' : ''); diff --git a/components/Storyblok/SbSection.tsx b/components/Storyblok/SbSection/SbSection.tsx similarity index 51% rename from components/Storyblok/SbSection.tsx rename to components/Storyblok/SbSection/SbSection.tsx index 162b6e10..3538e6dd 100644 --- a/components/Storyblok/SbSection.tsx +++ b/components/Storyblok/SbSection/SbSection.tsx @@ -5,24 +5,36 @@ import { cnb } from 'cnbuilder'; import { useScroll, m, useTransform } from 'framer-motion'; import { storyblokEditable, type SbBlokData } from '@storyblok/react/rsc'; import { type StoryblokRichtext } from 'storyblok-rich-text-react-renderer-ts'; -import { CreateBloks } from '../CreateBloks'; -import { FlexBox } from '../FlexBox'; +import { CreateBloks } from '@/components/CreateBloks'; +import { FlexBox } from '@/components/FlexBox'; import { Heading, type HeadingType, SrOnlyText, Text, Paragraph, -} from '../Typography'; -import { Container, type BgColorType } from '../Container'; -import { ImageOverlay } from '../ImageOverlay'; -import { RichText } from '../RichText'; -import { WidthBox } from '../WidthBox'; +} from '@/components/Typography'; +import { Container, type BgColorType } from '@/components/Container'; +import { ImageOverlay } from '@/components/ImageOverlay'; +import { RichText } from '@/components/RichText'; +import { WidthBox } from '@/components/WidthBox'; import { accentBgColors, type PaddingType, type MarginType } from '@/utilities/datasource'; import { paletteAccentColors, type PaletteAccentHexColorType } from '@/utilities/colorPalettePlugin'; -import { type SbImageType, type SbColorStopProps } from './Storyblok.types'; +import { type SbImageType, type SbColorStopProps } from '../Storyblok.types'; import { getProcessedImage } from '@/utilities/getProcessedImage'; import { hasRichText } from '@/utilities/hasRichText'; +import { getNumBloks } from '@/utilities/getNumBloks'; +import { + gradientFroms, + type GradientFromType, + gradientTos, + type GradientToType, + gradientVias, + type GradientViaType, + bgBlurs, + type BgBlurType, +} from '@/utilities/datasource'; +import * as styles from './SbSection.styles'; type SbSectionProps = { blok: { @@ -30,17 +42,23 @@ type SbSectionProps = { content?: SbBlokData[]; superhead?: string; heading?: string; + headerAlign?: styles.AlignType; isSmallHeading?: boolean; headingLevel?: HeadingType; + isSerifHeader?: boolean; subheading?: string; + cta?: SbBlokData[]; caption?: StoryblokRichtext; captionColumnWidth?: '12' | '10' | '8' | '6' | '4'; - rightAlignHeader?: boolean; barColor?: { value?: PaletteAccentHexColorType; } bgColor?: BgColorType; bgImage?: SbImageType; + gradientTop?: GradientToType; + gradientBottom?: GradientFromType; + gradientVia?: GradientViaType; + bgBlur?: BgBlurType; bgColorStops?: SbColorStopProps[]; paddingTop?: PaddingType; paddingBottom?: PaddingType; @@ -54,15 +72,21 @@ export const SbSection = ({ content, superhead, heading, + headerAlign, isSmallHeading, + isSerifHeader, headingLevel, subheading, + cta, caption, captionColumnWidth, - rightAlignHeader, barColor: { value: barColorValue } = {}, bgColor, bgImage: { filename, focus } = {}, + gradientTop, + gradientBottom, + gradientVia, + bgBlur, bgColorStops, paddingTop, paddingBottom, @@ -72,6 +96,8 @@ export const SbSection = ({ blok, }: SbSectionProps) => { const hasHeader = heading || superhead || subheading; + // To render a dark overlay, both a top and bottom gradient color must be selected + const hasBgGradient = !!gradientTop && !!gradientBottom; const ref = useRef(null); const stops = []; @@ -104,10 +130,11 @@ export const SbSection = ({ return ( {/* Add background color animation if there are at least 2 color stops */} @@ -117,37 +144,71 @@ export const SbSection = ({ bgColor={bgColor} pt={paddingTop} pb={paddingBottom} - className="relative overflow-hidden" + className={styles.wrapper} > - {filename && ( - + {!!filename && ( + + + + + + + )} - {(heading || superhead) && ( - - {barColorValue && ( -
+ {/* Render the overlay if there's a background image, and if background blur or/and gradient is selected */} + {!!filename && (bgBlur !== 'none' || hasBgGradient) && ( +
+ )} + {(heading || superhead) && ( + + {barColorValue && headerAlign !== 'center' && ( +
)} - > +
{superhead && ( {superhead} @@ -156,12 +217,11 @@ export const SbSection = ({ {heading && ( {superhead && {`${superhead}:`}}{heading} @@ -173,19 +233,25 @@ export const SbSection = ({ {subheading} )} - + + {!!getNumBloks(cta) && ( + + + + )} {hasRichText(caption) && ( @@ -193,7 +259,7 @@ export const SbSection = ({ )} diff --git a/components/Storyblok/SbSection/index.ts b/components/Storyblok/SbSection/index.ts new file mode 100644 index 00000000..01d1cfbb --- /dev/null +++ b/components/Storyblok/SbSection/index.ts @@ -0,0 +1 @@ +export * from './SbSection'; diff --git a/components/Storyblok/SbStoryCard.tsx b/components/Storyblok/SbStoryCard.tsx index 9c4206aa..eb786961 100644 --- a/components/Storyblok/SbStoryCard.tsx +++ b/components/Storyblok/SbStoryCard.tsx @@ -31,6 +31,7 @@ export type SbStoryCardProps = { link?: SbLinkType; animation?: AnimationType; delay?: number; + isHorizontal?: boolean; }; }; @@ -58,6 +59,7 @@ export const SbStoryCard = ({ link, animation, delay, + isHorizontal, }, blok, }: SbStoryCardProps) => ( @@ -75,5 +77,6 @@ export const SbStoryCard = ({ animation={animation} delay={delay} taxonomy={topics} + isHorizontal={isHorizontal} /> ); diff --git a/components/Storyblok/SbStoryMvp/SbStoryMvp.tsx b/components/Storyblok/SbStoryMvp/SbStoryMvp.tsx index 4e675db2..b1cbaddf 100644 --- a/components/Storyblok/SbStoryMvp/SbStoryMvp.tsx +++ b/components/Storyblok/SbStoryMvp/SbStoryMvp.tsx @@ -30,6 +30,8 @@ export const SbStoryMvp = ({ byline, dek, publishedDate, + heroVariant, + heroBgColor, heroImage, bgImage, bgImageAlt, @@ -70,6 +72,8 @@ export const SbStoryMvp = ({ dek={dek} byline={byline} publishedDate={publishedDate} + heroVariant={heroVariant} + heroBgColor={heroBgColor} heroImage={heroImage} bgImage={bgImage} bgImageAlt={bgImageAlt} diff --git a/components/Storyblok/Storyblok.types.ts b/components/Storyblok/Storyblok.types.ts index de2aab84..716215b7 100644 --- a/components/Storyblok/Storyblok.types.ts +++ b/components/Storyblok/Storyblok.types.ts @@ -50,3 +50,8 @@ export type SbColorStopProps = { stop: string; hexColor: string; }; + +// Storyblok Native Color Picker +export type SbColorPickerType = { + color?: string; +}; diff --git a/components/StoryblokProvider.tsx b/components/StoryblokProvider.tsx index 3c47df94..c0f9aaa5 100644 --- a/components/StoryblokProvider.tsx +++ b/components/StoryblokProvider.tsx @@ -1,30 +1,34 @@ 'use client'; import { storyblokInit, apiPlugin } from '@storyblok/react/rsc'; -import { SbBanner } from './Storyblok/SbBanner'; -import { SbBasicPage } from './Storyblok/SbBasicPage'; -import { SbBlurryPoster } from './Storyblok/SbBlurryPoster'; -import { SbCardWysiwyg } from './Storyblok/SbCardWysiwyg'; -import { SbChangemakerCard } from './Storyblok/SbChangemakerCard'; -import { SbCta } from './Storyblok/SbCta'; -import { SbEmbedMedia } from './Storyblok/SbEmbedMedia'; -import { SbGrid } from './Storyblok/SbGrid'; -import { SbGridAlternating } from './Storyblok/SbGridAlternating'; -import { SbFeatureMasonry } from './Storyblok/SbFeatureMasonry'; -import { SbHomepageMvp } from './Storyblok/SbHomepageMvp'; -import { SbInitiativeCard } from './Storyblok/SbInitiativeCard'; -import { SbQuote } from './Storyblok/SbQuote'; -import { SbScrollytelling } from './Storyblok/SbScrollytelling'; -import { SbSection } from './Storyblok/SbSection'; -import { SbSidebarCard } from './Storyblok/SbSidebarCard'; -import { SbStoryMvp } from './Storyblok/SbStoryMvp/SbStoryMvp'; -import { SbStoryCard } from './Storyblok/SbStoryCard'; -import { SbStoryImage } from './Storyblok/SbStoryImage'; -import { SbText } from './Storyblok/SbText'; -import { SbTextCard } from './Storyblok/SbTextCard'; -import { SbTexturedBar } from './Storyblok/SbTexturedBar'; -import { SbTriangle } from './Storyblok/SbTriangle'; -import { SbTypeform } from './Storyblok/SbTypeform'; -import { SbWysiwyg } from './Storyblok/SbWysiwyg'; +import { SbBanner } from '@/components/Storyblok/SbBanner'; +import { SbBasicPage } from '@/components/Storyblok/SbBasicPage'; +import { SbBlurryPoster } from '@/components/Storyblok/SbBlurryPoster'; +import { SbCardWysiwyg } from '@/components/Storyblok/SbCardWysiwyg'; +import { SbChangemakerCard } from '@/components/Storyblok/SbChangemakerCard'; +import { SbCta } from '@/components/Storyblok/SbCta'; +import { SbDataCard } from '@/components/Storyblok/SbDataCard'; +import { SbEmbed } from '@/components/Storyblok/SbEmbed'; +import { SbEmbedMedia } from '@/components/Storyblok/SbEmbedMedia'; +import { SbGrid } from '@/components/Storyblok/SbGrid'; +import { SbGridAlternating } from '@/components/Storyblok/SbGridAlternating'; +import { SbFeatureMasonry } from '@/components/Storyblok/SbFeatureMasonry'; +import { SbHomepageMvp } from '@/components/Storyblok/SbHomepageMvp'; +import { SbHomepageThemeSection } from '@/components/Storyblok/SbHomepageThemeSection'; +import { SbInitiativeCard } from '@/components/Storyblok/SbInitiativeCard'; +import { SbMomentPoster } from './Storyblok/SbMomentPoster'; +import { SbQuote } from '@/components/Storyblok/SbQuote'; +import { SbScrollytelling } from '@/components/Storyblok/SbScrollytelling'; +import { SbSection } from '@/components/Storyblok/SbSection'; +import { SbSidebarCard } from '@/components/Storyblok/SbSidebarCard'; +import { SbStoryMvp } from '@/components/Storyblok/SbStoryMvp/SbStoryMvp'; +import { SbStoryCard } from '@/components/Storyblok/SbStoryCard'; +import { SbStoryImage } from '@/components/Storyblok/SbStoryImage'; +import { SbText } from '@/components/Storyblok/SbText'; +import { SbTextCard } from '@/components/Storyblok/SbTextCard'; +import { SbTexturedBar } from '@/components/Storyblok/SbTexturedBar'; +import { SbTriangle } from '@/components/Storyblok/SbTriangle'; +import { SbTypeform } from '@/components/Storyblok/SbTypeform'; +import { SbWysiwyg } from '@/components/Storyblok/SbWysiwyg'; import ComponentNotFound from '@/components/Storyblok/ComponentNotFound'; export const components = { @@ -34,12 +38,16 @@ export const components = { sbCardWysiwyg: SbCardWysiwyg, sbChangemakerCard: SbChangemakerCard, sbCta: SbCta, + sbDataCard: SbDataCard, + sbEmbedScript: SbEmbed, sbEmbedMedia: SbEmbedMedia, sbGrid: SbGrid, sbGridAlternating: SbGridAlternating, sbFeatureMasonry: SbFeatureMasonry, sbHomepageMvp: SbHomepageMvp, + sbHomepageThemeSection: SbHomepageThemeSection, sbInitiativeCard: SbInitiativeCard, + sbMomentPoster: SbMomentPoster, sbQuote: SbQuote, sbScrollytelling: SbScrollytelling, sbSection: SbSection, diff --git a/components/Temporary/DemoContent.tsx b/components/Temporary/DemoContent.tsx index bb2f9aed..2254fed3 100644 --- a/components/Temporary/DemoContent.tsx +++ b/components/Temporary/DemoContent.tsx @@ -10,7 +10,7 @@ export const DemoContent = () => (
-
+
To infinity and beyond @@ -110,14 +110,14 @@ export const DemoContent = () => ( Animated counters - +
-
+
diff --git a/components/TexturedBar/TexturedBar.tsx b/components/TexturedBar/TexturedBar.tsx index 8af45692..82a792b2 100644 --- a/components/TexturedBar/TexturedBar.tsx +++ b/components/TexturedBar/TexturedBar.tsx @@ -23,7 +23,7 @@ export const TexturedBar = ({ width={3000} height={60} sizes="100vw" - className="w-full h-full object-cover" + className="size-full object-cover" />
); diff --git a/components/ThemeCard/ThemeCard.styles.tsx b/components/ThemeCard/ThemeCard.styles.tsx index bd28ff74..b328998b 100644 --- a/components/ThemeCard/ThemeCard.styles.tsx +++ b/components/ThemeCard/ThemeCard.styles.tsx @@ -4,7 +4,7 @@ export const cardWrapper = 'group relative'; export const imageWrapper = 'transition-all aspect-w-1 aspect-h-1 overflow-hidden'; -export const image = 'object-cover w-full h-full group-hocus-within:scale-105 transition-transform'; +export const image = 'object-cover size-full group-hocus-within:scale-105 transition-transform'; export const heading = 'mt-06em rs-mb-neg1 text-current'; diff --git a/components/Typography/typography.styles.ts b/components/Typography/typography.styles.ts index bd83d5b1..dc9ea56c 100644 --- a/components/Typography/typography.styles.ts +++ b/components/Typography/typography.styles.ts @@ -44,6 +44,7 @@ export const fontLeadings = { cozy: 'leading-cozy', // 1.4 normal: 'leading', // 1.5 trim: 'leading-trim', // 0.75 + druk: 'leading-druk', // 0.9 }; export const textAligns = { @@ -61,6 +62,7 @@ export const textColors = { 'black-40': 'text-black-40', 'black-60': 'text-black-60', 'black-80': 'text-black-80', + 'black-90': 'text-black-90', }; export const textVariants = { @@ -71,15 +73,15 @@ export const textVariants = { big: 'big-paragraph', subheading: 'subheading', /** - * Campaign typography styles - * (-gc ones are Decanter styles with Campaign modifications) + * Momentum typography styles + * (-gc ones are Decanter styles with Momentum modifications) */ caption: 'caption', card: 'gc-card', changemaker: 'gc-changemaker', intro: 'gc-intro-text', /** - * Campaign only styles + * Momentum only styles * No gc- prefix because no Decanter equivalent */ overview: 'overview', diff --git a/components/VerticalPoster/VerticalPoster.styles.ts b/components/VerticalPoster/VerticalPoster.styles.ts index da66160b..eb8a4aeb 100644 --- a/components/VerticalPoster/VerticalPoster.styles.ts +++ b/components/VerticalPoster/VerticalPoster.styles.ts @@ -1,7 +1,7 @@ import { cnb } from 'cnbuilder'; export const root = 'relative overflow-hidden break-words'; -export const blurWrapper = 'w-full h-full backdrop-blur-md'; +export const blurWrapper = 'size-full backdrop-blur-md'; export const grid = 'w-full'; diff --git a/netlify.toml b/netlify.toml index 07979dba..e5c33163 100644 --- a/netlify.toml +++ b/netlify.toml @@ -40,7 +40,7 @@ Content-Security-Policy = "default-src https: 'unsafe-inline' 'unsafe-eval'; form-action https:; img-src https: data:; frame-ancestors https://app.storyblok.com" X-Content-Type-Options = "nosniff" Referrer-Policy = "strict-origin-when-cross-origin" - Strict-Transport-Security = "max-age=2592000" + Strict-Transport-Security = "max-age=31536000" Permissions-Policy = "vibrate=(), geolocation=(), midi=(), notifications=(), push=(), microphone=(), camera=(), magnetometer=(), gyroscope=(), speaker=()" [[redirects]] diff --git a/package-lock.json b/package-lock.json index c6a1e67d..9b659b37 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "ood-giving-campaign", - "version": "1.0.1", + "version": "1.3.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ood-giving-campaign", - "version": "1.0.1", + "version": "1.3.3", "dependencies": { "@heroicons/react": "^2.1.1", "@storyblok/react": "^3.0.8", @@ -2390,9 +2390,9 @@ "dev": true }, "node_modules/follow-redirects": { - "version": "1.15.4", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.4.tgz", - "integrity": "sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw==", + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", "funding": [ { "type": "individual", diff --git a/package.json b/package.json index 0345fb4c..76f56a61 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ood-giving-campaign", - "version": "1.0.1", + "version": "1.3.3", "description": "Momentum", "author": "Stanford University", "keywords": [ diff --git a/tailwind.config.ts b/tailwind.config.ts index b2153464..f71636f4 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -19,10 +19,11 @@ export default { ], theme: { containers: require(`${dir}/theme/gc-containers.js`)(), - // Campaign themes extending our Decanter ones + // Momentum themes extending our Decanter ones extend: { colors: require(`${dir}/theme/gc-colors.js`)(), fontFamily: require(`${dir}/theme/gc-fontFamily.js`)(), + lineHeight: require(`${dir}/theme/gc-lineHeight.js`)(), screens: require(`${dir}/theme/gc-screens.js`)(), }, }, @@ -30,5 +31,6 @@ export default { require('@tailwindcss/container-queries'), require(`${dir}/base/gc-base.js`)(), require(`${dir}/components/gc-typography.js`)(), + require(`${dir}/components/gc-text-shadow.js`)(), ], } satisfies Config; diff --git a/tailwind/plugins/base/gc-base.js b/tailwind/plugins/base/gc-base.js index a238ed01..7dedde13 100644 --- a/tailwind/plugins/base/gc-base.js +++ b/tailwind/plugins/base/gc-base.js @@ -1,5 +1,5 @@ /** - * Campaign custom base styles extending Decanter 7 base + * Momentum custom base styles extending Decanter 7 base */ module.exports = function () { @@ -7,7 +7,7 @@ module.exports = function () { addBase({ html: { overflowY: 'visible !important', // Need this for sticky nav to work - color: '#17171A', // Campaign black + color: '#17171A', // Momentum black }, body: { fontSize: '1.8rem', diff --git a/tailwind/plugins/components/gc-text-shadow.js b/tailwind/plugins/components/gc-text-shadow.js new file mode 100644 index 00000000..421316f8 --- /dev/null +++ b/tailwind/plugins/components/gc-text-shadow.js @@ -0,0 +1,14 @@ +/** + * Text shadow styles + */ +module.exports = function () { + return function ({ addComponents }) { + const components = { + '.text-shadow-sm': { + textShadow: 'rgba(0, 0, 0, 20%) 0 0 18px, rgba(0, 0, 0, 60%) 0 0 2px', + }, + }; + + addComponents(components); + }; +}; diff --git a/tailwind/plugins/components/gc-typography.js b/tailwind/plugins/components/gc-typography.js index 0a5b92a7..6ad1a516 100644 --- a/tailwind/plugins/components/gc-typography.js +++ b/tailwind/plugins/components/gc-typography.js @@ -1,5 +1,5 @@ /** - * Campaign specific typography styles + * Momentum specific typography styles */ module.exports = function () { return function ({ addComponents, theme }) { diff --git a/tailwind/plugins/theme/gc-colors.js b/tailwind/plugins/theme/gc-colors.js index 1ddcc350..976c97a7 100644 --- a/tailwind/plugins/theme/gc-colors.js +++ b/tailwind/plugins/theme/gc-colors.js @@ -1,5 +1,5 @@ /** - * Giving Campaign colors + * Momentum colors */ module.exports = function () { return { diff --git a/tailwind/plugins/theme/gc-fontFamily.js b/tailwind/plugins/theme/gc-fontFamily.js index b9ae020b..d1f54568 100644 --- a/tailwind/plugins/theme/gc-fontFamily.js +++ b/tailwind/plugins/theme/gc-fontFamily.js @@ -1,5 +1,5 @@ /** - * Giving Campaign fonts + * Momentum fonts */ module.exports = function () { return { diff --git a/tailwind/plugins/theme/gc-lineHeight.js b/tailwind/plugins/theme/gc-lineHeight.js new file mode 100644 index 00000000..f657481b --- /dev/null +++ b/tailwind/plugins/theme/gc-lineHeight.js @@ -0,0 +1,9 @@ +/** + * Momentum line heights + */ +module.exports = function () { + return { + // Extra tight line height for Druk font in some components, e.g., Data Card, Moment Poster + druk: '0.9', + }; +}; diff --git a/utilities/datasource.ts b/utilities/datasource.ts index 5bfee141..5e9872bc 100644 --- a/utilities/datasource.ts +++ b/utilities/datasource.ts @@ -58,6 +58,90 @@ export const accentTextColors = { }; export type AccentTextColorType = AccentColorType; +export const bgBlurs = { + none: '', + 4: 'backdrop-blur-sm', + 8: 'backdrop-blur', + 12: 'backdrop-blur-md', + 16: 'backdrop-blur-lg', +}; +export type BgBlurType = keyof typeof bgBlurs; + +export const gradientTos = { + transparent: '', + 'black-10': 'to-black-true/10', + 'black-20': 'to-black-true/20', + 'black-30': 'to-black-true/30', + 'black-40': 'to-black-true/40', + 'black-50': 'to-black-true/50', + 'black-60': 'to-black-true/60', + 'black-70': 'to-black-true/70', + 'black-80': 'to-black-true/80', + 'black-90': 'to-black-true/90', + 'gc-black': 'to-gc-black', + 'white-10': 'to-white/10', + 'white-20': 'to-white/20', + 'white-30': 'to-white/30', + 'white-40': 'to-white/40', + 'white-50': 'to-white/50', + 'white-60': 'to-white/60', + 'white-70': 'to-white/70', + 'white-80': 'to-white/80', + 'white-90': 'to-white/90', + white: 'to-white', +}; +export type GradientToType = keyof typeof gradientTos; + +export const gradientFroms = { + transparent: 'from-transparent', + 'black-10': 'from-black-true/10', + 'black-20': 'from-black-true/20', + 'black-30': 'from-black-true/30', + 'black-40': 'from-black-true/40', + 'black-50': 'from-black-true/50', + 'black-60': 'from-black-true/60', + 'black-70': 'from-black-true/70', + 'black-80': 'from-black-true/80', + 'black-90': 'from-black-true/90', + 'gc-black': 'from-gc-black', + 'white-10': 'from-white/10', + 'white-20': 'from-white/20', + 'white-30': 'from-white/30', + 'white-40': 'from-white/40', + 'white-50': 'from-white/50', + 'white-60': 'from-white/60', + 'white-70': 'from-white/70', + 'white-80': 'from-white/80', + 'white-90': 'from-white/90', + white: 'from-white', +}; +export type GradientFromType = keyof typeof gradientFroms; + +export const gradientVias = { + transparent: 'via-transparent', + 'black-10': 'via-black-true/10', + 'black-20': 'via-black-true/20', + 'black-30': 'via-black-true/30', + 'black-40': 'via-black-true/40', + 'black-50': 'via-black-true/50', + 'black-60': 'via-black-true/60', + 'black-70': 'via-black-true/70', + 'black-80': 'via-black-true/80', + 'black-90': 'via-black-true/90', + 'gc-black': 'via-gc-black', + 'white-10': 'via-white/10', + 'white-20': 'via-white/20', + 'white-30': 'via-white/30', + 'white-40': 'via-white/40', + 'white-50': 'via-white/50', + 'white-60': 'via-white/60', + 'white-70': 'via-white/70', + 'white-80': 'via-white/80', + 'white-90': 'via-white/90', + white: 'via-white', +}; +export type GradientViaType = keyof typeof gradientVias; + export const imageAspectRatios = { '1x1': 'aspect-w-1 aspect-h-1', '1x2': 'aspect-w-1 aspect-h-2', @@ -89,7 +173,7 @@ export const storyHeroAspectRatiosDesktop = { '2x1': 'lg:aspect-w-2 lg:aspect-h-1', '5x8': 'lg:aspect-w-5 lg:aspect-h-8', '16x9': 'lg:aspect-w-16 lg:aspect-h-9', - 'free': '', + free: '', }; export const mediaAspectRatios = { @@ -127,7 +211,7 @@ export type BgTextColorPairType = keyof typeof bgTextColorPairs; export const heroOverlays = { none: 'bg-black-true/20 lg:bg-transparent', - 'black-10': 'bg-black-true/20', + 'black-10': 'bg-black-true/10', 'black-20': 'bg-black-true/20', 'black-30': 'bg-black-true/30', 'black-40': 'bg-black-true/40', diff --git a/utilities/splitNumberString.ts b/utilities/splitNumberString.ts new file mode 100644 index 00000000..c45a1802 --- /dev/null +++ b/utilities/splitNumberString.ts @@ -0,0 +1,23 @@ +/** + * Separates a string into three parts: text before the number, the number itself, and text after the number. + * + * @param str The input string containing a number. + * @returns An object with three properties: beforeNumber, number, and afterNumber. + */ +export const splitNumberString = (str: string): { beforeNumber: string; number?: number; afterNumber: string } => { + const numberRegex = /\d+/; + const numberMatch = str.match(numberRegex); + + if (numberMatch) { + const number = parseFloat(numberMatch[0]); + const numberIndex = numberMatch.index ?? 0; + const beforeNumber = str.substring(0, numberIndex); + const afterNumber = str.substring(numberIndex + (numberMatch[0]?.length ?? 0)); + + return { + beforeNumber: beforeNumber ?? '', + number: number, + afterNumber: afterNumber ?? '', + }; + } +};