style: redesign app following HIG with larger hero images and refined typography

This commit is contained in:
2025-12-28 20:38:10 +01:00
parent c51cd23d5e
commit 332bfdaf02
8 changed files with 273 additions and 245 deletions

28
hig.md Normal file
View File

@@ -0,0 +1,28 @@
# Apple Human Interface Guidelines (HIG) - Core Principles for iOS
@Context: Mobile Whisky Tasting App (Dark Mode)
## 1. Layout & Structure
- **Safe Areas:** Always respect the top (Dynamic Island/Notch) and bottom (Home Indicator) safe areas. Never place interactive elements (like the "Save Tasting" sticky button) directly on the bottom edge; add bottom padding.
- **Modality (Sheets):** For the "Session Context" (Step C in our flow), use native-style Sheets. Supports "detents" (medium/large). Sheets should be dismissible by dragging down.
- **Navigation:** Use a Navigation Bar for hierarchy. The title (e.g., "Tasting Editor") should be large (Large Title) on top of the scroll view and collapse to a small title on scroll.
## 2. Touch & Interaction
- **Hit Targets:** Minimum tappable area is **44x44 pt**. Ensure the "Smart Tags" in the form are large enough.
- **Feedback:** Use Haptics (Haptic Feedback) for significant actions (e.g., `success` haptic when "Save Tasting" is clicked, `selection` haptic when moving sliders).
- **Gestures:** Support "Swipe Back" to navigate to the previous screen. Do not block this gesture with custom UI.
## 3. Visual Design (Dark Mode)
- **Colors:** - Never use pure black (`#000000`) for backgrounds. Use semantic system colors or generic dark grays (e.g., `systemBackground` / `#1C1C1E`).
- Use `systemGray` to `systemGray6` for elevation levels (cards on top of background).
- **Typography:**
- Use San Francisco (SF Pro) or the defined app fonts (Inter/Playfair).
- Respect Dynamic Type sizes so users can scale text.
- **Icons:** Use SF Symbols (or Lucide variants closely matching SF Symbols) with consistent stroke weights (usually "Medium" or "Semibold" for active states).
## 4. Specific Component Rules
- **Buttons:**
- "Primary" buttons (Save) should use high-contrast background colors.
- "Secondary" buttons (Cancel/Back) should be plain text or tinted glyphs.
- Avoid using multiple primary buttons on one screen.
- **Inputs:** - Text fields must clearly indicate focus.
- Keyboard: Use the correct keyboard type (e.g., `decimalPad` for ABV input).

2
next-env.d.ts vendored
View File

@@ -1,6 +1,6 @@
/// <reference types="next" /> /// <reference types="next" />
/// <reference types="next/image-types/global" /> /// <reference types="next/image-types/global" />
import "./.next/dev/types/routes.d.ts"; import "./.next/types/routes.d.ts";
// NOTE: This file should not be edited // NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

View File

