@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.
- package/dist/esm/definitions.d.ts +43 -1
- package/dist/esm/web.d.ts +4 -1
- package/dist/esm/web.js +9 -0
- package/dist/plugin.cjs.js +9 -0
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +9 -0
- package/dist/plugin.js.map +1 -1
- package/ios/Sources/LiquidGlassPlugin/LiquidGlassPlugin.swift +80 -0
- package/ios/Sources/LiquidGlassPlugin/LiquidGlassSearchOverlay.swift +172 -0
- package/ios/Sources/LiquidGlassPlugin/LiquidGlassTabBarOverlay.swift +33 -3
- package/package.json +1 -1
|
@@ -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
|
}
|
package/dist/plugin.cjs.js
CHANGED
|
@@ -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({
|
package/dist/plugin.cjs.js.map
CHANGED
|
@@ -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({
|
package/dist/plugin.js.map
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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.
|
|
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",
|