|
|
@@ -41,9 +41,53 @@ struct KeyboardHackField: UIViewRepresentable {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-// MARK: - Root Tabs View
|
|
|
+// MARK: - Root Tabs View (dispatch to 26+ vs 16–25)
|
|
|
|
|
|
struct LiquidTabsRootView: View {
|
|
|
+ let navController: UINavigationController
|
|
|
+
|
|
|
+ var body: some View {
|
|
|
+ if #available(iOS 26.0, *) {
|
|
|
+ LiquidTabs26View(navController: navController)
|
|
|
+ } else {
|
|
|
+ LiquidTabs16View(navController: navController)
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// MARK: - Shared selection helpers
|
|
|
+
|
|
|
+enum LiquidTabsTabSelection: Hashable {
|
|
|
+ case content(Int)
|
|
|
+ case search
|
|
|
+}
|
|
|
+
|
|
|
+private extension LiquidTabsStore {
|
|
|
+ var firstTab: LiquidTab? { tabs.first }
|
|
|
+
|
|
|
+ func tabId(for selection: LiquidTabsTabSelection) -> String? {
|
|
|
+ switch selection {
|
|
|
+ case .content(let index):
|
|
|
+ guard index >= 0 && index < tabs.count else { return nil }
|
|
|
+ return tabs[index].id
|
|
|
+ case .search:
|
|
|
+ return "search"
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ func selection(forId id: String) -> LiquidTabsTabSelection? {
|
|
|
+ if id == "search" { return .search }
|
|
|
+ if let idx = tabs.firstIndex(where: { $0.id == id }) {
|
|
|
+ return .content(idx)
|
|
|
+ }
|
|
|
+ return nil
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// MARK: - iOS 26+ implementation using Tab(...) API + search role
|
|
|
+
|
|
|
+@available(iOS 26.0, *)
|
|
|
+private struct LiquidTabs26View: View {
|
|
|
@StateObject private var store = LiquidTabsStore.shared
|
|
|
let navController: UINavigationController
|
|
|
|
|
|
@@ -51,84 +95,37 @@ struct LiquidTabsRootView: View {
|
|
|
@State private var isSearchPresented: Bool = false
|
|
|
@FocusState private var isSearchFocused: Bool
|
|
|
|
|
|
- // Controls whether the hidden UITextField should grab keyboard focus.
|
|
|
@State private var hackShowKeyboard: Bool = false
|
|
|
+ @State private var selectedTab: LiquidTabsTabSelection = .content(0)
|
|
|
|
|
|
- // Native selection type: dynamic tabs + search
|
|
|
- enum TabSelection: Hashable {
|
|
|
- case content(Int) // index into store.tabs
|
|
|
- case search
|
|
|
- }
|
|
|
-
|
|
|
- @State private var selectedTab: TabSelection = .content(0)
|
|
|
-
|
|
|
- // (optional) cap number of main tabs if you like
|
|
|
private let maxMainTabs = 6
|
|
|
|
|
|
- // MARK: - Re-Tap Logic
|
|
|
-
|
|
|
- /// Proxy binding to intercept TabView interactions
|
|
|
- private var tabSelectionProxy: Binding<TabSelection> {
|
|
|
+ // Proxy binding to intercept re-taps
|
|
|
+ private var tabSelectionProxy: Binding<LiquidTabsTabSelection> {
|
|
|
Binding(
|
|
|
get: { selectedTab },
|
|
|
set: { newValue in
|
|
|
if newValue == selectedTab {
|
|
|
- // --- CAPTURE RE-TAP ---
|
|
|
handleRetap(on: newValue)
|
|
|
} else {
|
|
|
- // --- NORMAL SELECTION ---
|
|
|
selectedTab = newValue
|
|
|
}
|
|
|
}
|
|
|
)
|
|
|
}
|
|
|
|
|
|
- private func handleRetap(on selection: TabSelection) {
|
|
|
+ private func handleRetap(on selection: LiquidTabsTabSelection) {
|
|
|
print("User re-tapped tab: \(selection)")
|
|
|
-
|
|
|
- // 1. Standard iOS Behavior: Pop to root
|
|
|
navController.popToRootViewController(animated: true)
|
|
|
|
|
|
- // 2. Notify Plugin
|
|
|
- if let id = tabId(for: selection) {
|
|
|
+ if let id = store.tabId(for: selection) {
|
|
|
LiquidTabsPlugin.shared?.notifyTabSelected(id: id)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- // MARK: - Tab Helpers
|
|
|
-
|
|
|
- private var firstTab: LiquidTab? {
|
|
|
- store.tabs.first
|
|
|
- }
|
|
|
-
|
|
|
- /// Get tab id for a selection
|
|
|
- private func tabId(for selection: TabSelection) -> String? {
|
|
|
- switch selection {
|
|
|
- case .content(let index):
|
|
|
- guard index >= 0 && index < store.tabs.count else { return nil }
|
|
|
- return store.tabs[index].id
|
|
|
- case .search:
|
|
|
- return "search"
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- /// Map a tab id back to TabSelection
|
|
|
- private func selection(forId id: String) -> TabSelection? {
|
|
|
- if id == "search" {
|
|
|
- return .search
|
|
|
- }
|
|
|
-
|
|
|
- if let index = store.tabs.firstIndex(where: { $0.id == id }) {
|
|
|
- return .content(index)
|
|
|
- }
|
|
|
-
|
|
|
- return nil
|
|
|
- }
|
|
|
-
|
|
|
- /// Compute initial selection based on store.selectedId or available tabs
|
|
|
- private func initialSelection() -> TabSelection {
|
|
|
+ private func initialSelection() -> LiquidTabsTabSelection {
|
|
|
if let id = store.selectedId,
|
|
|
- let sel = selection(forId: id) {
|
|
|
+ let sel = store.selection(forId: id) {
|
|
|
return sel
|
|
|
}
|
|
|
|
|
|
@@ -139,191 +136,138 @@ struct LiquidTabsRootView: View {
|
|
|
return .search
|
|
|
}
|
|
|
|
|
|
- // MARK: - Body
|
|
|
-
|
|
|
var body: some View {
|
|
|
- if #available(iOS 26.0, *) {
|
|
|
- if store.tabs.isEmpty {
|
|
|
- NativeNavHost(navController: navController)
|
|
|
- .ignoresSafeArea()
|
|
|
- .background(Color.logseqBackground)
|
|
|
- } else {
|
|
|
- ZStack {
|
|
|
- Color.logseqBackground.ignoresSafeArea()
|
|
|
-
|
|
|
- // Main TabView using the PROXY BINDING
|
|
|
- TabView(selection: tabSelectionProxy) {
|
|
|
-
|
|
|
- // ---- Dynamic main tabs, using Tab(...) API ----
|
|
|
- ForEach(Array(store.tabs.prefix(maxMainTabs).enumerated()),
|
|
|
- id: \.element.id) { index, tab in
|
|
|
- Tab(
|
|
|
- tab.title,
|
|
|
- systemImage: tab.systemImage,
|
|
|
- value: TabSelection.content(index)
|
|
|
- ) {
|
|
|
- NativeNavHost(navController: navController)
|
|
|
- .ignoresSafeArea()
|
|
|
- .background(Color.logseqBackground)
|
|
|
- }
|
|
|
- }
|
|
|
+ if store.tabs.isEmpty {
|
|
|
+ // bootstrap webview so JS can configure tabs
|
|
|
+ NativeNavHost(navController: navController)
|
|
|
+ .ignoresSafeArea()
|
|
|
+ .background(Color.logseqBackground)
|
|
|
+ } else {
|
|
|
+ ZStack {
|
|
|
+ Color.logseqBackground.ignoresSafeArea()
|
|
|
|
|
|
- // ---- Search Tab ----
|
|
|
- Tab(value: TabSelection.search, role: .search) {
|
|
|
- SearchTabHost(
|
|
|
- navController: navController,
|
|
|
- isSearchFocused: $isSearchFocused,
|
|
|
- selectedTab: $selectedTab,
|
|
|
- firstTabId: store.tabs.first?.id,
|
|
|
- store: store
|
|
|
- )
|
|
|
- .ignoresSafeArea()
|
|
|
+ TabView(selection: tabSelectionProxy) {
|
|
|
+ // Dynamic main tabs using Tab(...) API
|
|
|
+ ForEach(Array(store.tabs.prefix(maxMainTabs).enumerated()),
|
|
|
+ id: \.element.id) { index, tab in
|
|
|
+ Tab(
|
|
|
+ tab.title,
|
|
|
+ systemImage: tab.systemImage,
|
|
|
+ value: LiquidTabsTabSelection.content(index)
|
|
|
+ ) {
|
|
|
+ NativeNavHost(navController: navController)
|
|
|
+ .ignoresSafeArea()
|
|
|
+ .background(Color.logseqBackground)
|
|
|
}
|
|
|
}
|
|
|
- // SwiftUI search system integration
|
|
|
- .searchable(
|
|
|
- text: $searchText,
|
|
|
- isPresented: $isSearchPresented
|
|
|
- )
|
|
|
- .searchFocused($isSearchFocused)
|
|
|
- .searchToolbarBehavior(.minimize)
|
|
|
- .onChange(of: searchText) { query in
|
|
|
- LiquidTabsPlugin.shared?.notifySearchChanged(query: query)
|
|
|
+
|
|
|
+ // Search Tab
|
|
|
+ Tab(value: .search, role: .search) {
|
|
|
+ SearchTabHost26(
|
|
|
+ navController: navController,
|
|
|
+ isSearchFocused: $isSearchFocused,
|
|
|
+ selectedTab: $selectedTab,
|
|
|
+ firstTabId: store.tabs.first?.id,
|
|
|
+ store: store
|
|
|
+ )
|
|
|
+ .ignoresSafeArea()
|
|
|
}
|
|
|
- .background(Color.logseqBackground)
|
|
|
+ }
|
|
|
+ .searchable(
|
|
|
+ text: $searchText,
|
|
|
+ isPresented: $isSearchPresented
|
|
|
+ )
|
|
|
+ .searchFocused($isSearchFocused)
|
|
|
+ .searchToolbarBehavior(.minimize)
|
|
|
+ .onChange(of: searchText) { query in
|
|
|
+ LiquidTabsPlugin.shared?.notifySearchChanged(query: query)
|
|
|
+ }
|
|
|
+ .background(Color.logseqBackground)
|
|
|
|
|
|
- // Hidden UITextField that pre-invokes keyboard
|
|
|
- KeyboardHackField(shouldShow: $hackShowKeyboard)
|
|
|
- .frame(width: 0, height: 0)
|
|
|
+ // Hidden UITextField that pre-invokes keyboard
|
|
|
+ KeyboardHackField(shouldShow: $hackShowKeyboard)
|
|
|
+ .frame(width: 0, height: 0)
|
|
|
+ }
|
|
|
+ .onAppear {
|
|
|
+ let initial = initialSelection()
|
|
|
+ selectedTab = initial
|
|
|
+ if case .search = initial {
|
|
|
+ isSearchPresented = true
|
|
|
}
|
|
|
- .onAppear {
|
|
|
- let initial = initialSelection()
|
|
|
- selectedTab = initial
|
|
|
- if case .search = initial {
|
|
|
- isSearchPresented = true
|
|
|
- }
|
|
|
|
|
|
- let appearance = UITabBarAppearance()
|
|
|
- appearance.configureWithTransparentBackground()
|
|
|
+ let appearance = UITabBarAppearance()
|
|
|
+ appearance.configureWithTransparentBackground()
|
|
|
|
|
|
- // Selected text color
|
|
|
- appearance.stackedLayoutAppearance.selected.titleTextAttributes = [
|
|
|
- .foregroundColor: UIColor.label
|
|
|
- ]
|
|
|
+ // Selected text color
|
|
|
+ appearance.stackedLayoutAppearance.selected.titleTextAttributes = [
|
|
|
+ .foregroundColor: UIColor.label
|
|
|
+ ]
|
|
|
|
|
|
- // Unselected text color (70%)
|
|
|
- let dimmed = UIColor.label.withAlphaComponent(0.7)
|
|
|
- appearance.stackedLayoutAppearance.normal.titleTextAttributes = [
|
|
|
- .foregroundColor: dimmed
|
|
|
- ]
|
|
|
+ // Unselected text color (70%)
|
|
|
+ let dimmed = UIColor.label.withAlphaComponent(0.7)
|
|
|
+ appearance.stackedLayoutAppearance.normal.titleTextAttributes = [
|
|
|
+ .foregroundColor: dimmed
|
|
|
+ ]
|
|
|
|
|
|
- appearance.stackedLayoutAppearance.normal.iconColor = UIColor.label.withAlphaComponent(0.9)
|
|
|
+ // Unselected icon color (90%)
|
|
|
+ appearance.stackedLayoutAppearance.normal.iconColor =
|
|
|
+ UIColor.label.withAlphaComponent(0.9)
|
|
|
|
|
|
- // Apply the appearance
|
|
|
- let tabBar = UITabBar.appearance()
|
|
|
- tabBar.tintColor = .label
|
|
|
- tabBar.standardAppearance = appearance
|
|
|
- tabBar.scrollEdgeAppearance = appearance
|
|
|
+ let tabBar = UITabBar.appearance()
|
|
|
+ tabBar.tintColor = .label
|
|
|
+ tabBar.standardAppearance = appearance
|
|
|
+ tabBar.scrollEdgeAppearance = appearance
|
|
|
+ }
|
|
|
+ .onChange(of: selectedTab) { newValue in
|
|
|
+ if let id = store.tabId(for: newValue) {
|
|
|
+ store.selectedId = id
|
|
|
+ LiquidTabsPlugin.shared?.notifyTabSelected(id: id)
|
|
|
}
|
|
|
- // Handle STANDARD tab selection changes
|
|
|
- .onChange(of: selectedTab) { newValue in
|
|
|
- if let id = tabId(for: newValue) {
|
|
|
- store.selectedId = id
|
|
|
- LiquidTabsPlugin.shared?.notifyTabSelected(id: id)
|
|
|
- }
|
|
|
|
|
|
- switch newValue {
|
|
|
- case .search:
|
|
|
- isSearchPresented = true
|
|
|
- case .content:
|
|
|
- hackShowKeyboard = false
|
|
|
- isSearchFocused = false
|
|
|
- isSearchPresented = false
|
|
|
- }
|
|
|
- }
|
|
|
- .onChange(of: isSearchPresented) { presented in
|
|
|
- if presented {
|
|
|
- // kick the keyboard hack after a short delay
|
|
|
- DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) {
|
|
|
- hackShowKeyboard = true
|
|
|
- isSearchFocused = true
|
|
|
- }
|
|
|
- DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
|
|
|
- hackShowKeyboard = false
|
|
|
- }
|
|
|
- } else {
|
|
|
- isSearchFocused = false
|
|
|
- hackShowKeyboard = false
|
|
|
- }
|
|
|
+ switch newValue {
|
|
|
+ case .search:
|
|
|
+ isSearchPresented = true
|
|
|
+ case .content:
|
|
|
+ hackShowKeyboard = false
|
|
|
+ isSearchFocused = false
|
|
|
+ isSearchPresented = false
|
|
|
}
|
|
|
- .onChange(of: store.selectedId) { newId in
|
|
|
- guard let id = newId,
|
|
|
- let newSelection = selection(forId: id) else {
|
|
|
- return
|
|
|
+ }
|
|
|
+ .onChange(of: isSearchPresented) { presented in
|
|
|
+ if presented {
|
|
|
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) {
|
|
|
+ hackShowKeyboard = true
|
|
|
+ isSearchFocused = true
|
|
|
}
|
|
|
-
|
|
|
- // If it's already selected, treat it as a no-op for programmatic changes
|
|
|
- if newSelection == selectedTab {
|
|
|
- return
|
|
|
+ DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
|
|
|
+ hackShowKeyboard = false
|
|
|
}
|
|
|
-
|
|
|
- selectedTab = newSelection
|
|
|
+ } else {
|
|
|
+ isSearchFocused = false
|
|
|
+ hackShowKeyboard = false
|
|
|
}
|
|
|
- // Disable content animation on selection changes (only tab bar animates)
|
|
|
- .animation(nil, value: selectedTab)
|
|
|
}
|
|
|
+ .onChange(of: store.selectedId) { newId in
|
|
|
+ guard let id = newId,
|
|
|
+ let newSelection = store.selection(forId: id) else {
|
|
|
+ return
|
|
|
+ }
|
|
|
|
|
|
- } else {
|
|
|
- // MARK: Fallback for iOS < 26
|
|
|
- ZStack {
|
|
|
- Color.logseqBackground.ignoresSafeArea()
|
|
|
-
|
|
|
- if store.tabs.isEmpty {
|
|
|
- // 🔑 Bootstrapping path: attach the shared webview
|
|
|
- // so JS can run and configure tabs.
|
|
|
- NativeNavHost(navController: navController)
|
|
|
- .ignoresSafeArea()
|
|
|
- .background(Color.logseqBackground)
|
|
|
- } else {
|
|
|
- TabView(selection: Binding(
|
|
|
- get: { store.selectedId ?? firstTab?.id },
|
|
|
- set: { newValue in
|
|
|
- guard let id = newValue else { return }
|
|
|
-
|
|
|
- // Fallback Re-Tap Logic
|
|
|
- if id == store.selectedId {
|
|
|
- navController.popToRootViewController(animated: true)
|
|
|
- LiquidTabsPlugin.shared?.notifyTabSelected(id: id)
|
|
|
- } else {
|
|
|
- store.selectedId = id
|
|
|
- LiquidTabsPlugin.shared?.notifyTabSelected(id: id)
|
|
|
- }
|
|
|
- }
|
|
|
- )) {
|
|
|
- ForEach(store.tabs) { tab in
|
|
|
- NativeNavHost(navController: navController)
|
|
|
- .ignoresSafeArea()
|
|
|
- .background(Color.logseqBackground)
|
|
|
- .tabItem {
|
|
|
- Label(tab.title, systemImage: tab.systemImage)
|
|
|
- }
|
|
|
- .tag(tab.id as String?)
|
|
|
- }
|
|
|
- }
|
|
|
- .background(Color.logseqBackground)
|
|
|
- .toolbarBackground(Color.logseqBackground, for: .tabBar)
|
|
|
+ if newSelection != selectedTab {
|
|
|
+ selectedTab = newSelection
|
|
|
}
|
|
|
}
|
|
|
+ .animation(nil, value: selectedTab)
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-// MARK: - Search Tab Host
|
|
|
-
|
|
|
-private struct SearchTabHost: View {
|
|
|
+// Search host for 26+ (unchanged)
|
|
|
+@available(iOS 26.0, *)
|
|
|
+private struct SearchTabHost26: View {
|
|
|
let navController: UINavigationController
|
|
|
@FocusState.Binding var isSearchFocused: Bool
|
|
|
- var selectedTab: Binding<LiquidTabsRootView.TabSelection>
|
|
|
+ var selectedTab: Binding<LiquidTabsTabSelection>
|
|
|
let firstTabId: String?
|
|
|
let store: LiquidTabsStore
|
|
|
|
|
|
@@ -359,3 +303,159 @@ private struct SearchTabHost: View {
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+// MARK: - iOS 16–25 implementation
|
|
|
+// Classic TabView + .tabItem; Search tab shows a custom search bar pinned at top.
|
|
|
+
|
|
|
+private struct LiquidTabs16View: View {
|
|
|
+ @StateObject private var store = LiquidTabsStore.shared
|
|
|
+ let navController: UINavigationController
|
|
|
+
|
|
|
+ @State private var searchText: String = ""
|
|
|
+ @State private var hackShowKeyboard: Bool = false
|
|
|
+
|
|
|
+ var body: some View {
|
|
|
+ ZStack {
|
|
|
+ Color.logseqBackground.ignoresSafeArea()
|
|
|
+
|
|
|
+ if store.tabs.isEmpty {
|
|
|
+ // bootstrapping: attach shared webview until JS configures tabs
|
|
|
+ NativeNavHost(navController: navController)
|
|
|
+ .ignoresSafeArea()
|
|
|
+ .background(Color.logseqBackground)
|
|
|
+ } else {
|
|
|
+ ZStack {
|
|
|
+ Color.logseqBackground.ignoresSafeArea()
|
|
|
+
|
|
|
+ TabView(selection: Binding<String?>(
|
|
|
+ get: {
|
|
|
+ store.selectedId ?? store.firstTab?.id
|
|
|
+ },
|
|
|
+ set: { newValue in
|
|
|
+ guard let id = newValue else { return }
|
|
|
+
|
|
|
+ // Re-tap: pop to root
|
|
|
+ if id == store.selectedId {
|
|
|
+ navController.popToRootViewController(animated: true)
|
|
|
+ LiquidTabsPlugin.shared?.notifyTabSelected(id: id)
|
|
|
+ } else {
|
|
|
+ store.selectedId = id
|
|
|
+ LiquidTabsPlugin.shared?.notifyTabSelected(id: id)
|
|
|
+ }
|
|
|
+
|
|
|
+ // Basic keyboard hack when selecting Search tab
|
|
|
+ if id == "search" {
|
|
|
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) {
|
|
|
+ hackShowKeyboard = true
|
|
|
+ }
|
|
|
+ DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
|
|
|
+ hackShowKeyboard = false
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ hackShowKeyboard = false
|
|
|
+ }
|
|
|
+ }
|
|
|
+ )) {
|
|
|
+ // --- Normal dynamic tabs ---
|
|
|
+ ForEach(store.tabs) { tab in
|
|
|
+ NativeNavHost(navController: navController)
|
|
|
+ .ignoresSafeArea()
|
|
|
+ .background(Color.logseqBackground)
|
|
|
+ .tabItem {
|
|
|
+ Label(tab.title, systemImage: tab.systemImage)
|
|
|
+ }
|
|
|
+ .tag(tab.id as String?)
|
|
|
+ }
|
|
|
+
|
|
|
+ // --- 🔍 SEARCH TAB (iOS 16–25) ---
|
|
|
+ SearchTab16Host(
|
|
|
+ navController: navController,
|
|
|
+ searchText: $searchText
|
|
|
+ )
|
|
|
+ .ignoresSafeArea()
|
|
|
+ .tabItem {
|
|
|
+ Label("Search", systemImage: "magnifyingglass")
|
|
|
+ }
|
|
|
+ .tag("search" as String?)
|
|
|
+ }
|
|
|
+
|
|
|
+ // Hidden UITextField that pre-invokes keyboard
|
|
|
+ KeyboardHackField(shouldShow: $hackShowKeyboard)
|
|
|
+ .frame(width: 0, height: 0)
|
|
|
+ }
|
|
|
+ .onAppear {
|
|
|
+ if store.selectedId == nil {
|
|
|
+ store.selectedId = store.tabs.first?.id
|
|
|
+ }
|
|
|
+
|
|
|
+ let appearance = UITabBarAppearance()
|
|
|
+ appearance.configureWithTransparentBackground()
|
|
|
+
|
|
|
+ appearance.stackedLayoutAppearance.selected.titleTextAttributes = [
|
|
|
+ .foregroundColor: UIColor.label
|
|
|
+ ]
|
|
|
+
|
|
|
+ let dimmed = UIColor.label.withAlphaComponent(0.7)
|
|
|
+ appearance.stackedLayoutAppearance.normal.titleTextAttributes = [
|
|
|
+ .foregroundColor: dimmed
|
|
|
+ ]
|
|
|
+ appearance.stackedLayoutAppearance.normal.iconColor =
|
|
|
+ UIColor.label.withAlphaComponent(0.9)
|
|
|
+
|
|
|
+ let tabBar = UITabBar.appearance()
|
|
|
+ tabBar.tintColor = .label
|
|
|
+ tabBar.standardAppearance = appearance
|
|
|
+ tabBar.scrollEdgeAppearance = appearance
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+private struct SearchTab16Host: View {
|
|
|
+ let navController: UINavigationController
|
|
|
+ @Binding var searchText: String
|
|
|
+
|
|
|
+ var body: some View {
|
|
|
+ NavigationStack {
|
|
|
+ ZStack {
|
|
|
+ // Main content (fills whole screen)
|
|
|
+ NativeNavHost(navController: navController)
|
|
|
+ .ignoresSafeArea()
|
|
|
+
|
|
|
+ // Bottom search bar
|
|
|
+ VStack {
|
|
|
+ Spacer()
|
|
|
+
|
|
|
+ HStack(spacing: 8) {
|
|
|
+ Image(systemName: "magnifyingglass")
|
|
|
+ .font(.system(size: 16))
|
|
|
+
|
|
|
+ TextField("Search", text: $searchText)
|
|
|
+ .textInputAutocapitalization(.none)
|
|
|
+ .disableAutocorrection(true)
|
|
|
+
|
|
|
+ if !searchText.isEmpty {
|
|
|
+ Button("Clear") {
|
|
|
+ searchText = ""
|
|
|
+ }
|
|
|
+ .font(.system(size: 14, weight: .medium))
|
|
|
+ }
|
|
|
+ }
|
|
|
+ .padding(.horizontal, 12)
|
|
|
+ .padding(.vertical, 10)
|
|
|
+ .background(
|
|
|
+ RoundedRectangle(cornerRadius: 14)
|
|
|
+ .fill(Color(.systemGray5))
|
|
|
+ )
|
|
|
+ .padding(.horizontal, 16)
|
|
|
+ .padding(.bottom, 12)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ .navigationBarHidden(true)
|
|
|
+ }
|
|
|
+ .onChange(of: searchText) { query in
|
|
|
+ LiquidTabsPlugin.shared?.notifySearchChanged(query: query)
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|