@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.
- package/dist/esm/definitions.d.ts +50 -0
- 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 +85 -3
- package/ios/Sources/LiquidGlassPlugin/LiquidGlassSearchOverlay.swift +172 -0
- package/ios/Sources/LiquidGlassPlugin/LiquidGlassTabBarOverlay.swift +91 -13
- package/package.json +1 -1
|
@@ -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
|
}
|
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 {
|
|
@@ -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
|
-
|
|
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
|
-
|
|
48
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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",
|