@ajuarezso/capacitor-liquid-glass 0.1.0 → 0.2.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.
@@ -9,12 +9,23 @@ export interface LiquidGlassTabItem {
9
9
  /** Optional badge value (e.g. "3" or "•"). */
10
10
  badge?: string;
11
11
  }
12
+ /**
13
+ * Visual style of the tab bar background.
14
+ * - `'default'` (default): Liquid Glass on iOS 26+ (translucent blurred).
15
+ * - `'ultraThin'`: minimal blur (`UIBlurEffect.Style.systemUltraThinMaterial`),
16
+ * more see-through than default.
17
+ * - `'transparent'`: no background, no blur — content behind shows through 100%.
18
+ * Trade-off: legibilidad puede sufrir sobre contenido caótico.
19
+ */
20
+ export type TabBarStyle = 'default' | 'ultraThin' | 'transparent';
12
21
  export interface ShowTabBarOptions {
13
22
  items: LiquidGlassTabItem[];
14
23
  /** Index of the initially selected item. Defaults to 0. */
15
24
  selectedIndex?: number;
16
25
  /** Tint color for selected state, hex "#RRGGBB". Defaults to iOS system tint. */
17
26
  tintColor?: string;
27
+ /** Visual style of the tab bar background. Defaults to `'default'`. */
28
+ tabBarStyle?: TabBarStyle;
18
29
  }
