diff --git a/src/components/BottomNav.css b/src/components/BottomNav.css new file mode 100644 index 0000000..3d84fc8 --- /dev/null +++ b/src/components/BottomNav.css @@ -0,0 +1,167 @@ +/* BottomNav — mobile bottom navigation */ + +.bottom-nav { + display: none; + position: fixed; + bottom: 0; + left: 0; + right: 0; + height: var(--bottom-nav-h); + padding-bottom: var(--safe-area-bottom); + background: var(--bg-secondary); + border-top: 1px solid var(--border-line); + backdrop-filter: blur(20px); + -webkit-backdrop-filter: blur(20px); + z-index: 300; + align-items: stretch; + justify-content: space-around; +} + +@media (max-width: 768px) { + .bottom-nav { + display: flex; + } +} + +/* Primary nav items */ +.bottom-nav__item { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + flex: 1; + min-width: 48px; + min-height: 48px; + gap: 3px; + color: var(--text-dim); + text-decoration: none; + font-family: var(--font-body); + font-size: 10px; + font-weight: 500; + letter-spacing: 0.02em; + transition: color 0.18s var(--ease-out); + -webkit-tap-highlight-color: transparent; + outline: none; + border: none; + background: none; + cursor: pointer; + padding: 4px 2px; +} + +.bottom-nav__item:hover, +.bottom-nav__item.is-active, +.bottom-nav__item--active { + color: var(--neon-cyan); +} + +.bottom-nav__item:hover .bottom-nav__icon, +.bottom-nav__item.is-active .bottom-nav__icon, +.bottom-nav__item--active .bottom-nav__icon { + filter: drop-shadow(0 0 6px var(--neon-cyan-dim)); +} + +/* Icon wrapper */ +.bottom-nav__icon { + display: flex; + align-items: center; + justify-content: center; + width: 22px; + height: 22px; + flex-shrink: 0; + transition: filter 0.18s var(--ease-out); +} + +.bottom-nav__icon svg, +.bottom-nav__icon > * { + width: 22px; + height: 22px; +} + +/* Label */ +.bottom-nav__label { + line-height: 1; + white-space: nowrap; +} + +/* ---- More overlay backdrop ---- */ +.bottom-nav__more-overlay { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.55); + backdrop-filter: blur(4px); + -webkit-backdrop-filter: blur(4px); + z-index: 298; + opacity: 0; + pointer-events: none; + transition: opacity 0.22s var(--ease-out); +} + +.bottom-nav__more-overlay.is-open { + opacity: 1; + pointer-events: auto; +} + +/* ---- More panel ---- */ +.bottom-nav__more-panel { + position: fixed; + bottom: calc(var(--bottom-nav-h) + var(--safe-area-bottom)); + left: 0; + right: 0; + z-index: 299; + padding: 16px 12px 12px; + background: var(--surface-raised); + border-top: 1px solid var(--border-line); + border-radius: var(--radius-lg) var(--radius-lg) 0 0; + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 10px; + transform: translateY(100%); + transition: transform 0.25s var(--ease-out); +} + +.bottom-nav__more-panel.is-open { + transform: translateY(0); +} + +/* More panel item */ +.bottom-nav__more-item { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 6px; + padding: 12px 4px; + color: var(--text-dim); + text-decoration: none; + font-family: var(--font-body); + font-size: 11px; + font-weight: 500; + background: var(--surface); + border: 1px solid var(--border-line); + border-radius: var(--radius-md); + transition: color 0.18s var(--ease-out), border-color 0.18s var(--ease-out); + -webkit-tap-highlight-color: transparent; + cursor: pointer; +} + +.bottom-nav__more-item:hover, +.bottom-nav__more-item.is-active { + color: var(--neon-cyan); + border-color: var(--neon-cyan-dim); +} + +.bottom-nav__more-item:hover .bottom-nav__icon, +.bottom-nav__more-item.is-active .bottom-nav__icon { + filter: drop-shadow(0 0 6px var(--neon-cyan-dim)); +} + +/* Reduce motion */ +@media (prefers-reduced-motion: reduce) { + .bottom-nav__item, + .bottom-nav__icon, + .bottom-nav__more-overlay, + .bottom-nav__more-panel, + .bottom-nav__more-item { + transition: none; + } +} diff --git a/src/components/BottomNav.jsx b/src/components/BottomNav.jsx new file mode 100644 index 0000000..258be6f --- /dev/null +++ b/src/components/BottomNav.jsx @@ -0,0 +1,114 @@ +import { useState, useCallback } from 'react'; +import { NavLink, useLocation } from 'react-router-dom'; +import { navLinks } from '../routes'; +import './BottomNav.css'; + +const PRIMARY_PATHS = ['/', '/lotto', '/stock', '/travel']; + +// Vertical dots (three circles) icon for "more" +function MoreDotsIcon() { + return ( + + ); +} + +const primaryLinks = navLinks.filter((link) => + PRIMARY_PATHS.includes(link.path) +); +// Preserve the order defined in PRIMARY_PATHS +const orderedPrimaryLinks = PRIMARY_PATHS.map((p) => + primaryLinks.find((l) => l.path === p) +).filter(Boolean); + +const moreLinks = navLinks.filter( + (link) => !PRIMARY_PATHS.includes(link.path) +); + +export default function BottomNav() { + const [moreOpen, setMoreOpen] = useState(false); + const location = useLocation(); + + const openMore = useCallback(() => setMoreOpen(true), []); + const closeMore = useCallback(() => setMoreOpen(false), []); + const toggleMore = useCallback(() => setMoreOpen((prev) => !prev), []); + + // Highlight the "more" button when the current path belongs to moreLinks + const isMoreActive = + moreOpen || moreLinks.some((link) => location.pathname === link.path); + + return ( + <> + {/* Backdrop */} +
+ + {/* More panel */} +