@honeypathkar/react-native-lq-bottom-bar 0.0.0-stage → 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.
Files changed (34) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +341 -2
  3. package/android/build.gradle +78 -0
  4. package/android/src/main/AndroidManifest.xml +1 -0
  5. package/android/src/main/java/com/honeypathkar/lqbottombar/BackdropBlur.kt +428 -0
  6. package/android/src/main/java/com/honeypathkar/lqbottombar/LQBottomBarPackage.kt +34 -0
  7. package/android/src/main/java/com/honeypathkar/lqbottombar/LQBottomBarView.kt +503 -0
  8. package/android/src/main/java/com/honeypathkar/lqbottombar/LQBottomBarViewManager.kt +119 -0
  9. package/android/src/main/java/com/honeypathkar/lqbottombar/TabSelectedEvent.kt +30 -0
  10. package/ios/LQBottomBarView.swift +287 -0
  11. package/ios/LQBottomBarViewManager.m +21 -0
  12. package/ios/LQBottomBarViewManager.swift +36 -0
  13. package/lib/commonjs/NativeBottomBar.d.ts +33 -0
  14. package/lib/commonjs/NativeBottomBar.d.ts.map +1 -0
  15. package/lib/commonjs/NativeBottomBar.js +9 -0
  16. package/lib/commonjs/NativeBottomBar.js.map +1 -0
  17. package/lib/commonjs/index.d.ts +6 -0
  18. package/lib/commonjs/index.d.ts.map +1 -0
  19. package/lib/commonjs/index.js +131 -0
  20. package/lib/commonjs/index.js.map +1 -0
  21. package/lib/commonjs/rasterise.d.ts +20 -0
  22. package/lib/commonjs/rasterise.d.ts.map +1 -0
  23. package/lib/commonjs/rasterise.js +398 -0
  24. package/lib/commonjs/rasterise.js.map +1 -0
  25. package/lib/commonjs/types.d.ts +119 -0
  26. package/lib/commonjs/types.d.ts.map +1 -0
  27. package/lib/commonjs/types.js +3 -0
  28. package/lib/commonjs/types.js.map +1 -0
  29. package/package.json +65 -4
  30. package/react-native-lq-bottom-bar.podspec +27 -0
  31. package/src/NativeBottomBar.ts +43 -0
  32. package/src/index.tsx +139 -0
  33. package/src/rasterise.tsx +444 -0
  34. package/src/types.ts +136 -0
