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/image-types/global" />
import "./.next/dev/types/routes.d.ts";
import "./.next/types/routes.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

View File

@@ -4,24 +4,24 @@
@layer base {
:root {
--background: #09090b;
/* zinc-950 */
--surface: #18181b;
/* zinc-900 */
--background: #1c1c1e;
/* systemBackground */
--surface: #2c2c2e;
/* secondarySystemBackground */
--primary: #ea580c;
/* orange-600 */
--secondary: #f97316;
/* orange-500 */
--text-primary: #fafafa;
--text-secondary: #a1a1aa;
--border: #27272a;
/* zinc-800 */
--border: #38383a;
/* separator */
--ring: #f97316;
}
}
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";
}

View File

@@ -204,7 +204,7 @@ export default function Home() {
}
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">
<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">

View File

@@ -39,11 +39,22 @@ export default function BottleDetails({ bottleId, sessionId, userId }: BottleDet
const handleQuickUpdate = async (newPrice?: string, newStatus?: string) => {
if (isOffline) return;
setIsUpdating(true);
// Haptic feedback for interaction
if (window.navigator.vibrate) {
window.navigator.vibrate(10);
}
try {
await updateBottle(bottleId, {
purchase_price: newPrice !== undefined ? (newPrice ? parseFloat(newPrice) : null) : (price ? parseFloat(price) : null),
status: newStatus !== undefined ? newStatus : status
} as any);
// Success haptic
if (window.navigator.vibrate) {
window.navigator.vibrate([10, 50, 10]);
}
} catch (err) {
console.error('Quick update failed:', err);
} finally {
@@ -82,79 +93,65 @@ export default function BottleDetails({ bottleId, sessionId, userId }: BottleDet
if (!bottle) return null; // Should not happen due to check above
return (
<div className="max-w-2xl mx-auto px-4 pb-12 space-y-8">
{/* Back Button */}
<div className="pt-4">
<div className="max-w-4xl mx-auto pb-24">
{/* Header / Hero Section */}
<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
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} />
Zurück
<ChevronLeft size={24} />
</Link>
</div>
{isOffline && (
<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">
<WifiOff size={16} className="text-orange-600" />
<p className="text-[10px] font-bold uppercase tracking-widest text-orange-500">Offline-Modus</p>
</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" />
{/* Hero Image - Slightly More Compact Aspect for better title flow */}
<div className="relative aspect-[4/3] md:aspect-[16/8] w-full flex items-center justify-center p-6 md:p-10 overflow-hidden">
{/* Background Glow */}
<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" />
<img
src={getStorageUrl(bottle.image_url)}
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>
{/* 3. Metadata Consolidation (Info Row) */}
<div className="flex flex-wrap items-center justify-center md:justify-start gap-2 pt-2">
<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>
{/* Info Overlay - Mobile Gradient */}
<div className="absolute inset-x-0 bottom-0 h-48 bg-gradient-to-t from-[var(--background)] to-transparent pointer-events-none" />
</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" />
<span className="text-[11px] font-black uppercase tracking-wider text-zinc-300">{bottle.abv}%</span>
{/* Content Container */}
<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>
{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">
<Calendar size={14} className="text-zinc-500" />
<span className="text-[11px] font-black uppercase tracking-wider text-zinc-300">{bottle.age}J.</span>
</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 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">Age</p>
<p className="text-sm font-black text-zinc-100 uppercase">{bottle.age} Years</p>
</div>
)}
{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}`}
target="_blank"
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" />
<span className="text-[10px] font-black uppercase tracking-wider text-zinc-500">WB {bottle.whiskybase_id}</span>
<p className="text-[10px] font-bold text-orange-200 uppercase tracking-widest mb-0.5">Whiskybase</p>
<p className="text-sm font-black text-white uppercase flex items-center gap-2">
#{bottle.whiskybase_id} <ExternalLink size={14} />
</p>
</a>
)}
</div>
</div>
{/* 4. Inventory Section (Cohesive Container) */}
<section className="bg-zinc-900/30 border border-zinc-800/50 rounded-[32px] p-6 space-y-6">
<div className="flex items-center justify-between mb-2">
<h3 className="text-[10px] font-black uppercase tracking-[0.2em] text-zinc-500">My Bottle</h3>
<Package size={14} className="text-zinc-700" />
<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-4">
<h3 className="text-xs font-black uppercase tracking-[0.2em] text-zinc-500">Collection Stats</h3>
<Package size={18} className="text-zinc-700" />
</div>
<div className="space-y-6">
@@ -333,5 +332,6 @@ export default function BottleDetails({ bottleId, sessionId, userId }: BottleDet
</div>
</section>
</div>
</div>
);
}

View File

@@ -36,7 +36,7 @@ function BottleCard({ bottle, sessionId }: BottleCardProps) {
return (
<Link
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 */}
<div className="aspect-[4/3] overflow-hidden">
@@ -50,10 +50,10 @@ function BottleCard({ bottle, sessionId }: BottleCardProps) {
{/* Info Layer - Clean Split Bottom */}
<div className="p-4 space-y-4">
<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}
</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')}
</h3>
</div>

View File

@@ -22,11 +22,11 @@ interface NavButtonProps {
const NavButton = ({ onClick, icon, label, ariaLabel }: NavButtonProps) => (
<button
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}
>
{icon}
<span className="text-[9px] font-medium tracking-wide">{label}</span>
<span className="text-[10px] font-bold tracking-tight">{label}</span>
</button>
);
@@ -55,7 +55,7 @@ export const BottomNavigation = ({ onHome, onShelf, onSearch, onProfile, onScan
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 */}
<NavButton
onClick={onHome}

View File

@@ -85,10 +85,10 @@ export default function SessionBottomSheet({ isOpen, onClose }: SessionBottomShe
animate={{ y: 0 }}
exit={{ y: '100%' }}
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 */}
<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>
@@ -126,7 +126,7 @@ export default function SessionBottomSheet({ isOpen, onClose }: SessionBottomShe
setActiveSession({ id: s.id, name: s.name });
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>
{activeSession?.id === s.id ? <Check size={20} /> : <ChevronRight size={20} className="text-zinc-700" />}