feat: Add scroll-hide header and contextual FAB system
Implemented:
1. Scroll-Hide App Bar (Mobile)
- Hides on scroll down (past 50px)
- Shows on scroll up
- Chrome URL bar behavior
- Smooth slide animation (300ms)
- Desktop always visible (md:translate-y-0)
2. Contextual FAB Hook
- useFABConfig() hook for pages
- Pre-configured for: orders, products, customers, coupons, dashboard
- Automatic cleanup on unmount
- Easy to use: useFABConfig('orders')
3. Removed Focus Styles
- Bottom nav links: focus:outline-none
- Cleaner mobile UX
Header Scroll Behavior:
- Scroll down > 50px: Header slides up (-translate-y-full)
- Scroll up: Header slides down (translate-y-0)
- Desktop: Always visible (md:translate-y-0)
- Smooth transition (duration-300)
FAB Configuration:
const configs = {
orders: 'Create Order' → /orders/new
products: 'Add Product' → /products/new
customers: 'Add Customer' → /customers/new
coupons: 'Create Coupon' → /coupons/new
dashboard: 'Quick Actions' → (future speed dial)
none: Hide FAB
}
Usage in Pages:
import { useFABConfig } from '@/hooks/useFABConfig';
function OrdersPage() {
useFABConfig('orders'); // Sets up FAB automatically
return <div>...</div>;
}
Next Steps:
- Add useFABConfig to actual pages
- Test scroll behavior on devices
- Implement speed dial for dashboard
Files Created:
- useFABConfig.tsx: Contextual FAB configuration hook
Files Modified:
- App.tsx: Scroll detection and header animation
- BottomNav.tsx: Removed focus outline styles
This commit is contained in:
@@ -264,6 +264,8 @@ function AddonRoute({ config }: { config: any }) {
|
|||||||
|
|
||||||
function Header({ onFullscreen, fullscreen, showToggle = true }: { onFullscreen: () => void; fullscreen: boolean; showToggle?: boolean }) {
|
function Header({ onFullscreen, fullscreen, showToggle = true }: { onFullscreen: () => void; fullscreen: boolean; showToggle?: boolean }) {
|
||||||
const [siteTitle, setSiteTitle] = React.useState((window as any).wnw?.siteTitle || 'WooNooW');
|
const [siteTitle, setSiteTitle] = React.useState((window as any).wnw?.siteTitle || 'WooNooW');
|
||||||
|
const [isVisible, setIsVisible] = React.useState(true);
|
||||||
|
const [lastScrollY, setLastScrollY] = React.useState(0);
|
||||||
const isStandalone = window.WNW_CONFIG?.standaloneMode ?? false;
|
const isStandalone = window.WNW_CONFIG?.standaloneMode ?? false;
|
||||||
|
|
||||||
// Listen for store settings updates
|
// Listen for store settings updates
|
||||||
@@ -278,6 +280,35 @@ function Header({ onFullscreen, fullscreen, showToggle = true }: { onFullscreen:
|
|||||||
return () => window.removeEventListener('woonoow:store:updated' as any, handleStoreUpdate);
|
return () => window.removeEventListener('woonoow:store:updated' as any, handleStoreUpdate);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Hide/show header on scroll (mobile only)
|
||||||
|
React.useEffect(() => {
|
||||||
|
const handleScroll = () => {
|
||||||
|
const currentScrollY = window.scrollY;
|
||||||
|
|
||||||
|
// Only apply on mobile (check window width)
|
||||||
|
if (window.innerWidth >= 768) {
|
||||||
|
setIsVisible(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentScrollY > lastScrollY && currentScrollY > 50) {
|
||||||
|
// Scrolling down & past threshold
|
||||||
|
setIsVisible(false);
|
||||||
|
} else if (currentScrollY < lastScrollY) {
|
||||||
|
// Scrolling up
|
||||||
|
setIsVisible(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
setLastScrollY(currentScrollY);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('scroll', handleScroll, { passive: true });
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('scroll', handleScroll);
|
||||||
|
};
|
||||||
|
}, [lastScrollY]);
|
||||||
|
|
||||||
const handleLogout = async () => {
|
const handleLogout = async () => {
|
||||||
try {
|
try {
|
||||||
await fetch((window.WNW_CONFIG?.restUrl || '') + '/auth/logout', {
|
await fetch((window.WNW_CONFIG?.restUrl || '') + '/auth/logout', {
|
||||||
@@ -291,7 +322,7 @@ function Header({ onFullscreen, fullscreen, showToggle = true }: { onFullscreen:
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className={`h-16 border-b border-border flex items-center px-4 justify-between sticky ${fullscreen ? `top-0` : `top-[32px]`} z-40 bg-background md:bg-background/95 md:backdrop-blur md:supports-[backdrop-filter]:bg-background/60`}>
|
<header className={`h-16 border-b border-border flex items-center px-4 justify-between sticky ${fullscreen ? `top-0` : `top-[32px]`} z-40 bg-background md:bg-background/95 md:backdrop-blur md:supports-[backdrop-filter]:bg-background/60 transition-transform duration-300 ${!isVisible ? '-translate-y-full md:translate-y-0' : 'translate-y-0'}`}>
|
||||||
<div className="font-semibold">{siteTitle}</div>
|
<div className="font-semibold">{siteTitle}</div>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="text-sm opacity-70 hidden sm:block">{window.WNW_API?.isDev ? 'Dev Server' : 'Production'}</div>
|
<div className="text-sm opacity-70 hidden sm:block">{window.WNW_API?.isDev ? 'Dev Server' : 'Production'}</div>
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ export function BottomNav() {
|
|||||||
<NavLink
|
<NavLink
|
||||||
key={item.to}
|
key={item.to}
|
||||||
to={item.to}
|
to={item.to}
|
||||||
className={`flex flex-col items-center justify-center flex-1 h-full gap-0.5 transition-colors ${
|
className={`flex flex-col items-center justify-center flex-1 h-full gap-0.5 transition-colors focus:outline-none focus-visible:outline-none ${
|
||||||
active
|
active
|
||||||
? 'text-primary'
|
? 'text-primary'
|
||||||
: 'text-muted-foreground hover:text-foreground'
|
: 'text-muted-foreground hover:text-foreground'
|
||||||
|
|||||||
73
admin-spa/src/hooks/useFABConfig.tsx
Normal file
73
admin-spa/src/hooks/useFABConfig.tsx
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { Plus } from 'lucide-react';
|
||||||
|
import { useFAB } from '@/contexts/FABContext';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook to configure FAB for different pages
|
||||||
|
* Usage: useFABConfig('orders') in Orders page component
|
||||||
|
*/
|
||||||
|
export function useFABConfig(page: 'orders' | 'products' | 'customers' | 'coupons' | 'dashboard' | 'none') {
|
||||||
|
const { setFAB, clearFAB } = useFAB();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
switch (page) {
|
||||||
|
case 'orders':
|
||||||
|
setFAB({
|
||||||
|
icon: <Plus className="w-6 h-6" />,
|
||||||
|
label: 'Create Order',
|
||||||
|
onClick: () => navigate('/orders/new'),
|
||||||
|
visible: true
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'products':
|
||||||
|
setFAB({
|
||||||
|
icon: <Plus className="w-6 h-6" />,
|
||||||
|
label: 'Add Product',
|
||||||
|
onClick: () => navigate('/products/new'),
|
||||||
|
visible: true
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'customers':
|
||||||
|
setFAB({
|
||||||
|
icon: <Plus className="w-6 h-6" />,
|
||||||
|
label: 'Add Customer',
|
||||||
|
onClick: () => navigate('/customers/new'),
|
||||||
|
visible: true
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'coupons':
|
||||||
|
setFAB({
|
||||||
|
icon: <Plus className="w-6 h-6" />,
|
||||||
|
label: 'Create Coupon',
|
||||||
|
onClick: () => navigate('/coupons/new'),
|
||||||
|
visible: true
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'dashboard':
|
||||||
|
// Dashboard could have a speed dial menu in the future
|
||||||
|
setFAB({
|
||||||
|
icon: <Plus className="w-6 h-6" />,
|
||||||
|
label: 'Quick Actions',
|
||||||
|
onClick: () => {
|
||||||
|
// TODO: Implement speed dial menu
|
||||||
|
console.log('Quick actions menu');
|
||||||
|
},
|
||||||
|
visible: true
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'none':
|
||||||
|
default:
|
||||||
|
clearFAB();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => clearFAB();
|
||||||
|
}, [page, navigate, setFAB, clearFAB]);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user