@ajuarezso/capacitor-liquid-glass 0.1.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -16,8 +16,11 @@ export interface LiquidGlassTabItem {
16
16
  * more see-through than default.
17
17
  * - `'transparent'`: no background, no blur — content behind shows through 100%.
18
18
  * Trade-off: legibilidad puede sufrir sobre contenido caótico.
19
+ * - `'liquidGlass'`: REAL iOS 26 `UIGlassEffect` — el mismo material que usan
20
+ * Music y App Store. En iOS < 26 cae a `UIBlurEffect.systemThinMaterial`
21
+ * (aproximación cercana con system vibrancy).
19
22
  */
20
- export type TabBarStyle = 'default' | 'ultraThin' | 'transparent';
23
+ export type TabBarStyle = 'default' | 'ultraThin' | 'transparent' | 'liquidGlass';
21
24
  export interface ShowTabBarOptions {
22
25
  items: LiquidGlassTabItem[];
23
26
  /** Index of the initially selected item. Defaults to 0. */
@@ -48,6 +51,33 @@ export interface TabBarLayoutEvent {
48
51
  /** Safe-area bottom inset the tab bar is sitting on (pt). */
49
52
  bottomSafeArea: number;
50
53
  }
54
+ /**
55
+ * Options for the native iOS Liquid Glass search bar overlay (top of window).
56
+ *
57
+ * On iOS 26+ the underlying `UISearchBar` adopts the system Liquid Glass look
58
+ * automatically; on earlier iOS we wrap it in a `systemUltraThinMaterial`
59
+ * blurred container so the visual is as close as possible.
60
+ */
61
+ export interface ShowSearchBarOptions {
62
+ /** Placeholder shown when the field is empty. */
63
+ placeholder?: string;
64
+ /** Initial text the field opens with. */
65
+ initialText?: string;
66
+ /** Custom label for the trailing "Cancel" button. */
67
+ cancelText?: string;
68
+ /** Tint color for the cursor + Cancel button, hex `"#RRGGBB"`. */
69
+ tintColor?: string;
70
+ /** Hide the trailing Cancel button (defaults to `false`). */
71
+ hideCancelButton?: boolean;
72
+ }
73
+ export interface SearchTextChangedEvent {
74
+ /** Current text in the search field. */
75
+ text: string;
76
+ }
77
+ export interface SearchSubmittedEvent {
78
+ /** Text the user submitted (return key on the keyboard). */
79
+ text: string;
80
+ }
51
81
  export interface LiquidGlassPlugin {
52
82
  /** Creates (or updates) and shows the native Liquid Glass tab bar. */
53
83
  showTabBar(options: ShowTabBarOptions): Promise<void>;
@@ -63,5 +93,17 @@ export interface LiquidGlassPlugin {
63
93
  addListener(eventName: 'tabSelected', listenerFunc: (event: TabSelectedEvent) => void): Promise<PluginListenerHandle>;
64
94
  /** Emitted when the tab bar's height or safe-area changes (rotation, etc.). */
65
95
  addListener(eventName: 'tabBarLayoutChanged', listenerFunc: (event: TabBarLayoutEvent) => void): Promise<PluginListenerHandle>;
96
+ /** Shows the native Liquid Glass search bar overlay anchored to the top. */
97
+ showSearchBar(options?: ShowSearchBarOptions): Promise<void>;
98
+ /** Hides the search bar without destroying configuration. */
99
+ hideSearchBar(): Promise<void>;
100
+ /** Clears the text in the search field without dismissing the overlay. */
101
+ clearSearchText(): Promise<void>;
102
+ /** Emitted on every keystroke while the user types in the search field. */
103
+ addListener(eventName: 'searchTextChanged', listenerFunc: (event: SearchTextChangedEvent) => void): Promise<PluginListenerHandle>;
104
+ /** Emitted when the user taps the keyboard's "Search" / return key. */
105
+ addListener(eventName: 'searchSubmitted', listenerFunc: (event: SearchSubmittedEvent) => void): Promise<PluginListenerHandle>;
106
+ /** Emitted when the user taps the trailing Cancel button. */
107
+ addListener(eventName: 'searchCancelled', listenerFunc: () => void): Promise<PluginListenerHandle>;
66
108
  removeAllListeners(): Promise<void>;
67
109
  }
package/dist/esm/web.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { WebPlugin } from '@capacitor/core';
2
- import type { LiquidGlassPlugin, SetSelectedTabOptions, ShowTabBarOptions, TabBarLayoutEvent, UpdateTabBadgeOptions } from './definitions';
2
+ import type { LiquidGlassPlugin, SetSelectedTabOptions, ShowSearchBarOptions, ShowTabBarOptions, TabBarLayoutEvent, UpdateTabBadgeOptions } from './definitions';
3
3
  /**
4
4
  * Web fallback — real Liquid Glass requires native iOS 26. On the web we
5
5
  * resolve no-ops so the app can run in the browser during development; the
@@ -12,4 +12,7 @@ export declare class LiquidGlassWeb extends WebPlugin implements LiquidGlassPlug
12
12
  setSelectedTab(_options: SetSelectedTabOptions): Promise<void>;
13
13
  updateTabBadge(_options: UpdateTabBadgeOptions): Promise<void>;
14
14
  getTabBarLayout(): Promise<TabBarLayoutEvent>;
15
+ showSearchBar(_options?: ShowSearchBarOptions): Promise<void>;
16
+ hideSearchBar(): Promise<void>;
17
+ clearSearchText(): Promise<void>;
15
18
  }
package/dist/esm/web.js CHANGED
@@ -21,4 +21,13 @@ export class LiquidGlassWeb extends WebPlugin {
21
21
  async getTabBarLayout() {
22
22
  return { height: 0, bottomSafeArea: 0 };
23
23
  }
24
+ async showSearchBar(_options) {
25
+ // no-op on web — caller should render its own DOM search input.
26
+ }
27
+ async hideSearchBar() {
28
+ // no-op on web
29
+ }
30
+ async clearSearchText() {
31
+ // no-op on web
32
+ }
24
33
  }
@@ -28,6 +28,15 @@ class LiquidGlassWeb extends core.WebPlugin {
28
28
  async getTabBarLayout() {
29
29
  return { height: 0, bottomSafeArea: 0 };
30
30
  }
31
+ async showSearchBar(_options) {
32
+ // no-op on web — caller should render its own DOM search input.
33
+ }
34
+ async hideSearchBar() {
35
+ // no-op on web
36
+ }
37
+ async clearSearchText() {
38
+ // no-op on web
39
+ }
31
40
  }
32
41
 
33
42
  var web = /*#__PURE__*/Object.freeze({
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.cjs.js","sources":["esm/index.js","esm/web.js"],"sourcesContent":["import { registerPlugin } from '@capacitor/core';\nconst LiquidGlass = registerPlugin('LiquidGlass', {\n web: () => import('./web').then((m) => new m.LiquidGlassWeb()),\n});\nexport * from './definitions';\nexport { LiquidGlass };\n","import { WebPlugin } from '@capacitor/core';\n/**\n * Web fallback — real Liquid Glass requires native iOS 26. On the web we\n * resolve no-ops so the app can run in the browser during development; the\n * Angular shell is expected to render its own CSS glassmorphism tab bar when\n * `isNativePlatform()` is false.\n */\nexport class LiquidGlassWeb extends WebPlugin {\n async showTabBar(_options) {\n // no-op on web\n }\n async hideTabBar() {\n // no-op on web\n }\n async setSelectedTab(_options) {\n // no-op on web\n }\n async updateTabBadge(_options) {\n // no-op on web\n }\n async getTabBarLayout() {\n return { height: 0, bottomSafeArea: 0 };\n }\n}\n"],"names":["registerPlugin","WebPlugin"],"mappings":";;;;AACK,MAAC,WAAW,GAAGA,mBAAc,CAAC,aAAa,EAAE;AAClD,IAAI,GAAG,EAAE,MAAM,mDAAe,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,cAAc,EAAE,CAAC;AAClE,CAAC;;ACFD;AACA;AACA;AACA;AACA;AACA;AACO,MAAM,cAAc,SAASC,cAAS,CAAC;AAC9C,IAAI,MAAM,UAAU,CAAC,QAAQ,EAAE;AAC/B;AACA,IAAI;AACJ,IAAI,MAAM,UAAU,GAAG;AACvB;AACA,IAAI;AACJ,IAAI,MAAM,cAAc,CAAC,QAAQ,EAAE;AACnC;AACA,IAAI;AACJ,IAAI,MAAM,cAAc,CAAC,QAAQ,EAAE;AACnC;AACA,IAAI;AACJ,IAAI,MAAM,eAAe,GAAG;AAC5B,QAAQ,OAAO,EAAE,MAAM,EAAE,CAAC,EAAE,cAAc,EAAE,CAAC,EAAE;AAC/C,IAAI;AACJ;;;;;;;;;"}
1
+ {"version":3,"file":"plugin.cjs.js","sources":["esm/index.js","esm/web.js"],"sourcesContent":["import { registerPlugin } from '@capacitor/core';\nconst LiquidGlass = registerPlugin('LiquidGlass', {\n web: () => import('./web').then((m) => new m.LiquidGlassWeb()),\n});\nexport * from './definitions';\nexport { LiquidGlass };\n","import { WebPlugin } from '@capacitor/core';\n/**\n * Web fallback — real Liquid Glass requires native iOS 26. On the web we\n * resolve no-ops so the app can run in the browser during development; the\n * Angular shell is expected to render its own CSS glassmorphism tab bar when\n * `isNativePlatform()` is false.\n */\nexport class LiquidGlassWeb extends WebPlugin {\n async showTabBar(_options) {\n // no-op on web\n }\n async hideTabBar() {\n // no-op on web\n }\n async setSelectedTab(_options) {\n // no-op on web\n }\n async updateTabBadge(_options) {\n // no-op on web\n }\n async getTabBarLayout() {\n return { height: 0, bottomSafeArea: 0 };\n }\n async showSearchBar(_options) {\n // no-op on web — caller should render its own DOM search input.\n }\n async hideSearchBar() {\n // no-op on web\n }\n async clearSearchText() {\n // no-op on web\n }\n}\n"],"names":["registerPlugin","WebPlugin"],"mappings":";;;;AACK,MAAC,WAAW,GAAGA,mBAAc,CAAC,aAAa,EAAE;AAClD,IAAI,GAAG,EAAE,MAAM,mDAAe,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,cAAc,EAAE,CAAC;AAClE,CAAC;;ACFD;AACA;AACA;AACA;AACA;AACA;AACO,MAAM,cAAc,SAASC,cAAS,CAAC;AAC9C,IAAI,MAAM,UAAU,CAAC,QAAQ,EAAE;AAC/B;AACA,IAAI;AACJ,IAAI,MAAM,UAAU,GAAG;AACvB;AACA,IAAI;AACJ,IAAI,MAAM,cAAc,CAAC,QAAQ,EAAE;AACnC;AACA,IAAI;AACJ,IAAI,MAAM,cAAc,CAAC,QAAQ,EAAE;AACnC;AACA,IAAI;AACJ,IAAI,MAAM,eAAe,GAAG;AAC5B,QAAQ,OAAO,EAAE,MAAM,EAAE,CAAC,EAAE,cAAc,EAAE,CAAC,EAAE;AAC/C,IAAI;AACJ,IAAI,MAAM,aAAa,CAAC,QAAQ,EAAE;AAClC;AACA,IAAI;AACJ,IAAI,MAAM,aAAa,GAAG;AAC1B;AACA,IAAI;AACJ,IAAI,MAAM,eAAe,GAAG;AAC5B;AACA,IAAI;AACJ;;;;;;;;;"}
package/dist/plugin.js CHANGED
@@ -27,6 +27,15 @@ var capacitorLiquidGlass = (function (exports, core) {
27
27
  async getTabBarLayout() {
28
28
  return { height: 0, bottomSafeArea: 0 };
29
29
  }
30
+ async showSearchBar(_options) {
31
+ // no-op on web — caller should render its own DOM search input.
32
+ }
33
+ async hideSearchBar() {
34
+ // no-op on web
35
+ }
36
+ async clearSearchText() {
37
+ // no-op on web
38
+ }
30
39
  }
31
40
 
32
41
  var web = /*#__PURE__*/Object.freeze({
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.js","sources":["esm/index.js","esm/web.js"],"sourcesContent":["import { registerPlugin } from '@capacitor/core';\nconst LiquidGlass = registerPlugin('LiquidGlass', {\n web: () => import('./web').then((m) => new m.LiquidGlassWeb()),\n});\nexport * from './definitions';\nexport { LiquidGlass };\n","import { WebPlugin } from '@capacitor/core';\n/**\n * Web fallback — real Liquid Glass requires native iOS 26. On the web we\n * resolve no-ops so the app can run in the browser during development; the\n * Angular shell is expected to render its own CSS glassmorphism tab bar when\n * `isNativePlatform()` is false.\n */\nexport class LiquidGlassWeb extends WebPlugin {\n async showTabBar(_options) {\n // no-op on web\n }\n async hideTabBar() {\n // no-op on web\n }\n async setSelectedTab(_options) {\n // no-op on web\n }\n async updateTabBadge(_options) {\n // no-op on web\n }\n async getTabBarLayout() {\n return { height: 0, bottomSafeArea: 0 };\n }\n}\n"],"names":["registerPlugin","WebPlugin"],"mappings":";;;AACK,UAAC,WAAW,GAAGA,mBAAc,CAAC,aAAa,EAAE;IAClD,IAAI,GAAG,EAAE,MAAM,mDAAe,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,cAAc,EAAE,CAAC;IAClE,CAAC;;ICFD;IACA;IACA;IACA;IACA;IACA;IACO,MAAM,cAAc,SAASC,cAAS,CAAC;IAC9C,IAAI,MAAM,UAAU,CAAC,QAAQ,EAAE;IAC/B;IACA,IAAI;IACJ,IAAI,MAAM,UAAU,GAAG;IACvB;IACA,IAAI;IACJ,IAAI,MAAM,cAAc,CAAC,QAAQ,EAAE;IACnC;IACA,IAAI;IACJ,IAAI,MAAM,cAAc,CAAC,QAAQ,EAAE;IACnC;IACA,IAAI;IACJ,IAAI,MAAM,eAAe,GAAG;IAC5B,QAAQ,OAAO,EAAE,MAAM,EAAE,CAAC,EAAE,cAAc,EAAE,CAAC,EAAE;IAC/C,IAAI;IACJ;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"plugin.js","sources":["esm/index.js","esm/web.js"],"sourcesContent":["import { registerPlugin } from '@capacitor/core';\nconst LiquidGlass = registerPlugin('LiquidGlass', {\n web: () => import('./web').then((m) => new m.LiquidGlassWeb()),\n});\nexport * from './definitions';\nexport { LiquidGlass };\n","import { WebPlugin } from '@capacitor/core';\n/**\n * Web fallback — real Liquid Glass requires native iOS 26. On the web we\n * resolve no-ops so the app can run in the browser during development; the\n * Angular shell is expected to render its own CSS glassmorphism tab bar when\n * `isNativePlatform()` is false.\n */\nexport class LiquidGlassWeb extends WebPlugin {\n async showTabBar(_options) {\n // no-op on web\n }\n async hideTabBar() {\n // no-op on web\n }\n async setSelectedTab(_options) {\n // no-op on web\n }\n async updateTabBadge(_options) {\n // no-op on web\n }\n async getTabBarLayout() {\n return { height: 0, bottomSafeArea: 0 };\n }\n async showSearchBar(_options) {\n // no-op on web — caller should render its own DOM search input.\n }\n async hideSearchBar() {\n // no-op on web\n }\n async clearSearchText() {\n // no-op on web\n }\n}\n"],"names":["registerPlugin","WebPlugin"],"mappings":";;;AACK,UAAC,WAAW,GAAGA,mBAAc,CAAC,aAAa,EAAE;IAClD,IAAI,GAAG,EAAE,MAAM,mDAAe,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,cAAc,EAAE,CAAC;IAClE,CAAC;;ICFD;IACA;IACA;IACA;IACA;IACA;IACO,MAAM,cAAc,SAASC,cAAS,CAAC;IAC9C,IAAI,MAAM,UAAU,CAAC,QAAQ,EAAE;IAC/B;IACA,IAAI;IACJ,IAAI,MAAM,UAAU,GAAG;IACvB;IACA,IAAI;IACJ,IAAI,MAAM,cAAc,CAAC,QAAQ,EAAE;IACnC;IACA,IAAI;IACJ,IAAI,MAAM,cAAc,CAAC,QAAQ,EAAE;IACnC;IACA,IAAI;IACJ,IAAI,MAAM,eAAe,GAAG;IAC5B,QAAQ,OAAO,EAAE,MAAM,EAAE,CAAC,EAAE,cAAc,EAAE,CAAC,EAAE;IAC/C,IAAI;IACJ,IAAI,MAAM,aAAa,CAAC,QAAQ,EAAE;IAClC;IACA,IAAI;IACJ,IAAI,MAAM,aAAa,GAAG;IAC1B;IACA,IAAI;IACJ,IAAI,MAAM,eAAe,GAAG;IAC5B;IACA,IAAI;IACJ;;;;;;;;;;;;;;;"}
@@ -12,9 +12,13 @@ public class LiquidGlassPlugin: CAPPlugin, CAPBridgedPlugin {
12
12
  CAPPluginMethod(name: "setSelectedTab", returnType: CAPPluginReturnPromise),
13
13
  CAPPluginMethod(name: "updateTabBadge", returnType: CAPPluginReturnPromise),
14
14
  CAPPluginMethod(name: "getTabBarLayout", returnType: CAPPluginReturnPromise),
15
+ CAPPluginMethod(name: "showSearchBar", returnType: CAPPluginReturnPromise),
16
+ CAPPluginMethod(name: "hideSearchBar", returnType: CAPPluginReturnPromise),
17
+ CAPPluginMethod(name: "clearSearchText", returnType: CAPPluginReturnPromise),
15
18
  ]
16
19
 
17
20
  private var tabBarOverlay: LiquidGlassTabBarOverlay?
21
+ private var searchOverlay: LiquidGlassSearchOverlay?
18
22
 
19
23
  @objc func showTabBar(_ call: CAPPluginCall) {
20
24
  guard let rawItems = call.getArray("items") as? [[String: Any]] else {
@@ -86,6 +90,67 @@ public class LiquidGlassPlugin: CAPPlugin, CAPBridgedPlugin {
86
90
  }
87
91
  }
88
92
 
93
+ // MARK: - Search Bar
94
+
95
+ @objc func showSearchBar(_ call: CAPPluginCall) {
96
+ let placeholder = call.getString("placeholder")
97
+ let initialText = call.getString("initialText")
98
+ let cancelText = call.getString("cancelText")
99
+ let tintHex = call.getString("tintColor")
100
+ let hideCancelButton = call.getBool("hideCancelButton") ?? false
101
+
102
+ DispatchQueue.main.async { [weak self] in
103
+ guard let self else { return }
104
+ self.presentSearchBar(
105
+ placeholder: placeholder,
106
+ initialText: initialText,
107
+ cancelText: cancelText,
108
+ tintHex: tintHex,
109
+ hideCancelButton: hideCancelButton
110
+ )
111
+ call.resolve()
112
+ }
113
+ }
114
+
115
+ @objc func hideSearchBar(_ call: CAPPluginCall) {
116
+ DispatchQueue.main.async { [weak self] in
117
+ self?.searchOverlay?.hide()
118
+ call.resolve()
119
+ }
120
+ }
121
+
122
+ @objc func clearSearchText(_ call: CAPPluginCall) {
123
+ DispatchQueue.main.async { [weak self] in
124
+ self?.searchOverlay?.clearText()
125
+ call.resolve()
126
+ }
127
+ }
128
+
129
+ private func presentSearchBar(
130
+ placeholder: String?,
131
+ initialText: String?,
132
+ cancelText: String?,
133
+ tintHex: String?,
134
+ hideCancelButton: Bool
135
+ ) {
136
+ guard let window = UIApplication.shared.capacitorWindow else { return }
137
+
138
+ if searchOverlay == nil {
139
+ let overlay = LiquidGlassSearchOverlay()
140
+ overlay.delegate = self
141
+ searchOverlay = overlay
142
+ }
143
+
144
+ searchOverlay?.configure(
145
+ placeholder: placeholder,
146
+ initialText: initialText,
147
+ cancelText: cancelText,
148
+ tintHex: tintHex,
149
+ hideCancelButton: hideCancelButton
150
+ )
151
+ searchOverlay?.show(on: window)
152
+ }
153
+
89
154
  private func presentTabBar(items: [LiquidGlassTabItem], selectedIndex: Int, tintHex: String?, styleRaw: String) {
90
155
  guard let window = UIApplication.shared.capacitorWindow else { return }
91
156
 
@@ -110,6 +175,21 @@ public class LiquidGlassPlugin: CAPPlugin, CAPBridgedPlugin {
110
175
  }
111
176
  }
112
177
 
178
+ // MARK: - LiquidGlassSearchOverlayDelegate
179
+ extension LiquidGlassPlugin: LiquidGlassSearchOverlayDelegate {
180
+ func searchOverlayDidChangeText(_ text: String) {
181
+ notifyListeners("searchTextChanged", data: ["text": text])
182
+ }
183
+
184
+ func searchOverlayDidSubmit(_ text: String) {
185
+ notifyListeners("searchSubmitted", data: ["text": text])
186
+ }
187
+
188
+ func searchOverlayDidCancel() {
189
+ notifyListeners("searchCancelled", data: [:])
190
+ }
191
+ }
192
+
113
193
  // MARK: - UIApplication helper
114
194
  private extension UIApplication {
115
195
  var capacitorWindow: UIWindow? {
@@ -0,0 +1,172 @@
1
+ import UIKit
2
+
3
+ /// Callbacks emitted from the search overlay back to the plugin so it can
4
+ /// `notifyListeners(...)` to JS land.
5
+ protocol LiquidGlassSearchOverlayDelegate: AnyObject {
6
+ func searchOverlayDidChangeText(_ text: String)
7
+ func searchOverlayDidSubmit(_ text: String)
8
+ func searchOverlayDidCancel()
9
+ }
10
+
11
+ /// A floating UISearchBar overlay wrapped in a UIVisualEffectView so it
12
+ /// inherits the iOS 26 Liquid Glass look (and falls back to
13
+ /// `systemUltraThinMaterial` blur on earlier iOS).
14
+ ///
15
+ /// Auto-layout pin: top of the keyWindow, respecting the safe area, full width.
16
+ final class LiquidGlassSearchOverlay: NSObject {
17
+
18
+ weak var delegate: LiquidGlassSearchOverlayDelegate?
19
+
20
+ // Internal state
21
+ private weak var window: UIWindow?
22
+ private var container: UIVisualEffectView?
23
+ private var searchBar: UISearchBar?
24
+
25
+ // Cached config (so re-configures persist across show/hide)
26
+ private var placeholder: String?
27
+ private var initialText: String?
28
+ private var cancelText: String?
29
+ private var tintHex: String?
30
+ private var hideCancelButton: Bool = false
31
+
32
+ /// Stores the configuration; safe to call before `show()`.
33
+ func configure(
34
+ placeholder: String?,
35
+ initialText: String?,
36
+ cancelText: String?,
37
+ tintHex: String?,
38
+ hideCancelButton: Bool
39
+ ) {
40
+ self.placeholder = placeholder
41
+ self.initialText = initialText
42
+ self.cancelText = cancelText
43
+ self.tintHex = tintHex
44
+ self.hideCancelButton = hideCancelButton
45
+ // If already attached, push config to live UI right away.
46
+ applyConfigToSearchBar()
47
+ }
48
+
49
+ /// Attach + show the overlay on the given window. Idempotent: re-attaching
50
+ /// to the same window is a no-op (just re-applies the config + first responder).
51
+ func show(on window: UIWindow) {
52
+ if self.window === window, container != nil {
53
+ applyConfigToSearchBar()
54
+ container?.isHidden = false
55
+ searchBar?.becomeFirstResponder()
56
+ return
57
+ }
58
+ self.window = window
59
+
60
+ // Container with blur — auto-becomes Liquid Glass on iOS 26 SDK.
61
+ let blur = UIBlurEffect(style: .systemUltraThinMaterial)
62
+ let container = UIVisualEffectView(effect: blur)
63
+ container.translatesAutoresizingMaskIntoConstraints = false
64
+
65
+ let searchBar = UISearchBar()
66
+ searchBar.translatesAutoresizingMaskIntoConstraints = false
67
+ searchBar.delegate = self
68
+ searchBar.searchBarStyle = .minimal
69
+ searchBar.backgroundImage = UIImage() // strip default chrome so blur shows through
70
+ searchBar.autocorrectionType = .no
71
+ searchBar.autocapitalizationType = .none
72
+ searchBar.returnKeyType = .search
73
+
74
+ container.contentView.addSubview(searchBar)
75
+ window.addSubview(container)
76
+
77
+ NSLayoutConstraint.activate([
78
+ container.leadingAnchor.constraint(equalTo: window.leadingAnchor),
79
+ container.trailingAnchor.constraint(equalTo: window.trailingAnchor),
80
+ container.topAnchor.constraint(equalTo: window.topAnchor),
81
+
82
+ searchBar.leadingAnchor.constraint(equalTo: container.contentView.leadingAnchor, constant: 8),
83
+ searchBar.trailingAnchor.constraint(equalTo: container.contentView.trailingAnchor, constant: -8),
84
+ searchBar.topAnchor.constraint(equalTo: window.safeAreaLayoutGuide.topAnchor),
85
+ searchBar.bottomAnchor.constraint(equalTo: container.contentView.bottomAnchor, constant: -8),
86
+ ])
87
+
88
+ self.container = container
89
+ self.searchBar = searchBar
90
+
91
+ applyConfigToSearchBar()
92
+ searchBar.becomeFirstResponder()
93
+ }
94
+
95
+ /// Hide overlay (don't tear down — keep config so next `show()` is fast).
96
+ func hide() {
97
+ searchBar?.resignFirstResponder()
98
+ UIView.animate(withDuration: 0.18, animations: { [weak self] in
99
+ self?.container?.alpha = 0
100
+ }, completion: { [weak self] _ in
101
+ self?.container?.isHidden = true
102
+ self?.container?.alpha = 1
103
+ })
104
+ }
105
+
106
+ /// Clear text without dismissing the overlay.
107
+ func clearText() {
108
+ searchBar?.text = ""
109
+ // Notify delegate so JS land sees the empty value.
110
+ delegate?.searchOverlayDidChangeText("")
111
+ }
112
+
113
+ // MARK: - Private
114
+
115
+ private func applyConfigToSearchBar() {
116
+ guard let searchBar else { return }
117
+ searchBar.placeholder = placeholder
118
+ if let initialText { searchBar.text = initialText }
119
+ searchBar.showsCancelButton = !hideCancelButton
120
+ if let tintHex, let tint = UIColor(searchBarHex: tintHex) {
121
+ searchBar.tintColor = tint
122
+ }
123
+ // Override the system "Cancel" label if a custom one was provided.
124
+ if let cancelText, !hideCancelButton {
125
+ // Walk subviews to find the cancel button (UIKit private path is
126
+ // historically how this is done; safe enough — falls through to no-op
127
+ // if the button can't be located on a future iOS).
128
+ if let button = findCancelButton(in: searchBar) {
129
+ button.setTitle(cancelText, for: .normal)
130
+ }
131
+ }
132
+ }
133
+
134
+ private func findCancelButton(in view: UIView) -> UIButton? {
135
+ for sub in view.subviews {
136
+ if let btn = sub as? UIButton { return btn }
137
+ if let nested = findCancelButton(in: sub) { return nested }
138
+ }
139
+ return nil
140
+ }
141
+ }
142
+
143
+ // MARK: - UISearchBarDelegate
144
+ extension LiquidGlassSearchOverlay: UISearchBarDelegate {
145
+ func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
146
+ delegate?.searchOverlayDidChangeText(searchText)
147
+ }
148
+
149
+ func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
150
+ delegate?.searchOverlayDidSubmit(searchBar.text ?? "")
151
+ searchBar.resignFirstResponder()
152
+ }
153
+
154
+ func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
155
+ searchBar.text = ""
156
+ searchBar.resignFirstResponder()
157
+ delegate?.searchOverlayDidCancel()
158
+ }
159
+ }
160
+
161
+ // MARK: - UIColor+hex (scoped to this file to avoid duplicate-symbol with TabBarOverlay)
162
+ private extension UIColor {
163
+ convenience init?(searchBarHex hex: String) {
164
+ var value = hex.trimmingCharacters(in: .whitespacesAndNewlines).uppercased()
165
+ if value.hasPrefix("#") { value.removeFirst() }
166
+ guard value.count == 6, let rgb = UInt64(value, radix: 16) else { return nil }
167
+ let r = CGFloat((rgb & 0xFF0000) >> 16) / 255
168
+ let g = CGFloat((rgb & 0x00FF00) >> 8) / 255
169
+ let b = CGFloat(rgb & 0x0000FF) / 255
170
+ self.init(red: r, green: g, blue: b, alpha: 1.0)
171
+ }
172
+ }
@@ -29,6 +29,9 @@ enum LiquidGlassTabBarStyle: String {
29
29
  case ultraThin
30
30
  /// No background, no blur — content behind shows through 100%.
31
31
  case transparent
32
+ /// REAL iOS 26 `UIGlassEffect` (Music + App Store material).
33
+ /// Fallback en iOS < 26 → `UIBlurEffect.systemThinMaterial`.
34
+ case liquidGlass
32
35
  }
33
36
 
34
37
  /// A floating UITabBar overlay that adopts iOS 26 Liquid Glass automatically
@@ -87,6 +90,23 @@ final class LiquidGlassTabBarOverlay: NSObject {
87
90
  appearance.backgroundEffect = nil
88
91
  appearance.backgroundColor = UIColor.clear
89
92
  appearance.shadowColor = UIColor.clear
93
+ case .liquidGlass:
94
+ // REAL iOS 26 Liquid Glass — el mismo material que usan Music y
95
+ // App Store. `UIGlassEffect` solo está disponible compilando con
96
+ // el SDK iOS 26+ (Xcode 26+). En iOS < 26 runtime cae a
97
+ // `systemThinMaterial`, que es la mejor aproximación legacy con
98
+ // vibrancy del sistema.
99
+ appearance.configureWithDefaultBackground()
100
+ #if compiler(>=6.1)
101
+ if #available(iOS 26.0, *) {
102
+ appearance.backgroundEffect = UIGlassEffect()
103
+ } else {
104
+ appearance.backgroundEffect = UIBlurEffect(style: .systemThinMaterial)
105
+ }
106
+ #else
107
+ appearance.backgroundEffect = UIBlurEffect(style: .systemThinMaterial)
108
+ #endif
109
+ appearance.backgroundColor = UIColor.clear
90
110
  }
91
111
  tabBar.standardAppearance = appearance
92
112
  if #available(iOS 15.0, *) {
@@ -121,9 +141,13 @@ final class LiquidGlassTabBarOverlay: NSObject {
121
141
 
122
142
  tabBar.setItems(uiItems, animated: false)
123
143
 
124
- if let previousSelectedId, let restoreIdx = items.firstIndex(where: { $0.id == previousSelectedId }) {
144
+ // Si el caller pidió explícitamente "no selection" (selectedIndex < 0),
145
+ // respetar eso y NO restaurar la selección previa — deja selectedItem = nil.
146
+ if selectedIndex < 0 {
147
+ tabBar.selectedItem = nil
148
+ } else if let previousSelectedId, let restoreIdx = items.firstIndex(where: { $0.id == previousSelectedId }) {
125
149
  tabBar.selectedItem = uiItems[restoreIdx]
126
- } else if selectedIndex >= 0, selectedIndex < uiItems.count {
150
+ } else if selectedIndex < uiItems.count {
127
151
  tabBar.selectedItem = uiItems[selectedIndex]
128
152
  }
129
153
 
@@ -185,7 +209,13 @@ final class LiquidGlassTabBarOverlay: NSObject {
185
209
  }
186
210
 
187
211
  func setSelectedIndex(_ index: Int) {
188
- guard let tabBar, let items = tabBar.items, index >= 0, index < items.count else { return }
212
+ guard let tabBar, let items = tabBar.items else { return }
213
+ // index < 0 → deseleccionar todos los tabs (selectedItem = nil)
214
+ if index < 0 {
215
+ tabBar.selectedItem = nil
216
+ return
217
+ }
218
+ guard index < items.count else { return }
189
219
  tabBar.selectedItem = items[index]
190
220
  }
191
221
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ajuarezso/capacitor-liquid-glass",
3
- "version": "0.1.1",
3
+ "version": "0.3.0",
4
4
  "description": "iOS 26 Liquid Glass native chrome (TabBar, NavigationBar, Alerts, Sheets) for Capacitor apps. Falls back gracefully on iOS < 26 and Android.",
5
5
  "main": "dist/plugin.cjs.js",
6
6
  "module": "dist/esm/index.js",