19
30
  export interface SetSelectedTabOptions {
20
31
  /** Either pass numeric index or the item id. */
@@ -37,6 +48,33 @@ export interface TabBarLayoutEvent {
37
48
  /** Safe-area bottom inset the tab bar is sitting on (pt). */
38
49
  bottomSafeArea: number;
39
50
  }
51
+ /**
52
+ * Options for the native iOS Liquid Glass search bar overlay (top of window).
53
+ *
54
+ * On iOS 26+ the underlying `UISearchBar` adopts the system Liquid Glass look
55
+ * automatically; on earlier iOS we wrap it in a `systemUltraThinMaterial`
56
+ * blurred container so the visual is as close as possible.
57
+ */
58
+ export interface ShowSearchBarOptions {
59
+ /** Placeholder shown when the field is empty. */
60
+ placeholder?: string;
61
+ /** Initial text the field opens with. */
62
+ initialText?: string;
63
+ /** Custom label for the trailing "Cancel" button. */
64
+ cancelText?: string;
65
+ /** Tint color for the cursor + Cancel button, hex `"#RRGGBB"`. */
66
+ tintColor?: string;
67
+ /** Hide the trailing Cancel button (defaults to `false`). */
68
+ hideCancelButton?: boolean;
69
+ }
70
+ export interface SearchTextChangedEvent {
71
+ /** Current text in the search field. */
72
+ text: string;
73
+ }
74
+ export interface SearchSubmittedEvent {
75
+ /** Text the user submitted (return key on the keyboard). */
76
+ text: string;
77
+ }
40
78
  export interface LiquidGlassPlugin {
41
79
  /** Creates (or updates) and shows the native Liquid Glass tab bar. */
42
80
  showTabBar(options: ShowTabBarOptions): Promise<void>;
@@ -52,5 +90,17 @@ export interface LiquidGlassPlugin {
52
90
  addListener(eventName: 'tabSelected', listenerFunc: (event: TabSelectedEvent) => void): Promise<PluginListenerHandle>;
53
91
  /** Emitted when the tab bar's height or safe-area changes (rotation, etc.). */
54
92
  addListener(eventName: 'tabBarLayoutChanged', listenerFunc: (event: TabBarLayoutEvent) => void): Promise<PluginListenerHandle>;
93
+ /** Shows the native Liquid Glass search bar overlay anchored to the top. */
94
+ showSearchBar(options?: ShowSearchBarOptions): Promise<void>;
95
+ /** Hides the search bar without destroying configuration. */
96
+ hideSearchBar(): Promise<void>;
97
+ /** Clears the text in the search field without dismissing the overlay. */
98
+ clearSearchText(): Promise<void>;
99
+ /** Emitted on every keystroke while the user types in the search field. */
100
+ addListener(eventName: 'searchTextChanged', listenerFunc: (event: SearchTextChangedEvent) => void): Promise<PluginListenerHandle>;
101
+ /** Emitted when the user taps the keyboard's "Search" / return key. */
102
+ addListener(eventName: 'searchSubmitted', listenerFunc: (event: SearchSubmittedEvent) => void): Promise<PluginListenerHandle>;
103
+ /** Emitted when the user taps the trailing Cancel button. */
104
+ addListener(eventName: 'searchCancelled', listenerFunc: () => void): Promise<PluginListenerHandle>;
55
105
  removeAllListeners(): Promise<void>;
56
106
  }
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 {
@@ -23,6 +27,7 @@ public class LiquidGlassPlugin: CAPPlugin, CAPBridgedPlugin {
23
27
  }
24
28
  let selectedIndex = call.getInt("selectedIndex") ?? 0
25
29
  let tintHex = call.getString("tintColor")
30
+ let styleRaw = call.getString("tabBarStyle") ?? "default"
26
31
 
27
32
  let items = rawItems.compactMap { LiquidGlassTabItem(dictionary: $0) }
28
33
  guard !items.isEmpty else {
@@ -32,7 +37,7 @@ public class LiquidGlassPlugin: CAPPlugin, CAPBridgedPlugin {
32
37
 
33
38
  DispatchQueue.main.async { [weak self] in
34
39
  guard let self else { return }
35
- self.presentTabBar(items: items, selectedIndex: selectedIndex, tintHex: tintHex)
40
+ self.presentTabBar(items: items, selectedIndex: selectedIndex, tintHex: tintHex, styleRaw: styleRaw)
36
41
  call.resolve()
37
42
  }
38
43
  }
@@ -85,7 +90,68 @@ public class LiquidGlassPlugin: CAPPlugin, CAPBridgedPlugin {
85
90
  }
86
91
  }
87
92
 
88
- private func presentTabBar(items: [LiquidGlassTabItem], selectedIndex: Int, tintHex: String?) {
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
+
154
+ private func presentTabBar(items: [LiquidGlassTabItem], selectedIndex: Int, tintHex: String?, styleRaw: String) {
89
155
  guard let window = UIApplication.shared.capacitorWindow else { return }
90
156
 
91
157
  if tabBarOverlay == nil {
@@ -102,12 +168,28 @@ public class LiquidGlassPlugin: CAPPlugin, CAPBridgedPlugin {
102
168
  tabBarOverlay = overlay
103
169
  }
104
170
 
171
+ let style = LiquidGlassTabBarStyle(rawValue: styleRaw) ?? .default
105
172
  tabBarOverlay?.attach(to: window)
106
- tabBarOverlay?.configure(items: items, selectedIndex: selectedIndex, tintHex: tintHex)
173
+ tabBarOverlay?.configure(items: items, selectedIndex: selectedIndex, tintHex: tintHex, style: style)
107
174
  tabBarOverlay?.show()
108
175
  }
109
176
  }
110
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
+
111
193
  // MARK: - UIApplication helper
112
194
  private extension UIApplication {
113
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
+ }
@@ -21,6 +21,16 @@ struct LiquidGlassTabItem {
21
21
  }
22
22
  }
23
23
 
24
+ /// Visual style of the tab bar background.
25
+ enum LiquidGlassTabBarStyle: String {
26
+ /// Translucent blurred — auto-applies Liquid Glass on iOS 26+ SDK.
27
+ case `default`
28
+ /// Minimal blur (`systemUltraThinMaterial`) — more see-through than default.
29
+ case ultraThin
30
+ /// No background, no blur — content behind shows through 100%.
31
+ case transparent
32
+ }
33
+
24
34
  /// A floating UITabBar overlay that adopts iOS 26 Liquid Glass automatically
25
35
  /// when the app is built against the iOS 26 SDK. On earlier iOS the bar falls
26
36
  /// back to the translucent blurred UITabBar appearance.
@@ -43,13 +53,9 @@ final class LiquidGlassTabBarOverlay: NSObject {
43
53
  tabBar.translatesAutoresizingMaskIntoConstraints = false
44
54
  tabBar.delegate = self
45
55
 
46
- // Default appearance triggers Liquid Glass automatically on iOS 26.
47
- let appearance = UITabBarAppearance()
48
- appearance.configureWithDefaultBackground()
49
- tabBar.standardAppearance = appearance
50
- if #available(iOS 15.0, *) {
51
- tabBar.scrollEdgeAppearance = appearance
52
- }
56
+ // Default appearance: triggers Liquid Glass automatically on iOS 26+.
57
+ // (Sobreescrito en `configure(...)` si el caller pidió otro estilo).
58
+ applyAppearance(tabBar, style: .default)
53
59
 
54
60
  window.addSubview(tabBar)
55
61
 
@@ -63,7 +69,32 @@ final class LiquidGlassTabBarOverlay: NSObject {
63
69
  emitLayout()
64
70
  }
65
71
 
66
- func configure(items: [LiquidGlassTabItem], selectedIndex: Int, tintHex: String?) {
72
+ /// Aplica el appearance correspondiente al estilo solicitado.
73
+ /// - `default`: Liquid Glass (iOS 26+) o blur translúcido como fallback.
74
+ /// - `ultraThin`: blur mínimo `systemUltraThinMaterial` — más ver-through.
75
+ /// - `transparent`: sin background ni blur — content behind se ve completo.
76
+ private func applyAppearance(_ tabBar: UITabBar, style: LiquidGlassTabBarStyle) {
77
+ let appearance = UITabBarAppearance()
78
+ switch style {
79
+ case .default:
80
+ appearance.configureWithDefaultBackground()
81
+ case .ultraThin:
82
+ appearance.configureWithDefaultBackground()
83
+ appearance.backgroundEffect = UIBlurEffect(style: .systemUltraThinMaterial)
84
+ appearance.backgroundColor = UIColor.clear
85
+ case .transparent:
86
+ appearance.configureWithTransparentBackground()
87
+ appearance.backgroundEffect = nil
88
+ appearance.backgroundColor = UIColor.clear
89
+ appearance.shadowColor = UIColor.clear
90
+ }
91
+ tabBar.standardAppearance = appearance
92
+ if #available(iOS 15.0, *) {
93
+ tabBar.scrollEdgeAppearance = appearance
94
+ }
95
+ }
96
+
97
+ func configure(items: [LiquidGlassTabItem], selectedIndex: Int, tintHex: String?, style: LiquidGlassTabBarStyle) {
67
98
  guard let tabBar else { return }
68
99
 
69
100
  // Preserve selection across re-configs (badge changes, etc.)
@@ -73,6 +104,9 @@ final class LiquidGlassTabBarOverlay: NSObject {
73
104
 
74
105
  self.items = items
75
106
 
107
+ // Re-aplica appearance por si el caller cambió de estilo en runtime.
108
+ applyAppearance(tabBar, style: style)
109
+
76
110
  let uiItems: [UITabBarItem] = items.enumerated().map { index, item in
77
111
  let tab = UITabBarItem(
78
112
  title: item.label,
@@ -87,9 +121,13 @@ final class LiquidGlassTabBarOverlay: NSObject {
87
121
 
88
122
  tabBar.setItems(uiItems, animated: false)
89
123
 
90
- if let previousSelectedId, let restoreIdx = items.firstIndex(where: { $0.id == previousSelectedId }) {
124
+ // Si el caller pidió explícitamente "no selection" (selectedIndex < 0),
125
+ // respetar eso y NO restaurar la selección previa — deja selectedItem = nil.
126
+ if selectedIndex < 0 {
127
+ tabBar.selectedItem = nil
128
+ } else if let previousSelectedId, let restoreIdx = items.firstIndex(where: { $0.id == previousSelectedId }) {
91
129
  tabBar.selectedItem = uiItems[restoreIdx]
92
- } else if selectedIndex >= 0, selectedIndex < uiItems.count {
130
+ } else if selectedIndex < uiItems.count {
93
131
  tabBar.selectedItem = uiItems[selectedIndex]
94
132
  }
95
133
 
@@ -108,16 +146,56 @@ final class LiquidGlassTabBarOverlay: NSObject {
108
146
  }
109
147
 
110
148
  func show() {
111
- tabBar?.isHidden = false
149
+ guard let tabBar = tabBar else { return }
150
+ // Si ya está visible, no hacer nada (evita flicker en re-show consecutivos)
151
+ if !tabBar.isHidden && tabBar.alpha == 1.0 { emitLayout(); return }
152
+ // Reset estado pre-animación
153
+ tabBar.isHidden = false
154
+ tabBar.alpha = 0
155
+ tabBar.transform = CGAffineTransform(translationX: 0, y: 20)
156
+ // Fade-in + slide-up (curva native iOS para apariciones)
157
+ UIView.animate(
158
+ withDuration: 0.28,
159
+ delay: 0,
160
+ usingSpringWithDamping: 0.85,
161
+ initialSpringVelocity: 0,
162
+ options: [.curveEaseOut, .allowUserInteraction],
163
+ animations: {
164
+ tabBar.alpha = 1
165
+ tabBar.transform = .identity
166
+ },
167
+ completion: nil
168
+ )
112
169
  emitLayout()
113
170
  }
114
171
 
115
172
  func hide() {
116
- tabBar?.isHidden = true
173
+ guard let tabBar = tabBar else { return }
174
+ if tabBar.isHidden { return }
175
+ // Fade-out + slide-down (más rápido que el show — el exit es snappy)
176
+ UIView.animate(
177
+ withDuration: 0.18,
178
+ delay: 0,
179
+ options: [.curveEaseIn, .allowUserInteraction],
180
+ animations: {
181
+ tabBar.alpha = 0
182
+ tabBar.transform = CGAffineTransform(translationX: 0, y: 20)
183
+ },
184
+ completion: { _ in
185
+ tabBar.isHidden = true
186
+ tabBar.transform = .identity
187
+ }
188
+ )
117
189
  }
118
190
 
119
191
  func setSelectedIndex(_ index: Int) {
120
- guard let tabBar, let items = tabBar.items, index >= 0, index < items.count else { return }
192
+ guard let tabBar, let items = tabBar.items else { return }
193
+ // index < 0 → deseleccionar todos los tabs (selectedItem = nil)
194
+ if index < 0 {
195
+ tabBar.selectedItem = nil
196
+ return
197
+ }
198
+ guard index < items.count else { return }
121
199
  tabBar.selectedItem = items[index]
122
200
  }
123
201
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ajuarezso/capacitor-liquid-glass",
3
- "version": "0.1.0",
3
+ "version": "0.2.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",