diff --git a/website/components/footer/style.css b/website/components/footer/style.css index 26edf1933..5f04743af 100644 --- a/website/components/footer/style.css +++ b/website/components/footer/style.css @@ -2,7 +2,6 @@ padding: 25px 0 17px 0; flex-shrink: 0; display: flex; - border-top: 1px solid var(--gray-5); & .g-grid-container { display: flex; diff --git a/website/components/io-card-container/index.tsx b/website/components/io-card-container/index.tsx new file mode 100644 index 000000000..e71ab886e --- /dev/null +++ b/website/components/io-card-container/index.tsx @@ -0,0 +1,82 @@ +import * as React from 'react' +import classNames from 'classnames' +import Button from '@hashicorp/react-button' +import IoCard, { IoCardProps } from 'components/io-card' +import s from './style.module.css' + +interface IoCardContaianerProps { + theme?: 'light' | 'dark' + heading?: string + description?: string + label?: string + cta?: { + url: string + text: string + } + cardsPerRow: 3 | 4 + cards: Array +} + +export default function IoCardContaianer({ + theme = 'light', + heading, + description, + label, + cta, + cardsPerRow = 3, + cards, +}: IoCardContaianerProps): React.ReactElement { + return ( +
+ {heading || description ? ( +
+ {heading ?

{heading}

: null} + {description ?

{description}

: null} +
+ ) : null} + {cards.length ? ( + <> + {label || cta ? ( +
+ {label ?

{label}

: null} + {cta ? ( +
+ ) : null} + + + ) : null} +
+ ) +} diff --git a/website/components/io-card-container/style.module.css b/website/components/io-card-container/style.module.css new file mode 100644 index 000000000..b7b9b08d2 --- /dev/null +++ b/website/components/io-card-container/style.module.css @@ -0,0 +1,114 @@ +.cardContainer { + position: relative; + + & + .cardContainer { + margin-top: 64px; + + @media (--medium-up) { + margin-top: 132px; + } + } +} + +.header { + margin: 0 auto 64px; + text-align: center; + max-width: 600px; +} + +.heading { + margin: 0; + composes: g-type-display-2 from global; + + @nest .dark & { + color: var(--white); + } +} + +.description { + margin: 8px 0 0; + composes: g-type-body-large from global; + + @nest .dark & { + color: var(--gray-5); + } +} + +.subHeader { + margin: 0 0 32px; + display: flex; + align-items: center; + justify-content: space-between; + + @nest .dark & { + color: var(--gray-5); + } +} + +.label { + margin: 0; + composes: g-type-display-4 from global; +} + +.cardList { + list-style: none; + + --minCol: 250px; + --columns: var(--length); + + position: relative; + gap: 32px; + padding: 0; + + @media (--small) { + display: flex; + overflow-x: auto; + -ms-overflow-style: none; + scrollbar-width: none; + margin: 0; + padding: 6px 24px; + left: 50%; + margin-left: -50vw; + width: 100vw; + + /* This is to ensure there is overflow padding right on mobile. */ + &::after { + content: ''; + display: block; + width: 1px; + flex-shrink: 0; + } + } + + @media (--medium-up) { + display: grid; + grid-template-columns: repeat(var(--columns), minmax(var(--minCol), 1fr)); + } + + &.threeUp { + @media (--medium-up) { + --columns: 3; + --minCol: 0; + } + } + + &.fourUp { + @media (--medium-up) { + --columns: 3; + --minCol: 0; + } + + @media (--large) { + --columns: 4; + } + } + + & > li { + display: flex; + + @media (--small) { + flex-shrink: 0; + width: 250px; + } + } +} diff --git a/website/components/io-card/index.tsx b/website/components/io-card/index.tsx new file mode 100644 index 000000000..64baa4081 --- /dev/null +++ b/website/components/io-card/index.tsx @@ -0,0 +1,124 @@ +import * as React from 'react' +import Link from 'next/link' +import InlineSvg from '@hashicorp/react-inline-svg' +import classNames from 'classnames' +import { IconArrowRight24 } from '@hashicorp/flight-icons/svg-react/arrow-right-24' +import { IconExternalLink24 } from '@hashicorp/flight-icons/svg-react/external-link-24' +import { productLogos } from './product-logos' +import s from './style.module.css' + +export interface IoCardProps { + variant?: 'light' | 'gray' | 'dark' + products?: Array<{ + name: keyof typeof productLogos + }> + link: { + url: string + type: 'inbound' | 'outbound' + } + inset?: 'none' | 'sm' | 'md' + eyebrow?: string + heading?: string + description?: string + children?: React.ReactNode +} + +function IoCard({ + variant = 'light', + products, + link, + inset = 'md', + eyebrow, + heading, + description, + children, +}: IoCardProps): React.ReactElement { + const LinkWrapper = ({ className, children }) => + link.type === 'inbound' ? ( + + {children} + + ) : ( + + {children} + + ) + + return ( +
+ + {children ? ( + children + ) : ( + <> + {eyebrow ? {eyebrow} : null} + {heading ? {heading} : null} + {description ? {description} : null} + + )} +
+ {products && ( +
    + {products.map(({ name }, index) => { + const key = name.toLowerCase() + const version = variant === 'dark' ? 'neutral' : 'color' + return ( + // eslint-disable-next-line react/no-array-index-key +
  • + +
  • + ) + })} +
+ )} + + {link.type === 'inbound' ? ( + + ) : ( + + )} + +
+
+
+ ) +} + +interface EyebrowProps { + children: string +} + +function Eyebrow({ children }: EyebrowProps) { + return

{children}

+} + +interface HeadingProps { + as?: 'h2' | 'h3' | 'h4' + children: React.ReactNode +} + +function Heading({ as: Component = 'h2', children }: HeadingProps) { + return {children} +} + +interface DescriptionProps { + children: string +} + +function Description({ children }: DescriptionProps) { + return

{children}

+} + +IoCard.Eyebrow = Eyebrow +IoCard.Heading = Heading +IoCard.Description = Description + +export default IoCard diff --git a/website/components/io-card/product-logos.ts b/website/components/io-card/product-logos.ts new file mode 100644 index 000000000..9c24e3bf4 --- /dev/null +++ b/website/components/io-card/product-logos.ts @@ -0,0 +1,34 @@ +export const productLogos = { + boundary: { + color: require('@hashicorp/mktg-logos/product/boundary/logomark/color.svg?include'), + neutral: require('@hashicorp/mktg-logos/product/boundary/logomark/white.svg?include'), + }, + consul: { + color: require('@hashicorp/mktg-logos/product/consul/logomark/color.svg?include'), + neutral: require('@hashicorp/mktg-logos/product/consul/logomark/white.svg?include'), + }, + nomad: { + color: require('@hashicorp/mktg-logos/product/nomad/logomark/color.svg?include'), + neutral: require('@hashicorp/mktg-logos/product/nomad/logomark/white.svg?include'), + }, + packer: { + color: require('@hashicorp/mktg-logos/product/packer/logomark/color.svg?include'), + neutral: require('@hashicorp/mktg-logos/product/packer/logomark/white.svg?include'), + }, + terraform: { + color: require('@hashicorp/mktg-logos/product/terraform/logomark/color.svg?include'), + neutral: require('@hashicorp/mktg-logos/product/terraform/logomark/white.svg?include'), + }, + vagrant: { + color: require('@hashicorp/mktg-logos/product/vagrant/logomark/color.svg?include'), + neutral: require('@hashicorp/mktg-logos/product/vagrant/logomark/white.svg?include'), + }, + vault: { + color: require('@hashicorp/mktg-logos/product/vault/logomark/color.svg?include'), + neutral: require('@hashicorp/mktg-logos/product/vault/logomark/white.svg?include'), + }, + waypoint: { + color: require('@hashicorp/mktg-logos/product/waypoint/logomark/color.svg?include'), + neutral: require('@hashicorp/mktg-logos/product/waypoint/logomark/white.svg?include'), + }, +} diff --git a/website/components/io-card/style.module.css b/website/components/io-card/style.module.css new file mode 100644 index 000000000..44df36ced --- /dev/null +++ b/website/components/io-card/style.module.css @@ -0,0 +1,148 @@ +.card { + /* Radii */ + --token-radius: 6px; + + /* Spacing */ + --token-spacing-03: 8px; + --token-spacing-04: 16px; + --token-spacing-05: 24px; + --token-spacing-06: 32px; + + /* Elevations */ + --token-elevation-mid: 0 2px 3px rgba(101, 106, 118, 0.1), + 0 8px 16px -10px rgba(101, 106, 118, 0.2); + --token-elevation-high: 0 2px 3px rgba(101, 106, 118, 0.15), + 0 16px 16px -10px rgba(101, 106, 118, 0.2); + + /* Transition */ + --token-transition: ease-in-out 0.2s; + + display: flex; + flex-direction: column; + flex-grow: 1; + min-height: 300px; + + & a { + display: flex; + flex-direction: column; + flex-grow: 1; + border-radius: var(--token-radius); + box-shadow: 0 0 0 1px rgba(38, 53, 61, 0.1), var(--token-elevation-mid); + transition: var(--token-transition); + transition-property: background-color, box-shadow; + + &:hover { + box-shadow: 0 0 0 2px rgba(38, 53, 61, 0.15), var(--token-elevation-high); + cursor: pointer; + } + + /* Variants */ + &.dark { + background-color: var(--gray-1); + + &:hover { + background-color: var(--gray-2); + } + } + + &.gray { + background-color: #f9f9fa; + } + + &.light { + background-color: var(--white); + } + + /* Spacing */ + &.none { + padding: 0; + } + + &.sm { + padding: var(--token-spacing-05); + } + + &.md { + padding: var(--token-spacing-06); + } + } +} + +.eyebrow { + margin: 0; + composes: g-type-label-small from global; + color: var(--gray-3); + + @nest .dark & { + color: var(--gray-5); + } +} + +.heading { + margin: 0; + composes: g-type-display-5 from global; + color: var(--black); + + @nest * + & { + margin-top: var(--token-spacing-05); + } + + @nest .dark & { + color: var(--white); + } +} + +.description { + margin: 0; + composes: g-type-body-small from global; + color: var(--gray-3); + + @nest * + & { + margin-top: var(--token-spacing-03); + } + + @nest .dark & { + color: var(--gray-5); + } +} + +.footer { + margin-top: auto; + display: flex; + justify-content: space-between; + align-items: flex-end; + padding-top: 32px; +} + +.products { + display: flex; + gap: 8px; + margin: 0; + padding: 0; + + & > li { + width: 32px; + height: 32px; + display: grid; + place-items: center; + } + + & .logo { + display: flex; + + & svg { + width: 32px; + height: 32px; + } + } +} + +.linkType { + margin-left: auto; + display: flex; + color: var(--black); + + @nest .dark & { + color: var(--white); + } +} diff --git a/website/components/io-dialog/index.tsx b/website/components/io-dialog/index.tsx new file mode 100644 index 000000000..14298b305 --- /dev/null +++ b/website/components/io-dialog/index.tsx @@ -0,0 +1,47 @@ +import * as React from 'react' +import { DialogOverlay, DialogContent, DialogOverlayProps } from '@reach/dialog' +import { AnimatePresence, motion } from 'framer-motion' +import s from './style.module.css' + +export interface IoDialogProps extends DialogOverlayProps { + label: string +} + +export default function IoDialog({ + isOpen, + onDismiss, + children, + label, +}: IoDialogProps): React.ReactElement { + const AnimatedDialogOverlay = motion(DialogOverlay) + return ( + + {isOpen && ( + +
+ + + + {children} + + +
+
+ )} +
+ ) +} diff --git a/website/components/io-dialog/style.module.css b/website/components/io-dialog/style.module.css new file mode 100644 index 000000000..306619ac8 --- /dev/null +++ b/website/components/io-dialog/style.module.css @@ -0,0 +1,62 @@ +.dialogOverlay { + background-color: rgba(0, 0, 0, 0.75); + height: 100%; + left: 0; + overflow-y: auto; + position: fixed; + top: 0; + width: 100%; + z-index: 666666667 /* higher than global nav */; +} + +.dialogWrapper { + display: grid; + min-height: 100vh; + padding: 24px; + place-items: center; +} + +.dialogContent { + background-color: var(--gray-1); + color: var(--white); + max-width: 800px; + outline: none; + overflow-y: auto; + padding: 24px; + position: relative; + width: 100%; + + @media (min-width: 768px) { + padding: 48px; + } +} + +.dialogClose { + appearance: none; + background-color: transparent; + border: 0; + composes: g-type-display-5 from global; + cursor: pointer; + margin: 0; + padding: 0; + position: absolute; + color: var(--white); + right: 24px; + top: 24px; + z-index: 1; + + @media (min-width: 768px) { + right: 48px; + top: 48px; + } + + @nest html[dir='rtl'] & { + left: 24px; + right: auto; + + @media (min-width: 768px) { + left: 48px; + right: auto; + } + } +} diff --git a/website/components/io-home-call-to-action/index.tsx b/website/components/io-home-call-to-action/index.tsx new file mode 100644 index 000000000..7296b361b --- /dev/null +++ b/website/components/io-home-call-to-action/index.tsx @@ -0,0 +1,39 @@ +import ReactCallToAction from '@hashicorp/react-call-to-action' +import { Products } from '@hashicorp/platform-product-meta' +import s from './style.module.css' + +interface IoHomeCallToActionProps { + brand: Products + heading: string + content: string + links: Array<{ + text: string + url: string + }> +} + +export default function IoHomeCallToAction({ + brand, + heading, + content, + links, +}: IoHomeCallToActionProps) { + return ( +
+ { + return { + text, + url, + type: index === 1 ? 'inbound' : null, + } + })} + /> +
+ ) +} diff --git a/website/components/io-home-call-to-action/style.module.css b/website/components/io-home-call-to-action/style.module.css new file mode 100644 index 000000000..76cb03446 --- /dev/null +++ b/website/components/io-home-call-to-action/style.module.css @@ -0,0 +1,12 @@ +.callToAction { + margin: 60px auto; + background-image: linear-gradient(52.3deg, #2c2d2f 39.83%, #626264 96.92%); + + @media (--medium-up) { + margin: 120px auto; + } + + & > * { + background-color: transparent; + } +} diff --git a/website/components/io-home-case-studies/index.tsx b/website/components/io-home-case-studies/index.tsx new file mode 100644 index 000000000..3155749e2 --- /dev/null +++ b/website/components/io-home-case-studies/index.tsx @@ -0,0 +1,81 @@ +import * as React from 'react' +import Image from 'next/image' +import { IconExternalLink16 } from '@hashicorp/flight-icons/svg-react/external-link-16' +import { IconArrowRight16 } from '@hashicorp/flight-icons/svg-react/arrow-right-16' +import s from './style.module.css' + +interface IoHomeCaseStudiesProps { + isInternalLink: (link: string) => boolean + heading: string + description: string + primary: Array<{ + thumbnail: { + url: string + alt: string + } + link: string + heading: string + }> + secondary: Array<{ + link: string + heading: string + }> +} + +export default function IoHomeCaseStudies({ + isInternalLink, + heading, + description, + primary, + secondary, +}: IoHomeCaseStudiesProps): React.ReactElement { + return ( +
+
+
+

{heading}

+

{description}

+
+
+ + + +
+
+
+ ) +} diff --git a/website/components/io-home-case-studies/style.module.css b/website/components/io-home-case-studies/style.module.css new file mode 100644 index 000000000..63ff3102f --- /dev/null +++ b/website/components/io-home-case-studies/style.module.css @@ -0,0 +1,170 @@ +.root { + position: relative; + margin: 60px auto; + max-width: 1600px; + + @media (--medium-up) { + margin: 120px auto; + } +} + +.container { + composes: g-grid-container from global; +} + +.header { + margin-bottom: 32px; + + @media (--medium-up) { + max-width: calc(100% * 5 / 12); + } +} + +.heading { + margin: 0; + composes: g-type-display-3 from global; +} + +.description { + margin: 8px 0 0; + composes: g-type-body from global; + color: var(--gray-3); +} + +.caseStudies { + --columns: 1; + + display: grid; + grid-template-columns: repeat(var(--columns), minmax(0, 1fr)); + gap: 32px; + + @media (--medium-up) { + --columns: 12; + } +} + +.primary { + --columns: 1; + + grid-column: 1 / -1; + list-style: none; + margin: 0; + padding: 0; + display: grid; + grid-template-columns: repeat(var(--columns), minmax(0, 1fr)); + gap: 32px; + + @media (--medium-up) { + --columns: 2; + } + + @media (--large) { + grid-column: 1 / 9; + } +} + +.primaryItem { + display: flex; +} + +.card { + position: relative; + overflow: hidden; + display: flex; + flex-direction: column; + flex-grow: 1; + justify-content: flex-end; + padding: 32px; + box-shadow: 0 8px 16px -10px rgba(101, 106, 118, 0.2); + background-color: #000; + border-radius: 6px; + color: var(--white); + transition: ease-in-out 0.2s; + transition-property: box-shadow; + min-height: 300px; + + &::before { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 10; + border-radius: 6px; + background-image: linear-gradient( + to bottom, + rgba(0, 0, 0, 0), + rgba(0, 0, 0, 0.45) + ); + transition: opacity ease-in-out 0.2s; + } + + &:hover { + box-shadow: 0 2px 3px rgba(101, 106, 118, 0.15), + 0 16px 16px -10px rgba(101, 106, 118, 0.2); + + &::before { + opacity: 0.75; + } + } +} + +.cardThumbnail { + transition: transform 0.4s; + + @nest .card:hover & { + transform: scale(1.04); + } +} + +.cardHeading { + margin: 0; + composes: g-type-display-4 from global; + z-index: 10; +} + +.secondary { + grid-column: 1 / -1; + list-style: none; + margin: 0; + padding: 0; + + @media (--large) { + margin-top: -32px; + grid-column: 9 / -1; + } +} + +.secondaryItem { + border-bottom: 1px solid var(--gray-5); +} + +.link { + display: flex; + width: 100%; + color: var(--black); +} + +.linkInner { + display: flex; + width: 100%; + justify-content: space-between; + padding-top: 32px; + padding-bottom: 32px; + transition: transform ease-in-out 0.2s; + + @nest .link:hover & { + transform: translateX(4px); + } + + & svg { + margin-top: 6px; + flex-shrink: 0; + } +} + +.linkHeading { + margin: 0 32px 0 0; + composes: g-type-display-6 from global; +} diff --git a/website/components/io-home-feature/index.tsx b/website/components/io-home-feature/index.tsx new file mode 100644 index 000000000..f3e910fcd --- /dev/null +++ b/website/components/io-home-feature/index.tsx @@ -0,0 +1,80 @@ +import * as React from 'react' +import Image from 'next/image' +import Link from 'next/link' +import { IconArrowRight16 } from '@hashicorp/flight-icons/svg-react/arrow-right-16' +import s from './style.module.css' + +export interface IoHomeFeatureProps { + isInternalLink: (link: string) => boolean + link?: string + image: { + url: string + alt: string + } + heading: string + description: string +} + +export default function IoHomeFeature({ + isInternalLink, + link, + image, + heading, + description, +}: IoHomeFeatureProps): React.ReactElement { + return ( + +
+ {image.alt} +
+
+

{heading}

+

{description}

+ {link ? ( + + Learn more{' '} + + + + + ) : null} +
+
+ ) +} + +interface IoHomeFeatureWrapProps { + isInternalLink: (link: string) => boolean + href: string + children: React.ReactNode +} + +function IoHomeFeatureWrap({ + isInternalLink, + href, + children, +}: IoHomeFeatureWrapProps) { + if (!href) { + return
{children}
+ } + + if (isInternalLink(href)) { + return ( + + {children} + + ) + } + + return ( + + {children} + + ) +} diff --git a/website/components/io-home-feature/style.module.css b/website/components/io-home-feature/style.module.css new file mode 100644 index 000000000..70c2cc510 --- /dev/null +++ b/website/components/io-home-feature/style.module.css @@ -0,0 +1,79 @@ +.feature { + display: flex; + align-items: center; + flex-direction: column; + padding: 32px; + gap: 24px 64px; + border-radius: 6px; + background-color: #f9f9fa; + color: var(--black); + box-shadow: 0 2px 3px rgba(101, 106, 118, 0.1), + 0 8px 16px -10px rgba(101, 106, 118, 0.2); + + @media (--medium-up) { + flex-direction: row; + } +} + +.featureLink { + transition: box-shadow ease-in-out 0.2s; + + &:hover { + box-shadow: 0 2px 3px rgba(101, 106, 118, 0.15), + 0 16px 16px -10px rgba(101, 106, 118, 0.2); + } +} + +.featureMedia { + flex-shrink: 0; + display: flex; + width: 100%; + border-radius: 6px; + overflow: hidden; + border: 1px solid var(--gray-5); + + @media (--medium-up) { + width: 300px; + } + + @media (--large) { + width: 400px; + } + + & > * { + width: 100%; + } +} + +.featureContent { + max-width: 520px; +} + +.featureHeading { + margin: 0; + composes: g-type-display-4 from global; +} + +.featureDescription { + margin: 8px 0 24px; + composes: g-type-body-small from global; + color: var(--gray-3); +} + +.featureCta { + display: inline-flex; + align-items: center; + + & > span { + display: flex; + margin-left: 12px; + + & > svg { + transition: transform 0.2s; + } + } + + @nest .feature:hover & span svg { + transform: translateX(2px); + } +} diff --git a/website/components/io-home-hero/index.tsx b/website/components/io-home-hero/index.tsx new file mode 100644 index 000000000..fabaafd37 --- /dev/null +++ b/website/components/io-home-hero/index.tsx @@ -0,0 +1,135 @@ +import * as React from 'react' +import { Products } from '@hashicorp/platform-product-meta' +import Button from '@hashicorp/react-button' +import classNames from 'classnames' +import s from './style.module.css' + +interface IoHomeHeroProps { + pattern: string + brand: Products | 'neutral' + heading: string + description: string + ctas: Array<{ + title: string + link: string + }> + cards: Array +} + +export default function IoHomeHero({ + pattern, + brand, + heading, + description, + ctas, + cards, +}: IoHomeHeroProps) { + const [loaded, setLoaded] = React.useState(false) + + React.useEffect(() => { + setTimeout(() => { + setLoaded(true) + }, 250) + }, []) + + return ( +
+ +
+
+

{heading}

+

{description}

+ {ctas && ( +
+ {ctas.map((cta, index) => { + return ( +
+ )} +
+ {cards && ( +
+ {cards.map((card, index) => { + return ( + + ) + })} +
+ )} +
+
+ ) +} + +interface IoHomeHeroCardProps { + index?: number + heading: string + description: string + cta: { + title: string + link: string + brand?: 'neutral' | Products + } + subText: string +} + +function IoHomeHeroCard({ + index, + heading, + description, + cta, + subText, +}: IoHomeHeroCardProps): React.ReactElement { + return ( +
+

{heading}

+

{description}

+
+ ) +} diff --git a/website/components/io-home-hero/style.module.css b/website/components/io-home-hero/style.module.css new file mode 100644 index 000000000..c7f47026f --- /dev/null +++ b/website/components/io-home-hero/style.module.css @@ -0,0 +1,148 @@ +.hero { + position: relative; + padding-top: 64px; + padding-bottom: 64px; + background: linear-gradient(180deg, #f9f9fa 0%, #fff 28.22%, #fff 100%); + + @media (--medium-up) { + padding-top: 128px; + padding-bottom: 128px; + } +} + +.pattern { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + max-width: 1600px; + width: 100%; + margin: auto; + + @media (--medium-up) { + background-image: var(--pattern); + background-repeat: no-repeat; + background-position: top right; + } +} + +.container { + --columns: 1; + + composes: g-grid-container from global; + display: grid; + grid-template-columns: repeat(var(--columns), minmax(0, 1fr)); + gap: 48px 32px; + + @media (--medium-up) { + --columns: 12; + } +} + +.content { + grid-column: 1 / -1; + + @media (--medium-up) { + grid-column: 1 / 6; + } + + & > * { + max-width: 415px; + } +} + +.heading { + margin: 0; + composes: g-type-display-1 from global; +} + +.description { + margin: 8px 0 0; + composes: g-type-body-small from global; + color: var(--gray-3); +} + +.ctas { + margin-top: 24px; + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 24px; +} + +.cards { + --columns: 1; + + grid-column: 1 / -1; + align-self: start; + display: grid; + grid-template-columns: repeat(var(--columns), minmax(0, 1fr)); + gap: 32px; + + @media (min-width: 600px) { + --columns: 2; + } + + @media (--medium-up) { + --columns: 1; + + grid-column: 7 / -1; + } + + @media (--large) { + --columns: 2; + + grid-column: 6 / -1; + } +} + +.card { + --token-radius: 6px; + --token-elevation-mid: 0 2px 3px rgba(101, 106, 118, 0.1), + 0 8px 16px -10px rgba(101, 106, 118, 0.2); + + opacity: 0; + padding: 40px 32px; + display: flex; + align-items: flex-start; + flex-direction: column; + flex-grow: 1; + background-color: var(--white); + border-radius: var(--token-radius); + box-shadow: 0 0 0 1px rgba(38, 53, 61, 0.1), var(--token-elevation-mid); + + @nest .loaded & { + animation-name: slideIn; + animation-duration: 0.5s; + animation-delay: calc(var(--index) * 0.1s); + animation-fill-mode: forwards; + } +} + +.cardHeading { + margin: 0; + composes: g-type-display-4 from global; +} + +.cardDescription { + margin: 8px 0 16px; + composes: g-type-display-6 from global; +} + +.cardSubText { + margin: 32px 0 0; + composes: g-type-body-small from global; + color: var(--gray-3); +} + +@keyframes slideIn { + from { + opacity: 0; + transform: translateY(50px); + } + to { + opacity: 1; + transform: translateY(0); + } +} diff --git a/website/components/io-home-in-practice/index.tsx b/website/components/io-home-in-practice/index.tsx new file mode 100644 index 000000000..6e145b2e9 --- /dev/null +++ b/website/components/io-home-in-practice/index.tsx @@ -0,0 +1,86 @@ +import * as React from 'react' +import Image from 'next/image' +import Button from '@hashicorp/react-button' +import { Products } from '@hashicorp/platform-product-meta' +import { IoCardProps } from 'components/io-card' +import IoCardContainer from 'components/io-card-container' +import s from './style.module.css' + +interface IoHomeInPracticeProps { + brand: Products + pattern: string + heading: string + description: string + cards: Array + cta: { + heading: string + description: string + link: string + image: { + url: string + alt: string + width: number + height: number + } + } +} + +export default function IoHomeInPractice({ + brand, + pattern, + heading, + description, + cards, + cta, +}: IoHomeInPracticeProps) { + return ( +
+
+ + + {cta.heading ? ( +
+
+

{cta.heading}

+ {cta.description ? ( +

{cta.description}

+ ) : null} + {cta.link ? ( +
+ {cta.image?.url ? ( +
+ {cta.image.alt} +
+ ) : null} +
+ ) : null} +
+
+ ) +} diff --git a/website/components/io-home-in-practice/style.module.css b/website/components/io-home-in-practice/style.module.css new file mode 100644 index 000000000..13ed2bfd9 --- /dev/null +++ b/website/components/io-home-in-practice/style.module.css @@ -0,0 +1,98 @@ +.inPractice { + position: relative; + margin: 60px auto; + padding: 64px 0; + max-width: 1600px; + + @media (--medium-up) { + padding: 80px 0; + margin: 120px auto; + } + + &::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: var(--black); + background-image: var(--pattern); + background-repeat: no-repeat; + background-size: 50%; + background-position: top 200px left; + + @media (--large) { + border-radius: 6px; + left: 24px; + right: 24px; + background-size: 35%; + background-position: top 64px left; + } + } +} + +.container { + composes: g-grid-container from global; +} + +.inPracticeCta { + --columns: 1; + + position: relative; + margin-top: 64px; + display: grid; + grid-template-columns: repeat(var(--columns), minmax(0, 1fr)); + gap: 64px 32px; + + @media (--medium-up) { + --columns: 12; + } + + &::before { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 100%; + bottom: -64px; + background-image: radial-gradient( + 42.33% 42.33% at 50% 100%, + #363638 0%, + #000 100% + ); + + @media (--medium-up) { + bottom: -80px; + } + } +} + +.inPracticeCtaContent { + position: relative; + grid-column: 1 / -1; + + @media (--medium-up) { + grid-column: 1 / 5; + } +} + +.inPracticeCtaMedia { + grid-column: 1 / -1; + + @media (--medium-up) { + grid-column: 6 / -1; + } +} + +.inPracticeCtaHeading { + margin: 0; + color: var(--white); + composes: g-type-display-3 from global; +} + +.inPracticeCtaDescription { + margin: 8px 0 32px; + color: var(--gray-5); + composes: g-type-body from global; +} diff --git a/website/components/io-home-intro/index.tsx b/website/components/io-home-intro/index.tsx new file mode 100644 index 000000000..c8081b4f7 --- /dev/null +++ b/website/components/io-home-intro/index.tsx @@ -0,0 +1,155 @@ +import * as React from 'react' +import Image from 'next/image' +import classNames from 'classnames' +import { Products } from '@hashicorp/platform-product-meta' +import Button from '@hashicorp/react-button' +import IoVideoCallout, { + IoHomeVideoCalloutProps, +} from 'components/io-video-callout' +import IoHomeFeature, { IoHomeFeatureProps } from 'components/io-home-feature' +import s from './style.module.css' + +interface IoHomeIntroProps { + isInternalLink: (link: string) => boolean + brand: Products + heading: string + description: string + features?: Array + offerings?: { + image: { + src: string + width: number + height: number + alt: string + } + list: Array<{ + heading: string + description: string + }> + cta?: { + title: string + link: string + } + } + video?: IoHomeVideoCalloutProps +} + +export default function IoHomeIntro({ + isInternalLink, + brand, + heading, + description, + features, + offerings, + video, +}: IoHomeIntroProps) { + return ( +
+
+
+
+

{heading}

+

{description}

+
+
+
+ + {features ? ( +
    + {features.map((feature, index) => { + return ( + // Index is stable + // eslint-disable-next-line react/no-array-index-key +
  • +
    + +
    +
  • + ) + })} +
+ ) : null} + + {offerings ? ( +
+ {offerings.image ? ( +
+ {offerings.image.alt} +
+ ) : null} +
+
    + {offerings.list.map((offering, index) => { + return ( + // Index is stable + // eslint-disable-next-line react/no-array-index-key +
  • +

    + {offering.heading} +

    +

    + {offering.description} +

    +
  • + ) + })} +
+ {offerings.cta ? ( +
+
+ ) : null} +
+
+ ) : null} + + {video ? ( +
+ +
+ ) : null} +
+ ) +} diff --git a/website/components/io-home-intro/style.module.css b/website/components/io-home-intro/style.module.css new file mode 100644 index 000000000..6227a49ba --- /dev/null +++ b/website/components/io-home-intro/style.module.css @@ -0,0 +1,169 @@ +.root { + position: relative; + margin-bottom: 60px; + + @media (--medium-up) { + margin-bottom: 120px; + } + + &.withOfferings:not(.withFeatures)::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-image: radial-gradient( + 93.55% 93.55% at 50% 0%, + var(--gray-6) 0%, + rgba(242, 242, 243, 0) 100% + ); + + @media (--large) { + border-radius: 6px; + left: 24px; + right: 24px; + } + } +} + +.container { + composes: g-grid-container from global; +} + +.header { + padding-top: 64px; + padding-bottom: 64px; + text-align: center; + + @nest .withFeatures & { + background-color: var(--brand); + } + + @nest .withFeatures.consul & { + color: var(--white); + } +} + +.headerInner { + margin: auto; + + @media (--medium-up) { + max-width: calc(100% * 7 / 12); + } +} + +.heading { + margin: 0; + composes: g-type-display-2 from global; +} + +.description { + margin: 24px 0 0; + composes: g-type-body-large from global; + + @nest .withOfferings:not(.withFeatures) & { + color: var(--gray-3); + } +} + +/* + * Features + */ + +.features { + list-style: none; + margin: 0; + padding: 0; + display: grid; + gap: 32px; + + & li:first-of-type { + background-image: linear-gradient( + to bottom, + var(--brand) 50%, + var(--white) 50% + ); + } +} + +/* + * Offerings + */ + +.offerings { + --columns: 1; + + composes: g-grid-container from global; + display: grid; + grid-template-columns: repeat(var(--columns), minmax(0, 1fr)); + gap: 64px 32px; + + @media (--medium-up) { + --columns: 12; + } + + @nest .features + & { + margin-top: 60px; + + @media (--medium-up) { + margin-top: 120px; + } + } +} + +.offeringsMedia { + grid-column: 1 / -1; + + @media (--medium-up) { + grid-column: 1 / 6; + } +} + +.offeringsContent { + grid-column: 1 / -1; + + @media (--medium-up) { + grid-column: 7 / -1; + } +} + +.offeringsList { + list-style: none; + margin: 0; + padding: 0; + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 32px; + + @media (--small) { + grid-template-columns: repeat(1, 1fr); + } +} + +.offeringsListHeading { + margin: 0; + composes: g-type-display-4 from global; +} + +.offeringsListDescription { + margin: 16px 0 0; + composes: g-type-body-small from global; +} + +.offeringsCta { + margin-top: 48px; +} + +/* + * Video + */ + +.video { + margin-top: 60px; + composes: g-grid-container from global; + + @media (--medium-up) { + margin-top: 120px; + } +} diff --git a/website/components/io-home-pre-footer/index.tsx b/website/components/io-home-pre-footer/index.tsx new file mode 100644 index 000000000..98127443d --- /dev/null +++ b/website/components/io-home-pre-footer/index.tsx @@ -0,0 +1,79 @@ +import * as React from 'react' +import classNames from 'classnames' +import { Products } from '@hashicorp/platform-product-meta' +import { IconArrowRight16 } from '@hashicorp/flight-icons/svg-react/arrow-right-16' +import s from './style.module.css' + +interface IoHomePreFooterProps { + brand: Products + heading: string + description: string + ctas: [IoHomePreFooterCard, IoHomePreFooterCard, IoHomePreFooterCard] +} + +export default function IoHomePreFooter({ + brand, + heading, + description, + ctas, +}: IoHomePreFooterProps) { + return ( +
+
+
+

{heading}

+

{description}

+
+
+ {ctas.map((cta, index) => { + return ( + + ) + })} +
+
+
+ ) +} + +interface IoHomePreFooterCard { + brand?: string + link: string + heading: string + description: string + cta: string +} + +function IoHomePreFooterCard({ + brand, + link, + heading, + description, + cta, +}: IoHomePreFooterCard): React.ReactElement { + return ( + +

{heading}

+

{description}

+ + {cta} + +
+ ) +} diff --git a/website/components/io-home-pre-footer/style.module.css b/website/components/io-home-pre-footer/style.module.css new file mode 100644 index 000000000..1273e2087 --- /dev/null +++ b/website/components/io-home-pre-footer/style.module.css @@ -0,0 +1,122 @@ +.preFooter { + margin: 60px auto; +} + +.container { + --columns: 1; + + composes: g-grid-container from global; + display: grid; + grid-template-columns: repeat(var(--columns), minmax(0, 1fr)); + gap: 32px; + + @media (--medium-up) { + --columns: 12; + } +} + +.content { + grid-column: 1 / -1; + + @media (--medium-up) { + grid-column: 1 / 6; + } + + @media (--large) { + grid-column: 1 / 4; + } +} + +.heading { + margin: 0; + composes: g-type-display-1 from global; +} + +.description { + margin: 24px 0 0; + composes: g-type-body from global; + color: var(--gray-3); +} + +.cards { + grid-column: 1 / -1; + + --columns: 1; + + display: grid; + grid-template-columns: repeat(var(--columns), minmax(0, 1fr)); + gap: 32px; + + @media (--medium-up) { + --columns: 3; + + grid-column: 1 / -1; + } + + @media (--large) { + grid-column: 5 / -1; + } +} + +.card { + display: flex; + flex-direction: column; + flex-grow: 1; + padding: 32px 24px; + background-color: var(--primary); + color: var(--black); + border-radius: 6px; + box-shadow: 0 2px 3px rgba(101, 106, 118, 0.1), + 0 8px 16px -10px rgba(101, 106, 118, 0.2); + transition: ease-in-out 0.2s; + transition-property: box-shadow; + + &:hover { + box-shadow: 0 2px 3px rgba(101, 106, 118, 0.15), + 0 16px 16px -10px rgba(101, 106, 118, 0.2); + } + + &:nth-of-type(1) { + color: var(--white); + + @nest .vault & { + color: var(--black); + } + } + + &:nth-of-type(2) { + background-color: var(--secondary); + } + + &:nth-of-type(3) { + background-color: var(--gray-6); + } +} + +.cardHeading { + margin: 0; + composes: g-type-display-4 from global; +} + +.cardDescription { + margin: 8px 0 0; + padding-bottom: 48px; + color: inherit; + composes: g-type-display-6 from global; +} + +.cardCta { + margin-top: auto; + display: inline-flex; + align-items: center; + composes: g-type-buttons-and-standalone-links from global; + + & svg { + margin-left: 12px; + transition: transform 0.2s; + } + + @nest .card:hover & svg { + transform: translate(2px); + } +} diff --git a/website/components/io-usecase-call-to-action/index.tsx b/website/components/io-usecase-call-to-action/index.tsx new file mode 100644 index 000000000..252be27f1 --- /dev/null +++ b/website/components/io-usecase-call-to-action/index.tsx @@ -0,0 +1,69 @@ +import Image from 'next/image' +import * as React from 'react' +import classNames from 'classnames' +import Button from '@hashicorp/react-button' +import s from './style.module.css' + +interface IoUsecaseCallToActionProps { + brand: string + theme?: 'light' | 'dark' + heading: string + description: string + links: Array<{ + text: string + url: string + }> + pattern: string +} + +export default function IoUsecaseCallToAction({ + brand, + theme, + heading, + description, + links, + pattern, +}: IoUsecaseCallToActionProps): React.ReactElement { + return ( +
+

{heading}

+
+

{description}

+
+ {links.map((link, index) => { + return ( +
+
+
+ +
+
+ ) +} diff --git a/website/components/io-usecase-call-to-action/style.module.css b/website/components/io-usecase-call-to-action/style.module.css new file mode 100644 index 000000000..1afcb903d --- /dev/null +++ b/website/components/io-usecase-call-to-action/style.module.css @@ -0,0 +1,66 @@ +.callToAction { + --columns: 1; + + position: relative; + display: grid; + grid-template-columns: repeat(var(--columns), minmax(0, 1fr)); + gap: 0 32px; + padding: 32px; + background-color: var(--background-color); + border-radius: 6px; + + &.light { + color: var(--black); + } + + &.dark { + color: var(--white); + } + + @media (--medium-up) { + --columns: 12; + + padding: 0; + } +} + +.heading { + grid-column: 1 / -1; + margin: 0 0 16px; + composes: g-type-display-3 from global; + + @media (--medium-up) { + grid-column: 1 / 6; + padding: 88px 32px 88px 64px; + } +} + +.content { + grid-column: 1 / -1; + + @media (--medium-up) { + grid-column: 6 / 11; + padding: 88px 0; + } +} + +.description { + margin: 0 0 32px; + composes: g-type-body-large from global; +} + +.links { + display: flex; + flex-wrap: wrap; + gap: 16px 32px; +} + +.pattern { + position: relative; + display: none; + + @media (--medium-up) { + grid-column: 11 / -1; + display: flex; + } +} diff --git a/website/components/io-usecase-customer/index.tsx b/website/components/io-usecase-customer/index.tsx new file mode 100644 index 000000000..288b953b8 --- /dev/null +++ b/website/components/io-usecase-customer/index.tsx @@ -0,0 +1,86 @@ +import * as React from 'react' +import Image from 'next/image' +import Button from '@hashicorp/react-button' +import s from './style.module.css' + +interface IoUsecaseCustomerProps { + media: { + src: string + width: string + height: string + alt: string + } + logo: { + src: string + width: string + height: string + alt: string + } + heading: string + description: string + stats?: Array<{ + value: string + key: string + }> + link: string +} + +export default function IoUsecaseCustomer({ + media, + logo, + heading, + description, + stats, + link, +}: IoUsecaseCustomerProps): React.ReactElement { + return ( +
+
+
+
+ {/* eslint-disable-next-line jsx-a11y/alt-text */} + +
+
+
+
+ {/* eslint-disable-next-line jsx-a11y/alt-text */} + +
+ Customer case study +
+

{heading}

+

{description}

+ {link ? ( +
+
+ ) : null} +
+
+ {stats.length > 0 ? ( +
    + {stats.map(({ key, value }, index) => { + return ( + // Index is stable + // eslint-disable-next-line react/no-array-index-key +
  • +

    {value}

    +

    {key}

    +
  • + ) + })} +
+ ) : null} +
+
+ ) +} diff --git a/website/components/io-usecase-customer/style.module.css b/website/components/io-usecase-customer/style.module.css new file mode 100644 index 000000000..b88156073 --- /dev/null +++ b/website/components/io-usecase-customer/style.module.css @@ -0,0 +1,119 @@ +.customer { + position: relative; + background-color: var(--black); + color: var(--white); + padding-bottom: 64px; + + @media (--medium-up) { + padding-bottom: 132px; + } +} + +.container { + composes: g-grid-container from global; +} + +.columns { + --columns: 1; + + display: grid; + grid-template-columns: repeat(var(--columns), minmax(0, 1fr)); + gap: 64px 32px; + + @media (--medium-up) { + --columns: 12; + } +} + +.media { + margin-top: -64px; + grid-column: 1 / -1; + + @media (--medium-up) { + grid-column: 1 / 7; + } +} + +.content { + grid-column: 1 / -1; + + @media (--medium-up) { + padding-top: 64px; + grid-column: 8 / -1; + } +} + +.eyebrow { + display: flex; + align-items: center; +} + +.eyebrowLogo { + display: flex; + max-width: 120px; +} + +.eyebrowLabel { + padding-top: 8px; + padding-bottom: 8px; + padding-left: 12px; + margin-left: 12px; + border-left: 1px solid var(--gray-5); + align-self: center; + composes: g-type-label-small-strong from global; +} + +.heading { + margin: 32px 0 24px; + composes: g-type-display-2 from global; +} + +.description { + margin: 0; + composes: g-type-body from global; +} + +.cta { + margin-top: 32px; +} + +.stats { + --columns: 1; + + list-style: none; + margin: 64px 0 0; + padding: 0; + display: grid; + grid-template-columns: repeat(var(--columns), minmax(0, 1fr)); + gap: 32px; + + @media (--medium-up) { + --columns: 12; + + margin-top: 132px; + } + + & > li { + border-top: 1px solid var(--gray-2); + grid-column: span 4; + } +} + +.value { + margin: 0; + padding-top: 32px; + font-family: var(--font-display); + font-size: 50px; + font-weight: 700; + line-height: 1; + + @media (--large) { + font-size: 80px; + } +} + +.key { + margin: 12px 0 0; + composes: g-type-display-4 from global; + color: var(--gray-3); +} diff --git a/website/components/io-usecase-hero/index.tsx b/website/components/io-usecase-hero/index.tsx new file mode 100644 index 000000000..4838678e8 --- /dev/null +++ b/website/components/io-usecase-hero/index.tsx @@ -0,0 +1,41 @@ +import * as React from 'react' +import Image from 'next/image' +import s from './style.module.css' + +interface IoUsecaseHeroProps { + eyebrow: string + heading: string + description: string + pattern?: string +} + +export default function IoUsecaseHero({ + eyebrow, + heading, + description, + pattern, +}: IoUsecaseHeroProps): React.ReactElement { + return ( +
+
+
+ {pattern ? ( + + ) : null} +
+
+

{eyebrow}

+

{heading}

+

{description}

+
+
+
+ ) +} diff --git a/website/components/io-usecase-hero/pattern.svg b/website/components/io-usecase-hero/pattern.svg new file mode 100644 index 000000000..f4b1ef3af --- /dev/null +++ b/website/components/io-usecase-hero/pattern.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/website/components/io-usecase-hero/style.module.css b/website/components/io-usecase-hero/style.module.css new file mode 100644 index 000000000..5fd729c8e --- /dev/null +++ b/website/components/io-usecase-hero/style.module.css @@ -0,0 +1,83 @@ +.hero { + position: relative; + max-width: 1600px; + margin-right: auto; + margin-left: auto; + + &::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-image: radial-gradient( + 95.97% 95.97% at 50% 100%, + #f2f2f3 0%, + rgba(242, 242, 243, 0) 100% + ); + + @media (--medium-up) { + border-radius: 6px; + left: 24px; + right: 24px; + } + } +} + +.container { + @media (--medium-up) { + display: grid; + grid-template-columns: 1fr max-content 1fr; + gap: 32px; + } +} + +.pattern { + margin-left: 24px; + transform: translateY(24px); + position: relative; + display: flex; + flex-direction: column; + justify-content: flex-end; + + @media (--small) { + display: none; + } + + @media (--medium) { + & > * { + display: none !important; + } + } +} + +.content { + position: relative; + max-width: 520px; + width: 100%; + margin-right: auto; + margin-left: auto; + padding: 64px 24px; + + @media (--medium-up) { + padding-top: 132px; + padding-bottom: 132px; + } +} + +.eyebrow { + margin: 0; + composes: g-type-label-strong from global; +} + +.heading { + margin: 24px 0; + composes: g-type-display-1 from global; +} + +.description { + margin: 0; + composes: g-type-body-large from global; + color: var(--gray-2); +} diff --git a/website/components/io-usecase-section/index.tsx b/website/components/io-usecase-section/index.tsx new file mode 100644 index 000000000..11ed7917f --- /dev/null +++ b/website/components/io-usecase-section/index.tsx @@ -0,0 +1,81 @@ +import * as React from 'react' +import { Products } from '@hashicorp/platform-product-meta' +import classNames from 'classnames' +import Image from 'next/image' +import Button from '@hashicorp/react-button' +import s from './style.module.css' + +interface IoUsecaseSectionProps { + brand?: Products | 'neutral' + bottomIsFlush?: boolean + eyebrow: string + heading: string + description: string + media?: { + src: string + width: string + height: string + alt: string + } + cta?: { + text: string + link: string + } +} + +export default function IoUsecaseSection({ + brand = 'neutral', + bottomIsFlush = false, + eyebrow, + heading, + description, + media, + cta, +}: IoUsecaseSectionProps): React.ReactElement { + return ( +
+
+

{eyebrow}

+
+
+

{heading}

+ {media?.src ? ( +
+ ) : null} + {cta?.link && cta?.text ? ( +
+
+ ) : null} +
+
+ {media?.src ? ( + // eslint-disable-next-line jsx-a11y/alt-text + + ) : ( +
+ )} +
+
+
+
+ ) +} diff --git a/website/components/io-usecase-section/style.module.css b/website/components/io-usecase-section/style.module.css new file mode 100644 index 000000000..a2b56d1f5 --- /dev/null +++ b/website/components/io-usecase-section/style.module.css @@ -0,0 +1,106 @@ +.section { + position: relative; + max-width: 1600px; + margin-right: auto; + margin-left: auto; + padding-top: 64px; + padding-bottom: 64px; + + @media (--medium-up) { + padding-top: 132px; + padding-bottom: 132px; + } + + & + .section { + padding-bottom: 132px; + + &::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: var(--gray-6); + opacity: 0.4; + + @media (--medium-up) { + border-radius: 6px; + left: 24px; + right: 24px; + } + } + } + + &.isFlush { + padding-bottom: 96px; + + @media (--medium-up) { + padding-bottom: 164px; + } + + &::before { + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; + } + } +} + +.container { + composes: g-grid-container from global; +} + +.columns { + --columns: 1; + + display: grid; + grid-template-columns: repeat(var(--columns), minmax(0, 1fr)); + gap: 32px; + + @media (--medium-up) { + --columns: 12; + } +} + +.column { + &:nth-child(1) { + @media (--medium-up) { + grid-column: 1 / 7; + } + } + + &:nth-child(2) { + @media (--medium-up) { + grid-column: 8 / -1; + padding-top: 16px; + } + } +} + +.eyebrow { + margin: 0; + composes: g-type-display-5 from global; +} + +.heading { + margin: 16px 0 32px; + padding-bottom: 32px; + composes: g-type-display-3 from global; + border-bottom: 1px solid var(--black); +} + +.description { + composes: g-type-body from global; + + & > p { + margin: 0; + + & + p { + margin-top: 16px; + } + } +} + +.cta { + margin-top: 32px; +} diff --git a/website/components/io-video-callout/index.tsx b/website/components/io-video-callout/index.tsx new file mode 100644 index 000000000..7889348d8 --- /dev/null +++ b/website/components/io-video-callout/index.tsx @@ -0,0 +1,80 @@ +import * as React from 'react' +import Image from 'next/image' +import ReactPlayer from 'react-player' +import VisuallyHidden from '@reach/visually-hidden' +import IoDialog from 'components/io-dialog' +import PlayIcon from './play-icon' +import s from './style.module.css' + +export interface IoHomeVideoCalloutProps { + youtubeId: string + thumbnail: string + heading: string + description: string + person: { + avatar: string + name: string + description: string + } +} + +export default function IoVideoCallout({ + youtubeId, + thumbnail, + heading, + description, + person, +}: IoHomeVideoCalloutProps): React.ReactElement { + const [showDialog, setShowDialog] = React.useState(false) + const showVideo = () => setShowDialog(true) + const hideVideo = () => setShowDialog(false) + return ( + <> +
+ +
+

{heading}

+

{description}

+ {person && ( +
+ {person.avatar ? ( +
+ {`${person.name} +
+ ) : null} +
+

{person.name}

+

{person.description}

+
+
+ )} +
+
+ +

{heading}

+
+ +
+
+ + ) +} diff --git a/website/components/io-video-callout/play-icon.tsx b/website/components/io-video-callout/play-icon.tsx new file mode 100644 index 000000000..37395ba2b --- /dev/null +++ b/website/components/io-video-callout/play-icon.tsx @@ -0,0 +1,23 @@ +import * as React from 'react' + +export default function PlayIcon(): React.ReactElement { + return ( + + + + + ) +} diff --git a/website/components/io-video-callout/style.module.css b/website/components/io-video-callout/style.module.css new file mode 100644 index 000000000..815601ff0 --- /dev/null +++ b/website/components/io-video-callout/style.module.css @@ -0,0 +1,128 @@ +.videoCallout { + --columns: 1; + + margin: 0; + display: grid; + grid-template-columns: repeat(var(--columns), minmax(0, 1fr)); + gap: 32px; + background-color: var(--black); + border-radius: 6px; + overflow: hidden; + + @media (--medium-up) { + --columns: 12; + } +} + +.thumbnail { + position: relative; + display: grid; + place-items: center; + grid-column: 1 / -1; + background-color: transparent; + border: 0; + cursor: pointer; + padding: 96px 32px; + min-height: 300px; + + @media (--medium-up) { + grid-column: 1 / 7; + } + + @media (--large) { + grid-column: 1 / 9; + } + + & > svg { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + z-index: 1; + + @media (--small) { + width: 52px; + height: 52px; + } + } + + &::after { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: #000; + opacity: 0.45; + transition: opacity ease-in-out 0.2s; + } + + &:hover::after { + opacity: 0.2; + } +} + +.content { + padding: 32px; + grid-column: 1 / -1; + + @media (--medium-up) { + padding: 80px 32px; + grid-column: 7 / -1; + } + + @media (--large) { + grid-column: 9 / -1; + } +} + +.heading { + margin: 0; + composes: g-type-display-4 from global; + color: var(--white); +} + +.description { + margin: 8px 0 0; + composes: g-type-body-small from global; + color: var(--white); +} + +.person { + margin-top: 64px; + display: flex; + align-items: center; + gap: 16px; +} + +.personThumbnail { + display: flex; + border-radius: 9999px; + overflow: hidden; +} + +.personName { + margin: 0; + composes: g-type-body-strong from global; + color: var(--white); +} + +.personDescription { + margin: 4px 0 0; + composes: g-type-label-strong from global; + color: var(--gray-3); +} + +.videoHeading { + margin-top: 0; + margin-bottom: 32px; + padding-right: 100px; + composes: g-type-display-4 from global; +} + +.video { + position: relative; + background-color: var(--gray-2); + aspect-ratio: 16 / 9; +} diff --git a/website/components/subnav/index.jsx b/website/components/subnav/index.jsx index 219545e33..4a70b89c1 100644 --- a/website/components/subnav/index.jsx +++ b/website/components/subnav/index.jsx @@ -1,14 +1,15 @@ import Subnav from '@hashicorp/react-subnav' -import subnavItems from '../../data/subnav' import { useRouter } from 'next/router' +import s from './style.module.css' -export default function ConsulSubnav() { +export default function ConsulSubnav({ menuItems }) { const router = useRouter() return ( diff --git a/website/components/subnav/style.module.css b/website/components/subnav/style.module.css new file mode 100644 index 000000000..5cb3cbccd --- /dev/null +++ b/website/components/subnav/style.module.css @@ -0,0 +1,3 @@ +.subnav { + border-top: 1px solid transparent; +} diff --git a/website/data/subnav.js b/website/data/subnav.js deleted file mode 100644 index cc14febd2..000000000 --- a/website/data/subnav.js +++ /dev/null @@ -1,56 +0,0 @@ -export default [ - { text: 'Overview', url: '/' }, - { - text: 'Use Cases', - submenu: [ - { - text: 'Service Discovery and Health Checking', - url: '/use-cases/service-discovery-and-health-checking', - }, - { - text: 'Network Infrastructure Automation', - url: '/use-cases/network-infrastructure-automation', - }, - { - text: 'Multi-Platform Service Mesh', - url: '/use-cases/multi-platform-service-mesh', - }, - { - text: 'Consul on Kubernetes', - url: '/consul-on-kubernetes', - }, - ], - }, - { - text: 'Enterprise', - url: - 'https://www.hashicorp.com/products/consul/?utm_source=oss&utm_medium=header-nav&utm_campaign=consul', - type: 'outbound', - }, - 'divider', - { - text: 'Tutorials', - url: 'https://learn.hashicorp.com/consul', - type: 'outbound', - }, - { - text: 'Docs', - url: '/docs', - type: 'inbound', - }, - { - text: 'API', - url: '/api-docs', - type: 'inbound', - }, - { - text: 'CLI', - url: '/commands', - type: 'inbound,', - }, - { - text: 'Community', - url: '/community', - type: 'inbound', - }, -] diff --git a/website/layouts/standard/index.tsx b/website/layouts/standard/index.tsx new file mode 100644 index 000000000..38bb3f8c5 --- /dev/null +++ b/website/layouts/standard/index.tsx @@ -0,0 +1,76 @@ +import query from './query.graphql' +import ProductSubnav from 'components/subnav' +import Footer from 'components/footer' +import { open } from '@hashicorp/react-consent-manager' + +export default function StandardLayout(props: Props): React.ReactElement { + const { useCaseNavItems } = props.data + + return ( + <> + { + return { + text: item.text, + url: `/use-cases/${item.url}`, + } + }), + ].sort((a, b) => a.text.localeCompare(b.text)), + }, + { + text: 'Enterprise', + url: + 'https://www.hashicorp.com/products/consul/?utm_source=oss&utm_medium=header-nav&utm_campaign=consul', + type: 'outbound', + }, + 'divider', + { + text: 'Tutorials', + url: 'https://learn.hashicorp.com/consul', + type: 'outbound', + }, + { + text: 'Docs', + url: '/docs', + type: 'inbound', + }, + { + text: 'API', + url: '/api-docs', + type: 'inbound', + }, + { + text: 'CLI', + url: '/commands', + type: 'inbound,', + }, + { + text: 'Community', + url: '/community', + type: 'inbound', + }, + ]} + /> + {props.children} +