diff --git a/package-lock.json b/package-lock.json
index 072727b..d7a2524 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,13 +9,14 @@
"version": "0.0.1",
"dependencies": {
"@expo/vector-icons": "^15.0.2",
+ "@react-native-community/blur": "^4.4.1",
"@react-navigation/bottom-tabs": "^7.4.0",
"@react-navigation/elements": "^2.6.3",
"@react-navigation/native": "^7.1.8",
"@reduxjs/toolkit": "^2.9.0",
"expo": "~54.0.13",
"expo-constants": "~18.0.9",
- "expo-dev-client": "~6.0.15",
+ "expo-dev-client": "~6.0.16",
"expo-font": "~14.0.9",
"expo-haptics": "~15.0.7",
"expo-image": "~3.0.9",
@@ -30,6 +31,7 @@
"react-dom": "19.1.0",
"react-native": "0.81.4",
"react-native-gesture-handler": "~2.28.0",
+ "react-native-keyboard-controller": "^1.19.2",
"react-native-reanimated": "~4.1.1",
"react-native-safe-area-context": "~5.6.0",
"react-native-screens": "~4.16.0",
@@ -3243,6 +3245,16 @@
"react-native": "^0.0.0-0 || >=0.65 <1.0"
}
},
+ "node_modules/@react-native-community/blur": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/@react-native-community/blur/-/blur-4.4.1.tgz",
+ "integrity": "sha512-XBSsRiYxE/MOEln2ayunShfJtWztHwUxLFcSL20o+HNNRnuUDv+GXkF6FmM2zE8ZUfrnhQ/zeTqvnuDPGw6O8A==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "*",
+ "react-native": "*"
+ }
+ },
"node_modules/@react-native/assets-registry": {
"version": "0.81.4",
"resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.81.4.tgz",
@@ -7920,13 +7932,13 @@
}
},
"node_modules/expo-dev-client": {
- "version": "6.0.15",
- "resolved": "https://registry.npmjs.org/expo-dev-client/-/expo-dev-client-6.0.15.tgz",
- "integrity": "sha512-zdpuK7wPp7q01qE90EDQunL49cumGPEKCGDykB86K0myCPZt1lvkpyy4OHTh3urm3pkikWOb9biuVgLdq7oq/g==",
+ "version": "6.0.16",
+ "resolved": "https://registry.npmjs.org/expo-dev-client/-/expo-dev-client-6.0.16.tgz",
+ "integrity": "sha512-8GLud/dtNteqChL9pNGqLBSHd7of2scFmsgN5WwWgtt2dET7+EJM/K1zp0FYUzfmIF5NLsf5xUDg6AjDldOLqg==",
"license": "MIT",
"dependencies": {
- "expo-dev-launcher": "6.0.15",
- "expo-dev-menu": "7.0.14",
+ "expo-dev-launcher": "6.0.16",
+ "expo-dev-menu": "7.0.15",
"expo-dev-menu-interface": "2.0.0",
"expo-manifests": "~1.0.8",
"expo-updates-interface": "~2.0.0"
@@ -7936,12 +7948,12 @@
}
},
"node_modules/expo-dev-launcher": {
- "version": "6.0.15",
- "resolved": "https://registry.npmjs.org/expo-dev-launcher/-/expo-dev-launcher-6.0.15.tgz",
- "integrity": "sha512-aFRKg9qcq47Y/1UGTPhtOWrbI5jOFgttOfhdBu9knLKl72jGXpDTdBHsHPkCfIezzDrSoZccl482Hv7RvKfrqA==",
+ "version": "6.0.16",
+ "resolved": "https://registry.npmjs.org/expo-dev-launcher/-/expo-dev-launcher-6.0.16.tgz",
+ "integrity": "sha512-OVg5T5ip7evh8zHJeIj2IYgtvTeY8EOiwNQYlmN0JHAw8wlUxYHnSf08RcevVgYTKcIryCyeLG5UHxsQQWbycA==",
"license": "MIT",
"dependencies": {
- "expo-dev-menu": "7.0.14",
+ "expo-dev-menu": "7.0.15",
"expo-manifests": "~1.0.8"
},
"peerDependencies": {
@@ -7949,9 +7961,9 @@
}
},
"node_modules/expo-dev-menu": {
- "version": "7.0.14",
- "resolved": "https://registry.npmjs.org/expo-dev-menu/-/expo-dev-menu-7.0.14.tgz",
- "integrity": "sha512-nWyzSztFWfnhDOiKJ6DuZLjIbq+tG9e5y4TDmW6wYlSKKPBXbdOn2UdxaknhSqZrw6NwWMcjyhS+QG4MoDGD8w==",
+ "version": "7.0.15",
+ "resolved": "https://registry.npmjs.org/expo-dev-menu/-/expo-dev-menu-7.0.15.tgz",
+ "integrity": "sha512-aThUhoBUuQVbCS2k0MwP28/au46FqOXAAiGtCYIWp+Hne95RgFO+KaO0VGksJFwK7I9IPbbminm8ijZDf2KzXg==",
"license": "MIT",
"dependencies": {
"expo-dev-menu-interface": "2.0.0"
@@ -13805,6 +13817,20 @@
"react-native": "*"
}
},
+ "node_modules/react-native-keyboard-controller": {
+ "version": "1.19.2",
+ "resolved": "https://registry.npmjs.org/react-native-keyboard-controller/-/react-native-keyboard-controller-1.19.2.tgz",
+ "integrity": "sha512-121W+WLnYMTpVyAwm2tPjBc6NLKS2srQOvlkFMYZEYNIsZPvWLQ92n7kBlR32Wpsm8t9rE9xfWZEyNPhCoe+mQ==",
+ "license": "MIT",
+ "dependencies": {
+ "react-native-is-edge-to-edge": "^1.2.1"
+ },
+ "peerDependencies": {
+ "react": "*",
+ "react-native": "*",
+ "react-native-reanimated": ">=3.0.0"
+ }
+ },
"node_modules/react-native-reanimated": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-4.1.3.tgz",
diff --git a/package.json b/package.json
index f9a52f0..902c79d 100644
--- a/package.json
+++ b/package.json
@@ -13,13 +13,14 @@
},
"dependencies": {
"@expo/vector-icons": "^15.0.2",
+ "@react-native-community/blur": "^4.4.1",
"@react-navigation/bottom-tabs": "^7.4.0",
"@react-navigation/elements": "^2.6.3",
"@react-navigation/native": "^7.1.8",
"@reduxjs/toolkit": "^2.9.0",
"expo": "~54.0.13",
"expo-constants": "~18.0.9",
- "expo-dev-client": "~6.0.15",
+ "expo-dev-client": "~6.0.16",
"expo-font": "~14.0.9",
"expo-haptics": "~15.0.7",
"expo-image": "~3.0.9",
@@ -34,6 +35,7 @@
"react-dom": "19.1.0",
"react-native": "0.81.4",
"react-native-gesture-handler": "~2.28.0",
+ "react-native-keyboard-controller": "^1.19.2",
"react-native-reanimated": "~4.1.1",
"react-native-safe-area-context": "~5.6.0",
"react-native-screens": "~4.16.0",
diff --git a/src/app/_layout.tsx b/src/app/_layout.tsx
index f8105be..f4296c8 100644
--- a/src/app/_layout.tsx
+++ b/src/app/_layout.tsx
@@ -4,6 +4,7 @@ import { Stack } from "expo-router";
import { StatusBar } from "expo-status-bar";
import React from "react";
import { ActivityIndicator, Text, View } from "react-native";
+import { KeyboardProvider } from "react-native-keyboard-controller";
import { SafeAreaProvider } from "react-native-safe-area-context";
import { Provider } from "react-redux";
import { PersistGate } from "redux-persist/integration/react";
@@ -40,7 +41,9 @@ export default function RootLayout() {
} persistor={persistor}>
-
+
+
+
diff --git a/src/app/index.tsx b/src/app/index.tsx
index 9bcf09d..1403f22 100644
--- a/src/app/index.tsx
+++ b/src/app/index.tsx
@@ -1,59 +1,5 @@
-import { useSettings } from "@/src/features/settings/hooks/useSettings";
-import { useTheme } from "@/src/theme/useTheme";
-import { Pressable, Text, View } from "react-native";
+import BrowserScreen from "@/src/features/browser/screens/BrowserScreen";
export default function Index() {
- const theme = useTheme();
- const settings = useSettings();
-
- return (
-
-
- The current theme color is {theme.name}.{"\n"}
- The current theme mode is {settings.theme}.
-
- settings.switchTheme("system")}>
-
- System Theme
-
-
- settings.switchTheme("light")}>
-
- Light Theme
-
-
- settings.switchTheme("dark")}>
-
- Dark Theme
-
-
-
- );
+ return ;
}
diff --git a/src/components/IconButton.tsx b/src/components/IconButton.tsx
new file mode 100644
index 0000000..1132f81
--- /dev/null
+++ b/src/components/IconButton.tsx
@@ -0,0 +1,57 @@
+import { Icon, IconSizes } from "@/src/constants/Icons";
+import { Ionicons } from "@expo/vector-icons";
+import { ColorValue, Pressable, StyleSheet, ViewStyle } from "react-native";
+
+interface IconButtonProps {
+ icon: Icon;
+ size?: number;
+ color?: ColorValue;
+ disabled?: boolean;
+ accessibilityLabel?: string;
+ style?: ViewStyle;
+ onPress?: () => void;
+}
+
+export default function IconButton({
+ icon,
+ size = IconSizes.medium,
+ color = "#000",
+ disabled = false,
+ accessibilityLabel,
+ style,
+ onPress,
+}: IconButtonProps) {
+ return (
+ [
+ styles.button,
+ style,
+ disabled && styles.disabled,
+ pressed && !disabled && styles.pressed,
+ ]}
+ onPress={onPress}
+ disabled={disabled}
+ accessibilityRole="button"
+ accessibilityLabel={accessibilityLabel}
+ >
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ button: {
+ padding: 8,
+ borderRadius: 20,
+ alignItems: "center",
+ justifyContent: "center",
+ },
+ disabled: {
+ opacity: 0.4,
+ },
+ pressed: {
+ opacity: 0.7,
+ },
+});
+
+export { IconSizes };
diff --git a/src/constants/Icons.ts b/src/constants/Icons.ts
index 9799262..c6260c6 100644
--- a/src/constants/Icons.ts
+++ b/src/constants/Icons.ts
@@ -27,6 +27,9 @@ export const Icons = {
secure: "lock-closed" as const,
insecure: "warning" as const,
search: "search" as const,
+ microphone: "mic-outline" as const,
+ deleteInput: "close-circle" as const,
+ readerOutline: "reader-outline" as const,
// Tab management
closeTab: "close" as const,
@@ -54,8 +57,8 @@ export const Icons = {
textSize: "text" as const,
// Bookmarks and favorites
- bookmark: "bookmark-outline" as const,
- bookmarkFilled: "bookmark" as const,
+ bookmark: "book-outline" as const,
+ bookmarkFilled: "book" as const,
star: "star-outline" as const,
starFilled: "star" as const,
diff --git a/src/features/browser/screens/BrowserScreen/AddressBar.tsx b/src/features/browser/screens/BrowserScreen/AddressBar.tsx
new file mode 100644
index 0000000..d3ebde0
--- /dev/null
+++ b/src/features/browser/screens/BrowserScreen/AddressBar.tsx
@@ -0,0 +1,128 @@
+import IconButton from "@/src/components/IconButton";
+import { Icons } from "@/src/constants/Icons";
+import { useTheme } from "@/src/theme/useTheme";
+import { useEffect, useState } from "react";
+import { StyleSheet, TextInput, View } from "react-native";
+import { useBrowser } from "../../hooks/useBrowser";
+import { formatUrlForDisplay } from "../../utils/urlUtils";
+
+interface AddressBarProps {
+ onFocusChange?: (isFocused: boolean) => void;
+}
+
+export default function AddressBar({ onFocusChange }: AddressBarProps) {
+ const theme = useTheme();
+ const { activeTab, navigateTab, activeTabId } = useBrowser();
+
+ const [input, setInput] = useState(activeTab?.url ?? "");
+ const [isFocused, setIsFocused] = useState(false);
+
+ const showDeleteIcon = isFocused && input.length > 0;
+
+ // Update input field when active tab changes
+ useEffect(() => {
+ setInput(activeTab?.url ?? "");
+ }, [activeTab?.url]);
+
+ const handleSubmitEditing = () => {
+ if (!activeTabId) return;
+ navigateTab(activeTabId, input);
+ };
+
+ const handleFocus = () => {
+ setIsFocused(true);
+ onFocusChange?.(true);
+ };
+
+ const handleBlur = () => {
+ setIsFocused(false);
+ onFocusChange?.(false);
+ };
+
+ return (
+
+ {!isFocused && (
+
+
+
+ )}
+
+
+ {
+ if (activeTabId && activeTab?.url) {
+ navigateTab(activeTabId, activeTab.url);
+ }
+ }}
+ />
+ {showDeleteIcon && (
+ setInput("")}
+ />
+ )}
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flexDirection: "row",
+ alignItems: "center",
+ paddingHorizontal: 8,
+ paddingVertical: 2,
+ borderRadius: 12,
+ marginTop: 10,
+ marginBottom: 16,
+ marginHorizontal: 32,
+ },
+ input: {
+ flex: 1,
+ fontSize: 16,
+ paddingLeft: 6,
+ borderRadius: 6,
+ backgroundColor: "transparent",
+ textAlign: "center",
+ },
+ iconLeft: {
+ marginRight: 4,
+ },
+ iconRight: {
+ marginLeft: 2,
+ flexDirection: "row",
+ alignItems: "center",
+ },
+});
diff --git a/src/features/browser/screens/BrowserScreen/AddressBarDisplay.tsx b/src/features/browser/screens/BrowserScreen/AddressBarDisplay.tsx
new file mode 100644
index 0000000..b02bea7
--- /dev/null
+++ b/src/features/browser/screens/BrowserScreen/AddressBarDisplay.tsx
@@ -0,0 +1,36 @@
+import { useTheme } from "@/src/theme/useTheme";
+import { StyleSheet, Text, View } from "react-native";
+import { useBrowser } from "../../hooks/useBrowser";
+import { formatUrlForDisplay } from "../../utils/urlUtils";
+export default function AddressBarDisplay() {
+ const theme = useTheme();
+ const { activeTab } = useBrowser();
+ const { url } = activeTab || {};
+
+ return (
+
+
+ {formatUrlForDisplay(url || "about:blank")}
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ justifyContent: "center",
+ alignItems: "center",
+ backgroundColor: "transparent",
+ width: "100%",
+ paddingTop: 2,
+ paddingBottom: 8,
+ paddingHorizontal: 16,
+ borderTopWidth: StyleSheet.hairlineWidth,
+ borderTopColor: "#909090",
+ },
+ text: {
+ color: "#2c3e50",
+ },
+});
diff --git a/src/features/browser/screens/BrowserScreen/Toolbar.tsx b/src/features/browser/screens/BrowserScreen/Toolbar.tsx
new file mode 100644
index 0000000..ffbb65b
--- /dev/null
+++ b/src/features/browser/screens/BrowserScreen/Toolbar.tsx
@@ -0,0 +1,88 @@
+import IconButton from "@/src/components/IconButton";
+import { Icons } from "@/src/constants/Icons";
+import { useTheme } from "@/src/theme/useTheme";
+import React from "react";
+import { StyleSheet } from "react-native";
+import { SafeAreaView } from "react-native-safe-area-context";
+import { useBrowser } from "../../hooks/useBrowser";
+
+export default function Toolbar() {
+ const theme = useTheme();
+ const {
+ activeTab,
+ createNewTab,
+ closeTabById,
+ toggleTabs,
+ toggleBookmark,
+ isBookmarked,
+ } = useBrowser();
+
+ return (
+
+ {
+ // WebView navigation logic will go here
+ }}
+ />
+ {
+ // WebView navigation logic will go here
+ }}
+ />
+ {
+ // More actions (settings, share, etc.)
+ }}
+ />
+ {
+ if (activeTab?.url) toggleBookmark(activeTab.url);
+ }}
+ />
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flexDirection: "row",
+ alignItems: "center",
+ justifyContent: "space-around",
+ paddingBottom: 6,
+ backgroundColor: "transparent",
+ },
+});
diff --git a/src/features/browser/screens/BrowserScreen/WebViewComponent.tsx b/src/features/browser/screens/BrowserScreen/WebViewComponent.tsx
new file mode 100644
index 0000000..406eeb3
--- /dev/null
+++ b/src/features/browser/screens/BrowserScreen/WebViewComponent.tsx
@@ -0,0 +1,65 @@
+import { useRef } from "react";
+import { StyleSheet, View, ViewProps } from "react-native";
+import { WebView } from "react-native-webview";
+import { useBrowser } from "../../hooks/useBrowser";
+
+type WebViewComponentProps = {
+ bottomPadding?: number;
+ onScrollDirectionChange?: (direction: "up" | "down") => void;
+} & ViewProps;
+
+const SCROLL_DIRECTION_THRESHOLD = 8;
+
+export default function WebViewComponent({
+ bottomPadding = 0,
+ style,
+ onScrollDirectionChange,
+ ...props
+}: WebViewComponentProps) {
+ const { activeTab, updateTabById } = useBrowser();
+ const { url } = activeTab || {};
+ // Inject CSS to add bottom padding to the body
+ const injectedCSS = `
+ const style = document.createElement('style');
+ style.innerHTML = 'body { padding-bottom: ${bottomPadding}px !important; box-sizing: border-box; }';
+ document.head.appendChild(style);
+ `;
+
+ const lastScrollY = useRef(0);
+ const lastDirection = useRef<"up" | "down" | null>(null); // pixels
+ const handleScroll = (event: any) => {
+ const currentY = event.nativeEvent.contentOffset?.y ?? 0;
+ const diff = currentY - lastScrollY.current;
+
+ if (Math.abs(diff) > SCROLL_DIRECTION_THRESHOLD) {
+ const newDirection = diff > 0 ? "down" : "up";
+ if (newDirection !== lastDirection.current) {
+ onScrollDirectionChange?.(newDirection);
+ lastDirection.current = newDirection;
+ }
+ }
+ lastScrollY.current = currentY;
+ };
+
+ return (
+
+ {
+ if (activeTab?.id) {
+ updateTabById(activeTab!.id, { url: navState.url });
+ }
+ }}
+ onScroll={handleScroll}
+ />
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: "#ecf0f1",
+ },
+});
diff --git a/src/features/browser/screens/BrowserScreen/index.tsx b/src/features/browser/screens/BrowserScreen/index.tsx
new file mode 100644
index 0000000..d3fc774
--- /dev/null
+++ b/src/features/browser/screens/BrowserScreen/index.tsx
@@ -0,0 +1,136 @@
+import { useTheme } from "@/src/theme/useTheme";
+import { useCallback, useEffect, useMemo, useState } from "react";
+import { Animated, StyleSheet, View } from "react-native";
+import {
+ KeyboardEvents,
+ useKeyboardAnimation,
+} from "react-native-keyboard-controller";
+import { SafeAreaView } from "react-native-safe-area-context";
+import { useBrowser } from "../../hooks/useBrowser";
+import AddressBar from "./AddressBar";
+import AddressBarDisplay from "./AddressBarDisplay";
+import Toolbar from "./Toolbar";
+import WebViewComponent from "./WebViewComponent";
+
+const KEYBOARD_OPENED_HEIGHT = 102;
+const KEYBOARD_CLOSED_HEIGHT = 0;
+
+export default function BrowserScreen() {
+ const theme = useTheme();
+ const { showTabs } = useBrowser();
+ const { height, progress } = useKeyboardAnimation();
+
+ const [isAddressBarFocused, setAddressBarFocused] = useState(false);
+ const [isKeyboardVisible, setKeyboardVisible] = useState(false);
+
+ const [scrollDirection, setScrollDirection] = useState<"up" | "down" | null>(
+ null
+ );
+
+ const showAddressBar = !isKeyboardVisible || isAddressBarFocused;
+ const showBottomContainer = scrollDirection !== "down";
+
+ useEffect(() => {
+ const show = KeyboardEvents.addListener("keyboardWillShow", (e) => {
+ setKeyboardVisible(true);
+ });
+
+ const hide = KeyboardEvents.addListener("keyboardWillHide", (e) => {
+ setKeyboardVisible(false);
+ });
+
+ return () => {
+ show.remove();
+ hide.remove();
+ };
+ }, []);
+
+ const onAddressBarFocusChange = useCallback((isFocused: boolean) => {
+ setAddressBarFocused(isFocused);
+ setKeyboardVisible(false);
+ }, []);
+
+ const offset = progress.interpolate({
+ inputRange: [0, 1],
+ outputRange: [KEYBOARD_CLOSED_HEIGHT, KEYBOARD_OPENED_HEIGHT],
+ });
+
+ const addressBarStyle = useMemo(
+ () => [
+ {
+ transform: [
+ {
+ translateY: Animated.add(height, offset),
+ },
+ ],
+ },
+ { backgroundColor: "white" },
+ ],
+ [height, offset]
+ );
+
+ const bottomContainerY = useMemo(() => new Animated.Value(0), []);
+
+ useEffect(() => {
+ Animated.timing(bottomContainerY, {
+ toValue: showBottomContainer ? 0 : 200,
+ duration: 400,
+ useNativeDriver: true,
+ }).start();
+ }, [showBottomContainer, bottomContainerY]);
+
+ return (
+
+
+
+
+
+
+
+
+
+ {showAddressBar && (
+
+ )}
+ {!showAddressBar && }
+
+ {/* Toolbar contains the SafeAreaView */}
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ topContainer: {
+ flex: 1,
+ },
+ webViewContainer: {
+ flex: 1,
+ backgroundColor: "transparent",
+ },
+ bottomContainer: {
+ width: "100%",
+ },
+});
diff --git a/src/features/browser/utils/urlUtils.ts b/src/features/browser/utils/urlUtils.ts
index b22f256..9e26511 100644
--- a/src/features/browser/utils/urlUtils.ts
+++ b/src/features/browser/utils/urlUtils.ts
@@ -105,13 +105,13 @@ export const formatUrlForDisplay = (url: string): string => {
const urlObj = new URL(url);
let displayUrl = urlObj.hostname;
- if (urlObj.pathname !== "/") {
- displayUrl += urlObj.pathname;
- }
+ // if (urlObj.pathname !== "/") {
+ // displayUrl += urlObj.pathname;
+ // }
- if (urlObj.search) {
- displayUrl += urlObj.search;
- }
+ // if (urlObj.search) {
+ // displayUrl += urlObj.search;
+ // }
return displayUrl;
} catch {