@honeypathkar/react-native-lq-bottom-bar 0.0.0-stage → 0.1.1

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 +349 -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 +328 -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,328 @@
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 {
47
+ /*
48
+ REBUILD ONLY WHEN THE ITEMS ACTUALLY CHANGED.
49
+
50
+ The JS side rebuilds its `items` array on every render, so this prop
51
+ arrives constantly with identical contents. Acting on each one calls
52
+ `setItems`, which THROWS AWAY the existing `UITabBarItem`s and the
53
+ selection animation running on them — a rebuild landing mid-animation
54
+ is how a press ends up looking like it half-happened.
55
+
56
+ `isEqual` on the arrays is a deep compare of the dictionaries inside,
57
+ which is what "the same bar" means here. The icon values are cached
58
+ data URIs on the JS side, so equal renders really do produce equal
59
+ strings rather than merely equivalent ones.
60
+ */
61
+ if items.isEqual(oldValue) { return }
62
+ rebuildItems()
63
+ }
64
+ }
65
+
66
+ /// Which item is lit. Setting it animates, which is the point.
67
+ @objc public var selectedIndex: NSNumber = 0 {
68
+ didSet { applySelection() }
69
+ }
70
+
71
+ /// Tint for the selected item. `nil` leaves the system default.
72
+ @objc public var selectedColor: UIColor? {
73
+ didSet { tabBar.tintColor = selectedColor }
74
+ }
75
+
76
+ /// Tint for everything else.
77
+ @objc public var unselectedColor: UIColor? {
78
+ didSet { applyAppearance() }
79
+ }
80
+
81
+ /**
82
+ Hide the labels and keep the glyphs.
83
+
84
+ UIKit has no switch for this, so it is done the way Apple's own compact
85
+ bars do it: the title is dropped and the image is nudged down by half the
86
+ space the title would have taken, which re-centres the glyph in the item.
87
+ */
88
+ @objc public var labeled: Bool = true {
89
+ didSet { rebuildItems() }
90
+ }
91
+
92
+ /// Fired on tap. The JS side owns the selection; this only reports.
93
+ @objc public var onTabSelected: RCTDirectEventBlock?
94
+
95
+ // MARK: - View
96
+
97
+ private let tabBar = UITabBar()
98
+
99
+ public override init(frame: CGRect) {
100
+ super.init(frame: frame)
101
+ setUp()
102
+ }
103
+
104
+ required init?(coder: NSCoder) {
105
+ super.init(coder: coder)
106
+ setUp()
107
+ }
108
+
109
+ private func setUp() {
110
+ tabBar.delegate = self
111
+ tabBar.translatesAutoresizingMaskIntoConstraints = false
112
+ addSubview(tabBar)
113
+
114
+ // Pinned to all four edges: React Native sizes this view through Yoga, so
115
+ // the bar takes whatever the layout gives it rather than carrying an
116
+ // intrinsic height of its own.
117
+ NSLayoutConstraint.activate([
118
+ tabBar.leadingAnchor.constraint(equalTo: leadingAnchor),
119
+ tabBar.trailingAnchor.constraint(equalTo: trailingAnchor),
120
+ tabBar.topAnchor.constraint(equalTo: topAnchor),
121
+ tabBar.bottomAnchor.constraint(equalTo: bottomAnchor),
122
+ ])
123
+
124
+ applyAppearance()
125
+ }
126
+
127
+ // MARK: - Appearance
128
+
129
+ private func applyAppearance() {
130
+ let appearance = UITabBarAppearance()
131
+
132
+ /*
133
+ `configureWithDefaultBackground()`, NOT a flat colour.
134
+
135
+ This is what opts the bar into the system material — on iOS 26 that is
136
+ liquid glass, below it a blur. Setting `backgroundColor` instead paints
137
+ an opaque sheet over the material, which is exactly how every hand-built
138
+ version of this bar ended up looking like a milky panel.
139
+ */
140
+ appearance.configureWithDefaultBackground()
141
+
142
+ if let unselected = unselectedColor {
143
+ let items: [UITabBarItemAppearance] = [
144
+ appearance.stackedLayoutAppearance,
145
+ appearance.inlineLayoutAppearance,
146
+ appearance.compactInlineLayoutAppearance,
147
+ ]
148
+ for item in items {
149
+ item.normal.iconColor = unselected
150
+ item.normal.titleTextAttributes = [.foregroundColor: unselected]
151
+ }
152
+ }
153
+
154
+ tabBar.standardAppearance = appearance
155
+ /*
156
+ The SCROLL EDGE appearance is a separate object and must be set too.
157
+
158
+ Without it the bar reverts to a transparent variant the moment a scroll
159
+ view underneath reaches its end, which reads as the bar vanishing. Using
160
+ the same appearance for both keeps the material constant.
161
+ */
162
+ tabBar.scrollEdgeAppearance = appearance
163
+ }
164
+
165
+ // MARK: - Items
166
+
167
+ private func rebuildItems() {
168
+ var built: [UITabBarItem] = []
169
+
170
+ for (index, raw) in items.enumerated() {
171
+ guard let spec = raw as? NSDictionary else { continue }
172
+
173
+ let title = labeled ? spec["title"] as? String : nil
174
+ let image = loadImage(spec["icon"])
175
+
176
+ let item = UITabBarItem(title: title, image: image, tag: index)
177
+
178
+ /*
179
+ `alwaysTemplate` so UIKit tints the glyph.
180
+
181
+ A PNG loaded as-is keeps its own colours and ignores `tintColor`, so
182
+ the selected state would look identical to the unselected one. The
183
+ alpha channel is the shape; the colour is the system's to decide.
184
+
185
+ `tintable: false` opts out, for an icon whose own colours ARE the
186
+ content — an avatar, a brand mark. Such an item then looks the same
187
+ selected as not, which is the caller's choice to make.
188
+ */
189
+ let tintable = (spec["tintable"] as? NSNumber)?.boolValue ?? true
190
+ item.image = image?.withRenderingMode(tintable ? .alwaysTemplate : .alwaysOriginal)
191
+ item.selectedImage = item.image
192
+
193
+ // Falls back to the title, which is what UIKit would have read anyway.
194
+ if let label = spec["accessibilityLabel"] as? String {
195
+ item.accessibilityLabel = label
196
+ }
197
+
198
+ if !labeled {
199
+ // Half the title's line height, so the glyph sits centred in the
200
+ // item rather than riding high where the label used to be.
201
+ item.imageInsets = UIEdgeInsets(top: 6, left: 0, bottom: -6, right: 0)
202
+ }
203
+
204
+ built.append(item)
205
+ }
206
+
207
+ tabBar.setItems(built, animated: false)
208
+ applySelection()
209
+ }
210
+
211
+ /**
212
+ React Native hands over either a bundled asset (a `uri` dictionary) or a
213
+ name in the app's own asset catalogue. Both are accepted so the JS side can
214
+ use `require('./icon.png')` in development, where assets are served over
215
+ HTTP from Metro, and a catalogue name in release.
216
+ */
217
+ private func loadImage(_ raw: Any?) -> UIImage? {
218
+ if let name = raw as? String {
219
+ return UIImage(named: name)
220
+ }
221
+ if let dict = raw as? NSDictionary {
222
+ if let name = dict["name"] as? String, let image = UIImage(named: name) {
223
+ return image
224
+ }
225
+ if let symbol = dict["sfSymbol"] as? String {
226
+ return UIImage(systemName: symbol)
227
+ }
228
+ if let uri = dict["uri"] as? String {
229
+ /*
230
+ A RASTERISED REACT ICON, which is the main path now.
231
+
232
+ `src/rasterise.tsx` draws a lucide or SVG icon off-screen and captures
233
+ it with react-native-svg's `toDataURL`, so the icon arrives as an
234
+ inline base64 PNG rather than as a file or a URL. Without this branch
235
+ it would fall through to `UIImage(contentsOfFile:)`, which cannot
236
+ read it, and the bar would show titles with no glyphs — the failure
237
+ is silent, which is why it is worth naming here.
238
+
239
+ THE SCALE IS NOT OPTIONAL. The capture renders through a
240
+ `UIGraphicsImageRenderer` at the screen's scale, so a 24-point glyph
241
+ is a 72x72 pixel PNG on a 3x device. PNG carries no scale of its own,
242
+ so decoding without one yields a 72-POINT image and a glyph three
243
+ times too large. Passing the screen scale is what cancels it back.
244
+ */
245
+ if uri.hasPrefix("data:") {
246
+ if let comma = uri.firstIndex(of: ","),
247
+ let data = Data(
248
+ base64Encoded: String(uri[uri.index(after: comma)...]),
249
+ options: .ignoreUnknownCharacters
250
+ )
251
+ {
252
+ return UIImage(data: data, scale: UIScreen.main.scale)
253
+ }
254
+ return nil
255
+ }
256
+
257
+ // A Metro-served asset. Loaded synchronously on purpose: a tab bar
258
+ // with no glyphs for a frame is worse than a frame's hitch, and these
259
+ // are a few hundred bytes each.
260
+ if uri.hasPrefix("http"), let url = URL(string: uri),
261
+ let data = try? Data(contentsOf: url)
262
+ {
263
+ return UIImage(data: data, scale: UIScreen.main.scale)
264
+ }
265
+ return UIImage(contentsOfFile: uri)
266
+ }
267
+ }
268
+ return nil
269
+ }
270
+
271
+ /**
272
+ `selectedItem` is a plain property, and that is the whole API.
273
+
274
+ `UITabBarController` has an animated setter; `UITabBar` does not. It does
275
+ not need one: the bar animates the change itself when it is on screen, so
276
+ assigning the property runs the same icon and label transition UIKit uses
277
+ for its own bars. There is nothing to opt into.
278
+ */
279
+ private func applySelection() {
280
+ let index = selectedIndex.intValue
281
+ guard let items = tabBar.items, index >= 0, index < items.count else {
282
+ return
283
+ }
284
+ let target = items[index]
285
+ // Already there. Re-assigning restarts the capsule animation, and this
286
+ // runs on every prop transaction, so the bar would shimmer continuously.
287
+ if tabBar.selectedItem === target { return }
288
+ tabBar.selectedItem = target
289
+ }
290
+
291
+ // MARK: - UITabBarDelegate
292
+
293
+ public func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) {
294
+ /*
295
+ REPORT, DO NOT DECIDE.
296
+
297
+ The selection is JS state: a tab press can be vetoed (the Home tab opens
298
+ a premise detail instead of switching, for one), so the bar must not
299
+ consider itself switched until JS says so. `selectedIndex` coming back
300
+ down is what moves it.
301
+ */
302
+ onTabSelected?(["index": item.tag])
303
+
304
+ /*
305
+ THE SNAP-BACK HAS TO WAIT A TURN, and this is the whole reason the bar
306
+ can end up lit on a tab the app is not showing.
307
+
308
+ UIKit calls this delegate method while it is still settling its OWN
309
+ selection — on iOS 26 the capsule is mid-animation — so assigning
310
+ `selectedItem` from in here is assigning into a change that has not
311
+ finished. UIKit wins, the highlight stays on the tapped item, and because
312
+ `selectedIndex` never changed VALUE, the prop never fires `didSet` again
313
+ and nothing ever corrects it. The bar is then permanently out of step with
314
+ the app: Events lit while the Profile screen is on screen.
315
+
316
+ Hopping to the next runloop turn puts the assignment after UIKit has
317
+ finished, where it sticks.
318
+
319
+ Reading `selectedIndex` late is a second benefit rather than a hazard: if
320
+ JS accepted the press and sent a new index in the meantime, this applies
321
+ THAT, so an accepted press does not visibly bounce back before moving
322
+ forward again.
323
+ */
324
+ DispatchQueue.main.async { [weak self] in
325
+ self?.applySelection()
326
+ }
327
+ }
328
+ }
@@ -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"}