feat(mobile): bottom tab bar + responsive loaders/modals

- BottomNav: mobile-only fixed bottom tab bar with the top ~5 sections
  (thumb-friendly), respecting the iOS safe-area inset; the full list +
  account stay in the TopNav hamburger drawer. SideRail remains lg-only.
- Shell: bottom padding on mobile so content clears the bar.
- LottieLoader: accept responsive width/height via className (not just a
  fixed px size); upload/trends/studio animations now scale down on phones
  (w-28 → sm:w-40ish) and their modals tighten padding on small screens.

Pages (Fabrics/Studio/Trends) already stacked responsively; this fills the
navigation + animation-sizing gaps. tsc -b + build clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Ajay Sable
2026-06-30 13:34:26 +05:30
co-authored by Claude Opus 4.8
parent a385a8b506
commit 22b0380d27
7 changed files with 56 additions and 12 deletions
+7 -2
View File
@@ -1,6 +1,7 @@
import { BrowserRouter, Routes, Route, Navigate, useParams, useSearchParams, useLocation } from 'react-router-dom'; import { BrowserRouter, Routes, Route, Navigate, useParams, useSearchParams, useLocation } from 'react-router-dom';
import TopNav from './components/TopNav'; import TopNav from './components/TopNav';
import SideRail from './components/SideRail'; import SideRail from './components/SideRail';
import BottomNav from './components/BottomNav';
import Library from './pages/Library'; import Library from './pages/Library';
import Boards from './pages/Boards'; import Boards from './pages/Boards';
import BoardDetail from './pages/BoardDetail'; import BoardDetail from './pages/BoardDetail';
@@ -99,8 +100,11 @@ function OrderDetailRedirect() {
// the login screen and for signed-out visitors (the rail isn't shown there). // the login screen and for signed-out visitors (the rail isn't shown there).
function Shell({ children }: { readonly children: ReactNode }) { function Shell({ children }: { readonly children: ReactNode }) {
const { pathname } = useLocation(); const { pathname } = useLocation();
const offset = getUser() && pathname !== '/login' ? 'lg:pl-[60px]' : ''; // Desktop: inset for the SideRail. Mobile: bottom padding so content clears the
return <div className={offset}>{children}</div>; // fixed bottom tab bar (+ the iOS home-indicator safe area).
const signedIn = getUser() && pathname !== '/login';
const cls = signedIn ? 'lg:pl-[60px] pb-[calc(3.5rem+env(safe-area-inset-bottom))] lg:pb-0' : '';
return <div className={cls}>{children}</div>;
} }
export default function App() { export default function App() {
@@ -109,6 +113,7 @@ export default function App() {
<SearchOverlayProvider> <SearchOverlayProvider>
<TopNav /> <TopNav />
<SideRail /> <SideRail />
<BottomNav />
<Shell> <Shell>
<Routes> <Routes>
<Route path="/login" element={<Login />} /> <Route path="/login" element={<Login />} />
+36
View File
@@ -0,0 +1,36 @@
import { NavLink, useLocation } from 'react-router-dom';
import { visibleNav } from '../lib/nav';
// Mobile-only bottom tab bar: the top ~5 sections for thumb-friendly access.
// The full section list + account live in the TopNav hamburger drawer. Hidden on
// lg+ (the SideRail takes over). Respects the iOS home-indicator safe area.
export default function BottomNav() {
const { pathname } = useLocation();
if (pathname === '/login') return null;
const links = visibleNav().slice(0, 5);
if (links.length === 0) return null; // signed out / public page
return (
<nav
className="lg:hidden fixed inset-x-0 bottom-0 z-50 flex items-stretch
bg-ivory/85 backdrop-blur-2xl backdrop-saturate-150 border-t border-white/40
shadow-[0_-1px_24px_rgba(27,24,19,0.06)] pb-[env(safe-area-inset-bottom)]"
>
{links.map(({ to, label, end, icon }) => (
<NavLink
key={to}
to={to}
end={end}
className={({ isActive }) =>
'flex-1 h-14 flex flex-col items-center justify-center gap-1 transition-colors ' +
(isActive ? 'text-ink' : 'text-muted hover:text-ink')
}
>
<span className="w-[18px] flex items-center justify-center">{icon}</span>
<span className="text-[9px] tracking-wide leading-none truncate max-w-[64px]">{label}</span>
</NavLink>
))}
</nav>
);
}
+6 -3
View File
@@ -3,15 +3,18 @@ import uploadSrc from '../assets/Upload_file_icon_Animation.lottie?url';
import searchingSrc from '../assets/Searching.lottie?url'; import searchingSrc from '../assets/Searching.lottie?url';
import designerSrc from '../assets/Digital_Designer.lottie?url'; import designerSrc from '../assets/Digital_Designer.lottie?url';
// Looping dotLottie animation sized to a square box. Replaces the static // Looping dotLottie animation. Pass a fixed `size` (px) OR responsive width/height
// via `className` (e.g. "w-28 h-28 sm:w-44 sm:h-44"). Replaces the static
// border-spinners in the upload / trends / studio loading states. // border-spinners in the upload / trends / studio loading states.
function LottieLoader({ src, size = 96, className = '' }: { function LottieLoader({ src, size, className = '' }: {
readonly src: string; readonly src: string;
readonly size?: number; readonly size?: number;
readonly className?: string; readonly className?: string;
}) { }) {
// Fall back to 96px only when neither a size nor a sizing className is given.
const style = size != null ? { width: size, height: size } : (className ? undefined : { width: 96, height: 96 });
return ( return (
<div className={className} style={{ width: size, height: size }} aria-hidden> <div className={className} style={style} aria-hidden>
<DotLottieReact src={src} loop autoplay style={{ width: '100%', height: '100%' }} /> <DotLottieReact src={src} loop autoplay style={{ width: '100%', height: '100%' }} />
</div> </div>
); );
+1 -1
View File
@@ -524,7 +524,7 @@ export default function ConceptStudio() {
<div className="relative flex-1 min-h-[55vh] lg:min-h-[300px] rounded border border-line bg-ivory overflow-hidden flex items-center justify-center"> <div className="relative flex-1 min-h-[55vh] lg:min-h-[300px] rounded border border-line bg-ivory overflow-hidden flex items-center justify-center">
{busy ? ( {busy ? (
<div className="flex flex-col items-center gap-2 text-muted"> <div className="flex flex-col items-center gap-2 text-muted">
<DesignerLottie size={160} /> <DesignerLottie className="w-28 h-28 sm:w-40 sm:h-40" />
<span className="text-[12px]">Composing — this can take ~30–60s</span> <span className="text-[12px]">Composing — this can take ~30–60s</span>
</div> </div>
) : result ? ( ) : result ? (
+3 -3
View File
@@ -349,9 +349,9 @@ export default function Fabrics() {
{/* Bulk-upload progress — centred modal with the animation + live tally */} {/* Bulk-upload progress — centred modal with the animation + live tally */}
{uploading && progress && ( {uploading && progress && (
<div className="fixed inset-0 z-[60] bg-ink/40 backdrop-blur-sm flex items-center justify-center p-4"> <div className="fixed inset-0 z-[60] bg-ink/40 backdrop-blur-sm flex items-center justify-center p-4">
<div className="bg-paper rounded-xl border border-line w-full max-w-sm px-9 py-10 flex flex-col items-center text-center gap-3 shadow-card"> <div className="bg-paper rounded-xl border border-line w-full max-w-sm px-6 py-8 sm:px-9 sm:py-10 flex flex-col items-center text-center gap-3 shadow-card">
<UploadLottie size={184} /> <UploadLottie className="w-32 h-32 sm:w-44 sm:h-44" />
<p className="font-display text-[22px] text-ink leading-tight">Reading {progress.done} of {progress.total}…</p> <p className="font-display text-[20px] sm:text-[22px] text-ink leading-tight">Reading {progress.done} of {progress.total}…</p>
<p className="text-[12.5px] text-muted"> <p className="text-[12.5px] text-muted">
{progress.ok} added {progress.ok} added
{progress.skipped ? ` · ${progress.skipped} duplicate${progress.skipped > 1 ? 's' : ''}` : ''} {progress.skipped ? ` · ${progress.skipped} duplicate${progress.skipped > 1 ? 's' : ''}` : ''}
+1 -1
View File
@@ -219,7 +219,7 @@ export default function ProductStudio() {
<div className="relative flex-1 min-h-[55vh] lg:min-h-[300px] rounded border border-line bg-ivory overflow-hidden flex items-center justify-center"> <div className="relative flex-1 min-h-[55vh] lg:min-h-[300px] rounded border border-line bg-ivory overflow-hidden flex items-center justify-center">
{busy ? ( {busy ? (
<div className="flex flex-col items-center gap-2 text-muted"> <div className="flex flex-col items-center gap-2 text-muted">
<DesignerLottie size={160} /> <DesignerLottie className="w-28 h-28 sm:w-40 sm:h-40" />
<span className="text-[12px]">Directing the shoot — ~30–90s at 4K</span> <span className="text-[12px]">Directing the shoot — ~30–90s at 4K</span>
</div> </div>
) : result ? ( ) : result ? (
+2 -2
View File
@@ -40,8 +40,8 @@ function AgentModal({ search, onClose }: { readonly search: TrendSearch | null;
<div className="relative bg-paper rounded-xl border border-line w-full max-w-md max-h-[88vh] flex flex-col overflow-hidden shadow-card" onClick={(e) => e.stopPropagation()}> <div className="relative bg-paper rounded-xl border border-line w-full max-w-md max-h-[88vh] flex flex-col overflow-hidden shadow-card" onClick={(e) => e.stopPropagation()}>
<button onClick={onClose} className="absolute top-3.5 right-4 z-10 font-plex text-[10px] uppercase tracking-[0.16em] text-muted hover:text-ink">Hide</button> <button onClick={onClose} className="absolute top-3.5 right-4 z-10 font-plex text-[10px] uppercase tracking-[0.16em] text-muted hover:text-ink">Hide</button>
<div className="flex flex-col items-center text-center gap-1.5 px-8 pt-9 pb-5 border-b border-line"> <div className="flex flex-col items-center text-center gap-1.5 px-5 sm:px-8 pt-8 sm:pt-9 pb-5 border-b border-line">
<SearchingLottie size={150} /> <SearchingLottie className="w-28 h-28 sm:w-36 sm:h-36" />
{search ? ( {search ? (
<> <>
<span className="font-plex text-[9.5px] uppercase tracking-[0.2em] text-muted">Researching · {search.buyer_name}</span> <span className="font-plex text-[9.5px] uppercase tracking-[0.2em] text-muted">Researching · {search.buyer_name}</span>