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:
co-authored by
Claude Opus 4.8
parent
a385a8b506
commit
22b0380d27
@@ -1,6 +1,7 @@
|
||||
import { BrowserRouter, Routes, Route, Navigate, useParams, useSearchParams, useLocation } from 'react-router-dom';
|
||||
import TopNav from './components/TopNav';
|
||||
import SideRail from './components/SideRail';
|
||||
import BottomNav from './components/BottomNav';
|
||||
import Library from './pages/Library';
|
||||
import Boards from './pages/Boards';
|
||||
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).
|
||||
function Shell({ children }: { readonly children: ReactNode }) {
|
||||
const { pathname } = useLocation();
|
||||
const offset = getUser() && pathname !== '/login' ? 'lg:pl-[60px]' : '';
|
||||
return <div className={offset}>{children}</div>;
|
||||
// Desktop: inset for the SideRail. Mobile: bottom padding so content clears the
|
||||
// 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() {
|
||||
@@ -109,6 +113,7 @@ export default function App() {
|
||||
<SearchOverlayProvider>
|
||||
<TopNav />
|
||||
<SideRail />
|
||||
<BottomNav />
|
||||
<Shell>
|
||||
<Routes>
|
||||
<Route path="/login" element={<Login />} />
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -3,15 +3,18 @@ import uploadSrc from '../assets/Upload_file_icon_Animation.lottie?url';
|
||||
import searchingSrc from '../assets/Searching.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.
|
||||
function LottieLoader({ src, size = 96, className = '' }: {
|
||||
function LottieLoader({ src, size, className = '' }: {
|
||||
readonly src: string;
|
||||
readonly size?: number;
|
||||
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 (
|
||||
<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%' }} />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -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">
|
||||
{busy ? (
|
||||
<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>
|
||||
</div>
|
||||
) : result ? (
|
||||
|
||||
@@ -349,9 +349,9 @@ export default function Fabrics() {
|
||||
{/* Bulk-upload progress — centred modal with the animation + live tally */}
|
||||
{uploading && progress && (
|
||||
<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">
|
||||
<UploadLottie size={184} />
|
||||
<p className="font-display text-[22px] text-ink leading-tight">Reading {progress.done} of {progress.total}…</p>
|
||||
<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 className="w-32 h-32 sm:w-44 sm:h-44" />
|
||||
<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">
|
||||
{progress.ok} added
|
||||
{progress.skipped ? ` · ${progress.skipped} duplicate${progress.skipped > 1 ? 's' : ''}` : ''}
|
||||
|
||||
@@ -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">
|
||||
{busy ? (
|
||||
<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>
|
||||
</div>
|
||||
) : result ? (
|
||||
|
||||
@@ -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()}>
|
||||
<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">
|
||||
<SearchingLottie size={150} />
|
||||
<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 className="w-28 h-28 sm:w-36 sm:h-36" />
|
||||
{search ? (
|
||||
<>
|
||||
<span className="font-plex text-[9.5px] uppercase tracking-[0.2em] text-muted">Researching · {search.buyer_name}</span>
|
||||
|
||||
Reference in New Issue
Block a user