diff --git a/hig.md b/hig.md new file mode 100644 index 0000000..8d3a2c0 --- /dev/null +++ b/hig.md @@ -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). \ No newline at end of file diff --git a/next-env.d.ts b/next-env.d.ts index c4b7818..9edff1c 100644 --- a/next-env.d.ts +++ b/next-env.d.ts @@ -1,6 +1,6 @@ /// /// -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. diff --git a/src/app/globals.css b/src/app/globals.css index 971b5b2..93ea4bc 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -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"; } diff --git a/src/app/page.tsx b/src/app/page.tsx index 29fd8e8..b40013d 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -204,7 +204,7 @@ export default function Home() { } return ( -
+
diff --git a/src/components/BottleDetails.tsx b/src/components/BottleDetails.tsx index 8672cd2..503e6df 100644 --- a/src/components/BottleDetails.tsx +++ b/src/components/BottleDetails.tsx @@ -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,256 +93,245 @@ export default function BottleDetails({ bottleId, sessionId, userId }: BottleDet if (!bottle) return null; // Should not happen due to check above return ( -
- {/* Back Button */} -
- - - Zurück - -
- - {isOffline && ( -
- -

Offline-Modus

-
- )} - - {/* Header & Hero Section */} -
- {/* 1. Header (Title at top) */} -
-

- {bottle.name} -

-

- {bottle.distillery} -

+
+ {/* Header / Hero Section */} +
+ {/* Back Button Overlay */} +
+ + +
- {/* 2. Image (Below title) */} -
-
+ {/* Hero Image - Slightly More Compact Aspect for better title flow */} +
+ {/* Background Glow */} +
{bottle.name}
- {/* 3. Metadata Consolidation (Info Row) */} -
-
- - {bottle.category || 'Whisky'} -
-
- - {bottle.abv}% -
- {bottle.age && ( -
- - {bottle.age}J. -
- )} - {bottle.distilled_at && ( -
- - Dist. {bottle.distilled_at} -
- )} - {bottle.bottled_at && ( -
- - Bott. {bottle.bottled_at} -
- )} - {bottle.batch_info && ( -
- - {bottle.batch_info} -
- )} - {bottle.whiskybase_id && ( - - - WB {bottle.whiskybase_id} - - )} -
+ {/* Info Overlay - Mobile Gradient */} +
- {/* 4. Inventory Section (Cohesive Container) */} -
-
-

My Bottle

- -
- -
- {/* Segmented Control for Status */} -
- -
- {['sealed', 'open', 'empty'].map((s) => ( - - ))} + {/* Content Container */} +
+ {/* Title Section - HIG Large Title Pattern */} +
+ {isOffline && ( +
+ +

Offline

-
- -
-
- -
- setPrice(e.target.value)} - onBlur={() => handleQuickUpdate(price)} - placeholder="0.00" - className="w-full bg-zinc-950 border border-zinc-800 rounded-2xl pl-4 pr-8 py-3 text-sm font-bold text-zinc-100 focus:outline-none focus:border-orange-600" - /> -
-
-
- -
- -
- - {tastings && tastings.length > 0 - ? new Date(tastings[0].created_at).toLocaleDateString(locale === 'de' ? 'de-DE' : 'en-US', { day: '2-digit', month: '2-digit' }) - : '-'} -
-
-
-
-
- - {/* 5. Editing Form (Accordion) */} -
- - - - {isFormVisible && ( - -
- setIsFormVisible(false)} - /> -
-
)} -
+

+ {bottle.distillery} +

+

+ {bottle.name} +

- {!isOffline && ( -
- - - Split starten - -
- + {/* Metadata Items - Text based for better readability */} +
+
+

Category

+

{bottle.category || 'Whisky'}

+
+
+

ABV

+

{bottle.abv}%

+
+ {bottle.age && ( +
+

Age

+

{bottle.age} Years

+
+ )} + {bottle.whiskybase_id && ( + +

Whiskybase

+

+ #{bottle.whiskybase_id} +

+
+ )} +
+
+ + {/* 4. Inventory Section (Cohesive Container) */} +
+
+

Collection Stats

+ +
+ +
+ {/* Segmented Control for Status */} +
+ +
+ {['sealed', 'open', 'empty'].map((s) => ( + + ))} +
+
+ +
+
+ +
+ setPrice(e.target.value)} + onBlur={() => handleQuickUpdate(price)} + placeholder="0.00" + className="w-full bg-zinc-950 border border-zinc-800 rounded-2xl pl-4 pr-8 py-3 text-sm font-bold text-zinc-100 focus:outline-none focus:border-orange-600" + /> +
+
+
+ +
+ +
+ + {tastings && tastings.length > 0 + ? new Date(tastings[0].created_at).toLocaleDateString(locale === 'de' ? 'de-DE' : 'en-US', { day: '2-digit', month: '2-digit' }) + : '-'} +
+
- )} -
+
-
+ {/* 5. Editing Form (Accordion) */} +
+ - {/* Tasting Notes Section */} -
-
-
-

Tasting Notes

-

Hier findest du deine bisherigen Eindrücke.

-
-
+ + {isFormVisible && ( + +
+ setIsFormVisible(false)} + /> +
+
+ )} +
-
- {/* Form */} -
- +
+ )} +
- - {isFormVisible && ( - -
-

- Dram bewerten -

- setIsFormVisible(false)} - /> -
-
- )} -
+
+ + {/* Tasting Notes Section */} +
+
+
+

Tasting Notes

+

Hier findest du deine bisherigen Eindrücke.

+
- {/* List */} -
- +
+ {/* Form */} +
+ + + + {isFormVisible && ( + +
+

+ Dram bewerten +

+ setIsFormVisible(false)} + /> +
+
+ )} +
+
+ + {/* List */} +
+ +
-
-
+
+
); } diff --git a/src/components/BottleGrid.tsx b/src/components/BottleGrid.tsx index a061d18..cbabc2a 100644 --- a/src/components/BottleGrid.tsx +++ b/src/components/BottleGrid.tsx @@ -36,7 +36,7 @@ function BottleCard({ bottle, sessionId }: BottleCardProps) { return ( {/* Image Layer - Clean Split Top */}
@@ -50,10 +50,10 @@ function BottleCard({ bottle, sessionId }: BottleCardProps) { {/* Info Layer - Clean Split Bottom */}
-

+

{bottle.distillery}

-

+

{bottle.name || t('grid.unknownBottle')}

diff --git a/src/components/BottomNavigation.tsx b/src/components/BottomNavigation.tsx index d4207fc..1669f2a 100644 --- a/src/components/BottomNavigation.tsx +++ b/src/components/BottomNavigation.tsx @@ -22,11 +22,11 @@ interface NavButtonProps { const NavButton = ({ onClick, icon, label, ariaLabel }: NavButtonProps) => ( ); @@ -55,7 +55,7 @@ export const BottomNavigation = ({ onHome, onShelf, onSearch, onProfile, onScan className="hidden" /> -
+
{/* Left Items */} {/* Drag Handle */} -
+

Tasting Session

@@ -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'}`} > {s.name} {activeSession?.id === s.id ? : }