From 7f91058ce4aa140e13ec60d7d94ff3f9a4ed1a94 Mon Sep 17 00:00:00 2001 From: Charles de Dreuille Date: Tue, 4 Nov 2025 19:02:24 +0000 Subject: [PATCH] Add mobile navigation Signed-off-by: Charles de Dreuille --- docs-ui/src/app/layout.tsx | 2 + .../AnimatedMenuIcon.module.css | 40 ++++ .../MobileBottomNav/AnimatedMenuIcon.tsx | 56 +++++ .../MobileBottomNav.module.css | 205 ++++++++++++++++++ .../MobileBottomNav/MobileBottomNav.tsx | 44 ++++ .../components/MobileBottomNav/MobileMenu.tsx | 32 +++ .../src/components/MobileBottomNav/index.tsx | 1 + .../Navigation/Navigation.module.css | 100 +++++++++ .../src/components/Navigation/Navigation.tsx | 125 +++++++++++ docs-ui/src/components/Navigation/index.tsx | 1 + .../src/components/Sidebar/Sidebar.module.css | 97 --------- docs-ui/src/components/Sidebar/Sidebar.tsx | 107 +-------- .../src/components/Toolbar/Toolbar.module.css | 22 +- docs-ui/src/components/Toolbar/Toolbar.tsx | 40 ++-- 14 files changed, 652 insertions(+), 220 deletions(-) create mode 100644 docs-ui/src/components/MobileBottomNav/AnimatedMenuIcon.module.css create mode 100644 docs-ui/src/components/MobileBottomNav/AnimatedMenuIcon.tsx create mode 100644 docs-ui/src/components/MobileBottomNav/MobileBottomNav.module.css create mode 100644 docs-ui/src/components/MobileBottomNav/MobileBottomNav.tsx create mode 100644 docs-ui/src/components/MobileBottomNav/MobileMenu.tsx create mode 100644 docs-ui/src/components/MobileBottomNav/index.tsx create mode 100644 docs-ui/src/components/Navigation/Navigation.module.css create mode 100644 docs-ui/src/components/Navigation/Navigation.tsx create mode 100644 docs-ui/src/components/Navigation/index.tsx diff --git a/docs-ui/src/app/layout.tsx b/docs-ui/src/app/layout.tsx index c91535a44b..ad056626d8 100644 --- a/docs-ui/src/app/layout.tsx +++ b/docs-ui/src/app/layout.tsx @@ -4,6 +4,7 @@ import { Toolbar } from '@/components/Toolbar'; import { Providers } from './providers'; import { CustomTheme } from '@/components/CustomTheme'; import { TableOfContents } from '@/components/TableOfContents'; +import { MobileBottomNav } from '@/components/MobileBottomNav'; import styles from './layout.module.css'; import '../css/globals.css'; @@ -62,6 +63,7 @@ export default async function RootLayout({ + diff --git a/docs-ui/src/components/MobileBottomNav/AnimatedMenuIcon.module.css b/docs-ui/src/components/MobileBottomNav/AnimatedMenuIcon.module.css new file mode 100644 index 0000000000..632f005b65 --- /dev/null +++ b/docs-ui/src/components/MobileBottomNav/AnimatedMenuIcon.module.css @@ -0,0 +1,40 @@ +.icon { + display: block; +} + +.topLine, +.middleLine, +.bottomLine { + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); +} + +/* Menu state (default) */ +.topLine { + transform-origin: 10px 5px; + transform: rotate(0deg); +} + +.middleLine { + opacity: 1; + transform-origin: 10px 10px; +} + +.bottomLine { + transform-origin: 10px 15px; + transform: rotate(0deg); +} + +/* Close state (when open) */ +.icon[data-open='true'] .topLine { + transform-origin: 10px 5px; + transform: translateY(5px) rotate(45deg); +} + +.icon[data-open='true'] .middleLine { + opacity: 0; +} + +.icon[data-open='true'] .bottomLine { + transform-origin: 10px 15px; + transform: translateY(-5px) rotate(-45deg); +} diff --git a/docs-ui/src/components/MobileBottomNav/AnimatedMenuIcon.tsx b/docs-ui/src/components/MobileBottomNav/AnimatedMenuIcon.tsx new file mode 100644 index 0000000000..80edd64d43 --- /dev/null +++ b/docs-ui/src/components/MobileBottomNav/AnimatedMenuIcon.tsx @@ -0,0 +1,56 @@ +import styles from './AnimatedMenuIcon.module.css'; + +interface AnimatedMenuIconProps { + isOpen: boolean; + size?: number; +} + +export const AnimatedMenuIcon = ({ + isOpen, + size = 20, +}: AnimatedMenuIconProps) => { + return ( + + + + + + + + ); +}; diff --git a/docs-ui/src/components/MobileBottomNav/MobileBottomNav.module.css b/docs-ui/src/components/MobileBottomNav/MobileBottomNav.module.css new file mode 100644 index 0000000000..9c128c3959 --- /dev/null +++ b/docs-ui/src/components/MobileBottomNav/MobileBottomNav.module.css @@ -0,0 +1,205 @@ +/* Mobile Bottom Navigation Island */ +.mobileBottomNav { + display: block; + position: fixed; + bottom: 0; + left: 0; + right: 0; + padding: 16px; + z-index: 300; + pointer-events: none; + + @media (min-width: 768px) { + display: none; + } +} + +.island { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + margin: 0 auto; + width: 50vw; + padding: 8px 12px 8px 12px; + border-radius: 32px; + pointer-events: auto; + background-color: #000; +} + +:global([data-theme-mode='dark']) .island { + background-color: #fff; + box-shadow: 0 4px 24px rgba(0, 0, 0, 0.3); +} + +.menuButton { + display: flex; + align-items: center; + justify-content: center; + background: none; + border: none; + cursor: pointer; + padding: 8px; + border-radius: 50%; + transition: background-color 0.2s ease-in-out; + + /* White icons on light mode (black island) */ + color: white; + + &:hover { + background-color: rgba(255, 255, 255, 0.1); + } +} + +:global([data-theme-mode='dark']) .menuButton { + /* Black icons on dark mode (white island) */ + color: black; + + &:hover { + background-color: rgba(0, 0, 0, 0.1); + } +} + +.buttonGroup { + display: flex; + align-items: center; + gap: 4px; + padding: 4px; + border-radius: 24px; + background-color: rgba(255, 255, 255, 0.2); +} + +:global([data-theme-mode='dark']) .buttonGroup { + background-color: rgba(0, 0, 0, 0.08); +} + +.buttonGroup button { + display: flex; + align-items: center; + justify-content: center; + background: none; + border: none; + cursor: pointer; + padding: 6px 12px; + border-radius: 20px; + transition: background-color 0.2s ease-in-out; + + /* White icons on light mode */ + color: white; + + &[data-selected] { + background-color: rgba(255, 255, 255, 0.2); + } + + &:hover { + background-color: rgba(255, 255, 255, 0.15); + } +} + +:global([data-theme-mode='dark']) .buttonGroup button { + /* Black icons on dark mode */ + color: black; + + &[data-selected] { + background-color: rgba(0, 0, 0, 0.2); + } + + &:hover { + background-color: rgba(0, 0, 0, 0.15); + } +} + +/* Mobile Menu Bottom Sheet */ +.overlay { + position: fixed; + inset: 0; + z-index: 200; + background-color: rgba(38, 38, 38, 0.7); + display: flex; + align-items: flex-end; + justify-content: center; + + &[data-entering] { + animation: overlayFadeIn 200ms ease-out; + } + + &[data-exiting] { + animation: overlayFadeOut 200ms ease-in; + } +} + +@keyframes overlayFadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +@keyframes overlayFadeOut { + from { + opacity: 1; + } + to { + opacity: 0; + } +} + +.modal { + width: calc(100% - 48px); + max-height: 50vh; + background-color: var(--bg); + border-radius: 16px; + box-shadow: 0 -4px 24px rgba(0, 0, 0, 0.2); + display: flex; + flex-direction: column; + outline: none; + margin-bottom: 96px; + + &[data-entering] { + animation: slideUp 300ms cubic-bezier(0.32, 0.72, 0, 1); + } + + &[data-exiting] { + animation: slideDown 200ms cubic-bezier(0.32, 0.72, 0, 1); + } +} + +@keyframes slideUp { + from { + transform: translateY(8%); + } + to { + transform: translateY(0); + } +} + +@keyframes slideDown { + from { + transform: translateY(0); + } + to { + transform: translateY(8%); + } +} + +.dialog { + display: flex; + flex-direction: column; + height: 100%; + max-height: 70vh; + outline: none; + overflow: hidden; +} + +.menuContent { + flex: 1; + overflow-y: auto; + overflow-x: hidden; + padding: 16px; + display: flex; + flex-direction: column; + gap: 2px; + min-height: 0; +} diff --git a/docs-ui/src/components/MobileBottomNav/MobileBottomNav.tsx b/docs-ui/src/components/MobileBottomNav/MobileBottomNav.tsx new file mode 100644 index 0000000000..52ec2ee9d9 --- /dev/null +++ b/docs-ui/src/components/MobileBottomNav/MobileBottomNav.tsx @@ -0,0 +1,44 @@ +'use client'; + +import { useState } from 'react'; +import { RiMoonLine, RiSunLine } from '@remixicon/react'; +import { Button, ToggleButton, ToggleButtonGroup } from 'react-aria-components'; +import styles from './MobileBottomNav.module.css'; +import { usePlayground } from '@/utils/playground-context'; +import { MobileMenu } from './MobileMenu'; +import { AnimatedMenuIcon } from './AnimatedMenuIcon'; + +export const MobileBottomNav = () => { + const [isMenuOpen, setIsMenuOpen] = useState(false); + const { selectedTheme, setSelectedTheme } = usePlayground(); + + return ( + <> +
+
+ + + + + + + + + +
+
+ setIsMenuOpen(false)} /> + + ); +}; diff --git a/docs-ui/src/components/MobileBottomNav/MobileMenu.tsx b/docs-ui/src/components/MobileBottomNav/MobileMenu.tsx new file mode 100644 index 0000000000..4d84527f30 --- /dev/null +++ b/docs-ui/src/components/MobileBottomNav/MobileMenu.tsx @@ -0,0 +1,32 @@ +'use client'; + +import { Button, Dialog, Modal, ModalOverlay } from 'react-aria-components'; +import { RiCloseLine } from '@remixicon/react'; +import styles from './MobileBottomNav.module.css'; +import { Navigation } from '@/components/Navigation'; + +interface MobileMenuProps { + isOpen: boolean; + onClose: () => void; +} + +export const MobileMenu = ({ isOpen, onClose }: MobileMenuProps) => { + return ( + + + + {({ close }) => ( +
+ +
+ )} +
+
+
+ ); +}; diff --git a/docs-ui/src/components/MobileBottomNav/index.tsx b/docs-ui/src/components/MobileBottomNav/index.tsx new file mode 100644 index 0000000000..4b7449c9c5 --- /dev/null +++ b/docs-ui/src/components/MobileBottomNav/index.tsx @@ -0,0 +1 @@ +export { MobileBottomNav } from './MobileBottomNav'; diff --git a/docs-ui/src/components/Navigation/Navigation.module.css b/docs-ui/src/components/Navigation/Navigation.module.css new file mode 100644 index 0000000000..f5e1312ab1 --- /dev/null +++ b/docs-ui/src/components/Navigation/Navigation.module.css @@ -0,0 +1,100 @@ +.topNav { + & ul { + margin: 0; + padding: 0; + list-style: none; + display: flex; + flex-direction: column; + gap: 2px; + } + + & li { + margin: 0; + padding: 0; + list-style: none; + } + + & li div, + & li a { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 12px; + border-radius: 4px; + cursor: pointer; + color: var(--primary); + text-decoration: none; + + &:hover { + background-color: var(--action); + + &[data-disabled='true'] { + background-color: transparent; + } + } + + &[data-active='true'] { + background-color: var(--action); + + &[data-disabled='true'] { + background-color: transparent; + } + } + + &[data-disabled='true'] { + opacity: 0.5; + cursor: not-allowed; + } + } +} + +.sectionTitle { + font-size: 0.6875rem; + font-weight: 500; + padding: 0 12px 4px; + color: var(--secondary); + margin-top: 40px; + text-transform: uppercase; + + &:first-child { + margin-top: 12px; + } +} + +.line { + text-decoration: none; + align-items: center; + width: 100%; + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + height: 28px; + padding: 0 12px; + border-radius: 4px; + color: var(--primary); + flex-shrink: 0; + + &:hover { + background-color: var(--action); + } + + &.active { + background-color: var(--action); + } + + &.active .lineTitle { + color: var(--primary); + } +} + +.lineTitle { + font-size: 14px; + font-weight: 400; + color: var(--primary); +} + +.lineStatus { + font-size: 14px; + color: var(--secondary); +} diff --git a/docs-ui/src/components/Navigation/Navigation.tsx b/docs-ui/src/components/Navigation/Navigation.tsx new file mode 100644 index 0000000000..13fa87661f --- /dev/null +++ b/docs-ui/src/components/Navigation/Navigation.tsx @@ -0,0 +1,125 @@ +'use client'; + +import Link from 'next/link'; +import { usePathname } from 'next/navigation'; +import { Fragment } from 'react'; +import clsx from 'clsx'; +import { + RiCollageLine, + RiFileHistoryLine, + RiHazeLine, + RiPaletteLine, + RiServiceLine, + RiStackLine, +} from '@remixicon/react'; +import { components, layoutComponents } from '@/utils/data'; +import styles from './Navigation.module.css'; + +interface NavigationProps { + onLinkClick?: () => void; +} + +const data = [ + { + title: 'Layout Components', + content: layoutComponents, + url: '/components', + }, + { + title: 'Components', + content: components, + url: '/components', + }, +]; + +export const Navigation = ({ onLinkClick }: NavigationProps) => { + const pathname = usePathname(); + + return ( + <> + + {data.map(section => { + return ( + +
{section.title}
+ + {section.content.map(item => { + const isActive = pathname === `${section.url}/${item.slug}`; + + return ( + +
{item.title}
+
+ {item.status === 'alpha' && 'Alpha'} + {item.status === 'beta' && 'Beta'} + {item.status === 'inProgress' && 'In Progress'} + {item.status === 'stable' && 'Stable'} + {item.status === 'deprecated' && 'Deprecated'} +
+ + ); + })} +
+ ); + })} + + ); +}; diff --git a/docs-ui/src/components/Navigation/index.tsx b/docs-ui/src/components/Navigation/index.tsx new file mode 100644 index 0000000000..61f39d15c9 --- /dev/null +++ b/docs-ui/src/components/Navigation/index.tsx @@ -0,0 +1 @@ +export { Navigation } from './Navigation'; diff --git a/docs-ui/src/components/Sidebar/Sidebar.module.css b/docs-ui/src/components/Sidebar/Sidebar.module.css index 426073c071..83c1e0bde5 100644 --- a/docs-ui/src/components/Sidebar/Sidebar.module.css +++ b/docs-ui/src/components/Sidebar/Sidebar.module.css @@ -73,100 +73,3 @@ gap: 2px; position: relative; } - -.topNav { - & ul { - margin: 0; - padding: 0; - list-style: none; - display: flex; - flex-direction: column; - gap: 2px; - } - - & li { - margin: 0; - padding: 0; - list-style: none; - } - - & li div, - & li a { - display: flex; - align-items: center; - gap: 8px; - padding: 8px 12px; - border-radius: 4px; - cursor: pointer; - - &:hover { - background-color: var(--action); - - &[data-disabled='true'] { - background-color: transparent; - } - } - - &[data-active='true'] { - background-color: var(--action); - - &[data-disabled='true'] { - background-color: transparent; - } - } - - &[data-disabled='true'] { - opacity: 0.5; - cursor: not-allowed; - } - } -} - -.sectionTitle { - font-size: 0.6875rem; - font-weight: 500; - padding: 0 12px 4px; - color: var(--secondary); - margin-top: 40px; - text-transform: uppercase; - - &:first-child { - margin-top: 12px; - } -} - -.line { - text-decoration: none; - align-items: center; - width: 100%; - display: flex; - flex-direction: row; - justify-content: space-between; - align-items: center; - height: 26px; - padding: 0 12px; - border-radius: 4px; - - &:hover { - background-color: var(--action); - } - - &.active { - background-color: var(--action); - } - - &.active .lineTitle { - color: var(--primary); - } -} - -.lineTitle { - font-size: 14px; - font-weight: 400; - color: var(--primary); -} - -.lineStatus { - font-size: 14px; - color: var(--secondary); -} diff --git a/docs-ui/src/components/Sidebar/Sidebar.tsx b/docs-ui/src/components/Sidebar/Sidebar.tsx index 08b9feac86..d9c16f1905 100644 --- a/docs-ui/src/components/Sidebar/Sidebar.tsx +++ b/docs-ui/src/components/Sidebar/Sidebar.tsx @@ -1,38 +1,11 @@ 'use client'; import styles from './Sidebar.module.css'; -import { components, layoutComponents } from '@/utils/data'; import { ScrollArea } from '@base-ui-components/react/scroll-area'; -import Link from 'next/link'; -import { usePathname } from 'next/navigation'; -import { Fragment } from 'react'; -import clsx from 'clsx'; -import { - RiCollageLine, - RiFileHistoryLine, - RiHazeLine, - RiPaletteLine, - RiServiceLine, - RiStackLine, -} from '@remixicon/react'; import { Logo } from './Logo'; - -const data = [ - { - title: 'Layout Components', - content: layoutComponents, - url: '/components', - }, - { - title: 'Components', - content: components, - url: '/components', - }, -]; +import { Navigation } from '@/components/Navigation'; export const Sidebar = () => { - const pathname = usePathname(); - return (
@@ -42,83 +15,7 @@ export const Sidebar = () => {
- - {data.map(section => { - return ( - -
{section.title}
- - {section.content.map(item => { - const isActive = - pathname === `${section.url}/${item.slug}`; - - return ( - -
{item.title}
-
- {item.status === 'alpha' && 'Alpha'} - {item.status === 'beta' && 'Beta'} - {item.status === 'inProgress' && 'In Progress'} - {item.status === 'stable' && 'Stable'} - {item.status === 'deprecated' && 'Deprecated'} -
- - ); - })} -
- ); - })} +
diff --git a/docs-ui/src/components/Toolbar/Toolbar.module.css b/docs-ui/src/components/Toolbar/Toolbar.module.css index 4566904df5..581fc599b3 100644 --- a/docs-ui/src/components/Toolbar/Toolbar.module.css +++ b/docs-ui/src/components/Toolbar/Toolbar.module.css @@ -18,6 +18,24 @@ font-weight: 500; } +.logoMobile { + display: block; + + @media (min-width: 768px) { + display: none; + } +} + +.breadcrumbDesktop { + display: none; + + @media (min-width: 768px) { + display: flex; + align-items: center; + gap: 0.5rem; + } +} + .breadcrumbLink { color: var(--secondary); text-decoration: none; @@ -70,7 +88,7 @@ align-items: center; justify-content: center; background-color: var(--bg); - border: 1px solid var(--border2); + border: 1px solid var(--border); border-radius: 32px; padding-inline: 16px; color: var(--primary); @@ -105,7 +123,7 @@ display: flex; align-items: center; gap: 4px; - border: 1px solid var(--border2); + border: 1px solid var(--border); border-radius: 32px; padding-inline: 4px; height: 32px; diff --git a/docs-ui/src/components/Toolbar/Toolbar.tsx b/docs-ui/src/components/Toolbar/Toolbar.tsx index 3ba8b76739..e7defd5e8a 100644 --- a/docs-ui/src/components/Toolbar/Toolbar.tsx +++ b/docs-ui/src/components/Toolbar/Toolbar.tsx @@ -22,6 +22,7 @@ import { usePlayground } from '@/utils/playground-context'; import { usePathname } from 'next/navigation'; import Link from 'next/link'; import { components, layoutComponents } from '@/utils/data'; +import { Logo } from '@/components/Sidebar/Logo'; interface ToolbarProps { version: string; @@ -86,23 +87,30 @@ export const Toolbar = ({ version }: ToolbarProps) => { return (
- {breadcrumb.section && breadcrumb.sectionLink ? ( - <> - - {breadcrumb.section} - - +
+ +
+
+ {breadcrumb.section && breadcrumb.sectionLink ? ( + <> + + {breadcrumb.section} + + + + {breadcrumb.title} + + + ) : ( {breadcrumb.title} - - ) : ( - {breadcrumb.title} - )} + )} +