@likith99/react-native-adaptive-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.
Files changed (77) hide show
  1. package/AdaptiveLiquidGlass.podspec +18 -0
  2. package/LICENSE +21 -0
  3. package/README.md +127 -0
  4. package/android/build.gradle +21 -0
  5. package/android/src/main/AndroidManifest.xml +1 -0
  6. package/android/src/main/java/com/adaptiveglass/ALGMenuManager.kt +31 -0
  7. package/android/src/main/java/com/adaptiveglass/ALGMenuView.kt +184 -0
  8. package/android/src/main/java/com/adaptiveglass/ALGSliderManager.kt +32 -0
  9. package/android/src/main/java/com/adaptiveglass/ALGSliderView.kt +132 -0
  10. package/android/src/main/java/com/adaptiveglass/ALGTabsManager.kt +24 -0
  11. package/android/src/main/java/com/adaptiveglass/ALGTabsView.kt +161 -0
  12. package/android/src/main/java/com/adaptiveglass/AdaptiveLiquidGlassPackage.kt +11 -0
  13. package/android/src/main/res/drawable/alg_tab_circle.xml +3 -0
  14. package/android/src/main/res/drawable/alg_tab_favorites.xml +3 -0
  15. package/android/src/main/res/drawable/alg_tab_home.xml +3 -0
  16. package/android/src/main/res/drawable/alg_tab_inbox.xml +3 -0
  17. package/android/src/main/res/drawable/alg_tab_library.xml +3 -0
  18. package/android/src/main/res/drawable/alg_tab_search.xml +3 -0
  19. package/android/src/main/res/drawable/alg_tab_settings.xml +3 -0
  20. package/ios/ALGActionClusterComponentView.h +3 -0
  21. package/ios/ALGActionClusterComponentView.mm +47 -0
  22. package/ios/ALGActionClusterView.swift +252 -0
  23. package/ios/ALGButtonComponentView.h +3 -0
  24. package/ios/ALGButtonComponentView.mm +37 -0
  25. package/ios/ALGButtonView.swift +113 -0
  26. package/ios/ALGMenuComponentView.h +3 -0
  27. package/ios/ALGMenuComponentView.mm +34 -0
  28. package/ios/ALGMenuView.swift +221 -0
  29. package/ios/ALGSegmentedComponentView.h +3 -0
  30. package/ios/ALGSegmentedComponentView.mm +36 -0
  31. package/ios/ALGSegmentedView.swift +58 -0
  32. package/ios/ALGSliderComponentView.h +3 -0
  33. package/ios/ALGSliderComponentView.mm +47 -0
  34. package/ios/ALGSliderView.swift +102 -0
  35. package/ios/ALGSurfaceComponentView.h +3 -0
  36. package/ios/ALGSurfaceComponentView.mm +45 -0
  37. package/ios/ALGSurfaceView.swift +108 -0
  38. package/ios/ALGTabsComponentView.h +3 -0
  39. package/ios/ALGTabsComponentView.mm +33 -0
  40. package/ios/ALGTabsView.swift +194 -0
  41. package/package.json +73 -0
  42. package/react-native.config.js +3 -0
  43. package/src/GlassActionCluster.ios.tsx +18 -0
  44. package/src/GlassActionCluster.tsx +1 -0
  45. package/src/GlassButton.ios.tsx +18 -0
  46. package/src/GlassButton.tsx +7 -0
  47. package/src/GlassContainer.ios.tsx +9 -0
  48. package/src/GlassContainer.tsx +6 -0
  49. package/src/GlassMenuButton.tsx +26 -0
  50. package/src/GlassPressable.tsx +17 -0
  51. package/src/GlassSegmentedControl.ios.tsx +21 -0
  52. package/src/GlassSegmentedControl.tsx +1 -0
  53. package/src/GlassSlider.tsx +24 -0
  54. package/src/GlassTabBar.tsx +25 -0
  55. package/src/GlassToolbar.tsx +25 -0
  56. package/src/GlassView.ios.tsx +13 -0
  57. package/src/GlassView.tsx +1 -0
  58. package/src/adaptiveHeight.ts +15 -0
  59. package/src/fallback/GlassActionCluster.tsx +25 -0
  60. package/src/fallback/GlassSegmentedControl.tsx +26 -0
  61. package/src/fallback/GlassView.tsx +12 -0
  62. package/src/fallback/NativeGlassButton.tsx +21 -0
  63. package/src/index.ts +17 -0
  64. package/src/menuTree.ts +35 -0
  65. package/src/sliderMath.ts +12 -0
  66. package/src/specs/ALGActionClusterNativeComponent.ts +15 -0
  67. package/src/specs/ALGButtonNativeComponent.ts +15 -0
  68. package/src/specs/ALGMenuNativeComponent.ts +17 -0
  69. package/src/specs/ALGSegmentedNativeComponent.ts +12 -0
  70. package/src/specs/ALGSliderNativeComponent.ts +19 -0
  71. package/src/specs/ALGSurfaceNativeComponent.ts +13 -0
  72. package/src/specs/ALGTabsNativeComponent.ts +11 -0
  73. package/src/support.ts +5 -0
  74. package/src/types.ts +181 -0
  75. package/src/validateActions.ts +10 -0
  76. package/src/validateSegments.ts +11 -0
  77. package/src/validateTabs.ts +17 -0
