react-native-tinyui 0.5.0 → 1.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 (140) hide show
  1. package/README.md +468 -28
  2. package/ios/ConcentricView/TinyuiConcentricView.h +12 -0
  3. package/ios/ConcentricView/TinyuiConcentricView.mm +60 -0
  4. package/ios/Core/Tinyui.mm +54 -0
  5. package/ios/Core/TinyuiComponentClasses.mm +44 -0
  6. package/ios/Menu/TinyuiMenuBuilder.swift +238 -0
  7. package/ios/Menu/TinyuiMenuProvider.swift +141 -75
  8. package/ios/Menu/TinyuiMenuView.mm +24 -35
  9. package/ios/Popover/TinyuiPopoverProvider.swift +198 -44
  10. package/ios/Popover/TinyuiPopoverView.mm +26 -1
  11. package/ios/SFSymbol/TinyuiSFSymbolEffects.h +9 -0
  12. package/ios/SFSymbol/TinyuiSFSymbolEffects.mm +180 -0
  13. package/ios/SFSymbol/TinyuiSFSymbolImage.h +8 -0
  14. package/ios/SFSymbol/TinyuiSFSymbolImage.mm +141 -0
  15. package/ios/SFSymbol/TinyuiSFSymbolShadowNode.h +31 -0
  16. package/ios/SFSymbol/TinyuiSFSymbolShadowNode.mm +30 -0
  17. package/ios/SFSymbol/TinyuiSFSymbolView.h +4 -0
  18. package/ios/SFSymbol/TinyuiSFSymbolView.mm +173 -0
  19. package/ios/Stepper/TinyuiStepperShadowNode.h +32 -0
  20. package/ios/Stepper/TinyuiStepperShadowNode.mm +28 -0
  21. package/ios/Stepper/TinyuiStepperView.h +12 -0
  22. package/ios/Stepper/TinyuiStepperView.mm +128 -0
  23. package/ios/TipKit/TinyuiTip.swift +31 -0
  24. package/ios/TipKit/TinyuiTipPopoverProvider.swift +276 -0
  25. package/ios/TipKit/TinyuiTipPopoverView.h +8 -0
  26. package/ios/TipKit/TinyuiTipPopoverView.mm +114 -0
  27. package/ios/TipKit/TinyuiTipsRuntime.swift +148 -0
  28. package/lib/module/ConcentricView/ConcentricViewNativeComponent.ts +14 -0
  29. package/lib/module/ConcentricView/index.js +29 -0
  30. package/lib/module/ConcentricView/index.js.map +1 -0
  31. package/lib/module/ConcentricView/types.js +4 -0
  32. package/lib/module/ConcentricView/types.js.map +1 -0
  33. package/lib/module/Menu/MenuNativeComponent.ts +12 -3
  34. package/lib/module/Menu/index.js +17 -12
  35. package/lib/module/Menu/index.js.map +1 -1
  36. package/lib/module/Menu/options.js +4 -12
  37. package/lib/module/Menu/options.js.map +1 -1
  38. package/lib/module/NativeTinyui.js.map +1 -1
  39. package/lib/module/Popover/index.js +4 -2
  40. package/lib/module/Popover/index.js.map +1 -1
  41. package/lib/module/SFSymbol/SFSymbolNativeComponent.ts +20 -0
  42. package/lib/module/SFSymbol/index.js +73 -0
  43. package/lib/module/SFSymbol/index.js.map +1 -0
  44. package/lib/module/SFSymbol/options.js +79 -0
  45. package/lib/module/SFSymbol/options.js.map +1 -0
  46. package/lib/module/SFSymbol/types.js +4 -0
  47. package/lib/module/SFSymbol/types.js.map +1 -0
  48. package/lib/module/Stepper/StepperNativeComponent.ts +29 -0
  49. package/lib/module/Stepper/index.js +75 -0
  50. package/lib/module/Stepper/index.js.map +1 -0
  51. package/lib/module/Stepper/types.js +4 -0
  52. package/lib/module/Stepper/types.js.map +1 -0
  53. package/lib/module/TipKit/Popover.js +89 -0
  54. package/lib/module/TipKit/Popover.js.map +1 -0
  55. package/lib/module/TipKit/TipPopoverNativeComponent.ts +37 -0
  56. package/lib/module/TipKit/index.js +28 -0
  57. package/lib/module/TipKit/index.js.map +1 -0
  58. package/lib/module/TipKit/types.js +4 -0
  59. package/lib/module/TipKit/types.js.map +1 -0
  60. package/lib/module/TipKit/validation.js +24 -0
  61. package/lib/module/TipKit/validation.js.map +1 -0
  62. package/lib/module/index.js +4 -0
  63. package/lib/module/index.js.map +1 -1
  64. package/lib/module/utils/getNativeTinyui.js.map +1 -1
  65. package/lib/typescript/src/ConcentricView/ConcentricViewNativeComponent.d.ts +7 -0
  66. package/lib/typescript/src/ConcentricView/ConcentricViewNativeComponent.d.ts.map +1 -0
  67. package/lib/typescript/src/ConcentricView/index.d.ts +4 -0
  68. package/lib/typescript/src/ConcentricView/index.d.ts.map +1 -0
  69. package/lib/typescript/src/ConcentricView/types.d.ts +6 -0
  70. package/lib/typescript/src/ConcentricView/types.d.ts.map +1 -0
  71. package/lib/typescript/src/Menu/MenuNativeComponent.d.ts +8 -4
  72. package/lib/typescript/src/Menu/MenuNativeComponent.d.ts.map +1 -1
  73. package/lib/typescript/src/Menu/index.d.ts +1 -1
  74. package/lib/typescript/src/Menu/index.d.ts.map +1 -1
  75. package/lib/typescript/src/Menu/options.d.ts +1 -5
  76. package/lib/typescript/src/Menu/options.d.ts.map +1 -1
  77. package/lib/typescript/src/Menu/types.d.ts +26 -7
  78. package/lib/typescript/src/Menu/types.d.ts.map +1 -1
  79. package/lib/typescript/src/NativeTinyui.d.ts +2 -0
  80. package/lib/typescript/src/NativeTinyui.d.ts.map +1 -1
  81. package/lib/typescript/src/Popover/index.d.ts.map +1 -1
  82. package/lib/typescript/src/Popover/types.d.ts +2 -0
  83. package/lib/typescript/src/Popover/types.d.ts.map +1 -1
  84. package/lib/typescript/src/SFSymbol/SFSymbolNativeComponent.d.ts +11 -0
  85. package/lib/typescript/src/SFSymbol/SFSymbolNativeComponent.d.ts.map +1 -0
  86. package/lib/typescript/src/SFSymbol/index.d.ts +4 -0
  87. package/lib/typescript/src/SFSymbol/index.d.ts.map +1 -0
  88. package/lib/typescript/src/SFSymbol/options.d.ts +86 -0
  89. package/lib/typescript/src/SFSymbol/options.d.ts.map +1 -0
  90. package/lib/typescript/src/SFSymbol/types.d.ts +113 -0
  91. package/lib/typescript/src/SFSymbol/types.d.ts.map +1 -0
  92. package/lib/typescript/src/Stepper/StepperNativeComponent.d.ts +21 -0
  93. package/lib/typescript/src/Stepper/StepperNativeComponent.d.ts.map +1 -0
  94. package/lib/typescript/src/Stepper/index.d.ts +4 -0
  95. package/lib/typescript/src/Stepper/index.d.ts.map +1 -0
  96. package/lib/typescript/src/Stepper/types.d.ts +24 -0
  97. package/lib/typescript/src/Stepper/types.d.ts.map +1 -0
  98. package/lib/typescript/src/TipKit/Popover.d.ts +3 -0
  99. package/lib/typescript/src/TipKit/Popover.d.ts.map +1 -0
  100. package/lib/typescript/src/TipKit/TipPopoverNativeComponent.d.ts +38 -0
  101. package/lib/typescript/src/TipKit/TipPopoverNativeComponent.d.ts.map +1 -0
  102. package/lib/typescript/src/TipKit/index.d.ts +10 -0
  103. package/lib/typescript/src/TipKit/index.d.ts.map +1 -0
  104. package/lib/typescript/src/TipKit/types.d.ts +46 -0
  105. package/lib/typescript/src/TipKit/types.d.ts.map +1 -0
  106. package/lib/typescript/src/TipKit/validation.d.ts +4 -0
  107. package/lib/typescript/src/TipKit/validation.d.ts.map +1 -0
  108. package/lib/typescript/src/index.d.ts +9 -1
  109. package/lib/typescript/src/index.d.ts.map +1 -1
  110. package/lib/typescript/src/utils/getNativeTinyui.d.ts +1 -1
  111. package/lib/typescript/src/utils/getNativeTinyui.d.ts.map +1 -1
  112. package/package.json +37 -2
  113. package/{Tinyui.podspec → react-native-tinyui.podspec} +23 -2
  114. package/src/ConcentricView/ConcentricViewNativeComponent.ts +14 -0
  115. package/src/ConcentricView/index.tsx +34 -0
  116. package/src/ConcentricView/types.ts +6 -0
  117. package/src/Menu/MenuNativeComponent.ts +12 -3
  118. package/src/Menu/index.tsx +35 -12
  119. package/src/Menu/options.ts +2 -20
  120. package/src/Menu/types.ts +28 -7
  121. package/src/NativeTinyui.ts +2 -0
  122. package/src/Popover/index.tsx +8 -3
  123. package/src/Popover/types.ts +2 -0
  124. package/src/SFSymbol/SFSymbolNativeComponent.ts +20 -0
  125. package/src/SFSymbol/index.tsx +89 -0
  126. package/src/SFSymbol/options.ts +116 -0
  127. package/src/SFSymbol/types.ts +158 -0
  128. package/src/Stepper/StepperNativeComponent.ts +29 -0
  129. package/src/Stepper/index.tsx +91 -0
  130. package/src/Stepper/types.ts +24 -0
  131. package/src/TipKit/Popover.tsx +87 -0
  132. package/src/TipKit/TipPopoverNativeComponent.ts +37 -0
  133. package/src/TipKit/index.ts +47 -0
  134. package/src/TipKit/types.ts +57 -0
  135. package/src/TipKit/validation.ts +39 -0
  136. package/src/index.tsx +31 -0
  137. package/src/utils/getNativeTinyui.ts +8 -1
  138. package/ios/Core/TinyuiHostingHelpers.swift +0 -74
  139. package/ios/Menu/TinyuiMenuViewSwift.swift +0 -326
  140. package/ios/Popover/TinyuiPopoverViewSwift.swift +0 -88
