diff --git a/packages/paste-website/src/components/customization-landing-page/WhyPaste.tsx b/packages/paste-website/src/components/customization-landing-page/WhyPaste.tsx index ffdbedef22..c1e33fe631 100644 --- a/packages/paste-website/src/components/customization-landing-page/WhyPaste.tsx +++ b/packages/paste-website/src/components/customization-landing-page/WhyPaste.tsx @@ -7,56 +7,12 @@ import Image from "next/image"; import { DoodleLoopArrowLarge } from "../../assets/illustrations/DoodleLoopArrowLarge"; import WhyPasteMobile from "../../assets/images/customization/why-paste-mobile.png"; import WhyPasteImg from "../../assets/images/customization/why-paste.png"; -import { useDarkModeContext } from "../../context/DarkModeContext"; -import { useSlantedSkew } from "../SlantedBackgroundGradient"; import { A11yIcon } from "../icons/A11yIcon"; import { CustomizableIcon } from "../icons/CustomizableIcon"; import { ThemableIcon } from "../icons/ThemableIcon"; import { LandingPageSection, LandingPageSectionContent } from "./LandingPageLayoutUtils"; import { ReasonBlock } from "./ReasonBlock"; -const WhyPasteTopAngle = (): JSX.Element => { - const [skewOffset] = useSlantedSkew(); - const { theme } = useDarkModeContext(); - - return ( - - ); -}; - -const WhyPasteBottomAngle = (): JSX.Element => { - const [skewOffset] = useSlantedSkew(-0.35); - const { theme } = useDarkModeContext(); - - return ( - - ); -}; - export const WhyPaste = (): JSX.Element => { return ( { maxWidth="96%" marginX="auto" > - - diff --git a/packages/paste-website/src/components/homepage/Accessibility.tsx b/packages/paste-website/src/components/homepage/Accessibility.tsx index 8750983fec..6c935d2e9a 100644 --- a/packages/paste-website/src/components/homepage/Accessibility.tsx +++ b/packages/paste-website/src/components/homepage/Accessibility.tsx @@ -19,7 +19,7 @@ import { Tab, TabList, TabPanel, TabPanels, Tabs } from "@twilio-paste/tabs"; import { useUID } from "@twilio-paste/uid-library"; import * as React from "react"; -import { SITE_CONTENT_MAX_WIDTH } from "../../constants"; +import { HOMEPAGE_SITE_CONTENT_MAX_WIDTH } from "../../constants"; import { BouncyAnchor } from "./BouncyAnchor"; import { SectionSeparator } from "./SectionSeparator"; @@ -32,7 +32,13 @@ const Accessibility: React.FC = (): React.ReactElement => { return ( - + Accessibility { Accessibility is more than just color contrast. We believe in designing for each user, not just “all users”. We build with these considerations in mind: - - - Screen magnification, voice dictation, color blindness, and those who require help with fine motor - control. - - Semantic HTML to better communicate with assistive technologies. - UI controls that are designed to be instantly recognizable and easy to see. - - Keyboard navigation and focus management to allow task completion through a variety of input devices. - - + + + Screen magnification, voice dictation, color blindness, and those who require help with fine motor + control. + + Semantic HTML to better communicate with assistive technologies. + UI controls that are designed to be instantly recognizable and easy to see. + + Keyboard navigation and focus management to allow task completion through a variety of input devices. + + diff --git a/packages/paste-website/src/components/homepage/CommunityOfBuilders.tsx b/packages/paste-website/src/components/homepage/CommunityOfBuilders.tsx index 7963069bbb..d1756d8033 100644 --- a/packages/paste-website/src/components/homepage/CommunityOfBuilders.tsx +++ b/packages/paste-website/src/components/homepage/CommunityOfBuilders.tsx @@ -5,7 +5,7 @@ import Image from "next/image"; import * as React from "react"; import DesignTool from "../../assets/illustrations/illo_design-tool.svg"; -import { SITE_CONTENT_MAX_WIDTH } from "../../constants"; +import { HOMEPAGE_SITE_CONTENT_MAX_WIDTH } from "../../constants"; const StatBox: React.FC<{ stat: string; description: string }> = ({ stat, description }): React.ReactElement => { return ( @@ -36,7 +36,7 @@ const CommunityOfBuilders: React.FC = (): React.ReactElement => { display="flex" flexDirection="column" width="100%" - maxWidth={SITE_CONTENT_MAX_WIDTH} + maxWidth={HOMEPAGE_SITE_CONTENT_MAX_WIDTH} backgroundColor="colorBackgroundWarningWeakest" paddingY="space170" // eslint-disable-next-line @typescript-eslint/ban-ts-comment diff --git a/packages/paste-website/src/components/homepage/DesignEfficiency.tsx b/packages/paste-website/src/components/homepage/DesignEfficiency.tsx index 74cabde30e..29e133c50b 100644 --- a/packages/paste-website/src/components/homepage/DesignEfficiency.tsx +++ b/packages/paste-website/src/components/homepage/DesignEfficiency.tsx @@ -7,13 +7,19 @@ import Image from "next/image"; import * as React from "react"; import TokenColors from "../../assets/illustrations/token_colors.svg"; -import { SITE_CONTENT_MAX_WIDTH } from "../../constants"; +import { HOMEPAGE_SITE_CONTENT_MAX_WIDTH } from "../../constants"; import { SectionSeparator } from "./SectionSeparator"; const DesignEfficiency: React.FC = (): React.ReactElement => { return ( - + Design efficiency diff --git a/packages/paste-website/src/components/homepage/NewHomeHero.tsx b/packages/paste-website/src/components/homepage/NewHomeHero.tsx index 50aa4b7459..89888c30e0 100644 --- a/packages/paste-website/src/components/homepage/NewHomeHero.tsx +++ b/packages/paste-website/src/components/homepage/NewHomeHero.tsx @@ -3,13 +3,13 @@ import { DisplayHeading } from "@twilio-paste/display-heading"; import { Text } from "@twilio-paste/text"; import { useTheme } from "@twilio-paste/theme"; -import { SITE_CONTENT_MAX_WIDTH } from "../../constants"; +import { HOMEPAGE_SITE_CONTENT_MAX_WIDTH } from "../../constants"; import CircleIcon from "../icons/CircleIcon"; import { BouncyAnchor } from "./BouncyAnchor"; import { SearchBox } from "./SearchBox"; import { ComponentShowcase } from "./component-showcase"; -const NewHomeHero = (): JSX.Element => { +const HomeHero = (): JSX.Element => { const theme = useTheme(); return ( @@ -18,10 +18,10 @@ const NewHomeHero = (): JSX.Element => { position="relative" display="grid" gridTemplateColumns="600px min-content" - maxWidth={SITE_CONTENT_MAX_WIDTH} + maxWidth={HOMEPAGE_SITE_CONTENT_MAX_WIDTH} marginLeft="auto" marginRight="auto" - overflow="hidden" + overflow="visible" element="HOME_HERO" > { ); }; -export { NewHomeHero }; +export { HomeHero }; diff --git a/packages/paste-website/src/components/homepage/NewSection.tsx b/packages/paste-website/src/components/homepage/NewSection.tsx index 4aec90ab8d..dc9266e981 100644 --- a/packages/paste-website/src/components/homepage/NewSection.tsx +++ b/packages/paste-website/src/components/homepage/NewSection.tsx @@ -2,7 +2,7 @@ import { Anchor } from "@twilio-paste/anchor"; import { Box } from "@twilio-paste/box"; import * as React from "react"; -import { SITE_CONTENT_MAX_WIDTH } from "../../constants"; +import { HOMEPAGE_SITE_CONTENT_MAX_WIDTH } from "../../constants"; import { WhatsNew } from "./WhatsNew"; const NewSection: React.FC = (): React.ReactElement => { @@ -16,7 +16,7 @@ const NewSection: React.FC = (): React.ReactElement => { justifyContent="space-between" paddingY="space200" width="size120" - maxWidth={SITE_CONTENT_MAX_WIDTH} + maxWidth={HOMEPAGE_SITE_CONTENT_MAX_WIDTH} > We're hiring a Product Designer! Apply here diff --git a/packages/paste-website/src/components/homepage/Templates.tsx b/packages/paste-website/src/components/homepage/Templates.tsx index a05852fb41..b14f96e8d4 100644 --- a/packages/paste-website/src/components/homepage/Templates.tsx +++ b/packages/paste-website/src/components/homepage/Templates.tsx @@ -5,12 +5,12 @@ import { Pagination, PaginationArrow, PaginationItems } from "@twilio-paste/pagi import { Text } from "@twilio-paste/text"; import * as React from "react"; -import { SITE_CONTENT_MAX_WIDTH } from "../../constants"; +import { HOMEPAGE_SITE_CONTENT_MAX_WIDTH } from "../../constants"; const Templates: React.FC = (): React.ReactElement => { return ( - + Explore our templates diff --git a/packages/paste-website/src/components/homepage/Themeable.tsx b/packages/paste-website/src/components/homepage/Themeable.tsx index 6000daed60..cfbf838a94 100644 --- a/packages/paste-website/src/components/homepage/Themeable.tsx +++ b/packages/paste-website/src/components/homepage/Themeable.tsx @@ -8,7 +8,7 @@ import * as React from "react"; import FigmaLogo from "../../assets/illustrations/figma-logo.svg"; import ReactLogo from "../../assets/illustrations/react-logo.svg"; import TypescriptLogo from "../../assets/illustrations/ts-logo.svg"; -import { SITE_CONTENT_MAX_WIDTH } from "../../constants"; +import { HOMEPAGE_SITE_CONTENT_MAX_WIDTH } from "../../constants"; import { SectionSeparator } from "./SectionSeparator"; const Themeable: React.FC = (): React.ReactElement => { @@ -23,7 +23,7 @@ const Themeable: React.FC = (): React.ReactElement => { justifyItems="center" rowGap="space100" width="100%" - maxWidth={SITE_CONTENT_MAX_WIDTH} + maxWidth={HOMEPAGE_SITE_CONTENT_MAX_WIDTH} > Themeable and composable diff --git a/packages/paste-website/src/components/icons/NewHeartDoodleIcon.tsx b/packages/paste-website/src/components/icons/NewHeartDoodleIcon.tsx new file mode 100644 index 0000000000..fbf3fc066e --- /dev/null +++ b/packages/paste-website/src/components/icons/NewHeartDoodleIcon.tsx @@ -0,0 +1,36 @@ +import { useUID } from "@twilio-paste/uid-library"; +import * as React from "react"; + +export interface HeartDoodleIconProps { + className?: string; + size?: number; + color?: string; + title?: string; + decorative?: boolean; +} + +const HeartDoodleIcon = React.memo( + ({ title = "Heart Doodle Icon", decorative = true, className, color, size }: HeartDoodleIconProps) => { + const titleId = useUID(); + return ( + + + {title ? {title} : null} + + + + ); + }, +); + +export default HeartDoodleIcon; diff --git a/packages/paste-website/src/components/site-wrapper-new/SiteBody.tsx b/packages/paste-website/src/components/site-wrapper-new/SiteBody.tsx new file mode 100644 index 0000000000..cba90c6e04 --- /dev/null +++ b/packages/paste-website/src/components/site-wrapper-new/SiteBody.tsx @@ -0,0 +1,140 @@ +import { Box } from "@twilio-paste/box"; +import { LogoPasteIcon } from "@twilio-paste/icons/esm/LogoPasteIcon"; +import { + Sidebar, + SidebarBody, + SidebarCollapseButton, + SidebarFooter, + SidebarHeader, + SidebarHeaderIconButton, + SidebarHeaderLabel, + SidebarPushContentWrapper, +} from "@twilio-paste/sidebar"; +import { type CSSObject, StylingGlobals } from "@twilio-paste/styling-library"; +import { useWindowSize } from "@twilio-paste/utils"; +import { useRouter } from "next/router"; +import * as React from "react"; + +import { + PASTE_DOCS_CONTENT_AREA, + PASTE_DOCS_SIDEBAR_NAV, + PASTE_DOCS_TOPBAR, + SITE_TOPBAR_HEIGHT, + TOKEN_LIST_PAGE_REGEX, + TOKEN_STICKY_FILTER_HEIGHT, +} from "../../constants"; +import { SiteMain } from "./SiteMain"; +import { SidebarNavigation } from "./sidebar/SidebarNavigation"; +import { SiteFooter } from "./site-footer"; +import { SiteHeader } from "./site-header"; + +// height of the topbar plus a little extra whitespace +const defaultScrollOffset = `calc(${SITE_TOPBAR_HEIGHT}px + 24px)`; + +const GlobalScrollBehaviourStyles = (scrollOffset = defaultScrollOffset): CSSObject => ({ + html: { + scrollBehavior: "smooth", + // compensate for the sticky topbar, this offset allows for the jump to links to keep the headings in view when you jump to the section + scrollPaddingTop: scrollOffset, + }, +}); + +export const SiteBody: React.FC = ({ children }) => { + const { breakpointIndex } = useWindowSize(); + const router = useRouter(); + // sidebar is not collapsed by default, most common use case for desktop viewing + const [sidebarCollapsed, setSidebarCollapsed] = React.useState(false); + const [mounted, setMounted] = React.useState(false); + const mobileSidebarCloseButton = React.useRef(null); + + /** + * Handle responsive sidebar collapse state for small screen sizes and initial render for SSR + */ + React.useEffect(() => { + // no breakpoints? No Javascript, do nothing, this is initial render + if (breakpointIndex === undefined) { + return; + } + // if the screen is small, collapse the sidebar on mount + if (breakpointIndex === 0) { + setSidebarCollapsed(true); + } else { + setSidebarCollapsed(false); + } + // track mounted state to help prevent flash of content on SSR + setMounted(true); + }, [breakpointIndex]); + + /** + * Handle user focus on mobile sidebar open, place them in the overlaid Sidebar + */ + React.useEffect(() => { + if (!sidebarCollapsed && mobileSidebarCloseButton.current) { + mobileSidebarCloseButton.current.focus(); + } + }, [sidebarCollapsed]); + + /** + * The tokens list page an extra sticky filter bar so the jump to scroll offset needs an extra offset. + * its the masthead height + the sticky filter height + */ + + let scrollOffset = defaultScrollOffset; + + if (breakpointIndex !== undefined && TOKEN_LIST_PAGE_REGEX.test(router.pathname)) { + scrollOffset = `calc(${TOKEN_STICKY_FILTER_HEIGHT[breakpointIndex]}px + ${defaultScrollOffset})`; + } + + return ( + <> + + {/** + * No judgement zone + * To successfully handle a single sidebar that is responsive but also adaptive in it's state handling + * We need to handle initial render for both mobile and desktop views because the sidebar has inverse initial state + * - On mobile, the sidebar should start collapsed + * - On desktop, the sidebar should start expanded + * We then do a little trickery with opocity and responsive values to stop you seeing the state change flash + * when it's not mounted we use a transparent sidebar on small screens, after it's mounted we switch to visible, + * but transition it and delay the transistion start. For desktop, we start visible and never transition. + */} + + + + + + + Twilio Paste + + + + + {breakpointIndex === 0 && ( + + setSidebarCollapsed(!sidebarCollapsed)} + /> + + )} + + + + + {children} + + + + ); +}; diff --git a/packages/paste-website/src/components/site-wrapper-new/SiteMain.tsx b/packages/paste-website/src/components/site-wrapper-new/SiteMain.tsx new file mode 100644 index 0000000000..957b9f6d30 --- /dev/null +++ b/packages/paste-website/src/components/site-wrapper-new/SiteMain.tsx @@ -0,0 +1,11 @@ +import { Box } from "@twilio-paste/box"; +import type { HTMLPasteProps } from "@twilio-paste/types"; +import * as React from "react"; + +export const SiteMain: React.FC>> = ({ children, ...props }) => { + return ( + + {children} + + ); +}; diff --git a/packages/paste-website/src/components/site-wrapper-new/index.tsx b/packages/paste-website/src/components/site-wrapper-new/index.tsx new file mode 100644 index 0000000000..2ec867930d --- /dev/null +++ b/packages/paste-website/src/components/site-wrapper-new/index.tsx @@ -0,0 +1,23 @@ +import * as React from "react"; + +import { NavigationContext } from "../../context/NavigationContext"; +import type { NavigationQuery } from "../../context/NavigationContext"; +import { SiteBody } from "./SiteBody"; + +export interface SiteWrapperProps { + navigationData: NavigationQuery; +} + +const SiteWrapper: React.FC> = ({ children, navigationData }) => { + return ( + /* + * TODO: move to server components and app directory to remove this provider. + * Removes the need for page level getNavigationData() in getStaticProps. Do it once for the whole app, not everypage + */ + + {children} + + ); +}; + +export { SiteWrapper }; diff --git a/packages/paste-website/src/components/site-wrapper-new/sidebar/SidebarAnchor.tsx b/packages/paste-website/src/components/site-wrapper-new/sidebar/SidebarAnchor.tsx new file mode 100644 index 0000000000..68cf171a1d --- /dev/null +++ b/packages/paste-website/src/components/site-wrapper-new/sidebar/SidebarAnchor.tsx @@ -0,0 +1,35 @@ +import { SidebarNavigationItem } from "@twilio-paste/sidebar"; +import Link from "next/link"; +import * as React from "react"; + +import { useLocationPathname } from "../../../utils/RouteUtils"; + +interface SidebarAnchorProps { + children: NonNullable; + selected?: boolean; + href: string; + onClick?: () => void; +} + +const SidebarAnchor: React.FC> = ({ + children, + href, + onClick, + selected, + ...props +}) => { + const pathname = useLocationPathname(); + const pathnameWithoutTrailingSlash = pathname.endsWith("/") ? pathname.slice(0, -1) : pathname; + const itemSelected = selected || pathnameWithoutTrailingSlash === href; + + return ( + + {/* @ts-expect-error using nextjs passHref means that we can't satisfy the nav item href required prop */} + + {children} + + + ); +}; + +export { SidebarAnchor }; diff --git a/packages/paste-website/src/components/site-wrapper-new/sidebar/SidebarNavigation.tsx b/packages/paste-website/src/components/site-wrapper-new/sidebar/SidebarNavigation.tsx new file mode 100644 index 0000000000..733db4d37a --- /dev/null +++ b/packages/paste-website/src/components/site-wrapper-new/sidebar/SidebarNavigation.tsx @@ -0,0 +1,482 @@ +import { LinkExternalIcon } from "@twilio-paste/icons/esm/LinkExternalIcon"; +import { + SidebarNavigation, + SidebarNavigationDisclosure, + SidebarNavigationDisclosureContent, + type SidebarNavigationDisclosureContentProps, + SidebarNavigationDisclosureHeading, + type SidebarNavigationDisclosureHeadingProps, + SidebarNavigationDisclosureHeadingWrapper, + SidebarNavigationSeparator, + useSidebarNavigationDisclosureState, +} from "@twilio-paste/sidebar"; +import kebabCase from "lodash/kebabCase"; +import * as React from "react"; + +import { SidebarCategoryRoutes } from "../../../constants"; +import { useNavigationContext } from "../../../context/NavigationContext"; +import { event } from "../../../lib/gtag"; +import { getNormalizedNavigationData } from "../../../utils/DataUtils"; +import { useLocationPathname } from "../../../utils/RouteUtils"; +import { alphabetizeComponents } from "../../../utils/componentFilters"; +import { SidebarAnchor } from "./SidebarAnchor"; + +const CY_BASE = "sidebar-disclosure"; + +const NavigationDisclosure: React.FC< + React.PropsWithChildren<{ + children: SidebarNavigationDisclosureContentProps["children"]; + categoryRoute: typeof SidebarCategoryRoutes[keyof typeof SidebarCategoryRoutes]; + buttonText: string; + onClick?: SidebarNavigationDisclosureHeadingProps["onClick"]; + }> +> = ({ children, categoryRoute, buttonText, onClick }) => { + const pathname = useLocationPathname(); + const disclosure = useSidebarNavigationDisclosureState({ + visible: pathname.startsWith(categoryRoute), + }); + const buttonAttribute = `${CY_BASE}-button-${kebabCase(buttonText)}`; + const contentAttribute = `${CY_BASE}-content-${kebabCase(buttonText)}`; + + return ( + + + + {buttonText} + + + {children} + + ); +}; + +const SiteSidebarNavigation = (): JSX.Element => { + // TODO: move to a server component in the App directory + const navigationData = useNavigationContext(); + const pathname = useLocationPathname(); + + // take airtable feature data and mutate it into navigation data + const { allPasteComponent, allPasteLayout, allPastePrimitive, allPastePattern, allPastePageTemplate } = + getNormalizedNavigationData(navigationData); + + const allComponentSidebarItems = [...allPasteComponent, ...allPasteLayout]; + const filteredComponentSidebarItems = allComponentSidebarItems.sort(alphabetizeComponents); + + const filteredPrimitives = allPastePrimitive?.sort(alphabetizeComponents); + + return ( + + + event({ + category: "Left Navigation", + action: "click-introduction", + label: "Introduction", + }) + } + > + About Paste + + + Design guidelines + + + + Quick start + + Manual installation + + + + Components + Icons + Patterns + + Working with us + + Accessibility + + + Colors + + event({ + category: "Left Navigation", + action: "click-content", + label: "Content", + }) + } + > + Overview + Content checklist + Voice and tone + + Product style guide + + Word list + + + Data visualization + + + event({ + category: "Left Navigation", + action: "click-illustrations", + label: "Illustrations", + }) + } + > + Illustrations + + + event({ + category: "Left Navigation", + action: "click-localization", + label: "Localization", + }) + } + > + Localization + + + event({ + category: "Left Navigation", + action: "click-spacing-and-layout", + label: "Spacing and layout", + }) + } + > + Spacing and layout + + + event({ + category: "Left Navigation", + action: "click-typography", + label: "Typography", + }) + } + > + Typography + + + + event({ + category: "Left Navigation", + action: "click-patterns", + label: "Patterns", + }) + } + > + Overview + {allPastePattern.map(({ name, slug }: { [key: string]: string }) => ( + + {name} + + ))} + + + Overview + {allPastePageTemplate.map(({ name, slug }: { [key: string]: string }) => ( + + {name} + + ))} + + + + event({ + category: "Left Navigation", + action: "click-components", + label: "Components", + }) + } + > + Overview + {filteredComponentSidebarItems.map(({ name, slug }: { [key: string]: string }) => { + const categoryRoute = `${SidebarCategoryRoutes.COMPONENTS}/${slug}`; + const selected = pathname.endsWith(categoryRoute) || pathname.includes(`${categoryRoute}/`); + if (name === "Icon") { + return ( + + event({ + category: "Left Navigation", + action: `click-${name}`, + label: name, + }) + } + > + {name} list + + Usage + + + ); + } + if (name === "Combobox") { + return ( + + event({ + category: "Left Navigation", + action: `click-${name}`, + label: name, + }) + } + > + + Singleselect + + + Multiselect + + + ); + } + if (name === "Multiselect Combobox") { + return null; + } + if (name === "Status") { + return ( + + event({ + category: "Left Navigation", + action: `click-${name}`, + label: name, + }) + } + > + + Status Badge + + + Status Menu + + + ); + } + if (name === "Sidebar") { + return ( + + event({ + category: "Left Navigation", + action: `click-${name}`, + label: name, + }) + } + > + + Sidebar Container + + + Sidebar Navigation + + + ); + } + if (name === "Sidebar Navigation") { + return null; + } + return ( + + {name} + + ); + })} + + + event({ + category: "Left Navigation", + action: "click-primitives", + label: "Primitives", + }) + } + > + Overview + {filteredPrimitives.map(({ name, slug }: { [key: string]: string }) => { + const selected = pathname.includes(`${SidebarCategoryRoutes.PRIMITIVES}/${slug}`); + return ( + + {name} + + ); + })} + + + event({ + category: "Left Navigation", + action: "click-design-tokens", + label: "Design Tokens", + }) + } + > + Overview + Token list + + Design tokens package + + + + event({ + category: "Left Navigation", + action: "click-core", + label: "Core", + }) + } + > + Paste core + Core changelog + Upgrade guide + + Overview + Code Editor + Codemods + + Data visualization + + + Clipboard Copy + + + + SVG-to-React + + + UID library + + VS Code Plugin + + + + event({ + category: "Left Navigation", + action: "click-theme", + label: "Theme", + }) + } + > + Overview + Dark theme + Changing the theme + + + event({ + category: "Left Navigation", + action: "click-customization", + label: "Customization", + }) + } + > + Overview + + Customization Provider + + + Customizing themes + + + Customizing components + + + Composing custom UI with tokens + + + + + event({ + category: "Left Navigation", + action: "click-blog", + label: "Blog", + }) + } + > + Blog + + + event({ + category: "Left Navigation", + action: "click-roadmap", + label: "Roadmap", + }) + } + > + Roadmap + + + ); +}; + +export { SiteSidebarNavigation as SidebarNavigation }; diff --git a/packages/paste-website/src/components/site-wrapper-new/site-footer/SiteFooterHeader.tsx b/packages/paste-website/src/components/site-wrapper-new/site-footer/SiteFooterHeader.tsx new file mode 100644 index 0000000000..94a4b84b62 --- /dev/null +++ b/packages/paste-website/src/components/site-wrapper-new/site-footer/SiteFooterHeader.tsx @@ -0,0 +1,25 @@ +import { Box } from "@twilio-paste/box"; +import { useTheme } from "@twilio-paste/theme"; + +import HeartDoodleIcon from "../../icons/NewHeartDoodleIcon"; + +const SiteFooterHeader = (): JSX.Element => { + const theme = useTheme(); + return ( + + + + ); +}; + +export { SiteFooterHeader }; diff --git a/packages/paste-website/src/components/site-wrapper-new/site-footer/SiteFooterIllustration.tsx b/packages/paste-website/src/components/site-wrapper-new/site-footer/SiteFooterIllustration.tsx new file mode 100644 index 0000000000..e99820202d --- /dev/null +++ b/packages/paste-website/src/components/site-wrapper-new/site-footer/SiteFooterIllustration.tsx @@ -0,0 +1,14 @@ +import { Box } from "@twilio-paste/box"; +import Image from "next/image"; + +import FooterImg from "../../../assets/illustrations/footer_img.svg"; + +const SiteFooterIllustration = (): JSX.Element => { + return ( + + + + ); +}; + +export { SiteFooterIllustration }; diff --git a/packages/paste-website/src/components/site-wrapper-new/site-footer/SiteFooterNav.tsx b/packages/paste-website/src/components/site-wrapper-new/site-footer/SiteFooterNav.tsx new file mode 100644 index 0000000000..87dbe5ece6 --- /dev/null +++ b/packages/paste-website/src/components/site-wrapper-new/site-footer/SiteFooterNav.tsx @@ -0,0 +1,207 @@ +import { Anchor } from "@twilio-paste/anchor"; +import { Box } from "@twilio-paste/box"; +import { Stack } from "@twilio-paste/stack"; +import { Text } from "@twilio-paste/text"; +import { useTheme } from "@twilio-paste/theme"; + +import { FIGMA_PROFILE_URL, REMIX_DOMAIN, STORYBOOK_DOMAIN } from "../../../constants"; +import { event } from "../../../lib/gtag"; +import TwilioLogo from "../../icons/TwilioLogo"; +import { SiteFooterHeader } from "./SiteFooterHeader"; + +const SiteFooterNav = (): JSX.Element => { + const theme = useTheme(); + return ( + + + + + + + Learn more + + + event({ + category: "Footer", + action: "click-about", + label: "About", + }) + } + > + About + + + event({ + category: "Footer", + action: "click-design-guidelines", + label: "Design guidelines", + }) + } + > + Design guidelines + + + event({ + category: "Footer", + action: "click-quickstart", + label: "Quick Start for engineers", + }) + } + > + Quick start for engineers + + + event({ + category: "Footer", + action: "click-our-roadmap", + label: "Roadmap", + }) + } + > + Roadmap + + + + + + + Get help + + + event({ + category: "Footer", + action: "click-github-discussions", + label: "Github discussions", + }) + } + > + Ask a question + + + event({ + category: "Footer", + action: "click-report-a-bug", + label: "Report a bug", + }) + } + > + Report a bug + + + event({ + category: "Footer", + action: "click-how-we-work", + label: "How we work", + }) + } + > + How we work + + + + + + + Build with Paste + + + event({ + category: "Footer", + action: "click-github", + label: "Github", + }) + } + > + Github + + + event({ + category: "Footer", + action: "click-figma", + label: "Figma", + }) + } + > + Figma + + + event({ + category: "Footer", + action: "click-storybook", + label: "Storybook", + }) + } + > + Storybook + + + event({ + category: "Footer", + action: "click-remix", + label: "Theme Designer", + }) + } + > + Paste Remix + + + + + + + + + + + ); +}; +export { SiteFooterNav }; diff --git a/packages/paste-website/src/components/site-wrapper-new/site-footer/index.tsx b/packages/paste-website/src/components/site-wrapper-new/site-footer/index.tsx new file mode 100644 index 0000000000..356c2f3bab --- /dev/null +++ b/packages/paste-website/src/components/site-wrapper-new/site-footer/index.tsx @@ -0,0 +1,32 @@ +import { Box } from "@twilio-paste/box"; + +import { HOMEPAGE_SITE_CONTENT_MAX_WIDTH } from "../../../constants"; +import { SiteFooterIllustration } from "./SiteFooterIllustration"; +import { SiteFooterNav } from "./SiteFooterNav"; + +const SiteFooter: React.FC = () => { + return ( + + + + + + + ); +}; +export { SiteFooter }; diff --git a/packages/paste-website/src/components/site-wrapper-new/site-header/DarkModeToggle.tsx b/packages/paste-website/src/components/site-wrapper-new/site-header/DarkModeToggle.tsx new file mode 100644 index 0000000000..b14275ff7b --- /dev/null +++ b/packages/paste-website/src/components/site-wrapper-new/site-header/DarkModeToggle.tsx @@ -0,0 +1,68 @@ +import { DarkModeIcon } from "@twilio-paste/icons/esm/DarkModeIcon"; +import { LightModeIcon } from "@twilio-paste/icons/esm/LightModeIcon"; +import { Menu, MenuButton, MenuItemRadio, useMenuState } from "@twilio-paste/menu"; +import { ScreenReaderOnly } from "@twilio-paste/screen-reader-only"; + +import { useDarkModeContext } from "../../../context/DarkModeContext"; +import { event } from "../../../lib/gtag"; + +export const DarkModeToggle = (): JSX.Element => { + const { theme, toggleMode } = useDarkModeContext(); + const menu = useMenuState(); + + const handleClick = (category: string, action: string, label: string): void => { + menu.hide(); + toggleMode(); + event({ + category, + action, + label, + }); + }; + + return ( + <> + + event({ + category: "Top Navigation", + action: "click-dark-mode", + label: "Switch", + }) + } + > + {theme === "twilio" ? ( + + ) : ( + + )} + Switch the site theme + + + handleClick("Top Navigation", "click-dark-mode", "Light")} + > + Light mode + + handleClick("Top Navigation", "click-dark-mode", "Dark")} + > + Dark mode + + + + ); +}; diff --git a/packages/paste-website/src/components/site-wrapper-new/site-header/FigmaButton.tsx b/packages/paste-website/src/components/site-wrapper-new/site-header/FigmaButton.tsx new file mode 100644 index 0000000000..0bd23b7053 --- /dev/null +++ b/packages/paste-website/src/components/site-wrapper-new/site-header/FigmaButton.tsx @@ -0,0 +1,43 @@ +import { secureExternalLink } from "@twilio-paste/anchor"; +import { Box } from "@twilio-paste/box"; +import React from "react"; + +import { FIGMA_PROFILE_URL } from "../../../constants"; +import { FigmaIcon } from "../../icons/FigmaIcon"; + +// eslint-disable-next-line no-empty-pattern +const FigmaButton = React.forwardRef(({}, ref) => { + return ( + + + Figma + + ); +}); + +FigmaButton.displayName = "FigmaButton"; + +export { FigmaButton }; diff --git a/packages/paste-website/src/components/site-wrapper-new/site-header/HamburgerToggle.tsx b/packages/paste-website/src/components/site-wrapper-new/site-header/HamburgerToggle.tsx new file mode 100644 index 0000000000..7503fc7146 --- /dev/null +++ b/packages/paste-website/src/components/site-wrapper-new/site-header/HamburgerToggle.tsx @@ -0,0 +1,69 @@ +import { Box } from "@twilio-paste/box"; +import type { BoxProps } from "@twilio-paste/box"; +import type { TextColorOptions } from "@twilio-paste/style-props"; +import { styled, themeGet } from "@twilio-paste/styling-library"; +import * as React from "react"; + +interface HamburgerToggleProps { + toggled: boolean; + color: TextColorOptions; +} + +interface StyledPattieProps extends Pick { + color: TextColorOptions; +} + +const StyledPattie = styled.span` + position: absolute; + width: ${themeGet("space.space50")}; + height: 3px; + border-radius: ${themeGet("radii.borderRadius30")}; + transition: transform 0.25s ease-in-out, opacity 0.25s ease-in-out; + transform-origin: left center; + background-color: ${({ color }) => themeGet(`textColors.${color}`)}; + top: ${({ top }) => top}; + left: ${({ left }) => left}; + transform: ${({ transform }) => transform}; + opacity: ${({ opacity }) => opacity}; +`; + +const HamburgerToggle = React.forwardRef(({ toggled, color }, ref) => { + return ( + + + + + + + ); +}); + +export { HamburgerToggle }; diff --git a/packages/paste-website/src/components/site-wrapper-new/site-header/SiteHeaderLogo.tsx b/packages/paste-website/src/components/site-wrapper-new/site-header/SiteHeaderLogo.tsx new file mode 100644 index 0000000000..cc5df23ff1 --- /dev/null +++ b/packages/paste-website/src/components/site-wrapper-new/site-header/SiteHeaderLogo.tsx @@ -0,0 +1,100 @@ +import { Box } from "@twilio-paste/box"; +import { MediaBody, MediaFigure, MediaObject } from "@twilio-paste/media-object"; +import { styled, themeGet } from "@twilio-paste/styling-library"; +import { Text } from "@twilio-paste/text"; +import { useTheme } from "@twilio-paste/theme"; +import Link from "next/link"; +import * as React from "react"; + +import { event } from "../../../lib/gtag"; +import { PasteIcon } from "../../icons/PasteIcon"; +import { PasteIconPride } from "../../icons/PasteIconPride"; + +const LogoLink = styled(Link)` + position: relative; + text-decoration: none; + color: ${themeGet("textColors.colorTextInverse")}; + + &:hover { + text-decoration: underline; + } + + &:focus { + outline: none; + box-shadow: ${themeGet("shadows.shadowFocus")}; + border-radius: ${themeGet("radii.borderRadius10")}; + } +`; + +interface SiteHeaderLogoProps { + title?: string; + subtitle?: string; +} + +/* + * Note: 'subtitle' isn't passed for the mobile view, so we use that fact + * to render different sizes and spacing in mobile + */ +const SiteHeaderLogo: React.FC> = ({ title, subtitle }) => { + const theme = useTheme(); + const [logoOpacity, setLogoOpacity] = React.useState(1); + const [hoverOpacity, setHoverOpacity] = React.useState(0); + const logoTransition = "ease-out 350ms"; + + return ( + + + event({ + category: "Top Navigation", + action: "click-paste-logo", + label: "Paste logo", + }) + } + onMouseEnter={() => { + setLogoOpacity(0); + setHoverOpacity(1); + }} + onMouseLeave={() => { + setLogoOpacity(1); + setHoverOpacity(0); + }} + > + + + + + + + + {title} + + {subtitle ? ( + + {subtitle} + + ) : null} + + + + + ); +}; + +export { SiteHeaderLogo }; diff --git a/packages/paste-website/src/components/site-wrapper-new/site-header/SiteHeaderSearch.tsx b/packages/paste-website/src/components/site-wrapper-new/site-header/SiteHeaderSearch.tsx new file mode 100644 index 0000000000..a9022fba50 --- /dev/null +++ b/packages/paste-website/src/components/site-wrapper-new/site-header/SiteHeaderSearch.tsx @@ -0,0 +1,74 @@ +import { Box } from "@twilio-paste/box"; +import { Button } from "@twilio-paste/button"; +import { SearchIcon } from "@twilio-paste/icons/esm/SearchIcon"; +import { InlineCode } from "@twilio-paste/inline-code"; +import { ScreenReaderOnly } from "@twilio-paste/screen-reader-only"; +import { Text } from "@twilio-paste/text"; +import * as React from "react"; +import { useHotkeys } from "react-hotkeys-hook"; + +import { SiteSearch } from "../../site-search"; + +const SiteHeaderSearch: React.FC = () => { + const [isOpen, setIsOpen] = React.useState(false); + + const onOpen = (): void => { + setIsOpen(true); + }; + + const onClose = (): void => { + setIsOpen(false); + }; + + useHotkeys("mod+k", onOpen); + + return ( + <> + + + + ); +}; + +export { SiteHeaderSearch }; diff --git a/packages/paste-website/src/components/site-wrapper-new/site-header/index.tsx b/packages/paste-website/src/components/site-wrapper-new/site-header/index.tsx new file mode 100644 index 0000000000..bc6b1ba4a5 --- /dev/null +++ b/packages/paste-website/src/components/site-wrapper-new/site-header/index.tsx @@ -0,0 +1,55 @@ +import { Box } from "@twilio-paste/box"; +import { Button } from "@twilio-paste/button"; +import { Topbar, TopbarActions } from "@twilio-paste/topbar"; +import { useWindowSize } from "@twilio-paste/utils"; +import * as React from "react"; +import GitHubButton from "react-github-button"; +import "react-github-button/assets/style.css"; + +import { PASTE_DOCS_TOPBAR } from "../../../constants"; +import { ContactUsMenu } from "../../ContactUsMenu"; +import { DarkModeToggle } from "./DarkModeToggle"; +import { FigmaButton } from "./FigmaButton"; +import { HamburgerToggle } from "./HamburgerToggle"; +import { SiteHeaderLogo } from "./SiteHeaderLogo"; +import { SiteHeaderSearch } from "./SiteHeaderSearch"; + +export const SiteHeader: React.FC<{ + sidebarMobileCollapsed: boolean; + setSidebarMobileCollapsed: (collapsed: boolean) => void; +}> = ({ sidebarMobileCollapsed, setSidebarMobileCollapsed }): JSX.Element => { + const { breakpointIndex } = useWindowSize(); + return ( + + + + + {breakpointIndex === 0 ? ( + + + + + + + ) : ( + + + + + + + + + + )} + + ); +}; diff --git a/packages/paste-website/src/constants.ts b/packages/paste-website/src/constants.ts index b4fa9d872c..faf02fb74f 100644 --- a/packages/paste-website/src/constants.ts +++ b/packages/paste-website/src/constants.ts @@ -5,6 +5,7 @@ export const PASTE_THEME_ALERT_HEIGHT = 54; export const SITE_TOPBAR_HEIGHT = 77; export const SITE_BREAKPOINTS = ["768px", "1024px", "1220px", "1880px"]; export const SITE_CONTENT_MAX_WIDTH = "1440px"; +export const HOMEPAGE_SITE_CONTENT_MAX_WIDTH = "1232px"; // Used to 'bleed' sections together. GettingStarted+Popular export const HOMEPAGE_SECTION_OVERFLOW_OFFSET = 150; diff --git a/packages/paste-website/src/pages/new/index.tsx b/packages/paste-website/src/pages/new/index.tsx index b60f1781d4..549f13e1ec 100644 --- a/packages/paste-website/src/pages/new/index.tsx +++ b/packages/paste-website/src/pages/new/index.tsx @@ -8,12 +8,12 @@ import { Accessibility } from "../../components/homepage/Accessibility"; import { CommunityOfBuilders } from "../../components/homepage/CommunityOfBuilders"; import { DesignEfficiency } from "../../components/homepage/DesignEfficiency"; import { ForTwilioCustomers } from "../../components/homepage/ForTwilioCustomers"; -import { NewHomeHero } from "../../components/homepage/NewHomeHero"; +import { HomeHero } from "../../components/homepage/NewHomeHero"; import { NewSection } from "../../components/homepage/NewSection"; import { Templates } from "../../components/homepage/Templates"; import { Themeable } from "../../components/homepage/Themeable"; import { WeDoTheThinking } from "../../components/homepage/WeDoTheThinking"; -import { SiteWrapper } from "../../components/site-wrapper"; +import { SiteWrapper } from "../../components/site-wrapper-new"; import { SiteMetaDefaults } from "../../constants"; import { getNavigationData } from "../../utils/api"; import type { Feature } from "../../utils/api"; @@ -33,10 +33,10 @@ const NewHomePage = ({ navigationData }: InferGetStaticPropsType {SiteMetaDefaults.TITLE} - - + + - + @@ -60,5 +60,4 @@ export const getStaticProps: GetStaticProps<{ navigationData: Feature[] }> = asy }, }; }; - export default NewHomePage;