+
{/* Results list — reversed so closest to input = most relevant */}
{hasResults && (
diff --git a/src/hooks/useHasUnreadNotifications.ts b/src/hooks/useHasUnreadNotifications.ts
index 82ba2ec6..c652f5f6 100644
--- a/src/hooks/useHasUnreadNotifications.ts
+++ b/src/hooks/useHasUnreadNotifications.ts
@@ -10,7 +10,7 @@ import { useEncryptedSettings } from './useEncryptedSettings';
* Fetches at most 1 event (using `since` to filter at the relay level),
* making it much cheaper than loading the full notification list.
*
- * Use this in navigation components (sidebar, bottom nav) for the dot indicator.
+ * Use this in navigation components (sidebar, mobile bottom nav) for the dot indicator.
* Use `useNotifications` on the actual notifications page where the full list is needed.
*/
export function useHasUnreadNotifications(): boolean {
diff --git a/src/hooks/useScrollDirection.ts b/src/hooks/useScrollDirection.ts
new file mode 100644
index 00000000..446d2090
--- /dev/null
+++ b/src/hooks/useScrollDirection.ts
@@ -0,0 +1,56 @@
+import { useEffect, useRef, useState } from 'react';
+
+/**
+ * Tracks the user's scroll direction and returns whether the mobile chrome
+ * (bottom nav) should be hidden.
+ *
+ * Rules:
+ * - Hidden when the user scrolls DOWN past a small threshold.
+ * - Revealed when the user scrolls UP by the same threshold.
+ * - Always revealed when the page is scrolled near the very top.
+ */
+export function useScrollDirection(): { hidden: boolean } {
+ const [hidden, setHidden] = useState(false);
+ const lastScrollY = useRef(0);
+ const accumulated = useRef(0);
+
+ useEffect(() => {
+ const THRESHOLD = 8; // px of continuous movement before toggling
+ const NEAR_TOP = 60; // px from top where chrome is always visible
+
+ const onScroll = () => {
+ const currentY = window.scrollY;
+
+ if (currentY <= NEAR_TOP) {
+ // Always show chrome near the top of the page
+ accumulated.current = 0;
+ setHidden(false);
+ lastScrollY.current = currentY;
+ return;
+ }
+
+ const delta = currentY - lastScrollY.current;
+ lastScrollY.current = currentY;
+
+ if (Math.sign(delta) !== Math.sign(accumulated.current)) {
+ // Direction changed — reset accumulator
+ accumulated.current = delta;
+ } else {
+ accumulated.current += delta;
+ }
+
+ if (accumulated.current > THRESHOLD) {
+ setHidden(true);
+ accumulated.current = 0;
+ } else if (accumulated.current < -THRESHOLD) {
+ setHidden(false);
+ accumulated.current = 0;
+ }
+ };
+
+ window.addEventListener('scroll', onScroll, { passive: true });
+ return () => window.removeEventListener('scroll', onScroll);
+ }, []);
+
+ return { hidden };
+}
diff --git a/src/index.css b/src/index.css
index 3e5076d4..30655575 100644
--- a/src/index.css
+++ b/src/index.css
@@ -40,9 +40,14 @@
bottom: env(safe-area-inset-bottom, 0px);
}
- /* FAB bottom offset: clears safe area inset */
+ /* FAB bottom offset: clears bottom nav + safe area inset on mobile */
.bottom-fab {
- bottom: calc(1.5rem + env(safe-area-inset-bottom, 0px));
+ bottom: calc(1.5rem + 3.5rem + env(safe-area-inset-bottom, 0px));
+ }
+
+ /* Position above mobile bottom nav (h-14 = 3.5rem) + safe area */
+ .bottom-mobile-nav {
+ bottom: calc(3.5rem + env(safe-area-inset-bottom, 0px));
}
/* Mobile top bar height (48px) + safe area inset for sticky elements */
@@ -50,20 +55,20 @@
top: calc(3rem + env(safe-area-inset-top, 0px));
}
- /* AI chat height on mobile: full viewport minus top bar and safe-area insets */
+ /* AI chat height on mobile: full viewport minus top bar, bottom nav (3.5rem), and safe-area insets */
.ai-chat-height {
- height: calc(100dvh - 3rem - env(safe-area-inset-top, 0px) - env(safe-area-inset-bottom, 0px));
+ height: calc(100dvh - 3rem - 3.5rem - env(safe-area-inset-top, 0px) - env(safe-area-inset-bottom, 0px));
}
- /* Live stream page height on mobile: full viewport minus top bar and safe-area insets */
+ /* Live stream page height on mobile: full viewport minus top bar, bottom nav, and safe-area insets */
.livestream-height {
- height: calc(100dvh - 3rem - env(safe-area-inset-bottom, 0px));
- max-height: calc(100dvh - 3rem - env(safe-area-inset-bottom, 0px));
+ height: calc(100dvh - 3rem - 3.5rem - env(safe-area-inset-bottom, 0px));
+ max-height: calc(100dvh - 3rem - 3.5rem - env(safe-area-inset-bottom, 0px));
}
- /* Vine feed slide height: full viewport minus top bar, tab bar, and safe-area insets */
+ /* Vine feed slide height: full viewport minus top bar, tab bar, bottom nav, and safe-area insets */
.vine-slide-height {
- height: calc(100dvh - env(safe-area-inset-top, 0px) - 3rem - 2.5rem - env(safe-area-inset-bottom, 0px));
+ height: calc(100dvh - env(safe-area-inset-top, 0px) - 3rem - 2.5rem - 3.5rem - env(safe-area-inset-bottom, 0px));
}
}