From ed86f7ef012fb558fe8a8974e1e162ce75cbfd15 Mon Sep 17 00:00:00 2001
From: Mohamed Bassem <me@mbassem.com>
Date: Tue, 26 Aug 2025 15:47:05 +0300
Subject: feat(mobile): Retheme the mobile app (#1872)

* Add nativewindui

* migrate to nativewindui text

* Replace buttons with nativewindui buttons

* Use nativewindui search input

* fix the divider color

* More changes

* fix manage tag icon

* fix styling of bookmark card

* fix ios compilation

* fix search clear

* fix tag pill border color

* Store theme setting in app settings

* fix setting color appearance

* fix coloring of search input

* fix following system theme

* add a save button to info

* fix the grey colors on android

* fix icon active tint color

* drop the use of TextField
---
 .../components/ui/SearchInput/SearchInput.ios.tsx  | 187 +++++++++++++++++++++
 1 file changed, 187 insertions(+)
 create mode 100644 apps/mobile/components/ui/SearchInput/SearchInput.ios.tsx

(limited to 'apps/mobile/components/ui/SearchInput/SearchInput.ios.tsx')

diff --git a/apps/mobile/components/ui/SearchInput/SearchInput.ios.tsx b/apps/mobile/components/ui/SearchInput/SearchInput.ios.tsx
new file mode 100644
index 00000000..969e48b2
--- /dev/null
+++ b/apps/mobile/components/ui/SearchInput/SearchInput.ios.tsx
@@ -0,0 +1,187 @@
+import type {
+  NativeSyntheticEvent,
+  TextInputFocusEventData,
+} from "react-native";
+import * as React from "react";
+import { Pressable, TextInput, View, ViewStyle } from "react-native";
+import Animated, {
+  measure,
+  useAnimatedRef,
+  useAnimatedStyle,
+  useDerivedValue,
+  withTiming,
+} from "react-native-reanimated";
+import { Text } from "@/components/ui/Text";
+import { useColorScheme } from "@/lib/useColorScheme";
+import { cn } from "@/lib/utils";
+import { useAugmentedRef, useControllableState } from "@rn-primitives/hooks";
+import { Icon } from "@roninoss/icons";
+
+import type { SearchInputProps } from "./types";
+
+// Add as class when possible: https://github.com/marklawlor/nativewind/issues/522
+const BORDER_CURVE: ViewStyle = {
+  borderCurve: "continuous",
+};
+
+const SearchInput = React.forwardRef<
+  React.ElementRef<typeof TextInput>,
+  SearchInputProps
+>(
+  (
+    {
+      value: valueProp,
+      onChangeText: onChangeTextProp,
+      onFocus: onFocusProp,
+      placeholder = "Search...",
+      cancelText = "Cancel",
+      containerClassName,
+      iconContainerClassName,
+      className,
+      iconColor,
+      onCancel,
+      ...props
+    },
+    ref,
+  ) => {
+    const { colors } = useColorScheme();
+    const inputRef = useAugmentedRef({ ref, methods: { focus, blur, clear } });
+    const [showCancel, setShowCancel] = React.useState(false);
+    const showCancelDerivedValue = useDerivedValue(
+      () => showCancel,
+      [showCancel],
+    );
+    const animatedRef = useAnimatedRef();
+
+    const [value = "", onChangeText] = useControllableState({
+      prop: valueProp,
+      defaultProp: valueProp ?? "",
+      onChange: onChangeTextProp,
+    });
+
+    const rootStyle = useAnimatedStyle(() => {
+      if (_WORKLET) {
+        // safely use measure
+        const measurement = measure(animatedRef);
+        return {
+          paddingRight: showCancelDerivedValue.value
+            ? withTiming(measurement?.width ?? cancelText.length * 11.2)
+            : withTiming(0),
+        };
+      }
+      return {
+        paddingRight: showCancelDerivedValue.value
+          ? withTiming(cancelText.length * 11.2)
+          : withTiming(0),
+      };
+    });
+    const buttonStyle3 = useAnimatedStyle(() => {
+      if (_WORKLET) {
+        // safely use measure
+        const measurement = measure(animatedRef);
+        return {
+          position: "absolute",
+          right: 0,
+          opacity: showCancelDerivedValue.value ? withTiming(1) : withTiming(0),
+          transform: [
+            {
+              translateX: showCancelDerivedValue.value
+                ? withTiming(0)
+                : measurement?.width
+                  ? withTiming(measurement.width)
+                  : cancelText.length * 11.2,
+            },
+          ],
+        };
+      }
+      return {
+        position: "absolute",
+        right: 0,
+        opacity: showCancelDerivedValue.value ? withTiming(1) : withTiming(0),
+        transform: [
+          {
+            translateX: showCancelDerivedValue.value
+              ? withTiming(0)
+              : withTiming(cancelText.length * 11.2),
+          },
+        ],
+      };
+    });
+
+    function focus() {
+      inputRef.current?.focus();
+    }
+
+    function blur() {
+      inputRef.current?.blur();
+    }
+
+    function clear() {
+      onChangeText("");
+    }
+
+    function onFocus(e: NativeSyntheticEvent<TextInputFocusEventData>) {
+      setShowCancel(true);
+      onFocusProp?.(e);
+    }
+
+    return (
+      <Animated.View className="flex-row items-center" style={rootStyle}>
+        <Animated.View
+          style={BORDER_CURVE}
+          className={cn(
+            "flex-1 flex-row rounded-lg bg-card",
+            containerClassName,
+          )}
+        >
+          <View
+            className={cn(
+              "absolute bottom-0 left-0 top-0 z-50 justify-center pl-1.5",
+              iconContainerClassName,
+            )}
+          >
+            <Icon color={iconColor ?? colors.grey3} name="magnify" size={22} />
+          </View>
+          <TextInput
+            ref={inputRef}
+            placeholder={placeholder}
+            className={cn(
+              !showCancel && "active:bg-muted/5 dark:active:bg-muted/20",
+              "flex-1 rounded-lg py-2 pl-8 pr-1  text-[17px] text-foreground",
+              className,
+            )}
+            value={value}
+            onChangeText={onChangeText}
+            onFocus={onFocus}
+            clearButtonMode="while-editing"
+            role="searchbox"
+            {...props}
+          />
+        </Animated.View>
+        <Animated.View
+          ref={animatedRef}
+          style={buttonStyle3}
+          pointerEvents={!showCancel ? "none" : "auto"}
+        >
+          <Pressable
+            onPress={() => {
+              onChangeText("");
+              inputRef.current?.blur();
+              setShowCancel(false);
+              onCancel?.();
+            }}
+            disabled={!showCancel}
+            pointerEvents={!showCancel ? "none" : "auto"}
+            className="flex-1 justify-center active:opacity-50"
+          >
+            <Text className="px-2 text-primary">{cancelText}</Text>
+          </Pressable>
+        </Animated.View>
+      </Animated.View>
+    );
+  },
+);
+
+SearchInput.displayName = "SearchInput";
+
+export { SearchInput };
-- 
cgit v1.2.3-70-g09d2

