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 { 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 />} />
|
||||||
|
|||||||
@@ -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 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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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 ? (
|
||||||
|
|||||||
@@ -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' : ''}` : ''}
|
||||||
|
|||||||
@@ -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 ? (
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user