@ajuarezso/capacitor-liquid-glass 0.1.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/CapacitorLiquidGlass.podspec +17 -0
- package/LICENSE +21 -0
- package/Package.swift +26 -0
- package/README.md +150 -0
- package/dist/esm/definitions.d.ts +56 -0
- package/dist/esm/definitions.js +1 -0
- package/dist/esm/index.d.ts +4 -0
- package/dist/esm/index.js +6 -0
- package/dist/esm/web.d.ts +15 -0
- package/dist/esm/web.js +24 -0
- package/dist/plugin.cjs.js +39 -0
- package/dist/plugin.cjs.js.map +1 -0
- package/dist/plugin.js +42 -0
- package/dist/plugin.js.map +1 -0
- package/ios/Sources/LiquidGlassPlugin/LiquidGlassPlugin.swift +123 -0
- package/ios/Sources/LiquidGlassPlugin/LiquidGlassTabBarOverlay.swift +162 -0
- package/package.json +58 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
|
|
3
|
+
package = JSON.parse(File.read(File.join(__dir__, 'package.json')))
|
|
4
|
+
|
|
5
|
+
Pod::Spec.new do |s|
|
|
6
|
+
s.name = 'CapacitorLiquidGlass'
|
|
7
|
+
s.version = package['version']
|
|
8
|
+
s.summary = package['description']
|
|
9
|
+
s.license = package['license']
|
|
10
|
+
s.homepage = 'https://github.com/anthonyjuarezsolis/capacitor-liquid-glass'
|
|
11
|
+
s.author = package['author']
|
|
12
|
+
s.source = { :git => 'https://github.com/anthonyjuarezsolis/capacitor-liquid-glass.git', :tag => s.version.to_s }
|
|
13
|
+
s.source_files = 'ios/Sources/**/*.{swift,h,m}'
|
|
14
|
+
s.ios.deployment_target = '15.0'
|
|
15
|
+
s.dependency 'Capacitor'
|
|
16
|
+
s.swift_version = '5.9'
|
|
17
|
+
end
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Anthony Juarez Solis
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/Package.swift
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// swift-tools-version: 5.9
|
|
2
|
+
|
|
3
|
+
import PackageDescription
|
|
4
|
+
|
|
5
|
+
let package = Package(
|
|
6
|
+
name: "AjuarezsoCapacitorLiquidGlass",
|
|
7
|
+
platforms: [.iOS(.v15)],
|
|
8
|
+
products: [
|
|
9
|
+
.library(
|
|
10
|
+
name: "AjuarezsoCapacitorLiquidGlass",
|
|
11
|
+
targets: ["LiquidGlassPlugin"])
|
|
12
|
+
],
|
|
13
|
+
dependencies: [
|
|
14
|
+
.package(url: "https://github.com/ionic-team/capacitor-swift-pm.git", from: "8.0.0")
|
|
15
|
+
],
|
|
16
|
+
targets: [
|
|
17
|
+
.target(
|
|
18
|
+
name: "LiquidGlassPlugin",
|
|
19
|
+
dependencies: [
|
|
20
|
+
.product(name: "Capacitor", package: "capacitor-swift-pm"),
|
|
21
|
+
.product(name: "Cordova", package: "capacitor-swift-pm"),
|
|
22
|
+
],
|
|
23
|
+
path: "ios/Sources/LiquidGlassPlugin"
|
|
24
|
+
)
|
|
25
|
+
]
|
|
26
|
+
)
|
package/README.md
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# @ajuarezso/capacitor-liquid-glass
|
|
2
|
+
|
|
3
|
+
Native iOS 26 **Liquid Glass** chrome (TabBar, NavigationBar, Alerts, Sheets, Menus) for Capacitor apps. Falls back gracefully on iOS < 26 and Android (no-op), so your app keeps its own CSS chrome on unsupported platforms.
|
|
4
|
+
|
|
5
|
+
This plugin exists because, as of 2026, no existing Capacitor plugin exposes the real iOS 26 `.glassEffect()` rendered by UIKit — only CSS `backdrop-filter` approximations. This plugin floats a real `UITabBar` (and friends) as a native overlay above your `WKWebView`, so you get the actual Apple-native morphing, refraction, and tint animations of Liquid Glass on iOS 26+.
|
|
6
|
+
|
|
7
|
+
> Status: `0.1.0` — **TabBar only**. NavigationBar, Toolbar, Sheet, Alert, Menu, Popover are on the roadmap.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install @ajuarezso/capacitor-liquid-glass
|
|
13
|
+
npx cap sync ios
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
iOS minimum target: `15.0`. Real Liquid Glass requires **iOS 26+**; on older versions the native `UITabBar` still renders but without the Liquid Glass effect.
|
|
17
|
+
|
|
18
|
+
## Quick start
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
import { LiquidGlass } from '@ajuarezso/capacitor-liquid-glass';
|
|
22
|
+
|
|
23
|
+
// Show the native tab bar
|
|
24
|
+
await LiquidGlass.showTabBar({
|
|
25
|
+
items: [
|
|
26
|
+
{ id: '/home', label: 'Home', sfSymbol: 'house' },
|
|
27
|
+
{ id: '/search', label: 'Search', sfSymbol: 'magnifyingglass' },
|
|
28
|
+
{ id: '/cart', label: 'Cart', sfSymbol: 'bag', badge: '3' },
|
|
29
|
+
{ id: '/profile', label: 'Profile', sfSymbol: 'person' },
|
|
30
|
+
],
|
|
31
|
+
selectedIndex: 0,
|
|
32
|
+
tintColor: '#FA7319',
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// Listen for taps
|
|
36
|
+
await LiquidGlass.addListener('tabSelected', ({ id, index }) => {
|
|
37
|
+
console.log('Tab tapped:', id, index);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// Update badge without rebuilding the tab bar
|
|
41
|
+
await LiquidGlass.updateTabBadge({ id: '/cart', badge: '5' });
|
|
42
|
+
|
|
43
|
+
// Programmatically change the selected tab
|
|
44
|
+
await LiquidGlass.setSelectedTab({ id: '/profile' });
|
|
45
|
+
|
|
46
|
+
// Hide (e.g. when opening a fullscreen modal)
|
|
47
|
+
await LiquidGlass.hideTabBar();
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## API
|
|
51
|
+
|
|
52
|
+
### `showTabBar(options)`
|
|
53
|
+
|
|
54
|
+
| Option | Type | Required | Description |
|
|
55
|
+
| --------------- | ----------------------- | -------- | ----------------------------------------------------------- |
|
|
56
|
+
| `items` | `LiquidGlassTabItem[]` | yes | At least one item. |
|
|
57
|
+
| `selectedIndex` | `number` | no | Initial selection. Defaults to `0`. |
|
|
58
|
+
| `tintColor` | `string` (`#RRGGBB`) | no | Color of the selected pill. Defaults to the system tint. |
|
|
59
|
+
|
|
60
|
+
### `LiquidGlassTabItem`
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
interface LiquidGlassTabItem {
|
|
64
|
+
/** Stable id emitted in `tabSelected` events. */
|
|
65
|
+
id: string;
|
|
66
|
+
/** Text under the icon. */
|
|
67
|
+
label: string;
|
|
68
|
+
/** SF Symbol name (e.g. 'house.fill'). */
|
|
69
|
+
sfSymbol: string;
|
|
70
|
+
/** Optional badge value (e.g. '3' or '•'). */
|
|
71
|
+
badge?: string;
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### `hideTabBar()`
|
|
76
|
+
|
|
77
|
+
Hides the tab bar without destroying it. Use this when opening fullscreen modals, maps, or any UI that should temporarily own the screen. Call `showTabBar(...)` again to restore it.
|
|
78
|
+
|
|
79
|
+
### `setSelectedTab({ index?, id? })`
|
|
80
|
+
|
|
81
|
+
Updates the selected tab programmatically. Useful when the user navigates via a deep link, a button inside a page, or any source other than the tab bar itself.
|
|
82
|
+
|
|
83
|
+
### `updateTabBadge({ id, badge })`
|
|
84
|
+
|
|
85
|
+
Updates a single tab's badge **without** reconfiguring the whole bar (preserves selection). Pass an empty string or omit `badge` to clear.
|
|
86
|
+
|
|
87
|
+
### `getTabBarLayout()`
|
|
88
|
+
|
|
89
|
+
Returns the current `{ height, bottomSafeArea }` in points so you can reserve content padding.
|
|
90
|
+
|
|
91
|
+
### Events
|
|
92
|
+
|
|
93
|
+
| Event | Payload |
|
|
94
|
+
| ---------------------- | ------------------------------------------- |
|
|
95
|
+
| `tabSelected` | `{ index: number, id: string }` |
|
|
96
|
+
| `tabBarLayoutChanged` | `{ height: number, bottomSafeArea: number }` |
|
|
97
|
+
|
|
98
|
+
## Angular example
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
import { bindLiquidGlassNav } from './liquid-glass-nav';
|
|
102
|
+
|
|
103
|
+
export class CustomerLayout {
|
|
104
|
+
private readonly nav = bindLiquidGlassNav({
|
|
105
|
+
items: [
|
|
106
|
+
{ id: '/home', label: 'Home', sfSymbol: 'house' },
|
|
107
|
+
{ id: '/orders', label: 'Orders', sfSymbol: 'list.bullet.clipboard' },
|
|
108
|
+
{ id: '/profile', label: 'Profile', sfSymbol: 'person' },
|
|
109
|
+
],
|
|
110
|
+
isFullscreen: this.isFullscreen, // signal<boolean>
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
readonly useNativeTabBar = this.nav.useNativeTabBar;
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
See the `example/` folder for a full wiring including router sync and HTML fallback for unsupported platforms.
|
|
118
|
+
|
|
119
|
+
## Platform behavior
|
|
120
|
+
|
|
121
|
+
| Widget | iOS 26+ | iOS 15–25 | Android | Web |
|
|
122
|
+
| ------------ | -------------------- | ------------------- | ------- | ---------- |
|
|
123
|
+
| TabBar | ✅ Liquid Glass real | ⚠️ Classic UITabBar | ❌ no-op | ❌ no-op |
|
|
124
|
+
|
|
125
|
+
When the plugin is a no-op (Android, Web, iOS < 26), render your own HTML / CSS fallback. The plugin exposes `Capacitor.getPlatform()` and your Angular / React code can conditionally swap to a `backdrop-filter` bar.
|
|
126
|
+
|
|
127
|
+
## Why not just CSS `backdrop-filter`?
|
|
128
|
+
|
|
129
|
+
Because it's not the same thing.
|
|
130
|
+
|
|
131
|
+
- **CSS `backdrop-filter`** gives you blur. That's it.
|
|
132
|
+
- **Liquid Glass** gives you blur + *dynamic refraction*, *edge highlights that shift with content*, *morphing between elements*, *touch-reactive deformation*, *tinted / clear variants*, and system-level Dynamic Island integration. These require private Metal shaders that Apple does not expose to `WKWebView`.
|
|
133
|
+
|
|
134
|
+
This plugin is the shortest path to Apple-authentic Liquid Glass from a Capacitor app.
|
|
135
|
+
|
|
136
|
+
## Roadmap
|
|
137
|
+
|
|
138
|
+
- [x] `TabBar`
|
|
139
|
+
- [ ] `NavigationBar` (large title, leading/trailing items)
|
|
140
|
+
- [ ] `Toolbar` (floating toolbar like Safari)
|
|
141
|
+
- [ ] `Alert` (standard and destructive)
|
|
142
|
+
- [ ] `Sheet` (with detents)
|
|
143
|
+
- [ ] `Menu` / `Popover`
|
|
144
|
+
- [ ] Android Material 3 Expressive equivalents
|
|
145
|
+
|
|
146
|
+
PRs welcome.
|
|
147
|
+
|
|
148
|
+
## License
|
|
149
|
+
|
|
150
|
+
MIT © Anthony Juarez Solis
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { PluginListenerHandle } from '@capacitor/core';
|
|
2
|
+
export interface LiquidGlassTabItem {
|
|
3
|
+
/** Stable id used in tab selection events. */
|
|
4
|
+
id: string;
|
|
5
|
+
/** Label rendered under the icon. */
|
|
6
|
+
label: string;
|
|
7
|
+
/** SF Symbol name for iOS (e.g. "house.fill"). */
|
|
8
|
+
sfSymbol: string;
|
|
9
|
+
/** Optional badge value (e.g. "3" or "•"). */
|
|
10
|
+
badge?: string;
|
|
11
|
+
}
|
|
12
|
+
export interface ShowTabBarOptions {
|
|
13
|
+
items: LiquidGlassTabItem[];
|
|
14
|
+
/** Index of the initially selected item. Defaults to 0. */
|
|
15
|
+
selectedIndex?: number;
|
|
16
|
+
/** Tint color for selected state, hex "#RRGGBB". Defaults to iOS system tint. */
|
|
17
|
+
tintColor?: string;
|
|
18
|
+
}
|
|
19
|
+
export interface SetSelectedTabOptions {
|
|
20
|
+
/** Either pass numeric index or the item id. */
|
|
21
|
+
index?: number;
|
|
22
|
+
id?: string;
|
|
23
|
+
}
|
|
24
|
+
export interface UpdateTabBadgeOptions {
|
|
25
|
+
/** Tab item id whose badge should change. */
|
|
26
|
+
id: string;
|
|
27
|
+
/** New badge value; pass empty string or omit to clear it. */
|
|
28
|
+
badge?: string;
|
|
29
|
+
}
|
|
30
|
+
export interface TabSelectedEvent {
|
|
31
|
+
index: number;
|
|
32
|
+
id: string;
|
|
33
|
+
}
|
|
34
|
+
export interface TabBarLayoutEvent {
|
|
35
|
+
/** Total height of the tab bar including internal padding (pt). */
|
|
36
|
+
height: number;
|
|
37
|
+
/** Safe-area bottom inset the tab bar is sitting on (pt). */
|
|
38
|
+
bottomSafeArea: number;
|
|
39
|
+
}
|
|
40
|
+
export interface LiquidGlassPlugin {
|
|
41
|
+
/** Creates (or updates) and shows the native Liquid Glass tab bar. */
|
|
42
|
+
showTabBar(options: ShowTabBarOptions): Promise<void>;
|
|
43
|
+
/** Hides the tab bar without destroying configuration. */
|
|
44
|
+
hideTabBar(): Promise<void>;
|
|
45
|
+
/** Updates the currently selected tab. */
|
|
46
|
+
setSelectedTab(options: SetSelectedTabOptions): Promise<void>;
|
|
47
|
+
/** Updates a single tab's badge without reconfiguring the whole bar. */
|
|
48
|
+
updateTabBadge(options: UpdateTabBadgeOptions): Promise<void>;
|
|
49
|
+
/** Current layout of the tab bar (height + safe area). */
|
|
50
|
+
getTabBarLayout(): Promise<TabBarLayoutEvent>;
|
|
51
|
+
/** Emitted every time the user taps a tab. */
|
|
52
|
+
addListener(eventName: 'tabSelected', listenerFunc: (event: TabSelectedEvent) => void): Promise<PluginListenerHandle>;
|
|
53
|
+
/** Emitted when the tab bar's height or safe-area changes (rotation, etc.). */
|
|
54
|
+
addListener(eventName: 'tabBarLayoutChanged', listenerFunc: (event: TabBarLayoutEvent) => void): Promise<PluginListenerHandle>;
|
|
55
|
+
removeAllListeners(): Promise<void>;
|
|
56
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { WebPlugin } from '@capacitor/core';
|
|
2
|
+
import type { LiquidGlassPlugin, SetSelectedTabOptions, ShowTabBarOptions, TabBarLayoutEvent, UpdateTabBadgeOptions } from './definitions';
|
|
3
|
+
/**
|
|
4
|
+
* Web fallback — real Liquid Glass requires native iOS 26. On the web we
|
|
5
|
+
* resolve no-ops so the app can run in the browser during development; the
|
|
6
|
+
* Angular shell is expected to render its own CSS glassmorphism tab bar when
|
|
7
|
+
* `isNativePlatform()` is false.
|
|
8
|
+
*/
|
|
9
|
+
export declare class LiquidGlassWeb extends WebPlugin implements LiquidGlassPlugin {
|
|
10
|
+
showTabBar(_options: ShowTabBarOptions): Promise<void>;
|
|
11
|
+
hideTabBar(): Promise<void>;
|
|
12
|
+
setSelectedTab(_options: SetSelectedTabOptions): Promise<void>;
|
|
13
|
+
updateTabBadge(_options: UpdateTabBadgeOptions): Promise<void>;
|
|
14
|
+
getTabBarLayout(): Promise<TabBarLayoutEvent>;
|
|
15
|
+
}
|
package/dist/esm/web.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { WebPlugin } from '@capacitor/core';
|
|
2
|
+
/**
|
|
3
|
+
* Web fallback — real Liquid Glass requires native iOS 26. On the web we
|
|
4
|
+
* resolve no-ops so the app can run in the browser during development; the
|
|
5
|
+
* Angular shell is expected to render its own CSS glassmorphism tab bar when
|
|
6
|
+
* `isNativePlatform()` is false.
|
|
7
|
+
*/
|
|
8
|
+
export class LiquidGlassWeb extends WebPlugin {
|
|
9
|
+
async showTabBar(_options) {
|
|
10
|
+
// no-op on web
|
|
11
|
+
}
|
|
12
|
+
async hideTabBar() {
|
|
13
|
+
// no-op on web
|
|
14
|
+
}
|
|
15
|
+
async setSelectedTab(_options) {
|
|
16
|
+
// no-op on web
|
|
17
|
+
}
|
|
18
|
+
async updateTabBadge(_options) {
|
|
19
|
+
// no-op on web
|
|
20
|
+
}
|
|
21
|
+
async getTabBarLayout() {
|
|
22
|
+
return { height: 0, bottomSafeArea: 0 };
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var core = require('@capacitor/core');
|
|
4
|
+
|
|
5
|
+
const LiquidGlass = core.registerPlugin('LiquidGlass', {
|
|
6
|
+
web: () => Promise.resolve().then(function () { return web; }).then((m) => new m.LiquidGlassWeb()),
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Web fallback — real Liquid Glass requires native iOS 26. On the web we
|
|
11
|
+
* resolve no-ops so the app can run in the browser during development; the
|
|
12
|
+
* Angular shell is expected to render its own CSS glassmorphism tab bar when
|
|
13
|
+
* `isNativePlatform()` is false.
|
|
14
|
+
*/
|
|
15
|
+
class LiquidGlassWeb extends core.WebPlugin {
|
|
16
|
+
async showTabBar(_options) {
|
|
17
|
+
// no-op on web
|
|
18
|
+
}
|
|
19
|
+
async hideTabBar() {
|
|
20
|
+
// no-op on web
|
|
21
|
+
}
|
|
22
|
+
async setSelectedTab(_options) {
|
|
23
|
+
// no-op on web
|
|
24
|
+
}
|
|
25
|
+
async updateTabBadge(_options) {
|
|
26
|
+
// no-op on web
|
|
27
|
+
}
|
|
28
|
+
async getTabBarLayout() {
|
|
29
|
+
return { height: 0, bottomSafeArea: 0 };
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
var web = /*#__PURE__*/Object.freeze({
|
|
34
|
+
__proto__: null,
|
|
35
|
+
LiquidGlassWeb: LiquidGlassWeb
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
exports.LiquidGlass = LiquidGlass;
|
|
39
|
+
//# sourceMappingURL=plugin.cjs.js.map
|
|
@@ -0,0 +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;;;;;;;;;"}
|
package/dist/plugin.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
var capacitorLiquidGlass = (function (exports, core) {
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const LiquidGlass = core.registerPlugin('LiquidGlass', {
|
|
5
|
+
web: () => Promise.resolve().then(function () { return web; }).then((m) => new m.LiquidGlassWeb()),
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Web fallback — real Liquid Glass requires native iOS 26. On the web we
|
|
10
|
+
* resolve no-ops so the app can run in the browser during development; the
|
|
11
|
+
* Angular shell is expected to render its own CSS glassmorphism tab bar when
|
|
12
|
+
* `isNativePlatform()` is false.
|
|
13
|
+
*/
|
|
14
|
+
class LiquidGlassWeb extends core.WebPlugin {
|
|
15
|
+
async showTabBar(_options) {
|
|
16
|
+
// no-op on web
|
|
17
|
+
}
|
|
18
|
+
async hideTabBar() {
|
|
19
|
+
// no-op on web
|
|
20
|
+
}
|
|
21
|
+
async setSelectedTab(_options) {
|
|
22
|
+
// no-op on web
|
|
23
|
+
}
|
|
24
|
+
async updateTabBadge(_options) {
|
|
25
|
+
// no-op on web
|
|
26
|
+
}
|
|
27
|
+
async getTabBarLayout() {
|
|
28
|
+
return { height: 0, bottomSafeArea: 0 };
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
var web = /*#__PURE__*/Object.freeze({
|
|
33
|
+
__proto__: null,
|
|
34
|
+
LiquidGlassWeb: LiquidGlassWeb
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
exports.LiquidGlass = LiquidGlass;
|
|
38
|
+
|
|
39
|
+
return exports;
|
|
40
|
+
|
|
41
|
+
})({}, capacitorExports);
|
|
42
|
+
//# sourceMappingURL=plugin.js.map
|
|
@@ -0,0 +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;;;;;;;;;;;;;;;"}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import Capacitor
|
|
3
|
+
import UIKit
|
|
4
|
+
|
|
5
|
+
@objc(LiquidGlassPlugin)
|
|
6
|
+
public class LiquidGlassPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
7
|
+
public let identifier = "LiquidGlassPlugin"
|
|
8
|
+
public let jsName = "LiquidGlass"
|
|
9
|
+
public let pluginMethods: [CAPPluginMethod] = [
|
|
10
|
+
CAPPluginMethod(name: "showTabBar", returnType: CAPPluginReturnPromise),
|
|
11
|
+
CAPPluginMethod(name: "hideTabBar", returnType: CAPPluginReturnPromise),
|
|
12
|
+
CAPPluginMethod(name: "setSelectedTab", returnType: CAPPluginReturnPromise),
|
|
13
|
+
CAPPluginMethod(name: "updateTabBadge", returnType: CAPPluginReturnPromise),
|
|
14
|
+
CAPPluginMethod(name: "getTabBarLayout", returnType: CAPPluginReturnPromise),
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
private var tabBarOverlay: LiquidGlassTabBarOverlay?
|
|
18
|
+
|
|
19
|
+
@objc func showTabBar(_ call: CAPPluginCall) {
|
|
20
|
+
guard let rawItems = call.getArray("items") as? [[String: Any]] else {
|
|
21
|
+
call.reject("items is required")
|
|
22
|
+
return
|
|
23
|
+
}
|
|
24
|
+
let selectedIndex = call.getInt("selectedIndex") ?? 0
|
|
25
|
+
let tintHex = call.getString("tintColor")
|
|
26
|
+
|
|
27
|
+
let items = rawItems.compactMap { LiquidGlassTabItem(dictionary: $0) }
|
|
28
|
+
guard !items.isEmpty else {
|
|
29
|
+
call.reject("items cannot be empty")
|
|
30
|
+
return
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
DispatchQueue.main.async { [weak self] in
|
|
34
|
+
guard let self else { return }
|
|
35
|
+
self.presentTabBar(items: items, selectedIndex: selectedIndex, tintHex: tintHex)
|
|
36
|
+
call.resolve()
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
@objc func hideTabBar(_ call: CAPPluginCall) {
|
|
41
|
+
DispatchQueue.main.async { [weak self] in
|
|
42
|
+
self?.tabBarOverlay?.hide()
|
|
43
|
+
call.resolve()
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
@objc func updateTabBadge(_ call: CAPPluginCall) {
|
|
48
|
+
guard let id = call.getString("id") else {
|
|
49
|
+
call.reject("id is required")
|
|
50
|
+
return
|
|
51
|
+
}
|
|
52
|
+
let badge = call.getString("badge")
|
|
53
|
+
|
|
54
|
+
DispatchQueue.main.async { [weak self] in
|
|
55
|
+
self?.tabBarOverlay?.updateBadge(id: id, badge: badge)
|
|
56
|
+
call.resolve()
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
@objc func setSelectedTab(_ call: CAPPluginCall) {
|
|
61
|
+
let index = call.getInt("index")
|
|
62
|
+
let id = call.getString("id")
|
|
63
|
+
|
|
64
|
+
DispatchQueue.main.async { [weak self] in
|
|
65
|
+
guard let overlay = self?.tabBarOverlay else {
|
|
66
|
+
call.reject("tab bar is not shown")
|
|
67
|
+
return
|
|
68
|
+
}
|
|
69
|
+
if let index {
|
|
70
|
+
overlay.setSelectedIndex(index)
|
|
71
|
+
} else if let id {
|
|
72
|
+
overlay.setSelected(id: id)
|
|
73
|
+
}
|
|
74
|
+
call.resolve()
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
@objc func getTabBarLayout(_ call: CAPPluginCall) {
|
|
79
|
+
DispatchQueue.main.async { [weak self] in
|
|
80
|
+
let layout = self?.tabBarOverlay?.currentLayout() ?? (height: 0.0, bottomSafeArea: 0.0)
|
|
81
|
+
call.resolve([
|
|
82
|
+
"height": layout.height,
|
|
83
|
+
"bottomSafeArea": layout.bottomSafeArea,
|
|
84
|
+
])
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
private func presentTabBar(items: [LiquidGlassTabItem], selectedIndex: Int, tintHex: String?) {
|
|
89
|
+
guard let window = UIApplication.shared.capacitorWindow else { return }
|
|
90
|
+
|
|
91
|
+
if tabBarOverlay == nil {
|
|
92
|
+
let overlay = LiquidGlassTabBarOverlay()
|
|
93
|
+
overlay.onTabSelected = { [weak self] index, id in
|
|
94
|
+
self?.notifyListeners("tabSelected", data: ["index": index, "id": id])
|
|
95
|
+
}
|
|
96
|
+
overlay.onLayoutChanged = { [weak self] height, bottomSafeArea in
|
|
97
|
+
self?.notifyListeners("tabBarLayoutChanged", data: [
|
|
98
|
+
"height": height,
|
|
99
|
+
"bottomSafeArea": bottomSafeArea,
|
|
100
|
+
])
|
|
101
|
+
}
|
|
102
|
+
tabBarOverlay = overlay
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
tabBarOverlay?.attach(to: window)
|
|
106
|
+
tabBarOverlay?.configure(items: items, selectedIndex: selectedIndex, tintHex: tintHex)
|
|
107
|
+
tabBarOverlay?.show()
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// MARK: - UIApplication helper
|
|
112
|
+
private extension UIApplication {
|
|
113
|
+
var capacitorWindow: UIWindow? {
|
|
114
|
+
return connectedScenes
|
|
115
|
+
.compactMap { $0 as? UIWindowScene }
|
|
116
|
+
.flatMap { $0.windows }
|
|
117
|
+
.first { $0.isKeyWindow } ??
|
|
118
|
+
connectedScenes
|
|
119
|
+
.compactMap { $0 as? UIWindowScene }
|
|
120
|
+
.flatMap { $0.windows }
|
|
121
|
+
.first
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import UIKit
|
|
2
|
+
|
|
3
|
+
struct LiquidGlassTabItem {
|
|
4
|
+
let id: String
|
|
5
|
+
let label: String
|
|
6
|
+
let sfSymbol: String
|
|
7
|
+
let badge: String?
|
|
8
|
+
|
|
9
|
+
init?(dictionary: [String: Any]) {
|
|
10
|
+
guard
|
|
11
|
+
let id = dictionary["id"] as? String,
|
|
12
|
+
let label = dictionary["label"] as? String,
|
|
13
|
+
let sfSymbol = dictionary["sfSymbol"] as? String
|
|
14
|
+
else {
|
|
15
|
+
return nil
|
|
16
|
+
}
|
|
17
|
+
self.id = id
|
|
18
|
+
self.label = label
|
|
19
|
+
self.sfSymbol = sfSymbol
|
|
20
|
+
self.badge = dictionary["badge"] as? String
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/// A floating UITabBar overlay that adopts iOS 26 Liquid Glass automatically
|
|
25
|
+
/// when the app is built against the iOS 26 SDK. On earlier iOS the bar falls
|
|
26
|
+
/// back to the translucent blurred UITabBar appearance.
|
|
27
|
+
final class LiquidGlassTabBarOverlay: NSObject {
|
|
28
|
+
|
|
29
|
+
// Public callbacks
|
|
30
|
+
var onTabSelected: ((Int, String) -> Void)?
|
|
31
|
+
var onLayoutChanged: ((Double, Double) -> Void)?
|
|
32
|
+
|
|
33
|
+
// Internal state
|
|
34
|
+
private weak var window: UIWindow?
|
|
35
|
+
private var tabBar: UITabBar?
|
|
36
|
+
private var items: [LiquidGlassTabItem] = []
|
|
37
|
+
|
|
38
|
+
func attach(to window: UIWindow) {
|
|
39
|
+
if self.window === window, tabBar != nil { return }
|
|
40
|
+
self.window = window
|
|
41
|
+
|
|
42
|
+
let tabBar = UITabBar()
|
|
43
|
+
tabBar.translatesAutoresizingMaskIntoConstraints = false
|
|
44
|
+
tabBar.delegate = self
|
|
45
|
+
|
|
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
|
+
}
|
|
53
|
+
|
|
54
|
+
window.addSubview(tabBar)
|
|
55
|
+
|
|
56
|
+
NSLayoutConstraint.activate([
|
|
57
|
+
tabBar.leadingAnchor.constraint(equalTo: window.leadingAnchor),
|
|
58
|
+
tabBar.trailingAnchor.constraint(equalTo: window.trailingAnchor),
|
|
59
|
+
tabBar.bottomAnchor.constraint(equalTo: window.bottomAnchor),
|
|
60
|
+
])
|
|
61
|
+
|
|
62
|
+
self.tabBar = tabBar
|
|
63
|
+
emitLayout()
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
func configure(items: [LiquidGlassTabItem], selectedIndex: Int, tintHex: String?) {
|
|
67
|
+
guard let tabBar else { return }
|
|
68
|
+
|
|
69
|
+
// Preserve selection across re-configs (badge changes, etc.)
|
|
70
|
+
let previousSelectedId: String? = tabBar.selectedItem.flatMap { current in
|
|
71
|
+
self.items.indices.contains(current.tag) ? self.items[current.tag].id : nil
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
self.items = items
|
|
75
|
+
|
|
76
|
+
let uiItems: [UITabBarItem] = items.enumerated().map { index, item in
|
|
77
|
+
let tab = UITabBarItem(
|
|
78
|
+
title: item.label,
|
|
79
|
+
image: UIImage(systemName: item.sfSymbol),
|
|
80
|
+
tag: index
|
|
81
|
+
)
|
|
82
|
+
if let badge = item.badge, !badge.isEmpty {
|
|
83
|
+
tab.badgeValue = badge
|
|
84
|
+
}
|
|
85
|
+
return tab
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
tabBar.setItems(uiItems, animated: false)
|
|
89
|
+
|
|
90
|
+
if let previousSelectedId, let restoreIdx = items.firstIndex(where: { $0.id == previousSelectedId }) {
|
|
91
|
+
tabBar.selectedItem = uiItems[restoreIdx]
|
|
92
|
+
} else if selectedIndex >= 0, selectedIndex < uiItems.count {
|
|
93
|
+
tabBar.selectedItem = uiItems[selectedIndex]
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if let tintHex, let tint = UIColor(hex: tintHex) {
|
|
97
|
+
tabBar.tintColor = tint
|
|
98
|
+
}
|
|
99
|
+
emitLayout()
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
func updateBadge(id: String, badge: String?) {
|
|
103
|
+
guard let tabBar, let tabBarItems = tabBar.items,
|
|
104
|
+
let idx = items.firstIndex(where: { $0.id == id }),
|
|
105
|
+
idx < tabBarItems.count else { return }
|
|
106
|
+
let value = (badge?.isEmpty ?? true) ? nil : badge
|
|
107
|
+
tabBarItems[idx].badgeValue = value
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
func show() {
|
|
111
|
+
tabBar?.isHidden = false
|
|
112
|
+
emitLayout()
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
func hide() {
|
|
116
|
+
tabBar?.isHidden = true
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
func setSelectedIndex(_ index: Int) {
|
|
120
|
+
guard let tabBar, let items = tabBar.items, index >= 0, index < items.count else { return }
|
|
121
|
+
tabBar.selectedItem = items[index]
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
func setSelected(id: String) {
|
|
125
|
+
guard let index = items.firstIndex(where: { $0.id == id }) else { return }
|
|
126
|
+
setSelectedIndex(index)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
func currentLayout() -> (height: Double, bottomSafeArea: Double) {
|
|
130
|
+
guard let tabBar else { return (0, 0) }
|
|
131
|
+
tabBar.layoutIfNeeded()
|
|
132
|
+
let bottomInset = tabBar.safeAreaInsets.bottom
|
|
133
|
+
return (Double(tabBar.frame.height), Double(bottomInset))
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
private func emitLayout() {
|
|
137
|
+
let layout = currentLayout()
|
|
138
|
+
onLayoutChanged?(layout.height, layout.bottomSafeArea)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// MARK: - UITabBarDelegate
|
|
143
|
+
extension LiquidGlassTabBarOverlay: UITabBarDelegate {
|
|
144
|
+
func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) {
|
|
145
|
+
guard item.tag >= 0, item.tag < items.count else { return }
|
|
146
|
+
let selected = items[item.tag]
|
|
147
|
+
onTabSelected?(item.tag, selected.id)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// MARK: - UIColor+hex
|
|
152
|
+
private extension UIColor {
|
|
153
|
+
convenience init?(hex: String) {
|
|
154
|
+
var value = hex.trimmingCharacters(in: .whitespacesAndNewlines).uppercased()
|
|
155
|
+
if value.hasPrefix("#") { value.removeFirst() }
|
|
156
|
+
guard value.count == 6, let rgb = UInt64(value, radix: 16) else { return nil }
|
|
157
|
+
let r = CGFloat((rgb & 0xFF0000) >> 16) / 255
|
|
158
|
+
let g = CGFloat((rgb & 0x00FF00) >> 8) / 255
|
|
159
|
+
let b = CGFloat(rgb & 0x0000FF) / 255
|
|
160
|
+
self.init(red: r, green: g, blue: b, alpha: 1.0)
|
|
161
|
+
}
|
|
162
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ajuarezso/capacitor-liquid-glass",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "iOS 26 Liquid Glass native chrome (TabBar, NavigationBar, Alerts, Sheets) for Capacitor apps. Falls back gracefully on iOS < 26 and Android.",
|
|
5
|
+
"main": "dist/plugin.cjs.js",
|
|
6
|
+
"module": "dist/esm/index.js",
|
|
7
|
+
"types": "dist/esm/index.d.ts",
|
|
8
|
+
"unpkg": "dist/plugin.js",
|
|
9
|
+
"files": [
|
|
10
|
+
"dist/",
|
|
11
|
+
"ios/Sources/",
|
|
12
|
+
"Package.swift",
|
|
13
|
+
"CapacitorLiquidGlass.podspec"
|
|
14
|
+
],
|
|
15
|
+
"author": "Anthony Juarez Solis <anthonyjuarezsolis@icloud.com>",
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"keywords": [
|
|
18
|
+
"capacitor",
|
|
19
|
+
"plugin",
|
|
20
|
+
"ios",
|
|
21
|
+
"liquid-glass",
|
|
22
|
+
"ios-26",
|
|
23
|
+
"tabbar",
|
|
24
|
+
"navigation",
|
|
25
|
+
"glassmorphism"
|
|
26
|
+
],
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "https://github.com/anthonyjuarezsolis/capacitor-liquid-glass.git"
|
|
30
|
+
},
|
|
31
|
+
"bugs": {
|
|
32
|
+
"url": "https://github.com/anthonyjuarezsolis/capacitor-liquid-glass/issues"
|
|
33
|
+
},
|
|
34
|
+
"homepage": "https://github.com/anthonyjuarezsolis/capacitor-liquid-glass#readme",
|
|
35
|
+
"publishConfig": {
|
|
36
|
+
"access": "public"
|
|
37
|
+
},
|
|
38
|
+
"scripts": {
|
|
39
|
+
"build": "npm run clean && npm run tsc && rollup -c rollup.config.mjs",
|
|
40
|
+
"clean": "rimraf ./dist",
|
|
41
|
+
"tsc": "tsc",
|
|
42
|
+
"watch": "tsc --watch"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@capacitor/core": "^8.0.0",
|
|
46
|
+
"rimraf": "^5.0.0",
|
|
47
|
+
"rollup": "^4.9.0",
|
|
48
|
+
"typescript": "^5.4.0"
|
|
49
|
+
},
|
|
50
|
+
"peerDependencies": {
|
|
51
|
+
"@capacitor/core": "^8.0.0"
|
|
52
|
+
},
|
|
53
|
+
"capacitor": {
|
|
54
|
+
"ios": {
|
|
55
|
+
"src": "ios"
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|