@@ -4,24 +4,24 @@
@layer base { @layer base {
:root { :root {
--background: #09090b; --background: #1c1c1e;
/* zinc-950 */ /* systemBackground */
--surface: #18181b; --surface: #2c2c2e;
/* zinc-900 */ /* secondarySystemBackground */
--primary: #ea580c; --primary: #ea580c;
/* orange-600 */ /* orange-600 */
--secondary: #f97316; --secondary: #f97316;
/* orange-500 */ /* orange-500 */
--text-primary: #fafafa; --text-primary: #fafafa;
--text-secondary: #a1a1aa; --text-secondary: #a1a1aa;
--border: #27272a; --border: #38383a;
/* zinc-800 */ /* separator */
--ring: #f97316; --ring: #f97316;
} }
} }
body { body {
@apply bg-[#09090b] text-[#fafafa] antialiased; @apply bg-[#1c1c1e] text-[#fafafa] antialiased selection:bg-orange-500/30;
font-feature-settings: "cv02", "cv03", "cv04", "cv11"; font-feature-settings: "cv02", "cv03", "cv04", "cv11";
} }

View File

@@ -204,7 +204,7 @@ export default function Home() {
} }
return ( return (
<main className="flex min-h-screen flex-col items-center gap-6 md:gap-12 p-4 md:p-24 bg-zinc-950 pb-32"> <main className="flex min-h-screen flex-col items-center gap-6 md:gap-12 p-4 md:p-24 bg-[var(--background)] pb-32">
<div className="z-10 max-w-5xl w-full flex flex-col items-center gap-12"> <div className="z-10 max-w-5xl w-full flex flex-col items-center gap-12">
<header className="w-full flex flex-col sm:flex-row justify-between items-center gap-4 sm:gap-0"> <header className="w-full flex flex-col sm:flex-row justify-between items-center gap-4 sm:gap-0">
<div className="flex flex-col items-center sm:items-start group"> <div className="flex flex-col items-center sm:items-start group">

View File

@@ -39,11 +39,22 @@ export default function BottleDetails({ bottleId, sessionId, userId }: BottleDet
const handleQuickUpdate = async (newPrice?: string, newStatus?: string) => { const handleQuickUpdate = async (newPrice?: string, newStatus?: string) => {
if (isOffline) return; if (isOffline) return;
setIsUpdating(true); setIsUpdating(true);
// Haptic feedback for interaction
if (window.navigator.vibrate) {
window.navigator.vibrate(10);
}
try { try {
await updateBottle(bottleId, { await updateBottle(bottleId, {
purchase_price: newPrice !== undefined ? (newPrice ? parseFloat(newPrice) : null) : (price ? parseFloat(price) : null), purchase_price: newPrice !== undefined ? (newPrice ? parseFloat(newPrice) : null) : (price ? parseFloat(price) : null),
status: newStatus !== undefined ? newStatus : status status: newStatus !== undefined ? newStatus : status
} as any); } as any);
// Success haptic
if (window.navigator.vibrate) {
window.navigator.vibrate([10, 50, 10]);
}
} catch (err) { } catch (err) {
console.error('Quick update failed:', err); console.error('Quick update failed:', err);
} finally { } finally {
@@ -82,79 +93,65 @@ export default function BottleDetails({ bottleId, sessionId, userId }: BottleDet
if (!bottle) return null; // Should not happen due to check above if (!bottle) return null; // Should not happen due to check above
return ( return (
<div className="max-w-2xl mx-auto px-4 pb-12 space-y-8"> <div className="max-w-4xl mx-auto pb-24">
{/* Back Button */} {/* Header / Hero Section */}
<div className="pt-4"> <div className="relative w-full overflow-hidden bg-[var(--surface)] shadow-2xl">
{/* Back Button Overlay */}
<div className="absolute top-6 left-6 z-20">
<Link <Link
href={`/${sessionId ? `?session_id=${sessionId}` : ''}`} href={`/${sessionId ? `?session_id=${sessionId}` : ''}`}
className="inline-flex items-center gap-2 text-zinc-500 hover:text-zinc-300 transition-colors font-bold text-sm tracking-tight" className="flex items-center justify-center w-10 h-10 rounded-full bg-black/40 backdrop-blur-md text-white border border-white/10 active:scale-95 transition-all"
> >
<ChevronLeft size={18} /> <ChevronLeft size={24} />
Zurück
</Link> </Link>
</div> </div>
{isOffline && ( {/* Hero Image - Slightly More Compact Aspect for better title flow */}
<div className="bg-orange-600/10 border border-orange-600/20 p-3 rounded-2xl flex items-center gap-3 animate-in fade-in slide-in-from-top-2"> <div className="relative aspect-[4/3] md:aspect-[16/8] w-full flex items-center justify-center p-6 md:p-10 overflow-hidden">
<WifiOff size={16} className="text-orange-600" /> {/* Background Glow */}
<p className="text-[10px] font-bold uppercase tracking-widest text-orange-500">Offline-Modus</p> <div className="absolute inset-0 bg-[radial-gradient(circle_at_center,_var(--tw-gradient-stops))] from-orange-600/10 via-transparent to-transparent opacity-30" />
</div>
)}
{/* Header & Hero Section */}
<div className="space-y-6">
{/* 1. Header (Title at top) */}
<div className="text-center md:text-left">
<h1 className="text-3xl md:text-5xl font-black text-white tracking-tighter uppercase leading-none">
{bottle.name}
</h1>
<h2 className="text-lg md:text-xl text-orange-600 font-bold mt-2 uppercase tracking-[0.2em]">
{bottle.distillery}
</h2>
</div>
{/* 2. Image (Below title) */}
<div className="relative aspect-video w-full max-h-[280px] rounded-3xl overflow-hidden bg-radial-dark border border-zinc-800/50 flex items-center justify-center p-4">
<div className="absolute inset-0 bg-[radial-gradient(circle_at_center,_var(--tw-gradient-stops))] from-zinc-800/20 via-transparent to-transparent opacity-50" />
<img <img
src={getStorageUrl(bottle.image_url)} src={getStorageUrl(bottle.image_url)}
alt={bottle.name} alt={bottle.name}
className="max-h-full max-w-full object-contain drop-shadow-[0_20px_50px_rgba(0,0,0,0.5)] z-10" className="max-h-full max-w-full object-contain drop-shadow-[0_20px_60px_rgba(0,0,0,0.6)] z-10 transition-transform duration-700 hover:scale-105"
/> />
</div> </div>
{/* 3. Metadata Consolidation (Info Row) */} {/* Info Overlay - Mobile Gradient */}
<div className="flex flex-wrap items-center justify-center md:justify-start gap-2 pt-2"> <div className="absolute inset-x-0 bottom-0 h-48 bg-gradient-to-t from-[var(--background)] to-transparent pointer-events-none" />
<div className="flex items-center gap-1.5 px-3 py-1.5 bg-zinc-900/80 border border-zinc-800 rounded-full">
<Wine size={14} className="text-orange-500" />
<span className="text-[11px] font-black uppercase tracking-wider text-zinc-300">{bottle.category || 'Whisky'}</span>
</div> </div>
<div className="flex items-center gap-1.5 px-3 py-1.5 bg-zinc-900/80 border border-zinc-800 rounded-full">
<Droplets size={14} className="text-blue-400" /> {/* Content Container */}
<span className="text-[11px] font-black uppercase tracking-wider text-zinc-300">{bottle.abv}%</span> <div className="px-6 md:px-12 -mt-12 relative z-10 space-y-12">
{/* Title Section - HIG Large Title Pattern */}
<div className="space-y-2">
{isOffline && (
<div className="inline-flex bg-orange-600/10 border border-orange-600/20 px-3 py-1 rounded-full items-center gap-2 mb-2">
<WifiOff size={12} className="text-orange-600" />
<p className="text-[9px] font-black uppercase tracking-widest text-orange-500">Offline</p>
</div>
)}
<h2 className="text-sm font-black text-orange-600 uppercase tracking-[0.2em]">
{bottle.distillery}
</h2>
<h1 className="text-3xl md:text-5xl font-extrabold text-white tracking-tight leading-[1.1]">
{bottle.name}
</h1>
{/* Metadata Items - Text based for better readability */}
<div className="flex flex-wrap items-center gap-3 pt-6">
<div className="px-4 py-2 bg-white/5 border border-white/10 rounded-xl">
<p className="text-[10px] font-bold text-zinc-500 uppercase tracking-widest mb-0.5">Category</p>
<p className="text-sm font-black text-zinc-100 uppercase">{bottle.category || 'Whisky'}</p>
</div>
<div className="px-4 py-2 bg-white/5 border border-white/10 rounded-xl">
<p className="text-[10px] font-bold text-zinc-500 uppercase tracking-widest mb-0.5">ABV</p>
<p className="text-sm font-black text-zinc-100 uppercase">{bottle.abv}%</p>
</div> </div>
{bottle.age && ( {bottle.age && (
<div className="flex items-center gap-1.5 px-3 py-1.5 bg-zinc-900/80 border border-zinc-800 rounded-full"> <div className="px-4 py-2 bg-white/5 border border-white/10 rounded-xl">
<Calendar size={14} className="text-zinc-500" /> <p className="text-[10px] font-bold text-zinc-500 uppercase tracking-widest mb-0.5">Age</p>
<span className="text-[11px] font-black uppercase tracking-wider text-zinc-300">{bottle.age}J.</span> <p className="text-sm font-black text-zinc-100 uppercase">{bottle.age} Years</p>
</div>
)}
{bottle.distilled_at && (
<div className="flex items-center gap-1.5 px-3 py-1.5 bg-zinc-900/80 border border-zinc-800 rounded-full">
<Calendar size={12} className="text-zinc-500" />
<span className="text-[10px] font-black uppercase tracking-wider text-zinc-400">Dist. {bottle.distilled_at}</span>
</div>
)}
{bottle.bottled_at && (
<div className="flex items-center gap-1.5 px-3 py-1.5 bg-zinc-900/80 border border-zinc-800 rounded-full">
<Package size={12} className="text-zinc-500" />
<span className="text-[10px] font-black uppercase tracking-wider text-zinc-400">Bott. {bottle.bottled_at}</span>
</div>
)}
{bottle.batch_info && (
<div className="flex items-center gap-1.5 px-3 py-1.5 bg-zinc-900/80 border border-zinc-800 rounded-full">
<Info size={12} className="text-zinc-500" />
<span className="text-[10px] font-black uppercase tracking-wider text-zinc-400">{bottle.batch_info}</span>
</div> </div>
)} )}
{bottle.whiskybase_id && ( {bottle.whiskybase_id && (
@@ -162,20 +159,22 @@ export default function BottleDetails({ bottleId, sessionId, userId }: BottleDet
href={`https://www.whiskybase.com/whiskies/whisky/${bottle.whiskybase_id}`} href={`https://www.whiskybase.com/whiskies/whisky/${bottle.whiskybase_id}`}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="flex items-center gap-1.5 px-3 py-1.5 bg-zinc-900/80 border border-zinc-800 rounded-full hover:border-orange-600/50 transition-colors" className="px-4 py-2 bg-orange-600 border border-orange-500 rounded-xl hover:bg-orange-500 transition-colors"
> >
<ExternalLink size={12} className="text-zinc-600" /> <p className="text-[10px] font-bold text-orange-200 uppercase tracking-widest mb-0.5">Whiskybase</p>
<span className="text-[10px] font-black uppercase tracking-wider text-zinc-500">WB {bottle.whiskybase_id}</span> <p className="text-sm font-black text-white uppercase flex items-center gap-2">
#{bottle.whiskybase_id} <ExternalLink size={14} />
</p>
</a> </a>
)} )}
</div> </div>
</div> </div>
{/* 4. Inventory Section (Cohesive Container) */} {/* 4. Inventory Section (Cohesive Container) */}
<section className="bg-zinc-900/30 border border-zinc-800/50 rounded-[32px] p-6 space-y-6"> <section className="bg-zinc-800/30 backdrop-blur-xl border border-white/5 rounded-[40px] p-8 space-y-8">
<div className="flex items-center justify-between mb-2"> <div className="flex items-center justify-between mb-4">
<h3 className="text-[10px] font-black uppercase tracking-[0.2em] text-zinc-500">My Bottle</h3> <h3 className="text-xs font-black uppercase tracking-[0.2em] text-zinc-500">Collection Stats</h3>
<Package size={14} className="text-zinc-700" /> <Package size={18} className="text-zinc-700" />
</div> </div>
<div className="space-y-6"> <div className="space-y-6">
@@ -333,5 +332,6 @@ export default function BottleDetails({ bottleId, sessionId, userId }: BottleDet
</div> </div>
</section> </section>
</div> </div>
</div>
); );
} }

View File

@@ -36,7 +36,7 @@ function BottleCard({ bottle, sessionId }: BottleCardProps) {
return ( return (
<Link <Link
href={`/bottles/${bottle.id}${sessionId ? `?session_id=${sessionId}` : ''}`} href={`/bottles/${bottle.id}${sessionId ? `?session_id=${sessionId}` : ''}`}
className="block h-fit group relative overflow-hidden rounded-xl bg-zinc-900 border border-zinc-800 transition-all duration-300 hover:border-zinc-700 active:scale-[0.98]" className="block h-fit group relative overflow-hidden rounded-2xl bg-zinc-800/20 backdrop-blur-sm border border-white/[0.05] transition-all duration-500 hover:border-orange-500/30 hover:shadow-2xl hover:shadow-orange-950/20 active:scale-[0.98]"
> >
{/* Image Layer - Clean Split Top */} {/* Image Layer - Clean Split Top */}
<div className="aspect-[4/3] overflow-hidden"> <div className="aspect-[4/3] overflow-hidden">
@@ -50,10 +50,10 @@ function BottleCard({ bottle, sessionId }: BottleCardProps) {
{/* Info Layer - Clean Split Bottom */} {/* Info Layer - Clean Split Bottom */}
<div className="p-4 space-y-4"> <div className="p-4 space-y-4">
<div className="space-y-1"> <div className="space-y-1">
<p className="text-[10px] font-bold text-orange-500 uppercase tracking-widest leading-none"> <p className="text-[10px] font-black text-orange-600 uppercase tracking-[0.2em] leading-none mb-1">
{bottle.distillery} {bottle.distillery}
</p> </p>
<h3 className="font-bold text-lg text-zinc-50 leading-tight"> <h3 className="font-bold text-xl text-zinc-50 leading-tight tracking-tight">
{bottle.name || t('grid.unknownBottle')} {bottle.name || t('grid.unknownBottle')}
</h3> </h3>
</div> </div>

View File

@@ -22,11 +22,11 @@ interface NavButtonProps {
const NavButton = ({ onClick, icon, label, ariaLabel }: NavButtonProps) => ( const NavButton = ({ onClick, icon, label, ariaLabel }: NavButtonProps) => (
<button <button
onClick={onClick} onClick={onClick}
className="flex flex-col items-center gap-0.5 px-3 py-1.5 text-zinc-400 hover:text-white transition-colors active:scale-95" className="flex flex-col items-center justify-center gap-1 w-full min-w-[44px] min-h-[44px] text-zinc-400 hover:text-white transition-colors active:scale-90"
aria-label={ariaLabel} aria-label={ariaLabel}
> >
{icon} {icon}
<span className="text-[9px] font-medium tracking-wide">{label}</span> <span className="text-[10px] font-bold tracking-tight">{label}</span>
</button> </button>
); );
@@ -55,7 +55,7 @@ export const BottomNavigation = ({ onHome, onShelf, onSearch, onProfile, onScan
className="hidden" className="hidden"
/> />
<div className="flex items-center justify-between px-1 py-1 bg-zinc-900/95 backdrop-blur-lg border border-zinc-800 rounded-full shadow-2xl pointer-events-auto"> <div className="flex items-center justify-between px-2 py-1 bg-[#1c1c1e]/80 backdrop-blur-2xl border border-white/10 rounded-full shadow-2xl pointer-events-auto ring-1 ring-black/20">
{/* Left Items */} {/* Left Items */}
<NavButton <NavButton
onClick={onHome} onClick={onHome}

View File

@@ -85,10 +85,10 @@ export default function SessionBottomSheet({ isOpen, onClose }: SessionBottomShe
animate={{ y: 0 }} animate={{ y: 0 }}
exit={{ y: '100%' }} exit={{ y: '100%' }}
transition={{ type: 'spring', damping: 25, stiffness: 200 }} transition={{ type: 'spring', damping: 25, stiffness: 200 }}
className="fixed bottom-0 left-0 right-0 bg-zinc-950 border-t border-zinc-800 rounded-t-[32px] z-[90] p-8 pb-12 max-h-[80vh] overflow-y-auto shadow-[0_-10px_40px_rgba(0,0,0,0.5)]" className="fixed bottom-0 left-0 right-0 bg-[var(--background)] border-t border-white/5 rounded-t-[40px] z-[90] p-8 pb-12 max-h-[85vh] overflow-y-auto shadow-[0_-20px_60px_rgba(0,0,0,0.8)] ring-1 ring-white/5"
> >
{/* Drag Handle */} {/* Drag Handle */}
<div className="w-12 h-1.5 bg-zinc-800 rounded-full mx-auto mb-8" /> <div className="w-10 h-1 bg-white/10 rounded-full mx-auto mb-8" />
<h2 className="text-2xl font-bold mb-6 text-zinc-50">Tasting Session</h2> <h2 className="text-2xl font-bold mb-6 text-zinc-50">Tasting Session</h2>
@@ -126,7 +126,7 @@ export default function SessionBottomSheet({ isOpen, onClose }: SessionBottomShe
setActiveSession({ id: s.id, name: s.name }); setActiveSession({ id: s.id, name: s.name });
onClose(); onClose();
}} }}
className={`w-full flex items-center justify-between p-4 rounded-2xl border transition-all ${activeSession?.id === s.id ? 'bg-orange-600/10 border-orange-600 text-orange-500' : 'bg-zinc-900 border-zinc-800 hover:border-zinc-700 text-zinc-50'}`} className={`w-full flex items-center justify-between p-5 rounded-[24px] border transition-all active:scale-[0.98] ${activeSession?.id === s.id ? 'bg-orange-600/10 border-orange-600 text-orange-500' : 'bg-white/5 border-white/5 hover:border-white/10 text-zinc-50'}`}
> >
<span className="font-bold">{s.name}</span> <span className="font-bold">{s.name}</span>
{activeSession?.id === s.id ? <Check size={20} /> : <ChevronRight size={20} className="text-zinc-700" />} {activeSession?.id === s.id ? <Check size={20} /> : <ChevronRight size={20} className="text-zinc-700" />}