@@ -0,0 +1,287 @@
1
+ import UIKit
2
+
3
+ /*
4
+ `RCTViewManager` and `RCTDirectEventBlock` are Objective-C types from
5
+ React-Core. Swift does not see them implicitly: the pod ships a module map
6
+ (`Headers/Public/React/React-Core.modulemap`), so the module has to be
7
+ imported by name like any other.
8
+ */
9
+ import React
10
+
11
+ /**
12
+ THE SYSTEM TAB BAR, HOSTED ANYWHERE.
13
+
14
+ The premise everything else in this app ran aground on was that a native tab
15
+ bar only exists inside a `UITabBarController`, and a `UITabBarController`
16
+ only draws its bar for its own child view controllers. That is true of the
17
+ CONTROLLER. It is not true of the BAR.
18
+
19
+ `UITabBar` is a plain `UIView` subclass. It can be added to any view
20
+ hierarchy, given `UITabBarItem`s, and told which one is selected. Doing that
21
+ gets the real thing and not a copy of it:
22
+
23
+ - On iOS 26 the system applies LIQUID GLASS to it through
24
+ `UITabBarAppearance`, including the scroll-edge treatment, because the
25
+ appearance is resolved by the bar itself and not by its controller.
26
+ - The selection animation, the icon bounce and the label crossfade are
27
+ UIKit's own, driven by `setSelectedItem(_:animated:)`.
28
+ - VoiceOver, Dynamic Type, Reduce Motion and the high-contrast variants
29
+ all come for free, because this IS the system control.
30
+
31
+ So a React Native view that wraps one gives every screen the same bar,
32
+ whether or not it happens to be a child of a tab controller. That is the
33
+ whole point of this package.
34
+
35
+ WHY NOT `UITabBarController` IN A CONTAINER: hosting the controller would
36
+ also host its content area, and React Native already owns the screen's
37
+ content. We want its bar and nothing else, so we take the bar.
38
+ */
39
+ @objc(LQBottomBarView)
40
+ public final class LQBottomBarView: UIView, UITabBarDelegate {
41
+
42
+ // MARK: - Bridged props
43
+
44
+ /// `[{ title, icon }]`, set from JS. Re-builds the items in place.
45
+ @objc public var items: NSArray = [] {
46
+ didSet { rebuildItems() }
47
+ }
48
+
49
+ /// Which item is lit. Setting it animates, which is the point.
50
+ @objc public var selectedIndex: NSNumber = 0 {
51
+ didSet { applySelection() }
52
+ }
53
+
54
+ /// Tint for the selected item. `nil` leaves the system default.
55
+ @objc public var selectedColor: UIColor? {
56
+ didSet { tabBar.tintColor = selectedColor }
57
+ }
58
+
59
+ /// Tint for everything else.
60
+ @objc public var unselectedColor: UIColor? {
61
+ didSet { applyAppearance() }
62
+ }
63
+
64
+ /**
65
+ Hide the labels and keep the glyphs.
66
+
67
+ UIKit has no switch for this, so it is done the way Apple's own compact
68
+ bars do it: the title is dropped and the image is nudged down by half the
69
+ space the title would have taken, which re-centres the glyph in the item.
70
+ */
71
+ @objc public var labeled: Bool = true {
72
+ didSet { rebuildItems() }
73
+ }
74
+
75
+ /// Fired on tap. The JS side owns the selection; this only reports.
76
+ @objc public var onTabSelected: RCTDirectEventBlock?
77
+
78
+ // MARK: - View
79
+
80
+ private let tabBar = UITabBar()
81
+
82
+ public override init(frame: CGRect) {
83
+ super.init(frame: frame)
84
+ setUp()
85
+ }
86
+
87
+ required init?(coder: NSCoder) {
88
+ super.init(coder: coder)
89
+ setUp()
90
+ }
91
+
92
+ private func setUp() {
93
+ tabBar.delegate = self
94
+ tabBar.translatesAutoresizingMaskIntoConstraints = false
95
+ addSubview(tabBar)
96
+
97
+ // Pinned to all four edges: React Native sizes this view through Yoga, so
98
+ // the bar takes whatever the layout gives it rather than carrying an
99
+ // intrinsic height of its own.
100
+ NSLayoutConstraint.activate([
101
+ tabBar.leadingAnchor.constraint(equalTo: leadingAnchor),
102
+ tabBar.trailingAnchor.constraint(equalTo: trailingAnchor),
103
+ tabBar.topAnchor.constraint(equalTo: topAnchor),
104
+ tabBar.bottomAnchor.constraint(equalTo: bottomAnchor),
105
+ ])
106
+
107
+ applyAppearance()
108
+ }
109
+
110
+ // MARK: - Appearance
111
+
112
+ private func applyAppearance() {
113
+ let appearance = UITabBarAppearance()
114
+
115
+ /*
116
+ `configureWithDefaultBackground()`, NOT a flat colour.
117
+
118
+ This is what opts the bar into the system material — on iOS 26 that is
119
+ liquid glass, below it a blur. Setting `backgroundColor` instead paints
120
+ an opaque sheet over the material, which is exactly how every hand-built
121
+ version of this bar ended up looking like a milky panel.
122
+ */
123
+ appearance.configureWithDefaultBackground()
124
+
125
+ if let unselected = unselectedColor {
126
+ let items: [UITabBarItemAppearance] = [
127
+ appearance.stackedLayoutAppearance,
128
+ appearance.inlineLayoutAppearance,
129
+ appearance.compactInlineLayoutAppearance,
130
+ ]
131
+ for item in items {
132
+ item.normal.iconColor = unselected
133
+ item.normal.titleTextAttributes = [.foregroundColor: unselected]
134
+ }
135
+ }
136
+
137
+ tabBar.standardAppearance = appearance
138
+ /*
139
+ The SCROLL EDGE appearance is a separate object and must be set too.
140
+
141
+ Without it the bar reverts to a transparent variant the moment a scroll
142
+ view underneath reaches its end, which reads as the bar vanishing. Using
143
+ the same appearance for both keeps the material constant.
144
+ */
145
+ tabBar.scrollEdgeAppearance = appearance
146
+ }
147
+
148
+ // MARK: - Items
149
+
150
+ private func rebuildItems() {
151
+ var built: [UITabBarItem] = []
152
+
153
+ for (index, raw) in items.enumerated() {
154
+ guard let spec = raw as? NSDictionary else { continue }
155
+
156
+ let title = labeled ? spec["title"] as? String : nil
157
+ let image = loadImage(spec["icon"])
158
+
159
+ let item = UITabBarItem(title: title, image: image, tag: index)
160
+
161
+ /*
162
+ `alwaysTemplate` so UIKit tints the glyph.
163
+
164
+ A PNG loaded as-is keeps its own colours and ignores `tintColor`, so
165
+ the selected state would look identical to the unselected one. The
166
+ alpha channel is the shape; the colour is the system's to decide.
167
+
168
+ `tintable: false` opts out, for an icon whose own colours ARE the
169
+ content — an avatar, a brand mark. Such an item then looks the same
170
+ selected as not, which is the caller's choice to make.
171
+ */
172
+ let tintable = (spec["tintable"] as? NSNumber)?.boolValue ?? true
173
+ item.image = image?.withRenderingMode(tintable ? .alwaysTemplate : .alwaysOriginal)
174
+ item.selectedImage = item.image
175
+
176
+ // Falls back to the title, which is what UIKit would have read anyway.
177
+ if let label = spec["accessibilityLabel"] as? String {
178
+ item.accessibilityLabel = label
179
+ }
180
+
181
+ if !labeled {
182
+ // Half the title's line height, so the glyph sits centred in the
183
+ // item rather than riding high where the label used to be.
184
+ item.imageInsets = UIEdgeInsets(top: 6, left: 0, bottom: -6, right: 0)
185
+ }
186
+
187
+ built.append(item)
188
+ }
189
+
190
+ tabBar.setItems(built, animated: false)
191
+ applySelection()
192
+ }
193
+
194
+ /**
195
+ React Native hands over either a bundled asset (a `uri` dictionary) or a
196
+ name in the app's own asset catalogue. Both are accepted so the JS side can
197
+ use `require('./icon.png')` in development, where assets are served over
198
+ HTTP from Metro, and a catalogue name in release.
199
+ */
200
+ private func loadImage(_ raw: Any?) -> UIImage? {
201
+ if let name = raw as? String {
202
+ return UIImage(named: name)
203
+ }
204
+ if let dict = raw as? NSDictionary {
205
+ if let name = dict["name"] as? String, let image = UIImage(named: name) {
206
+ return image
207
+ }
208
+ if let symbol = dict["sfSymbol"] as? String {
209
+ return UIImage(systemName: symbol)
210
+ }
211
+ if let uri = dict["uri"] as? String {
212
+ /*
213
+ A RASTERISED REACT ICON, which is the main path now.
214
+
215
+ `src/rasterise.tsx` draws a lucide or SVG icon off-screen and captures
216
+ it with react-native-svg's `toDataURL`, so the icon arrives as an
217
+ inline base64 PNG rather than as a file or a URL. Without this branch
218
+ it would fall through to `UIImage(contentsOfFile:)`, which cannot
219
+ read it, and the bar would show titles with no glyphs — the failure
220
+ is silent, which is why it is worth naming here.
221
+
222
+ THE SCALE IS NOT OPTIONAL. The capture renders through a
223
+ `UIGraphicsImageRenderer` at the screen's scale, so a 24-point glyph
224
+ is a 72x72 pixel PNG on a 3x device. PNG carries no scale of its own,
225
+ so decoding without one yields a 72-POINT image and a glyph three
226
+ times too large. Passing the screen scale is what cancels it back.
227
+ */
228
+ if uri.hasPrefix("data:") {
229
+ if let comma = uri.firstIndex(of: ","),
230
+ let data = Data(
231
+ base64Encoded: String(uri[uri.index(after: comma)...]),
232
+ options: .ignoreUnknownCharacters
233
+ )
234
+ {
235
+ return UIImage(data: data, scale: UIScreen.main.scale)
236
+ }
237
+ return nil
238
+ }
239
+
240
+ // A Metro-served asset. Loaded synchronously on purpose: a tab bar
241
+ // with no glyphs for a frame is worse than a frame's hitch, and these
242
+ // are a few hundred bytes each.
243
+ if uri.hasPrefix("http"), let url = URL(string: uri),
244
+ let data = try? Data(contentsOf: url)
245
+ {
246
+ return UIImage(data: data, scale: UIScreen.main.scale)
247
+ }
248
+ return UIImage(contentsOfFile: uri)
249
+ }
250
+ }
251
+ return nil
252
+ }
253
+
254
+ /**
255
+ `selectedItem` is a plain property, and that is the whole API.
256
+
257
+ `UITabBarController` has an animated setter; `UITabBar` does not. It does
258
+ not need one: the bar animates the change itself when it is on screen, so
259
+ assigning the property runs the same icon and label transition UIKit uses
260
+ for its own bars. There is nothing to opt into.
261
+ */
262
+ private func applySelection() {
263
+ let index = selectedIndex.intValue
264
+ guard let items = tabBar.items, index >= 0, index < items.count else {
265
+ return
266
+ }
267
+ tabBar.selectedItem = items[index]
268
+ }
269
+
270
+ // MARK: - UITabBarDelegate
271
+
272
+ public func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) {
273
+ /*
274
+ REPORT, DO NOT DECIDE.
275
+
276
+ The selection is JS state: a tab press can be vetoed (the Home tab opens
277
+ a premise detail instead of switching, for one), so the bar must not
278
+ consider itself switched until JS says so. `selectedIndex` coming back
279
+ down is what moves it.
280
+ */
281
+ onTabSelected?(["index": item.tag])
282
+
283
+ // Put the bar back where JS still thinks it is, so a vetoed press does
284
+ // not leave the highlight on the wrong item for a frame.
285
+ applySelection()
286
+ }
287
+ }
@@ -0,0 +1,21 @@
1
+ #import <React/RCTViewManager.h>
2
+ #import <React/RCTUIManager.h>
3
+
4
+ /**
5
+ Prop and event declarations for `LQBottomBarViewManager`.
6
+ *
7
+ These macros cannot be written in Swift — `RCT_EXPORT_VIEW_PROPERTY` is a
8
+ preprocessor construct — so the manager is split: behaviour in Swift,
9
+ declarations here. This is the standard arrangement for a Swift view manager
10
+ and the reason the pair exists.
11
+ */
12
+ @interface RCT_EXTERN_MODULE (LQBottomBarViewManager, RCTViewManager)
13
+
14
+ RCT_EXPORT_VIEW_PROPERTY(items, NSArray)
15
+ RCT_EXPORT_VIEW_PROPERTY(selectedIndex, NSNumber)
16
+ RCT_EXPORT_VIEW_PROPERTY(selectedColor, UIColor)
17
+ RCT_EXPORT_VIEW_PROPERTY(unselectedColor, UIColor)
18
+ RCT_EXPORT_VIEW_PROPERTY(labeled, BOOL)
19
+ RCT_EXPORT_VIEW_PROPERTY(onTabSelected, RCTDirectEventBlock)
20
+
21
+ @end
@@ -0,0 +1,36 @@
1
+ import UIKit
2
+
3
+ /*
4
+ `RCTViewManager` and `RCTDirectEventBlock` are Objective-C types from
5
+ React-Core. Swift does not see them implicitly: the pod ships a module map
6
+ (`Headers/Public/React/React-Core.modulemap`), so the module has to be
7
+ imported by name like any other.
8
+ */
9
+ import React
10
+
11
+ /**
12
+ The bridge's handle on `LQBottomBarView`.
13
+
14
+ A LEGACY view manager, deliberately, even though this app runs Fabric.
15
+ React Native 0.83 bridges legacy managers through its interop layer, and the
16
+ alternative — a Fabric component view — must be written in Objective-C++,
17
+ which cannot host Swift directly and interacts badly with the `use_frameworks!`
18
+ linkage this Podfile sets. One Swift file that works beats two that fight the
19
+ build.
20
+ */
21
+ @objc(LQBottomBarViewManager)
22
+ final class LQBottomBarViewManager: RCTViewManager {
23
+
24
+ override func view() -> UIView! {
25
+ return LQBottomBarView()
26
+ }
27
+
28
+ /**
29
+ The bar is a leaf with no React children and no layout of its own to
30
+ compute, so it can be created on the main queue without stalling the
31
+ shadow tree.
32
+ */
33
+ override static func requiresMainQueueSetup() -> Bool {
34
+ return true
35
+ }
36
+ }
@@ -0,0 +1,33 @@
1
+ import type { HostComponent, StyleProp, ViewProps, ViewStyle } from 'react-native';
2
+ /**
3
+ * The native view, on whichever platform this is.
4
+ *
5
+ * Both platforms register the same component name, and both are legacy view
6
+ * managers bridged through React Native's interop layer — so there is one
7
+ * `requireNativeComponent` call and no per-platform file.
8
+ *
9
+ * `null` on anything else rather than absent: `requireNativeComponent` throws
10
+ * at REQUIRE time where the view is not registered, so it cannot sit at module
11
+ * scope unguarded on, say, web.
12
+ */
13
+ /** The shape the native side is declared with, not the public API. */
14
+ export interface NativeBottomBarProps {
15
+ items: unknown[];
16
+ selectedIndex: number;
17
+ selectedColor?: string;
18
+ unselectedColor?: string;
19
+ labeled: boolean;
20
+ onTabSelected?: (event: {
21
+ nativeEvent: {
22
+ index: number;
23
+ };
24
+ }) => void;
25
+ style?: StyleProp<ViewStyle>;
26
+ pointerEvents?: ViewProps['pointerEvents'];
27
+ /** Android only; the iOS bar gets its material from UIKit. */
28
+ iconSize?: number;
29
+ blurAmount?: number;
30
+ scrimColor?: string;
31
+ }
32
+ export declare const NativeBottomBar: HostComponent<NativeBottomBarProps> | null;
33
+ //# sourceMappingURL=NativeBottomBar.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"NativeBottomBar.d.ts","sourceRoot":"","sources":["../../src/NativeBottomBar.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,aAAa,EACb,SAAS,EACT,SAAS,EACT,SAAS,EACV,MAAM,cAAc,CAAC;AAEtB;;;;;;;;;;GAUG;AAEH,sEAAsE;AACtE,MAAM,WAAW,oBAAoB;IACnC,KAAK,EAAE,OAAO,EAAE,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,OAAO,EAAE,OAAO,CAAC;IACjB,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE;QAAE,WAAW,EAAE;YAAE,KAAK,EAAE,MAAM,CAAA;SAAE,CAAA;KAAE,KAAK,IAAI,CAAC;IACpE,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7B,aAAa,CAAC,EAAE,SAAS,CAAC,eAAe,CAAC,CAAC;IAE3C,8DAA8D;IAC9D,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAID,eAAO,MAAM,eAAe,EAAE,aAAa,CAAC,oBAAoB,CAAC,GAAG,IAG1D,CAAC"}
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.NativeBottomBar = void 0;
4
+ const react_native_1 = require("react-native");
5
+ const SUPPORTED = react_native_1.Platform.OS === 'ios' || react_native_1.Platform.OS === 'android';
6
+ exports.NativeBottomBar = SUPPORTED
7
+ ? (0, react_native_1.requireNativeComponent)('LQBottomBarView')
8
+ : null;
9
+ //# sourceMappingURL=NativeBottomBar.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"NativeBottomBar.js","sourceRoot":"","sources":["../../src/NativeBottomBar.ts"],"names":[],"mappings":";;;AAAA,+CAAgE;AAqChE,MAAM,SAAS,GAAG,uBAAQ,CAAC,EAAE,KAAK,KAAK,IAAI,uBAAQ,CAAC,EAAE,KAAK,SAAS,CAAC;AAExD,QAAA,eAAe,GAC1B,SAAS;IACP,CAAC,CAAC,IAAA,qCAAsB,EAAuB,iBAAiB,CAAC;IACjE,CAAC,CAAC,IAAI,CAAC"}
@@ -0,0 +1,6 @@
1
+ import React from 'react';
2
+ import type { LQBottomBarItem, LQBottomBarProps, LQIcon, LQIconState } from './types';
3
+ export type { LQBottomBarItem, LQBottomBarProps, LQIcon, LQIconState };
4
+ declare const LQBottomBar: ({ items, selectedIndex, selectedColor, unselectedColor, labeled, iconSize, onTabSelected, style, pointerEvents, blurAmount, androidBackgroundColor, }: LQBottomBarProps) => React.JSX.Element | null;
5
+ export default LQBottomBar;
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAkB,MAAM,OAAO,CAAC;AAKvC,OAAO,KAAK,EACV,eAAe,EACf,gBAAgB,EAChB,MAAM,EACN,WAAW,EACZ,MAAM,SAAS,CAAC;AAEjB,YAAY,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,EAAE,WAAW,EAAE,CAAC;AAoDvE,QAAA,MAAM,WAAW,GAAI,uJAYlB,gBAAgB,6BA4DlB,CAAC;AAEF,eAAe,WAAW,CAAC"}
@@ -0,0 +1,131 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ const react_1 = __importStar(require("react"));
37
+ const react_native_1 = require("react-native");
38
+ const NativeBottomBar_1 = require("./NativeBottomBar");
39
+ const rasterise_1 = require("./rasterise");
40
+ /**
41
+ * THE SYSTEM TAB BAR, HOSTED ANYWHERE.
42
+ *
43
+ * A native tab bar normally only exists inside a `UITabBarController`, which
44
+ * only draws its bar for its own children — so a screen pushed above a tab
45
+ * navigator cannot have one. That is true of the CONTROLLER, not the BAR:
46
+ * `UITabBar` is a plain `UIView`, and hosting one directly gets the real
47
+ * thing rather than a drawing of it. Liquid glass on iOS 26, UIKit's own
48
+ * selection animation, and the platform's accessibility, on any screen.
49
+ *
50
+ * Android has no system bar to borrow, so it draws a matching one: same
51
+ * dimensions, same tint crossfade and icon bounce, with a blurred backdrop
52
+ * standing in for the iOS material.
53
+ *
54
+ * BOTH PLATFORMS DRAW THEIR OWN ITEMS, which is why `src/rasterise.tsx`
55
+ * exists: a view that renders its items natively cannot be handed a React
56
+ * element, so a lucide or SVG icon is captured to a PNG once and the capture
57
+ * is what crosses the bridge. See that file for the trade this makes and what
58
+ * it costs.
59
+ */
60
+ /**
61
+ * ASSETS ARE RESOLVED HERE, and they have to be.
62
+ *
63
+ * `require('./home.png')` evaluates to a NUMBER — an id into React Native's
64
+ * asset registry. The bridge converts that number into `{ uri, scale, ... }`
65
+ * only for props it knows are image sources, which it learns from the prop's
66
+ * declared type. `items` is declared as an array, so the bridge has no reason
67
+ * to look inside it: the number crosses to native as a number, the native
68
+ * loader finds nothing it recognises, and the bar draws labels with no
69
+ * glyphs. `Image.resolveAssetSource` does the same conversion by hand, which
70
+ * is what lets a caller keep writing `require(...)`.
71
+ */
72
+ const resolveIcon = (icon) => {
73
+ if (typeof icon === 'number')
74
+ return react_native_1.Image.resolveAssetSource(icon);
75
+ return icon;
76
+ };
77
+ /**
78
+ * `overflow: 'hidden'` on Android, always.
79
+ *
80
+ * The blurred backdrop is a real child view, so without clipping it squares
81
+ * off whatever `borderRadius` the caller gave the bar — the corners stay
82
+ * rounded on the bar's own background while the blur fills them in. iOS needs
83
+ * none of this: `UITabBar` shapes its own material.
84
+ */
85
+ const androidClip = react_native_1.StyleSheet.create({
86
+ bar: { overflow: 'hidden' },
87
+ });
88
+ const LQBottomBar = ({ items, selectedIndex, selectedColor, unselectedColor, labeled = true, iconSize = 24, onTabSelected, style, pointerEvents, blurAmount = 20, androidBackgroundColor = 'rgba(20, 20, 20, 0.45)', }) => {
89
+ const { sources, host } = (0, rasterise_1.useRasterisedIcons)(items, iconSize);
90
+ const nativeItems = (0, react_1.useMemo)(() => items.map((item, index) => {
91
+ var _a;
92
+ const rasterised = sources[index];
93
+ return {
94
+ title: item.title,
95
+ /*
96
+ A captured glyph wins; otherwise the icon passes through as the
97
+ image or symbol descriptor it already was — which only iOS can
98
+ resolve. Until a capture lands this is `undefined`, which draws the
99
+ item with its title and no glyph for a frame; see the cost note in
100
+ `rasterise.tsx`.
101
+ */
102
+ icon: rasterised ? { uri: rasterised } : resolveIcon(item.icon),
103
+ // Default true: one artwork, tinted by the platform for both states.
104
+ tintable: item.tintable !== false,
105
+ accessibilityLabel: (_a = item.accessibilityLabel) !== null && _a !== void 0 ? _a : item.title,
106
+ };
107
+ }), [items, sources]);
108
+ if (!NativeBottomBar_1.NativeBottomBar)
109
+ return null;
110
+ const platformProps = react_native_1.Platform.OS === 'android'
111
+ ? {
112
+ iconSize,
113
+ blurAmount,
114
+ scrimColor: androidBackgroundColor,
115
+ }
116
+ : // Passing Android-only props to the iOS manager would log an unknown
117
+ // prop warning for each one on every render.
118
+ null;
119
+ return (react_1.default.createElement(react_1.default.Fragment, null,
120
+ host,
121
+ react_1.default.createElement(NativeBottomBar_1.NativeBottomBar, { style: react_native_1.Platform.OS === 'android' ? [style, androidClip.bar] : style, pointerEvents: pointerEvents, items: nativeItems, selectedIndex: selectedIndex, selectedColor: selectedColor, unselectedColor: unselectedColor, labeled: labeled !== false, ...platformProps,
122
+ /*
123
+ A REPORT, NOT A COMMIT. Both bars put their highlight back where
124
+ `selectedIndex` says it is, so a tap the caller declines to act on —
125
+ one that opens a detail screen instead of switching tabs — cannot
126
+ leave the bar showing the wrong item.
127
+ */
128
+ onTabSelected: event => onTabSelected === null || onTabSelected === void 0 ? void 0 : onTabSelected(event.nativeEvent.index) })));
129
+ };
130
+ exports.default = LQBottomBar;
131
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.tsx"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,+CAAuC;AACvC,+CAA2D;AAE3D,uDAAoD;AACpD,2CAAiD;AAUjD;;;;;;;;;;;;;;;;;;;GAmBG;AAEH;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,GAAG,CAAC,IAAY,EAAW,EAAE;IAC5C,IAAI,OAAO,IAAI,KAAK,QAAQ;QAAE,OAAO,oBAAK,CAAC,kBAAkB,CAAC,IAAI,CAAC,CAAC;IACpE,OAAO,IAAI,CAAC;AACd,CAAC,CAAC;AAEF;;;;;;;GAOG;AACH,MAAM,WAAW,GAAG,yBAAU,CAAC,MAAM,CAAC;IACpC,GAAG,EAAE,EAAE,QAAQ,EAAE,QAAQ,EAAE;CAC5B,CAAC,CAAC;AAEH,MAAM,WAAW,GAAG,CAAC,EACnB,KAAK,EACL,aAAa,EACb,aAAa,EACb,eAAe,EACf,OAAO,GAAG,IAAI,EACd,QAAQ,GAAG,EAAE,EACb,aAAa,EACb,KAAK,EACL,aAAa,EACb,UAAU,GAAG,EAAE,EACf,sBAAsB,GAAG,wBAAwB,GAChC,EAAE,EAAE;IACrB,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,GAAG,IAAA,8BAAkB,EAAC,KAAK,EAAE,QAAQ,CAAC,CAAC;IAE9D,MAAM,WAAW,GAAG,IAAA,eAAO,EACzB,GAAG,EAAE,CACH,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE;;QACxB,MAAM,UAAU,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC;QAClC,OAAO;YACL,KAAK,EAAE,IAAI,CAAC,KAAK;YACjB;;;;;;cAME;YACF,IAAI,EAAE,UAAU,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,UAAU,EAAE,CAAC,CAAC,CAAC,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC;YAC/D,qEAAqE;YACrE,QAAQ,EAAE,IAAI,CAAC,QAAQ,KAAK,KAAK;YACjC,kBAAkB,EAAE,MAAA,IAAI,CAAC,kBAAkB,mCAAI,IAAI,CAAC,KAAK;SAC1D,CAAC;IACJ,CAAC,CAAC,EACJ,CAAC,KAAK,EAAE,OAAO,CAAC,CACjB,CAAC;IAEF,IAAI,CAAC,iCAAe;QAAE,OAAO,IAAI,CAAC;IAElC,MAAM,aAAa,GACjB,uBAAQ,CAAC,EAAE,KAAK,SAAS;QACvB,CAAC,CAAC;YACE,QAAQ;YACR,UAAU;YACV,UAAU,EAAE,sBAAsB;SACnC;QACH,CAAC,CAAC,qEAAqE;YACrE,6CAA6C;YAC7C,IAAI,CAAC;IAEX,OAAO,CACL;QACG,IAAI;QACL,8BAAC,iCAAe,IACd,KAAK,EAAE,uBAAQ,CAAC,EAAE,KAAK,SAAS,CAAC,CAAC,CAAC,CAAC,KAAK,EAAE,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,EACnE,aAAa,EAAE,aAAa,EAC5B,KAAK,EAAE,WAAW,EAClB,aAAa,EAAE,aAAa,EAC5B,aAAa,EAAE,aAAa,EAC5B,eAAe,EAAE,eAAe,EAChC,OAAO,EAAE,OAAO,KAAK,KAAK,KACtB,aAAa;YACjB;;;;;cAKE;YACF,aAAa,EAAE,KAAK,CAAC,EAAE,CAAC,aAAa,aAAb,aAAa,uBAAb,aAAa,CAAG,KAAK,CAAC,WAAW,CAAC,KAAK,CAAC,GAChE,CACD,CACJ,CAAC;AACJ,CAAC,CAAC;AAEF,kBAAe,WAAW,CAAC"}
@@ -0,0 +1,20 @@
1
+ import type { ReactElement } from 'react';
2
+ import type { LQBottomBarItem } from './types';
3
+ export interface RasterisedIcons {
4
+ /**
5
+ * Per item: the captured data URI, or `null` when the icon needs no
6
+ * rasterising (an image or SF Symbol) or has not been captured yet.
7
+ */
8
+ sources: (string | null)[];
9
+ /** Mount this somewhere in the tree. Renders nothing visible. */
10
+ host: ReactElement | null;
11
+ }
12
+ /**
13
+ * Capture every React-element icon in `items`, memoised by icon identity.
14
+ *
15
+ * Returns the captured sources plus the off-screen host that does the
16
+ * capturing — the caller has to render the host, since a hook cannot mount
17
+ * views on its own.
18
+ */
19
+ export declare const useRasterisedIcons: (items: LQBottomBarItem[], size: number) => RasterisedIcons;
20
+ //# sourceMappingURL=rasterise.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rasterise.d.ts","sourceRoot":"","sources":["../../src/rasterise.tsx"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,OAAO,CAAC;AAE1C,OAAO,KAAK,EAAE,eAAe,EAAU,MAAM,SAAS,CAAC;AAkTvD,MAAM,WAAW,eAAe;IAC9B;;;OAGG;IACH,OAAO,EAAE,CAAC,MAAM,GAAG,IAAI,CAAC,EAAE,CAAC;IAC3B,iEAAiE;IACjE,IAAI,EAAE,YAAY,GAAG,IAAI,CAAC;CAC3B;AAED;;;;;;GAMG;AACH,eAAO,MAAM,kBAAkB,GAC7B,OAAO,eAAe,EAAE,EACxB,MAAM,MAAM,KACX,eAyGF,CAAC"}