@@ -0,0 +1,108 @@
1
+ import UIKit
2
+
3
+ /// Material rendering lives entirely in UIKit. Fabric only supplies props and children.
4
+ @objc(ALGSurfaceView)
5
+ public final class ALGSurfaceView: UIView {
6
+ private let effectView = UIVisualEffectView()
7
+ private var material = "regular"
8
+ private var interactive = false
9
+ private var glassTint: UIColor?
10
+ private var radius: CGFloat = 24
11
+ private var isContainer = false
12
+ private var mergingEnabled = false
13
+ private var spacing: CGFloat = 20
14
+ private var duration: Double = 0.35
15
+ private var dirty = true
16
+ private var mounted = false
17
+ private var resetInteraction = false
18
+
19
+ @objc public var reactContentView: UIView { effectView.contentView }
20
+
21
+ public override init(frame: CGRect) {
22
+ super.init(frame: frame)
23
+ addSubview(effectView)
24
+ NotificationCenter.default.addObserver(self, selector: #selector(accessibilityChanged),
25
+ name: UIAccessibility.reduceTransparencyStatusDidChangeNotification, object: nil)
26
+ }
27
+ required init?(coder: NSCoder) { fatalError("init(coder:) is unavailable") }
28
+ deinit { NotificationCenter.default.removeObserver(self) }
29
+
30
+ @objc public func configure(_ material: String, interactive: Bool, tint: UIColor?,
31
+ radius: CGFloat, container: Bool, mergingEnabled: Bool, spacing: CGFloat, duration: Double, scheme: String) {
32
+ resetInteraction = resetInteraction || self.interactive != interactive || self.isContainer != container || self.mergingEnabled != mergingEnabled
33
+ dirty = dirty || self.material != material || self.interactive != interactive ||
34
+ self.glassTint != tint || self.isContainer != container || self.spacing != spacing || self.mergingEnabled != mergingEnabled
35
+ self.material = material
36
+ self.interactive = interactive
37
+ self.glassTint = tint
38
+ self.radius = max(0, radius)
39
+ self.isContainer = container
40
+ self.mergingEnabled = mergingEnabled
41
+ self.spacing = max(0, spacing)
42
+ self.duration = max(0, duration)
43
+ overrideUserInterfaceStyle = scheme == "dark" ? .dark : scheme == "light" ? .light : .unspecified
44
+ setNeedsLayout()
45
+ }
46
+
47
+ @objc private func accessibilityChanged() { dirty = true; setNeedsLayout() }
48
+
49
+ public override func didMoveToWindow() {
50
+ super.didMoveToWindow()
51
+ mounted = false
52
+ dirty = true
53
+ setNeedsLayout()
54
+ }
55
+
56
+ public override func layoutSubviews() {
57
+ super.layoutSubviews()
58
+ effectView.frame = bounds
59
+ if #available(iOS 26.0, *) {
60
+ effectView.cornerConfiguration = .uniformCorners(radius: .fixed(isContainer ? 0 : radius))
61
+ } else {
62
+ effectView.layer.cornerRadius = isContainer ? 0 : radius
63
+ effectView.clipsToBounds = !isContainer
64
+ }
65
+ guard window != nil, bounds.width > 0, bounds.height > 0, dirty else { return }
66
+ dirty = false
67
+ let animate = mounted && !resetInteraction && !UIAccessibility.isReduceMotionEnabled && duration > 0
68
+ if !mounted || resetInteraction { effectView.effect = UIVisualEffect() }
69
+ resetInteraction = false
70
+ mounted = true
71
+ let effect: UIVisualEffect
72
+ if #available(iOS 26.0, *), !UIAccessibility.isReduceTransparencyEnabled {
73
+ if isContainer {
74
+ if mergingEnabled {
75
+ let group = UIGlassContainerEffect()
76
+ group.spacing = spacing
77
+ effect = group
78
+ } else {
79
+ // Keep the children mounted, but remove shared glass compositing.
80
+ effect = UIVisualEffect()
81
+ }
82
+ } else if material != "none" {
83
+ let glass = UIGlassEffect(style: material == "clear" ? .clear : .regular)
84
+ glass.isInteractive = interactive
85
+ glass.tintColor = glassTint
86
+ effect = glass
87
+ } else { effect = UIVisualEffect() }
88
+ effectView.backgroundColor = .clear
89
+ } else if !UIAccessibility.isReduceTransparencyEnabled && !isContainer && material != "none" {
90
+ // Standard system material on pre-glass iOS. Keep the same contentView and
91
+ // dirty-prop gate, so layout/React updates do not allocate another blur.
92
+ effect = UIBlurEffect(style: material == "clear" ? .systemUltraThinMaterial : .systemMaterial)
93
+ effectView.backgroundColor = .clear
94
+ effectView.contentView.backgroundColor = glassTint ?? .clear
95
+ } else {
96
+ effect = UIVisualEffect()
97
+ effectView.backgroundColor = isContainer || material == "none" ? .clear : .secondarySystemBackground
98
+ }
99
+ if #available(iOS 26.0, *) {
100
+ effectView.contentView.backgroundColor = .clear
101
+ } else if UIAccessibility.isReduceTransparencyEnabled || isContainer || material == "none" {
102
+ effectView.contentView.backgroundColor = .clear
103
+ }
104
+ let apply = { self.effectView.effect = effect }
105
+ if animate { UIView.animate(withDuration: duration, animations: apply) } else { apply() }
106
+ effectView.contentView.isUserInteractionEnabled = true
107
+ }
108
+ }
@@ -0,0 +1,3 @@
1
+ #import <React/RCTViewComponentView.h>
2
+ @interface ALGTabsComponentView : RCTViewComponentView
3
+ @end
@@ -0,0 +1,33 @@
1
+ #import "ALGTabsComponentView.h"
2
+ #import <react/renderer/components/AdaptiveLiquidGlassSpec/ComponentDescriptors.h>
3
+ #import <react/renderer/components/AdaptiveLiquidGlassSpec/Props.h>
4
+ #import <react/renderer/components/AdaptiveLiquidGlassSpec/EventEmitters.h>
5
+ #import <React/RCTConversions.h>
6
+ #import "AdaptiveLiquidGlass-Swift.h"
7
+ using namespace facebook::react;
8
+ @implementation ALGTabsComponentView {
9
+ ALGTabsView *_tabs;
10
+ }
11
+ + (ComponentDescriptorProvider)componentDescriptorProvider { return concreteComponentDescriptorProvider<ALGTabsComponentDescriptor>(); }
12
+ - (instancetype)initWithFrame:(CGRect)frame {
13
+ if (self = [super initWithFrame:frame]) {
14
+ _props = std::make_shared<const ALGTabsProps>();
15
+ _tabs = [[ALGTabsView alloc] initWithFrame:CGRectZero];
16
+ self.contentView = _tabs;
17
+ __weak ALGTabsComponentView *weakSelf = self;
18
+ _tabs.onSelectionChange = ^(NSString *identifier) {
19
+ ALGTabsComponentView *self = weakSelf;
20
+ if (self && self->_eventEmitter) std::static_pointer_cast<const ALGTabsEventEmitter>(self->_eventEmitter)->onSelectionChange({std::string(identifier.UTF8String)});
21
+ };
22
+ }
23
+ return self;
24
+ }
25
+ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps {
26
+ const auto &p = *std::static_pointer_cast<const ALGTabsProps>(props);
27
+ // selectionRevision forces this transaction when React rejects a native tap.
28
+ [_tabs configure:@(p.itemsJSON.c_str()) selectedValue:@(p.selectedValue.c_str()) disabled:p.disabled
29
+ tint:RCTUIColorFromSharedColor(p.glassTint) identifier:@(p.controlTestID.c_str())];
30
+ [super updateProps:props oldProps:oldProps];
31
+ }
32
+ + (BOOL)shouldBeRecycled { return NO; }
33
+ @end
@@ -0,0 +1,194 @@
1
+ import UIKit
2
+
3
+ private struct TabItem: Decodable {
4
+ let id: String
5
+ let title: String
6
+ let icon: String?
7
+ let systemImage: String?
8
+ let selectedSystemImage: String?
9
+ let badge: Badge?
10
+ let disabled: Bool?
11
+ let accessibilityLabel: String?
12
+ enum Badge: Decodable {
13
+ case number(Int), dot
14
+ init(from decoder: Decoder) throws {
15
+ let value = try decoder.singleValueContainer()
16
+ if let number = try? value.decode(Int.self) { self = .number(number) }
17
+ else if try value.decode(String.self) == "dot" { self = .dot }
18
+ else { throw DecodingError.dataCorruptedError(in: value, debugDescription: "Invalid badge") }
19
+ }
20
+ var text: String {
21
+ switch self { case .dot: return ""; case .number(let number): return number > 999 ? "999+" : String(number) }
22
+ }
23
+ }
24
+ }
25
+ private final class TabDestination: UIViewController {
26
+ let tabID: String
27
+ init(id: String) { tabID = id; super.init(nibName: nil, bundle: nil) }
28
+ required init?(coder: NSCoder) { fatalError("init(coder:) is unavailable") }
29
+ override func loadView() { view = UIView(); view.backgroundColor = .clear }
30
+ }
31
+
32
+ /// The contained controller owns the native tab bar; React owns the screen above it.
33
+ @objc(ALGTabsView)
34
+ public final class ALGTabsView: UIView, UITabBarControllerDelegate {
35
+ private let controller = UITabBarController()
36
+ private var destinations: [String: TabDestination] = [:]
37
+ private var items: [TabItem] = []
38
+ private var lastJSON = ""
39
+ private var applying = false
40
+ private var disabled = false
41
+ @objc public var onSelectionChange: ((String) -> Void)?
42
+
43
+ public override init(frame: CGRect) {
44
+ super.init(frame: frame)
45
+ controller.delegate = self
46
+ controller.view.backgroundColor = .clear
47
+ controller.customizableViewControllers = []
48
+ if #available(iOS 17.0, *) { controller.traitOverrides.horizontalSizeClass = .compact }
49
+ if #available(iOS 18.0, *) { controller.mode = .tabBar }
50
+ if #available(iOS 26.0, *) { controller.tabBarMinimizeBehavior = .never }
51
+ }
52
+ required init?(coder: NSCoder) { fatalError("init(coder:) is unavailable") }
53
+
54
+ @objc public func configure(_ json: String, selectedValue: String, disabled: Bool,
55
+ tint: UIColor?, identifier: String) {
56
+ applying = true
57
+ defer { applying = false }
58
+ self.disabled = disabled
59
+ let metadataChanged = json != lastJSON
60
+ if metadataChanged {
61
+ lastJSON = json
62
+ items = (try? JSONDecoder().decode([TabItem].self, from: Data(json.utf8))) ?? []
63
+ var next: [String: TabDestination] = [:]
64
+ let ordered = items.map { item -> TabDestination in
65
+ let destination = destinations[item.id] ?? TabDestination(id: item.id)
66
+ next[item.id] = destination
67
+ return destination
68
+ }
69
+ destinations = next
70
+ if #available(iOS 18.4, *) {
71
+ // Modern UIKit owns enabled state on UITab, not the legacy tabBarItem.
72
+ let tabs = ordered.map { destination in
73
+ controller.tabs.first { $0.identifier == destination.tabID } ??
74
+ UITab(title: "", image: nil, identifier: destination.tabID) { _ in destination }
75
+ }
76
+ if controller.tabs.map(\.identifier) != tabs.map(\.identifier) {
77
+ controller.setTabs(tabs, animated: false)
78
+ }
79
+ }
80
+ // UIKit adopts the UITab API from the app's deployment target, not just the running
81
+ // OS, so #available alone can leave an app built for an older target with tabs that
82
+ // UIKit ignores and no view controllers at all. Drive the legacy array whenever the
83
+ // controller's own contents do not match, which is a no-op once UITab took effect.
84
+ let currentIDs = controller.viewControllers?.compactMap { ($0 as? TabDestination)?.tabID } ?? []
85
+ if currentIDs != ordered.map(\.tabID) {
86
+ controller.setViewControllers(ordered, animated: false)
87
+ controller.customizableViewControllers = []
88
+ }
89
+ }
90
+ for item in items {
91
+ guard let destination = destinations[item.id] else { continue }
92
+ if metadataChanged {
93
+ let preset: String
94
+ switch item.icon {
95
+ case "home": preset = "house"
96
+ case "search": preset = "magnifyingglass"
97
+ case "library": preset = "books.vertical"
98
+ case "favorites": preset = "heart"
99
+ case "inbox": preset = "tray"
100
+ case "settings": preset = "gearshape"
101
+ default: preset = "circle"
102
+ }
103
+ let symbol = item.systemImage ?? preset
104
+ destination.tabBarItem.title = item.title
105
+ destination.tabBarItem.image = UIImage(systemName: symbol) ?? UIImage(systemName: "circle")
106
+ destination.tabBarItem.selectedImage = item.selectedSystemImage.flatMap { UIImage(systemName: $0) }
107
+ destination.tabBarItem.badgeValue = item.badge?.text
108
+ }
109
+ // UIKit derives the tab button, its label and its selected trait from the tab itself.
110
+ // Neither UITab nor UITabBarItem declares a public accessibilityTraits property, so
111
+ // isEnabled is the only public control over the disabled state; see docs/accessibility.md.
112
+ let enabled = !disabled && item.disabled != true
113
+ destination.tabBarItem.isEnabled = enabled
114
+ destination.tabBarItem.accessibilityLabel = item.accessibilityLabel ?? item.title
115
+ destination.tabBarItem.accessibilityIdentifier = identifier.isEmpty ? item.id : "\(identifier)-\(item.id)"
116
+ if #available(iOS 18.4, *), let tab = controller.tab(forIdentifier: item.id) {
117
+ tab.title = item.title
118
+ tab.image = item.id == selectedValue
119
+ ? (destination.tabBarItem.selectedImage ?? destination.tabBarItem.image)
120
+ : destination.tabBarItem.image
121
+ tab.badgeValue = item.badge?.text
122
+ tab.isEnabled = enabled
123
+ tab.preferredPlacement = .fixed
124
+ tab.accessibilityLabel = item.accessibilityLabel ?? item.title
125
+ tab.accessibilityIdentifier = destination.tabBarItem.accessibilityIdentifier
126
+ }
127
+ }
128
+ // Fall through to the view-controller path when UITab is not in effect, rather than
129
+ // silently skipping selection on an app whose deployment target predates it.
130
+ if #available(iOS 18.4, *), let selected = controller.tab(forIdentifier: selectedValue) {
131
+ if controller.selectedTab !== selected { controller.selectedTab = selected }
132
+ } else if let selected = destinations[selectedValue], controller.selectedViewController !== selected {
133
+ controller.selectedViewController = selected
134
+ }
135
+ controller.tabBar.tintColor = tint
136
+ controller.tabBar.accessibilityIdentifier = identifier
137
+ controller.view.isHidden = items.isEmpty
138
+ setNeedsLayout()
139
+ }
140
+ private func currentItem(_ destination: UIViewController) -> TabItem? {
141
+ guard let tab = destination as? TabDestination, destinations[tab.tabID] === tab else { return nil }
142
+ return items.first { $0.id == tab.tabID }
143
+ }
144
+ public func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
145
+ guard !applying, window != nil, !disabled, let item = currentItem(viewController) else { return false }
146
+ return item.disabled != true
147
+ }
148
+ public func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) {
149
+ guard !applying, window != nil, !disabled, let item = currentItem(viewController), item.disabled != true else { return }
150
+ onSelectionChange?(item.id)
151
+ }
152
+ @available(iOS 18.0, *)
153
+ public func tabBarController(_ tabBarController: UITabBarController, shouldSelectTab tab: UITab) -> Bool {
154
+ guard !applying, window != nil, !disabled, controller.tab(forIdentifier: tab.identifier) === tab else { return false }
155
+ return items.contains { $0.id == tab.identifier && $0.disabled != true }
156
+ }
157
+ @available(iOS 18.0, *)
158
+ public func tabBarController(_ tabBarController: UITabBarController, didSelectTab selectedTab: UITab, previousTab: UITab?) {
159
+ guard self.tabBarController(tabBarController, shouldSelectTab: selectedTab) else { return }
160
+ onSelectionChange?(selectedTab.identifier)
161
+ }
162
+ private func attachController() {
163
+ guard window != nil else { return }
164
+ var responder: UIResponder? = superview
165
+ while let current = responder, !(current is UIViewController) { responder = current.next }
166
+ guard let parent = responder as? UIViewController, parent !== controller else { return }
167
+ if controller.parent !== parent {
168
+ detachController()
169
+ parent.addChild(controller)
170
+ addSubview(controller.view)
171
+ controller.didMove(toParent: parent)
172
+ }
173
+ controller.view.frame = bounds
174
+ }
175
+ private func detachController() {
176
+ guard controller.parent != nil else { return }
177
+ controller.willMove(toParent: nil)
178
+ controller.view.removeFromSuperview()
179
+ controller.removeFromParent()
180
+ }
181
+ public override func didMoveToWindow() {
182
+ super.didMoveToWindow()
183
+ if window == nil { detachController() }
184
+ else {
185
+ attachController()
186
+ DispatchQueue.main.async { [weak self] in self?.attachController() }
187
+ }
188
+ }
189
+ public override func layoutSubviews() {
190
+ super.layoutSubviews()
191
+ attachController()
192
+ controller.view.frame = bounds
193
+ }
194
+ }
package/package.json ADDED
@@ -0,0 +1,73 @@
1
+ {
2
+ "name": "@likith99/react-native-adaptive-liquid-glass",
3
+ "version": "0.1.0",
4
+ "description": "Native iOS Liquid Glass surfaces and controls with standard Android counterparts.",
5
+ "license": "MIT",
6
+ "author": "Likith Kanneganti",
7
+ "homepage": "https://github.com/likith099/liquedGlassRecreate#readme",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/likith099/liquedGlassRecreate.git",
11
+ "directory": "packages/liquid-glass"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/likith099/liquedGlassRecreate/issues"
15
+ },
16
+ "keywords": [
17
+ "react-native",
18
+ "ios",
19
+ "android",
20
+ "liquid-glass",
21
+ "glass",
22
+ "uikit",
23
+ "swiftui",
24
+ "material",
25
+ "fabric",
26
+ "new-architecture",
27
+ "tabs",
28
+ "toolbar",
29
+ "menu",
30
+ "accessibility"
31
+ ],
32
+ "main": "src/index.ts",
33
+ "react-native": "src/index.ts",
34
+ "types": "src/index.ts",
35
+ "files": [
36
+ "src",
37
+ "ios",
38
+ "android",
39
+ "!android/build",
40
+ "!android/.gradle",
41
+ "AdaptiveLiquidGlass.podspec",
42
+ "react-native.config.js",
43
+ "README.md",
44
+ "LICENSE"
45
+ ],
46
+ "peerDependencies": {
47
+ "react": ">=19.0.0",
48
+ "react-native": ">=0.81.0 <0.87.0"
49
+ },
50
+ "codegenConfig": {
51
+ "name": "AdaptiveLiquidGlassSpec",
52
+ "type": "components",
53
+ "jsSrcsDir": "src/specs",
54
+ "android": {
55
+ "javaPackageName": "com.adaptiveglass"
56
+ },
57
+ "ios": {
58
+ "componentProvider": {
59
+ "ALGSurface": "ALGSurfaceComponentView",
60
+ "ALGButton": "ALGButtonComponentView",
61
+ "ALGSegmented": "ALGSegmentedComponentView",
62
+ "ALGSlider": "ALGSliderComponentView",
63
+ "ALGActionCluster": "ALGActionClusterComponentView",
64
+ "ALGMenu": "ALGMenuComponentView",
65
+ "ALGTabs": "ALGTabsComponentView"
66
+ }
67
+ }
68
+ },
69
+ "publishConfig": {
70
+ "access": "public",
71
+ "registry": "https://registry.npmjs.org/"
72
+ }
73
+ }
@@ -0,0 +1,3 @@
1
+ module.exports = {
2
+ dependency: {platforms: {android: {componentDescriptors: ['ALGSliderComponentDescriptor', 'ALGMenuComponentDescriptor', 'ALGTabsComponentDescriptor']}}},
3
+ };
@@ -0,0 +1,18 @@
1
+ import React from 'react';
2
+ import NativeCluster from './specs/ALGActionClusterNativeComponent';
3
+ import Fallback from './fallback/GlassActionCluster';
4
+ import {isLiquidGlassSupported} from './support';
5
+ import {validateActions} from './validateActions';
6
+ import type {GlassActionClusterProps} from './types';
7
+ export default function GlassActionCluster({actions, onAction, onExpandedChange, tintColor,
8
+ animationDuration = 0.45, pressFeedback: _pressFeedback, forceFallback, toggleLabel = 'Actions', style, ...props}: GlassActionClusterProps) {
9
+ validateActions(actions);
10
+ if (forceFallback || !isLiquidGlassSupported()) {
11
+ return <Fallback {...props} actions={actions} onAction={onAction} onExpandedChange={onExpandedChange}
12
+ style={style} toggleLabel={toggleLabel} />;
13
+ }
14
+ return <NativeCluster {...props} style={[{height: 88, minWidth: 80}, style]}
15
+ actionsJSON={JSON.stringify(actions)} glassTint={tintColor} duration={animationDuration} toggleLabel={toggleLabel}
16
+ onAction={event => onAction(event.nativeEvent.id)}
17
+ onExpandedChange={event => onExpandedChange(event.nativeEvent.expanded)} />;
18
+ }
@@ -0,0 +1 @@
1
+ export {default} from './fallback/GlassActionCluster';
@@ -0,0 +1,18 @@
1
+ import React from 'react';
2
+ import {useWindowDimensions} from 'react-native';
3
+ import adaptiveHeight from './adaptiveHeight';
4
+ import NativeButton from './specs/ALGButtonNativeComponent';
5
+ import GlassPressable from './GlassPressable';
6
+ import Fallback from './fallback/NativeGlassButton';
7
+ import {isLiquidGlassSupported} from './support';
8
+ import type {GlassButtonProps} from './types';
9
+ export default function GlassButton(props: GlassButtonProps) {
10
+ const {fontScale} = useWindowDimensions();
11
+ if (props.title === undefined) return <GlassPressable {...props} />;
12
+ if (props.forceFallback || !isLiquidGlassSupported()) return <Fallback {...props} />;
13
+ const {onPress, tintColor, forceFallback: _fallback, accessibilityLabel, accessibilityHint,
14
+ accessibilityState: _state, testID, style, ...nativeProps} = props;
15
+ return <NativeButton {...nativeProps} style={[{height: adaptiveHeight(64, 20, fontScale)}, style]} glassTint={tintColor}
16
+ accessible={false} controlLabel={accessibilityLabel ?? props.title} controlHint={accessibilityHint}
17
+ controlTestID={testID} onActivate={() => {if (!props.disabled && !props.loading) onPress();}} />;
18
+ }
@@ -0,0 +1,7 @@
1
+ import React from 'react';
2
+ import GlassPressable from './GlassPressable';
3
+ import Fallback from './fallback/NativeGlassButton';
4
+ import type {GlassButtonProps} from './types';
5
+ export default function GlassButton(props: GlassButtonProps) {
6
+ return props.title === undefined ? <GlassPressable {...props} /> : <Fallback {...props} />;
7
+ }
@@ -0,0 +1,9 @@
1
+ import React from 'react';
2
+ import {View} from 'react-native';
3
+ import NativeSurface from './specs/ALGSurfaceNativeComponent';
4
+ import {isLiquidGlassSupported} from './support';
5
+ import type {GlassContainerProps} from './types';
6
+ export default function GlassContainer({forceFallback, mergingEnabled = false, spacing, ...props}: GlassContainerProps) {
7
+ return forceFallback || !isLiquidGlassSupported() ? <View {...props} /> :
8
+ <NativeSurface {...props} container mergingEnabled={mergingEnabled} spacing={spacing} />;
9
+ }
@@ -0,0 +1,6 @@
1
+ import React from 'react';
2
+ import {View} from 'react-native';
3
+ import type {GlassContainerProps} from './types';
4
+ export default function GlassContainer({spacing: _spacing, mergingEnabled: _merging, forceFallback: _force, ...props}: GlassContainerProps) {
5
+ return <View {...props} />;
6
+ }
@@ -0,0 +1,26 @@
1
+ import React from 'react';
2
+ import {useWindowDimensions} from 'react-native';
3
+ import adaptiveHeight from './adaptiveHeight';
4
+ import NativeMenu from './specs/ALGMenuNativeComponent';
5
+ import type {GlassMenuButtonProps, GlassMenuElement} from './types';
6
+ import {enabledMenuAction, validateMenuItems} from './menuTree';
7
+
8
+ export function validateMenu(title: string, items: readonly GlassMenuElement[]) {
9
+ if (!title.trim()) throw new Error('GlassMenuButton title must be nonempty.');
10
+ validateMenuItems(items);
11
+ }
12
+
13
+ export default function GlassMenuButton({title, items, onAction, systemImage, disabled = false,
14
+ tintColor, forceFallback = false, accessibilityLabel, accessibilityHint,
15
+ accessibilityState: _state, testID, style, ...props}: GlassMenuButtonProps) {
16
+ const {fontScale} = useWindowDimensions();
17
+ validateMenu(title, items);
18
+ return <NativeMenu {...props} accessible={false} style={[{height: adaptiveHeight(64, 20, fontScale), minWidth: 120}, style]}
19
+ title={title} systemImage={systemImage} itemsJSON={JSON.stringify(items)}
20
+ disabled={disabled || items.length === 0} glassTint={tintColor} forceFallback={forceFallback}
21
+ controlLabel={accessibilityLabel ?? title} controlHint={accessibilityHint} controlTestID={testID}
22
+ onMenuAction={event => {
23
+ const item = enabledMenuAction(items, event.nativeEvent.id);
24
+ if (!disabled && item) onAction(item.id);
25
+ }} />;
26
+ }
@@ -0,0 +1,17 @@
1
+ import React from 'react';
2
+ import {Platform, Pressable} from 'react-native';
3
+ import GlassView from './GlassView';
4
+ import type {GlassPressableProps} from './types';
5
+ export default function GlassPressable({children, style, contentStyle, material, tintColor, cornerRadius = 28,
6
+ fallbackStyle, forceFallback, disabled, accessibilityState, android_ripple, ...props}: GlassPressableProps) {
7
+ return <Pressable {...props} disabled={disabled} accessibilityRole="button"
8
+ accessibilityState={{...accessibilityState, disabled: !!disabled}}
9
+ android_ripple={android_ripple ?? {color: '#80808030', foreground: true}}
10
+ style={[{borderRadius: cornerRadius, overflow: Platform.OS === 'android' ? 'hidden' : 'visible'}, style]}>
11
+ <GlassView material={material} tintColor={tintColor} cornerRadius={cornerRadius}
12
+ interactive={!disabled} forceFallback={forceFallback} fallbackStyle={fallbackStyle}
13
+ style={[{minHeight: 52, paddingHorizontal: 22, paddingVertical: 14, justifyContent: 'center', alignItems: 'center'}, contentStyle]}>
14
+ {children}
15
+ </GlassView>
16
+ </Pressable>;
17
+ }
@@ -0,0 +1,21 @@
1
+ import React from 'react';
2
+ import {useWindowDimensions} from 'react-native';
3
+ import adaptiveHeight from './adaptiveHeight';
4
+ import NativeSegmented from './specs/ALGSegmentedNativeComponent';
5
+ import Fallback from './fallback/GlassSegmentedControl';
6
+ import {isLiquidGlassSupported} from './support';
7
+ import {validateSegments} from './validateSegments';
8
+ import type {GlassSegmentedControlProps} from './types';
9
+ export default function GlassSegmentedControl(props: GlassSegmentedControlProps) {
10
+ const {fontScale} = useWindowDimensions();
11
+ validateSegments(props.options, props.value);
12
+ if (fontScale > 1.3 || props.forceFallback || !isLiquidGlassSupported()) return <Fallback {...props} />;
13
+ const {options, value, onValueChange, tintColor, forceFallback: _fallback, accessibilityLabel,
14
+ accessibilityState: _state, testID, style, ...nativeProps} = props;
15
+ return <NativeSegmented {...nativeProps} style={[{height: adaptiveHeight(52, 18, fontScale)}, style]} accessible={false}
16
+ optionsJSON={JSON.stringify(options)} selectedValue={value ?? ''} glassTint={tintColor}
17
+ controlLabel={accessibilityLabel} controlTestID={testID} onSelectionChange={event => {
18
+ const option = options.find(item => item.value === event.nativeEvent.value);
19
+ if (!props.disabled && option && !option.disabled && option.value !== value) onValueChange(option.value);
20
+ }} />;
21
+ }
@@ -0,0 +1 @@
1
+ export {default} from './fallback/GlassSegmentedControl';
@@ -0,0 +1,24 @@
1
+ import React, {useState} from 'react';
2
+ import NativeSlider from './specs/ALGSliderNativeComponent';
3
+ import {normalizeSliderValue, validateSlider} from './sliderMath';
4
+ import type {GlassSliderProps} from './types';
5
+ export default function GlassSlider({value, minimumValue = 0, maximumValue = 1, step = 0,
6
+ disabled, tintColor, onValueChange, onSlidingStart, onSlidingComplete, onSlidingCancel,
7
+ accessibilityLabel, accessibilityHint, accessibilityState: _state, testID, style, ...props}: GlassSliderProps) {
8
+ validateSlider(value, minimumValue, maximumValue, step);
9
+ const [revision, setRevision] = useState(0);
10
+ const normalize = (next: number) => normalizeSliderValue(next, minimumValue, maximumValue, step);
11
+ const reconcile = () => setRevision(previous => (previous + 1) % 2147483647);
12
+ return <NativeSlider {...props} style={[{height: 52}, style]} accessible={false}
13
+ value={normalize(value)} minimumValue={minimumValue} maximumValue={maximumValue} step={step}
14
+ disabled={disabled} glassTint={tintColor} revision={revision} controlLabel={accessibilityLabel}
15
+ controlHint={accessibilityHint} controlTestID={testID}
16
+ onSliderChange={event => {if (!disabled) onValueChange(normalize(event.nativeEvent.value));}}
17
+ onSliderStart={event => {if (!disabled) onSlidingStart?.(normalize(event.nativeEvent.value));}}
18
+ onSliderComplete={event => {
19
+ // Also force native reconciliation if the parent deliberately keeps the same value.
20
+ reconcile();
21
+ if (!disabled) onSlidingComplete?.(normalize(event.nativeEvent.value));
22
+ }}
23
+ onSliderCancel={event => {reconcile(); onSlidingCancel?.(normalize(event.nativeEvent.value));}} />;
24
+ }
@@ -0,0 +1,25 @@
1
+ import React, {useReducer} from 'react';
2
+ import {useWindowDimensions} from 'react-native';
3
+ import adaptiveHeight from './adaptiveHeight';
4
+ import NativeTabs from './specs/ALGTabsNativeComponent';
5
+ import {validateTabs} from './validateTabs';
6
+ import type {GlassTabBarProps} from './types';
7
+
8
+ export default function GlassTabBar({items, value, onValueChange, onTabReselect, disabled = false,
9
+ tintColor, testID, style, accessibilityState: _state, ...props}: GlassTabBarProps) {
10
+ const {fontScale} = useWindowDimensions();
11
+ validateTabs(items, value);
12
+ const [selectionRevision, reconcile] = useReducer((revision: number) => (revision + 1) & 0x7fffffff, 0);
13
+ return <NativeTabs {...props} accessible={false} style={[{height: adaptiveHeight(80, 24, fontScale), minWidth: 180}, style]}
14
+ itemsJSON={JSON.stringify(items)} selectedValue={value ?? ''} selectionRevision={selectionRevision}
15
+ disabled={disabled || items.length === 0} glassTint={tintColor} controlTestID={testID}
16
+ onSelectionChange={event => {
17
+ // A native tap may select provisionally. Always send a prop transaction,
18
+ // even if React rejects the request and the controlled value is unchanged.
19
+ reconcile();
20
+ const item = items.find(candidate => candidate.id === event.nativeEvent.id);
21
+ if (disabled || !item || item.disabled) return;
22
+ if (item.id === value) onTabReselect?.(item.id);
23
+ else onValueChange(item.id);
24
+ }} />;
25
+ }
@@ -0,0 +1,25 @@
1
+ import React from 'react';
2
+ import {useWindowDimensions} from 'react-native';
3
+ import adaptiveHeight from './adaptiveHeight';
4
+ import NativeMenu from './specs/ALGMenuNativeComponent';
5
+ import {enabledMenuAction, validateMenuItems} from './menuTree';
6
+ import type {GlassToolbarProps} from './types';
7
+
8
+ export default function GlassToolbar({items, onAction, disabled = false, maxVisibleItems = 3,
9
+ mergingEnabled = false, tintColor, forceFallback = false, accessibilityLabel, accessibilityHint,
10
+ accessibilityState: _state, testID, style, ...props}: GlassToolbarProps) {
11
+ const {fontScale} = useWindowDimensions();
12
+ validateMenuItems(items);
13
+ if (!Number.isInteger(maxVisibleItems) || maxVisibleItems < 0 || maxVisibleItems > 256) {
14
+ throw new Error('GlassToolbar maxVisibleItems must be an integer from 0 to 256.');
15
+ }
16
+ return <NativeMenu {...props} accessible={false} toolbar title="" itemsJSON={JSON.stringify(items)}
17
+ style={[{height: adaptiveHeight(64, 20, fontScale), minWidth: 64}, style]} disabled={disabled || items.length === 0}
18
+ maxVisibleItems={maxVisibleItems} mergingEnabled={mergingEnabled}
19
+ glassTint={tintColor} forceFallback={forceFallback} controlLabel={accessibilityLabel}
20
+ controlHint={accessibilityHint} controlTestID={testID}
21
+ onMenuAction={event => {
22
+ const item = enabledMenuAction(items, event.nativeEvent.id);
23
+ if (!disabled && item) onAction(item.id);
24
+ }} />;
25
+ }
@@ -0,0 +1,13 @@
1
+ import React from 'react';
2
+ import NativeSurface from './specs/ALGSurfaceNativeComponent';
3
+ import Fallback from './fallback/GlassView';
4
+ import {isLiquidGlassSupported} from './support';
5
+ import type {GlassViewProps} from './types';
6
+ export default function GlassView({tintColor, cornerRadius = 24, fallbackStyle, forceFallback,
7
+ style, ...props}: GlassViewProps) {
8
+ if (forceFallback) {
9
+ return <Fallback {...props} style={style} tintColor={tintColor} cornerRadius={cornerRadius} fallbackStyle={fallbackStyle} />;
10
+ }
11
+ return <NativeSurface {...props} glassTint={tintColor} glassRadius={cornerRadius}
12
+ style={isLiquidGlassSupported() ? style : [style, fallbackStyle]} />;
13
+ }
@@ -0,0 +1 @@
1
+ export {default} from './fallback/GlassView';