diff --git a/docs/components/AtlantisThemeContext/AtlantisThemeContext.stories.mdx b/docs/components/AtlantisThemeContext/AtlantisThemeContext.stories.mdx index 4b7f3f7f8a..8d5026610a 100644 --- a/docs/components/AtlantisThemeContext/AtlantisThemeContext.stories.mdx +++ b/docs/components/AtlantisThemeContext/AtlantisThemeContext.stories.mdx @@ -13,8 +13,8 @@ tokens. ## Design & usage guidelines -Both the web and mobile components have the exact same API, except for one -minor difference in how you update the theme. +Both the web and mobile components have the exact same API, except for one minor +difference in how you update the theme. Each platform provides a `useAtlantisTheme` hook that you may use to access the `theme` and `tokens` in your components. @@ -80,7 +80,10 @@ function ThemedComponent() { ### Usage for mobile ```tsx -import { AtlantisThemeContextProvider, useAtlantisTheme } from "@jobber/components/AtlantisThemeContext"; +import { + AtlantisThemeContextProvider, + useAtlantisTheme, +} from "@jobber/components/AtlantisThemeContext"; function App() { return ( diff --git a/packages/site/src/layout/AnimatedPresenceDisclosure.tsx b/packages/site/src/layout/AnimatedPresenceDisclosure.tsx index 14b156bc32..1640567b53 100644 --- a/packages/site/src/layout/AnimatedPresenceDisclosure.tsx +++ b/packages/site/src/layout/AnimatedPresenceDisclosure.tsx @@ -2,6 +2,7 @@ import { AnimatedPresence, Button, Typography } from "@jobber/components"; import React, { useEffect, useMemo, useState } from "react"; import { Link, useLocation } from "react-router-dom"; import styles from "./NavMenu.module.css"; +import { useAtlantisSite } from "../providers/AtlantisSiteProvider"; interface AnimatedPresenceDisclosureProps { readonly children: React.ReactNode; @@ -23,6 +24,8 @@ function AnimatedPresenceDisclosure({ [children], ); + const { toggleMobileMenu } = useAtlantisSite(); + // Determine if any child is selected based on the current URL const hasSelectedChild = childrenArray.some( child => React.isValidElement(child) && pathname === child.props.to, @@ -63,7 +66,7 @@ function AnimatedPresenceDisclosure({ isTitleSelected ? styles.selected : "" } stickySectionHeader`} > - + {title} diff --git a/packages/site/src/layout/LeftDrawer.module.css b/packages/site/src/layout/LeftDrawer.module.css new file mode 100644 index 0000000000..4b1e9c6a77 --- /dev/null +++ b/packages/site/src/layout/LeftDrawer.module.css @@ -0,0 +1,49 @@ +.drawer { + position: fixed; + top: 0; + left: 0; + height: 100%; + width: 100%; + background: var(--color-surface--background); + z-index: var(--elevation-modal); + display: flex; + flex-direction: column; + animation: slideIn 200ms ease-out; +} + +@media screen and (min-width: 768px) { + .drawer { + display: none; + } +} + +.drawerClosing { + animation: slideOut 200ms ease-out; +} + +.header { + padding: var(--space-base); +} + +.content { + flex: 1; + overflow-y: hidden; +} + +@keyframes slideIn { + from { + transform: translateX(-100%); + } + to { + transform: translateX(0); + } +} + +@keyframes slideOut { + from { + transform: translateX(0); + } + to { + transform: translateX(-100%); + } +} diff --git a/packages/site/src/layout/LeftDrawer.tsx b/packages/site/src/layout/LeftDrawer.tsx new file mode 100644 index 0000000000..a1aab3a472 --- /dev/null +++ b/packages/site/src/layout/LeftDrawer.tsx @@ -0,0 +1,37 @@ +import { Box, Button } from "@jobber/components"; +import { PropsWithChildren, useState } from "react"; +import styles from "./LeftDrawer.module.css"; + +interface LeftDrawerProps extends PropsWithChildren { + readonly onClose: () => void; + readonly header?: React.ReactNode; +} + +export function LeftDrawer({ children, onClose, header }: LeftDrawerProps) { + const [isClosing, setIsClosing] = useState(false); + + const handleClose = () => { + setIsClosing(true); + setTimeout(() => { + onClose(); + }, 200); + }; + + return ( +
+ +
+ ); +} diff --git a/packages/site/src/layout/NavMenu.module.css b/packages/site/src/layout/NavMenu.module.css index faeef746d3..6b41118278 100644 --- a/packages/site/src/layout/NavMenu.module.css +++ b/packages/site/src/layout/NavMenu.module.css @@ -5,10 +5,14 @@ --navItemHeight: 40px; display: flex; flex-direction: column; - padding: 35px 0 0 0; - height: 100dvh; + height: 100%; min-width: var(--sideBarWidth); box-sizing: border-box; + + @media screen and (min-width: 768px) { + padding: 35px 0 0 0; + height: 100dvh; + } } .navMenuContainer>* { @@ -130,4 +134,20 @@ a.navFooterLink { margin-top: auto; font-size: var(--typography--fontSize-small); color: var(--color-text--secondary); +} + +.desktopNavContainer { + display: none; + + @media screen and (min-width: 768px) { + display: block; + } +} + +.navMenuHeaderLogo { + display: none; + + @media screen and (min-width: 768px) { + display: block; + } } \ No newline at end of file diff --git a/packages/site/src/layout/NavMenu.tsx b/packages/site/src/layout/NavMenu.tsx index 45581c95ac..e6c90b1d70 100644 --- a/packages/site/src/layout/NavMenu.tsx +++ b/packages/site/src/layout/NavMenu.tsx @@ -1,14 +1,16 @@ import { Box, Button, Typography } from "@jobber/components"; import { Link, useLocation } from "react-router-dom"; import { Fragment, PropsWithChildren, useRef } from "react"; +import { useBreakpoints } from "@jobber/hooks"; import AnimatedPresenceDisclosure from "./AnimatedPresenceDisclosure"; import styles from "./NavMenu.module.css"; +import { LeftDrawer } from "./LeftDrawer"; import { routes } from "../routes"; import { JobberLogo } from "../assets/JobberLogo.svg"; import { useAtlantisSite } from "../providers/AtlantisSiteProvider"; import { VisibleWhenFocused } from "../components/VisibleWhenFocused"; -interface NavMenuProps { +export interface NavMenuProps { readonly mainContentRef: React.RefObject; } @@ -17,7 +19,7 @@ interface NavMenuProps { * @returns ReactNode */ export const NavMenu = ({ mainContentRef }: NavMenuProps) => { - const { isMinimal } = useAtlantisSite(); + const { isMinimal, isMobileMenuOpen, toggleMobileMenu } = useAtlantisSite(); const { pathname } = useLocation(); const selectedRef = useRef(null); @@ -67,19 +69,20 @@ export const NavMenu = ({ mainContentRef }: NavMenuProps) => { const skipToContent = () => { mainContentRef.current?.focus(); + toggleMobileMenu(); }; - return ( + const menuContent = ( ); + + return ( + <> +
{menuContent}
+ {isMobileMenuOpen && ( + + + + + + } + > + {menuContent} + + )} + + ); }; const getLinkClassName = ( @@ -143,12 +166,15 @@ export const StyledLink = ({ isSelected, styles.selected, ); + const { toggleMobileMenu } = useAtlantisSite(); + const { mediumAndUp } = useBreakpoints(); return ( {children} @@ -170,12 +196,15 @@ export const StyledSubLink = ({ isSelected, styles.selected, ); + const { toggleMobileMenu } = useAtlantisSite(); + const { mediumAndUp } = useBreakpoints(); return ( {children} diff --git a/packages/site/src/layout/SearchButton.module.css b/packages/site/src/layout/SearchButton.module.css index 469f20ae5e..f1ae03f5f5 100644 --- a/packages/site/src/layout/SearchButton.module.css +++ b/packages/site/src/layout/SearchButton.module.css @@ -1,40 +1,47 @@ .searchButton { - display: flex; - align-items: center; - border: var(--border-base) solid var(--color-border--interactive); - border-radius: var(--radius-base); - padding: var(--space-small); - background: var(--color-surface); - outline: transparent; - cursor: pointer; - transition: all var(--timing-base) ease-out; + display: flex; + align-items: center; + border: var(--border-base) solid var(--color-border--interactive); + border-radius: var(--radius-base); + padding: var(--space-small); + background: var(--color-surface); + outline: transparent; + cursor: pointer; + transition: all var(--timing-base) ease-out; - &:focus-visible { - box-shadow: var(--shadow-focus); - background-color: var(--color-surface--background--hover); - } + &:focus-visible { + box-shadow: var(--shadow-focus); + background-color: var(--color-surface--background--hover); + } } .searchButton:hover { - background: var(--color-surface--background--hover); - border: var(--border-base) solid var(--color-interactive--subtle--hover); + background: var(--color-surface--background--hover); + border: var(--border-base) solid var(--color-interactive--subtle--hover); } .searchButtonText { - margin: 0 var(--space-small); - flex: 1; - text-align: start; + margin: 0 var(--space-small); + display: none; + flex: 1; + text-align: start; + @media screen and (min-width: 768px) { + display: block; + } } .searchKeyIndicator { - background: var(--color-surface--background); - width: 20px; - height: 20px; - font-size: var(--typography--fontSize-large); - color: var(--color-text); + background: var(--color-surface--background); + width: 20px; + height: 20px; + font-size: var(--typography--fontSize-large); + color: var(--color-text); + display: none; + align-items: center; + justify-content: center; + border-radius: var(--radius-small); + box-shadow: var(--shadow-low); + @media screen and (min-width: 768px) { display: flex; - align-items: center; - justify-content: center; - border-radius: var(--radius-small); - box-shadow: var(--shadow-low); + } } \ No newline at end of file diff --git a/packages/site/src/layout/TopNav.tsx b/packages/site/src/layout/TopNav.tsx index 6743027b1b..8f18f92e7d 100644 --- a/packages/site/src/layout/TopNav.tsx +++ b/packages/site/src/layout/TopNav.tsx @@ -1,33 +1,74 @@ import { Box, Button, Tooltip } from "@jobber/components"; +import { useBreakpoints } from "@jobber/hooks"; +import { Link } from "react-router-dom"; import { SearchButton } from "./SearchButton"; import { ToggleThemeButton } from "../components/ToggleThemeButton"; import { useTritonChat } from "../providers/TritonProvider"; +import { JobberLogo } from "../assets/JobberLogo.svg"; +import { useAtlantisSite } from "../providers/AtlantisSiteProvider"; export const TopNav = () => { const { onOpenTriton } = useTritonChat(); + const { mediumAndUp } = useBreakpoints(); + const { toggleMobileMenu } = useAtlantisSite(); return ( - + + {!mediumAndUp && ( + +