@@ -0,0 +1,276 @@
1
+ import TipKit
2
+ import UIKit
3
+
4
+ @objc public protocol TinyuiTipPopoverDelegate: AnyObject {
5
+ func onTipStatus(tipId: String, status: String, reason: String)
6
+ func onTipVisible(tipId: String, visible: Bool)
7
+ func onTipAction(tipId: String, id: String, title: String)
8
+ func onTipError(tipId: String, code: String, message: String)
9
+ }
10
+
11
+ @MainActor
12
+ @objc public final class TinyuiTipPopoverProvider: UIView, UIPopoverPresentationControllerDelegate {
13
+ private weak var delegate: TinyuiTipPopoverDelegate?
14
+ @objc public weak var parentViewController: UIViewController? {
15
+ didSet { TinyuiTipsRuntime.shared.requestUpdate() }
16
+ }
17
+ private var tip: TinyuiTip?
18
+ private var enabled = false
19
+ private var arrowEdge = "auto"
20
+ private var observation: Task<Void, Never>?
21
+ private var observationGeneration = 0
22
+ private var status: Tips.Status?
23
+ private var lastError: String?
24
+ private var suppressed = false
25
+ private var controller: TipUIPopoverViewController?
26
+ private var presentedTipId: String?
27
+ private var isPresenting = false
28
+ private var isDismissing = false
29
+ private var reportedVisible = false
30
+
31
+ @objc public convenience init(delegate: TinyuiTipPopoverDelegate) {
32
+ self.init(frame: .zero)
33
+ self.delegate = delegate
34
+ }
35
+
36
+ @objc public func configure(_ configuration: [String: Any], enabled: Bool, arrowEdge: String) {
37
+ let nextTip = TinyuiTip(
38
+ id: configuration["tipId"] as? String ?? "",
39
+ titleString: configuration["title"] as? String ?? "",
40
+ messageString: configuration["message"] as? String ?? "",
41
+ symbolName: configuration["systemImage"] as? String ?? "",
42
+ actionContents: (configuration["actions"] as? [[String: String]] ?? []).map {
43
+ TinyuiTip.ActionContent(id: $0["id"] ?? "", title: $0["title"] ?? "")
44
+ },
45
+ maximumDisplayCount: configuration["maxDisplayCount"] as? Int ?? 0,
46
+ ignoresFrequency: configuration["ignoresDisplayFrequency"] as? Bool ?? false
47
+ )
48
+ if tip?.id != nextTip.id {
49
+ stopObservation()
50
+ status = nil
51
+ lastError = nil
52
+ suppressed = false
53
+ }
54
+ if !self.enabled && enabled { suppressed = false }
55
+ tip = nextTip
56
+ self.enabled = enabled
57
+ self.arrowEdge = arrowEdge
58
+ TinyuiTipsRuntime.shared.requestUpdate()
59
+ }
60
+
61
+ override public func didMoveToWindow() {
62
+ super.didMoveToWindow()
63
+ if window == nil {
64
+ stopObservation()
65
+ dismissTip()
66
+ TinyuiTipsRuntime.shared.unregister(self)
67
+ } else {
68
+ TinyuiTipsRuntime.shared.register(self)
69
+ }
70
+ }
71
+
72
+ override public func layoutSubviews() {
73
+ super.layoutSubviews()
74
+ configureArrow()
75
+ TinyuiTipsRuntime.shared.requestUpdate()
76
+ }
77
+
78
+ var needsMonitoring: Bool {
79
+ window?.windowScene?.activationState == .foregroundActive &&
80
+ enabled && status == .available && !suppressed && lastError == nil
81
+ }
82
+
83
+ func updatePresentation() {
84
+ let runtime = TinyuiTipsRuntime.shared
85
+ guard let tip else { dismissTip(); return }
86
+ guard window != nil, runtime.isConfigured else {
87
+ if window != nil && enabled && !runtime.isConfigured {
88
+ reportError("E_TIPS_NOT_CONFIGURED", "Call and await TipKit.configure() before displaying tips.")
89
+ }
90
+ dismissTip()
91
+ return
92
+ }
93
+ do {
94
+ try runtime.remember(tip)
95
+ lastError = nil
96
+ } catch {
97
+ let error = error as NSError
98
+ reportError(error.userInfo["code"] as? String ?? "E_TIP", error.localizedDescription)
99
+ dismissTip()
100
+ return
101
+ }
102
+ startObservation(tip)
103
+
104
+ if let controller, !isPresenting, !isDismissing, controller.presentingViewController == nil {
105
+ // Dismissal may originate inside TipKit rather than through the delegate.
106
+ suppressed = true
107
+ finishDismissal(controller)
108
+ return
109
+ }
110
+ let presenter = eligiblePresenter()
111
+ guard enabled, status == .available, !suppressed, presenter != nil,
112
+ presentedTipId == nil || presentedTipId == tip.id else {
113
+ dismissTip()
114
+ return
115
+ }
116
+ guard controller == nil, runtime.activeProvider == nil, let presenter else { return }
117
+ runtime.activeProvider = self
118
+ let tipController = TipUIPopoverViewController(tip, sourceItem: self) { [weak self] action in
119
+ guard let self, self.tip?.id == tip.id,
120
+ let content = tip.actionContents.first(where: { $0.id == action.id }) else { return }
121
+ self.delegate?.onTipAction(tipId: tip.id, id: content.id, title: content.title)
122
+ }
123
+ tipController.presentationDelegate = self
124
+ controller = tipController
125
+ presentedTipId = tip.id
126
+ configureArrow()
127
+ isPresenting = true
128
+ presenter.present(tipController, animated: true) { [weak self, weak tipController] in
129
+ guard let self, let tipController, self.controller === tipController else { return }
130
+ self.isPresenting = false
131
+ if tipController.presentingViewController != nil {
132
+ self.reportedVisible = true
133
+ self.delegate?.onTipVisible(tipId: tip.id, visible: true)
134
+ }
135
+ TinyuiTipsRuntime.shared.requestUpdate()
136
+ }
137
+ }
138
+
139
+ private func startObservation(_ tip: TinyuiTip) {
140
+ guard observation == nil else { return }
141
+ observationGeneration += 1
142
+ let generation = observationGeneration
143
+ observation = Task { @MainActor [weak self] in
144
+ for await status in tip.statusUpdates {
145
+ guard !Task.isCancelled, let self, self.observationGeneration == generation else { break }
146
+ self.status = status
147
+ let state: String
148
+ let reason: String
149
+ switch status {
150
+ case .pending: state = "pending"; reason = ""
151
+ case .available: state = "available"; reason = ""
152
+ case .invalidated(let invalidation):
153
+ state = "invalidated"
154
+ switch invalidation {
155
+ case .actionPerformed: reason = "actionPerformed"
156
+ case .tipClosed: reason = "tipClosed"
157
+ case .displayCountExceeded: reason = "displayCountExceeded"
158
+ case .displayDurationExceeded: reason = "displayDurationExceeded"
159
+ @unknown default: reason = "unknown"
160
+ }
161
+ @unknown default: state = "pending"; reason = ""
162
+ }
163
+ self.delegate?.onTipStatus(tipId: tip.id, status: state, reason: reason)
164
+ TinyuiTipsRuntime.shared.requestUpdate()
165
+ }
166
+ }
167
+ }
168
+
169
+ private func stopObservation() {
170
+ observationGeneration += 1
171
+ observation?.cancel()
172
+ observation = nil
173
+ status = nil
174
+ }
175
+
176
+ /// Reject obscured/detached anchors and non-current navigation/tab screens.
177
+ private func eligiblePresenter() -> UIViewController? {
178
+ guard let window, window.windowScene?.activationState == .foregroundActive,
179
+ !window.isHidden, let parent = parentViewController,
180
+ parent.viewIfLoaded?.window === window, !bounds.isEmpty else { return nil }
181
+
182
+ var visibleRect = convert(bounds, to: window)
183
+ var ancestor: UIView? = self
184
+ while let view = ancestor {
185
+ guard !view.isHidden, view.alpha > 0.01 else { return nil }
186
+ if view.clipsToBounds {
187
+ visibleRect = visibleRect.intersection(view.convert(view.bounds, to: window))
188
+ }
189
+ ancestor = view.superview
190
+ }
191
+ guard !visibleRect.intersection(window.bounds).isEmpty else { return nil }
192
+
193
+ var child = parent
194
+ var ancestorController: UIViewController? = parent
195
+ while let current = ancestorController {
196
+ guard !current.isBeingDismissed, !current.isBeingPresented else { return nil }
197
+ if let navigation = current as? UINavigationController,
198
+ navigation.topViewController !== child { return nil }
199
+ if let tabs = current as? UITabBarController,
200
+ tabs.selectedViewController !== child { return nil }
201
+ if let presented = current.presentedViewController, presented !== controller { return nil }
202
+ // Wait for navigation animations before presenting. Do not treat our own
203
+ // popover's presentation transition as the anchor leaving the screen.
204
+ if controller == nil && current.transitionCoordinator != nil { return nil }
205
+ child = current
206
+ ancestorController = current.parent
207
+ }
208
+ return parent
209
+ }
210
+
211
+ private func configureArrow() {
212
+ guard let popover = controller?.popoverPresentationController else { return }
213
+ popover.sourceView = self
214
+ popover.sourceRect = bounds
215
+ let isRTL = effectiveUserInterfaceLayoutDirection == .rightToLeft
216
+ switch arrowEdge {
217
+ case "top": popover.permittedArrowDirections = .up
218
+ case "bottom": popover.permittedArrowDirections = .down
219
+ case "leading": popover.permittedArrowDirections = isRTL ? .right : .left
220
+ case "trailing": popover.permittedArrowDirections = isRTL ? .left : .right
221
+ default: popover.permittedArrowDirections = .any
222
+ }
223
+ }
224
+
225
+ private func dismissTip() {
226
+ guard let controller, !isPresenting, !isDismissing else { return }
227
+ guard controller.presentingViewController != nil else { finishDismissal(controller); return }
228
+ isDismissing = true
229
+ controller.dismiss(animated: window != nil) { [weak self, weak controller] in
230
+ guard let self, let controller else { return }
231
+ self.finishDismissal(controller)
232
+ }
233
+ }
234
+
235
+ private func finishDismissal(_ dismissed: TipUIPopoverViewController) {
236
+ guard controller === dismissed else { return }
237
+ if reportedVisible, let presentedTipId {
238
+ delegate?.onTipVisible(tipId: presentedTipId, visible: false)
239
+ }
240
+ controller = nil
241
+ presentedTipId = nil
242
+ isPresenting = false
243
+ isDismissing = false
244
+ reportedVisible = false
245
+ TinyuiTipsRuntime.shared.release(self)
246
+ }
247
+
248
+ public func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
249
+ guard let controller, presentationController.presentedViewController === controller else { return }
250
+ suppressed = true
251
+ finishDismissal(controller)
252
+ }
253
+
254
+ public func adaptivePresentationStyle(for controller: UIPresentationController) -> UIModalPresentationStyle { .none }
255
+
256
+ public func adaptivePresentationStyle(for controller: UIPresentationController, traitCollection: UITraitCollection) -> UIModalPresentationStyle { .none }
257
+
258
+ private func reportError(_ code: String, _ message: String) {
259
+ guard lastError != code else { return }
260
+ lastError = code
261
+ delegate?.onTipError(tipId: tip?.id ?? "", code: code, message: message)
262
+ }
263
+
264
+ @objc public func reset() {
265
+ enabled = false
266
+ tip = nil
267
+ suppressed = false
268
+ lastError = nil
269
+ stopObservation()
270
+ dismissTip()
271
+ parentViewController = nil
272
+ TinyuiTipsRuntime.shared.unregister(self)
273
+ }
274
+
275
+ deinit { observation?.cancel() }
276
+ }
@@ -0,0 +1,8 @@
1
+ #if TINYUI_FEATURE_TIP_KIT
2
+ #import <React/RCTViewComponentView.h>
3
+
4
+ NS_ASSUME_NONNULL_BEGIN
5
+ @interface TinyuiTipPopoverView : RCTViewComponentView
6
+ @end
7
+ NS_ASSUME_NONNULL_END
8
+ #endif
@@ -0,0 +1,114 @@
1
+ #if TINYUI_FEATURE_TIP_KIT
2
+ #import "TinyuiTipPopoverView.h"
3
+ #import <React/RCTConversions.h>
4
+ #import <React/UIView+React.h>
5
+ #import <react/renderer/components/TinyuiSpec/ComponentDescriptors.h>
6
+ #import <react/renderer/components/TinyuiSpec/EventEmitters.h>
7
+ #import <react/renderer/components/TinyuiSpec/Props.h>
8
+ #import <react/renderer/components/TinyuiSpec/RCTComponentViewHelpers.h>
9
+ #import "RCTFabricComponentsPlugins.h"
10
+ #import "Tinyui-Swift.h"
11
+
12
+ using namespace facebook::react;
13
+
14
+ @interface TinyuiTipPopoverView () <RCTTinyuiTipPopoverViewViewProtocol, TinyuiTipPopoverDelegate>
15
+ @end
16
+
17
+ @implementation TinyuiTipPopoverView {
18
+ TinyuiTipPopoverProvider *_provider;
19
+ }
20
+
21
+ + (ComponentDescriptorProvider)componentDescriptorProvider
22
+ {
23
+ return concreteComponentDescriptorProvider<TinyuiTipPopoverViewComponentDescriptor>();
24
+ }
25
+
26
+ - (instancetype)initWithFrame:(CGRect)frame
27
+ {
28
+ if (self = [super initWithFrame:frame]) {
29
+ static const auto defaultProps = std::make_shared<const TinyuiTipPopoverViewProps>();
30
+ _props = defaultProps;
31
+ _provider = [[TinyuiTipPopoverProvider alloc] initWithDelegate:self];
32
+ self.contentView = _provider;
33
+ }
34
+ return self;
35
+ }
36
+
37
+ - (void)didMoveToWindow
38
+ {
39
+ [super didMoveToWindow];
40
+ _provider.parentViewController = self.window != nil ? [self reactViewController] : nil;
41
+ }
42
+
43
+ - (void)mountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index
44
+ {
45
+ [_provider insertSubview:childComponentView atIndex:index];
46
+ }
47
+
48
+ - (void)unmountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index
49
+ {
50
+ [childComponentView removeFromSuperview];
51
+ }
52
+
53
+ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps
54
+ {
55
+ const auto &next = *std::static_pointer_cast<const TinyuiTipPopoverViewProps>(props);
56
+ NSMutableArray *actions = [NSMutableArray new];
57
+ for (const auto &action : next.actions) {
58
+ [actions addObject:@{@"id": RCTNSStringFromString(action.id), @"title": RCTNSStringFromString(action.title)}];
59
+ }
60
+ [_provider configure:@{
61
+ @"tipId": RCTNSStringFromString(next.tipId),
62
+ @"title": RCTNSStringFromString(next.title),
63
+ @"message": RCTNSStringFromString(next.message),
64
+ @"systemImage": RCTNSStringFromString(next.systemImage),
65
+ @"actions": actions,
66
+ @"maxDisplayCount": @(next.maxDisplayCount),
67
+ @"ignoresDisplayFrequency": @(next.ignoresDisplayFrequency),
68
+ } enabled:next.enabled arrowEdge:RCTNSStringFromString(next.arrowEdge)];
69
+ [super updateProps:props oldProps:oldProps];
70
+ }
71
+
72
+ - (void)onTipStatusWithTipId:(NSString *)tipId status:(NSString *)status reason:(NSString *)reason
73
+ {
74
+ if (_eventEmitter == nullptr) return;
75
+ std::static_pointer_cast<const TinyuiTipPopoverViewEventEmitter>(_eventEmitter)->onTipStatusChange({
76
+ .tipId = std::string(tipId.UTF8String ?: ""),
77
+ .status = std::string(status.UTF8String ?: ""),
78
+ .reason = std::string(reason.UTF8String ?: ""),
79
+ });
80
+ }
81
+
82
+ - (void)onTipVisibleWithTipId:(NSString *)tipId visible:(BOOL)visible
83
+ {
84
+ if (_eventEmitter == nullptr) return;
85
+ std::static_pointer_cast<const TinyuiTipPopoverViewEventEmitter>(_eventEmitter)->onTipVisibleChange({
86
+ .tipId = std::string(tipId.UTF8String ?: ""), .visible = static_cast<bool>(visible),
87
+ });
88
+ }
89
+
90
+ - (void)onTipActionWithTipId:(NSString *)tipId id:(NSString *)identifier title:(NSString *)title
91
+ {
92
+ if (_eventEmitter == nullptr) return;
93
+ std::static_pointer_cast<const TinyuiTipPopoverViewEventEmitter>(_eventEmitter)->onTipAction({
94
+ .tipId = std::string(tipId.UTF8String ?: ""),
95
+ .id = std::string(identifier.UTF8String ?: ""), .title = std::string(title.UTF8String ?: ""),
96
+ });
97
+ }
98
+
99
+ - (void)onTipErrorWithTipId:(NSString *)tipId code:(NSString *)code message:(NSString *)message
100
+ {
101
+ if (_eventEmitter == nullptr) return;
102
+ std::static_pointer_cast<const TinyuiTipPopoverViewEventEmitter>(_eventEmitter)->onTipError({
103
+ .tipId = std::string(tipId.UTF8String ?: ""),
104
+ .code = std::string(code.UTF8String ?: ""), .message = std::string(message.UTF8String ?: ""),
105
+ });
106
+ }
107
+
108
+ - (void)prepareForRecycle
109
+ {
110
+ [_provider reset];
111
+ [super prepareForRecycle];
112
+ }
113
+ @end
114
+ #endif
@@ -0,0 +1,148 @@
1
+ import TipKit
2
+ import UIKit
3
+
4
+ /// Shared across RN surfaces and Fast Refresh. TipKit's datastore owns history.
5
+ @MainActor
6
+ @objc public final class TinyuiTipsRuntime: NSObject {
7
+ static let shared = TinyuiTipsRuntime()
8
+
9
+ private final class WeakProvider {
10
+ weak var value: TinyuiTipPopoverProvider?
11
+ init(_ value: TinyuiTipPopoverProvider) { self.value = value }
12
+ }
13
+
14
+ private var frequency: String?
15
+ private var providers: [WeakProvider] = []
16
+ private var timer: Timer?
17
+ private var updateScheduled = false
18
+ private var knownTips: [String: TinyuiTip] = [:]
19
+ // Keep an in-flight presentation alive long enough to dismiss it even if
20
+ // Fabric unmounts/recycles its owning component during the animation.
21
+ var activeProvider: TinyuiTipPopoverProvider?
22
+
23
+ private override init() {
24
+ super.init()
25
+ NotificationCenter.default.addObserver(self, selector: #selector(applicationStateChanged), name: UIApplication.didBecomeActiveNotification, object: nil)
26
+ NotificationCenter.default.addObserver(self, selector: #selector(applicationStateChanged), name: UIApplication.willResignActiveNotification, object: nil)
27
+ }
28
+
29
+ @objc private func applicationStateChanged() { requestUpdate() }
30
+
31
+ var isConfigured: Bool { frequency != nil }
32
+
33
+ @objc public static func configure(_ frequency: String, completion: (NSError?) -> Void) {
34
+ do {
35
+ try shared.configure(frequency)
36
+ completion(nil)
37
+ } catch {
38
+ completion(error as NSError)
39
+ }
40
+ }
41
+
42
+ @objc public static func invalidate(_ tipId: String, reason: String, completion: (NSError?) -> Void) {
43
+ guard shared.isConfigured else {
44
+ completion(error("E_TIPS_NOT_CONFIGURED", "Call and await TipKit.configure() before invalidating a tip."))
45
+ return
46
+ }
47
+ guard !tipId.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty,
48
+ reason == "actionPerformed" || reason == "tipClosed" else {
49
+ completion(error("E_INVALID_TIP", "A nonempty tipId and a supported invalidation reason are required."))
50
+ return
51
+ }
52
+ // Tip identity is independent of its view or Swift type. This also works
53
+ // before the tip mounts, when the feature is discovered without a tip.
54
+ let tip = shared.knownTips[tipId] ?? TinyuiTip(id: tipId)
55
+ tip.invalidate(reason: reason == "tipClosed" ? .tipClosed : .actionPerformed)
56
+ shared.requestUpdate()
57
+ completion(nil)
58
+ }
59
+
60
+ private func configure(_ frequency: String) throws {
61
+ if let configuredFrequency = self.frequency {
62
+ guard configuredFrequency == frequency else {
63
+ throw Self.error("E_TIPS_ALREADY_CONFIGURED", "TipKit is already configured with a different displayFrequency. Configure it once at app startup.")
64
+ }
65
+ return
66
+ }
67
+ let nativeFrequency: Tips.ConfigurationOption.DisplayFrequency
68
+ switch frequency {
69
+ case "immediate": nativeFrequency = .immediate
70
+ case "hourly": nativeFrequency = .hourly
71
+ case "daily": nativeFrequency = .daily
72
+ case "weekly": nativeFrequency = .weekly
73
+ case "monthly": nativeFrequency = .monthly
74
+ default: throw Self.error("E_INVALID_TIPS_CONFIGURATION", "Unsupported displayFrequency.")
75
+ }
76
+ try Tips.configure([.displayFrequency(nativeFrequency)])
77
+ self.frequency = frequency
78
+ requestUpdate()
79
+ }
80
+
81
+ static func error(_ code: String, _ message: String) -> NSError {
82
+ NSError(domain: "TinyuiTips", code: 1, userInfo: [
83
+ NSLocalizedDescriptionKey: message,
84
+ "code": code,
85
+ ])
86
+ }
87
+
88
+ func register(_ provider: TinyuiTipPopoverProvider) {
89
+ providers.removeAll { $0.value == nil }
90
+ if !providers.contains(where: { $0.value === provider }) {
91
+ providers.append(WeakProvider(provider))
92
+ }
93
+ requestUpdate()
94
+ }
95
+
96
+ func unregister(_ provider: TinyuiTipPopoverProvider) {
97
+ providers.removeAll { $0.value == nil || $0.value === provider }
98
+ requestUpdate()
99
+ }
100
+
101
+ func remember(_ tip: TinyuiTip) throws {
102
+ if let previous = knownTips[tip.id],
103
+ previous.maximumDisplayCount != tip.maximumDisplayCount || previous.ignoresFrequency != tip.ignoresFrequency {
104
+ throw Self.error("E_CONFLICTING_TIP_OPTIONS", "maxDisplayCount and ignoresDisplayFrequency must remain consistent for the same tipId.")
105
+ }
106
+ knownTips[tip.id] = tip
107
+ }
108
+
109
+ func release(_ provider: TinyuiTipPopoverProvider) {
110
+ if activeProvider === provider { activeProvider = nil }
111
+ requestUpdate()
112
+ }
113
+
114
+ func requestUpdate() {
115
+ guard !updateScheduled else { return }
116
+ updateScheduled = true
117
+ DispatchQueue.main.async { [weak self] in
118
+ guard let self else { return }
119
+ self.updateScheduled = false
120
+ self.update()
121
+ }
122
+ }
123
+
124
+ private func update() {
125
+ providers.removeAll { $0.value == nil }
126
+ // Finish dismissing an old presentation before offering its slot to another.
127
+ activeProvider?.updatePresentation()
128
+ for provider in providers.compactMap(\.value) where provider !== activeProvider {
129
+ provider.updatePresentation()
130
+ }
131
+
132
+ // UIKit has no single notification for an anchor being scrolled offscreen,
133
+ // a navigation transition ending, or another modal being dismissed. Poll
134
+ // only while a tip is eligible/on screen; idle or disabled tips do no work.
135
+ let needsMonitoring = activeProvider != nil || providers.contains { $0.value?.needsMonitoring == true }
136
+ if needsMonitoring && timer == nil {
137
+ let timer = Timer(timeInterval: 0.25, repeats: true) { [weak self] _ in
138
+ Task { @MainActor [weak self] in self?.update() }
139
+ }
140
+ timer.tolerance = 0.05
141
+ RunLoop.main.add(timer, forMode: .common)
142
+ self.timer = timer
143
+ } else if !needsMonitoring {
144
+ timer?.invalidate()
145
+ timer = nil
146
+ }
147
+ }
148
+ }
@@ -0,0 +1,14 @@
1
+ import {
2
+ codegenNativeComponent,
3
+ type CodegenTypes,
4
+ type ViewProps,
5
+ } from 'react-native';
6
+
7
+ export interface NativeConcentricViewProps extends ViewProps {
8
+ minimumRadius?: CodegenTypes.WithDefault<CodegenTypes.Double, 0>;
9
+ }
10
+
11
+ export default codegenNativeComponent<NativeConcentricViewProps>(
12
+ 'TinyuiConcentricView',
13
+ { excludedPlatforms: ['android'] }
14
+ );
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+
3
+ import { forwardRef } from 'react';
4
+ import { StyleSheet } from 'react-native';
5
+ import { assertComponentEnabled } from "../utils/index.js";
6
+ import NativeConcentricView from './ConcentricViewNativeComponent';
7
+ import { jsx as _jsx } from "react/jsx-runtime";
8
+ export const ConcentricView = /*#__PURE__*/forwardRef(function ConcentricViewRoot({
9
+ minimumRadius = 0,
10
+ style,
11
+ ...viewProps
12
+ }, ref) {
13
+ assertComponentEnabled('ConcentricView');
14
+ if (!Number.isFinite(minimumRadius) || minimumRadius < 0) {
15
+ throw new RangeError('[TinyUI] ConcentricView minimumRadius must be a finite, nonnegative number.');
16
+ }
17
+ return /*#__PURE__*/_jsx(NativeConcentricView, {
18
+ ...viewProps,
19
+ ref: ref,
20
+ minimumRadius: minimumRadius,
21
+ style: [styles.container, style]
22
+ });
23
+ });
24
+ const styles = StyleSheet.create({
25
+ container: {
26
+ overflow: 'hidden'
27
+ }
28
+ });
29
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["forwardRef","StyleSheet","assertComponentEnabled","NativeConcentricView","jsx","_jsx","ConcentricView","ConcentricViewRoot","minimumRadius","style","viewProps","ref","Number","isFinite","RangeError","styles","container","create","overflow"],"sourceRoot":"../../../src","sources":["ConcentricView/index.tsx"],"mappings":";;AAAA,SAASA,UAAU,QAA2B,OAAO;AACrD,SAASC,UAAU,QAAmB,cAAc;AAEpD,SAASC,sBAAsB,QAAQ,mBAAU;AACjD,OAAOC,oBAAoB,MAAM,iCAAiC;AAAC,SAAAC,GAAA,IAAAC,IAAA;AAGnE,OAAO,MAAMC,cAAc,gBAAGN,UAAU,CAGtC,SAASO,kBAAkBA,CAAC;EAAEC,aAAa,GAAG,CAAC;EAAEC,KAAK;EAAE,GAAGC;AAAU,CAAC,EAAEC,GAAG,EAAE;EAC7ET,sBAAsB,CAAC,gBAAgB,CAAC;EAExC,IAAI,CAACU,MAAM,CAACC,QAAQ,CAACL,aAAa,CAAC,IAAIA,aAAa,GAAG,CAAC,EAAE;IACxD,MAAM,IAAIM,UAAU,CAClB,6EACF,CAAC;EACH;EAEA,oBACET,IAAA,CAACF,oBAAoB;IAAA,GACfO,SAAS;IACbC,GAAG,EAAEA,GAAI;IACTH,aAAa,EAAEA,aAAc;IAC7BC,KAAK,EAAE,CAACM,MAAM,CAACC,SAAS,EAAEP,KAAK;EAAE,CAClC,CAAC;AAEN,CAAC,CAAC;AAEF,MAAMM,MAAM,GAAGd,UAAU,CAACgB,MAAM,CAAC;EAC/BD,SAAS,EAAE;IAAEE,QAAQ,EAAE;EAAS;AAClC,CAAC,CAAC","ignoreList":[]}
@@ -0,0 +1,4 @@
1
+ "use strict";
2
+
3
+ export {};
4
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":[],"sourceRoot":"../../../src","sources":["ConcentricView/types.ts"],"mappings":"","ignoreList":[]}
@@ -1,6 +1,9 @@
1
+ import type * as React from 'react';
1
2
  import {
2
3
  codegenNativeComponent,
4
+ codegenNativeCommands,
3
5
  type CodegenTypes,
6
+ type HostComponent,
4
7
  type ViewProps,
5
8
  } from 'react-native';
6
9
  import type { DirectEventHandler } from 'react-native/Libraries/Types/CodegenTypes';
@@ -9,11 +12,17 @@ export interface NativeMenuProps extends ViewProps {
9
12
  menuConfig: CodegenTypes.UnsafeMixed;
10
13
  title?: string;
11
14
  disabled?: boolean;
12
- hasPrimaryAction?: boolean;
13
- onItemPress?: DirectEventHandler<Readonly<{ id: string }>>;
14
- onPrimaryAction?: DirectEventHandler<null>;
15
+ onActionPress?: DirectEventHandler<Readonly<{ id: string; title: string }>>;
15
16
  }
16
17
 
18
+ interface NativeCommands {
19
+ open: (viewRef: React.ComponentRef<HostComponent<NativeMenuProps>>) => void;
20
+ }
21
+
22
+ export const Commands = codegenNativeCommands<NativeCommands>({
23
+ supportedCommands: ['open'],
24
+ });
25
+
17
26
  export default codegenNativeComponent<NativeMenuProps>('TinyuiMenuView', {
18
27
  excludedPlatforms: ['android'],
19
28
  });