react-native-tinyui 0.4.0 → 1.0.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 (157) hide show
  1. package/README.md +383 -53
  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/LiquidGlassText/TinyuiLiquidGlassTextProvider.swift +64 -79
  7. package/ios/LiquidGlassText/TinyuiLiquidGlassTextShadowNode.h +46 -0
  8. package/ios/LiquidGlassText/TinyuiLiquidGlassTextShadowNode.mm +159 -0
  9. package/ios/LiquidGlassText/TinyuiLiquidGlassTextView.mm +6 -27
  10. package/ios/LiquidGlassText/TinyuiTextOutlineShape.swift +5 -2
  11. package/ios/Menu/TinyuiMenuBuilder.swift +238 -0
  12. package/ios/Menu/TinyuiMenuProvider.swift +51 -85
  13. package/ios/Menu/TinyuiMenuView.mm +13 -35
  14. package/ios/Popover/TinyuiPopoverProvider.swift +198 -44
  15. package/ios/Popover/TinyuiPopoverView.mm +26 -1
  16. package/ios/SFSymbol/TinyuiSFSymbolEffects.h +9 -0
  17. package/ios/SFSymbol/TinyuiSFSymbolEffects.mm +180 -0
  18. package/ios/SFSymbol/TinyuiSFSymbolImage.h +8 -0
  19. package/ios/SFSymbol/TinyuiSFSymbolImage.mm +141 -0
  20. package/ios/SFSymbol/TinyuiSFSymbolShadowNode.h +31 -0
  21. package/ios/SFSymbol/TinyuiSFSymbolShadowNode.mm +30 -0
  22. package/ios/SFSymbol/TinyuiSFSymbolView.h +4 -0
  23. package/ios/SFSymbol/TinyuiSFSymbolView.mm +173 -0
  24. package/ios/Stepper/TinyuiStepperShadowNode.h +32 -0
  25. package/ios/Stepper/TinyuiStepperShadowNode.mm +28 -0
  26. package/ios/Stepper/TinyuiStepperView.h +12 -0
  27. package/ios/Stepper/TinyuiStepperView.mm +128 -0
  28. package/ios/TipKit/TinyuiTip.swift +31 -0
  29. package/ios/TipKit/TinyuiTipPopoverProvider.swift +276 -0
  30. package/ios/TipKit/TinyuiTipPopoverView.h +8 -0
  31. package/ios/TipKit/TinyuiTipPopoverView.mm +114 -0
  32. package/ios/TipKit/TinyuiTipsRuntime.swift +148 -0
  33. package/lib/module/ConcentricView/ConcentricViewNativeComponent.ts +14 -0
  34. package/lib/module/ConcentricView/index.js +29 -0
  35. package/lib/module/ConcentricView/index.js.map +1 -0
  36. package/lib/module/ConcentricView/types.js +4 -0
  37. package/lib/module/ConcentricView/types.js.map +1 -0
  38. package/lib/module/LiquidGlassText/LiquidGlassTextNativeComponent.ts +2 -12
  39. package/lib/module/LiquidGlassText/index.js +60 -35
  40. package/lib/module/LiquidGlassText/index.js.map +1 -1
  41. package/lib/module/Menu/MenuNativeComponent.ts +7 -5
  42. package/lib/module/Menu/index.js +4 -9
  43. package/lib/module/Menu/index.js.map +1 -1
  44. package/lib/module/Menu/options.js +4 -12
  45. package/lib/module/Menu/options.js.map +1 -1
  46. package/lib/module/NativeTinyui.js.map +1 -1
  47. package/lib/module/Popover/index.js +4 -2
  48. package/lib/module/Popover/index.js.map +1 -1
  49. package/lib/module/SFSymbol/SFSymbolNativeComponent.ts +20 -0
  50. package/lib/module/SFSymbol/index.js +73 -0
  51. package/lib/module/SFSymbol/index.js.map +1 -0
  52. package/lib/module/SFSymbol/options.js +79 -0
  53. package/lib/module/SFSymbol/options.js.map +1 -0
  54. package/lib/module/SFSymbol/types.js +4 -0
  55. package/lib/module/SFSymbol/types.js.map +1 -0
  56. package/lib/module/Stepper/StepperNativeComponent.ts +29 -0
  57. package/lib/module/Stepper/index.js +75 -0
  58. package/lib/module/Stepper/index.js.map +1 -0
  59. package/lib/module/Stepper/types.js +4 -0
  60. package/lib/module/Stepper/types.js.map +1 -0
  61. package/lib/module/TipKit/Popover.js +89 -0
  62. package/lib/module/TipKit/Popover.js.map +1 -0
  63. package/lib/module/TipKit/TipPopoverNativeComponent.ts +37 -0
  64. package/lib/module/TipKit/index.js +28 -0
  65. package/lib/module/TipKit/index.js.map +1 -0
  66. package/lib/module/TipKit/types.js +4 -0
  67. package/lib/module/TipKit/types.js.map +1 -0
  68. package/lib/module/TipKit/validation.js +24 -0
  69. package/lib/module/TipKit/validation.js.map +1 -0
  70. package/lib/module/index.js +4 -0
  71. package/lib/module/index.js.map +1 -1
  72. package/lib/module/utils/getNativeTinyui.js.map +1 -1
  73. package/lib/typescript/src/ConcentricView/ConcentricViewNativeComponent.d.ts +7 -0
  74. package/lib/typescript/src/ConcentricView/ConcentricViewNativeComponent.d.ts.map +1 -0
  75. package/lib/typescript/src/ConcentricView/index.d.ts +4 -0
  76. package/lib/typescript/src/ConcentricView/index.d.ts.map +1 -0
  77. package/lib/typescript/src/ConcentricView/types.d.ts +6 -0
  78. package/lib/typescript/src/ConcentricView/types.d.ts.map +1 -0
  79. package/lib/typescript/src/LiquidGlassText/LiquidGlassTextNativeComponent.d.ts +2 -8
  80. package/lib/typescript/src/LiquidGlassText/LiquidGlassTextNativeComponent.d.ts.map +1 -1
  81. package/lib/typescript/src/LiquidGlassText/index.d.ts +1 -1
  82. package/lib/typescript/src/LiquidGlassText/index.d.ts.map +1 -1
  83. package/lib/typescript/src/LiquidGlassText/types.d.ts +13 -22
  84. package/lib/typescript/src/LiquidGlassText/types.d.ts.map +1 -1
  85. package/lib/typescript/src/Menu/MenuNativeComponent.d.ts +4 -5
  86. package/lib/typescript/src/Menu/MenuNativeComponent.d.ts.map +1 -1
  87. package/lib/typescript/src/Menu/index.d.ts +1 -1
  88. package/lib/typescript/src/Menu/index.d.ts.map +1 -1
  89. package/lib/typescript/src/Menu/options.d.ts +40 -4
  90. package/lib/typescript/src/Menu/options.d.ts.map +1 -1
  91. package/lib/typescript/src/Menu/types.d.ts +16 -4
  92. package/lib/typescript/src/Menu/types.d.ts.map +1 -1
  93. package/lib/typescript/src/NativeTinyui.d.ts +2 -0
  94. package/lib/typescript/src/NativeTinyui.d.ts.map +1 -1
  95. package/lib/typescript/src/Popover/index.d.ts.map +1 -1
  96. package/lib/typescript/src/Popover/types.d.ts +2 -0
  97. package/lib/typescript/src/Popover/types.d.ts.map +1 -1
  98. package/lib/typescript/src/SFSymbol/SFSymbolNativeComponent.d.ts +11 -0
  99. package/lib/typescript/src/SFSymbol/SFSymbolNativeComponent.d.ts.map +1 -0
  100. package/lib/typescript/src/SFSymbol/index.d.ts +4 -0
  101. package/lib/typescript/src/SFSymbol/index.d.ts.map +1 -0
  102. package/lib/typescript/src/SFSymbol/options.d.ts +86 -0
  103. package/lib/typescript/src/SFSymbol/options.d.ts.map +1 -0
  104. package/lib/typescript/src/SFSymbol/types.d.ts +113 -0
  105. package/lib/typescript/src/SFSymbol/types.d.ts.map +1 -0
  106. package/lib/typescript/src/Stepper/StepperNativeComponent.d.ts +21 -0
  107. package/lib/typescript/src/Stepper/StepperNativeComponent.d.ts.map +1 -0
  108. package/lib/typescript/src/Stepper/index.d.ts +4 -0
  109. package/lib/typescript/src/Stepper/index.d.ts.map +1 -0
  110. package/lib/typescript/src/Stepper/types.d.ts +24 -0
  111. package/lib/typescript/src/Stepper/types.d.ts.map +1 -0
  112. package/lib/typescript/src/TipKit/Popover.d.ts +3 -0
  113. package/lib/typescript/src/TipKit/Popover.d.ts.map +1 -0
  114. package/lib/typescript/src/TipKit/TipPopoverNativeComponent.d.ts +38 -0
  115. package/lib/typescript/src/TipKit/TipPopoverNativeComponent.d.ts.map +1 -0
  116. package/lib/typescript/src/TipKit/index.d.ts +10 -0
  117. package/lib/typescript/src/TipKit/index.d.ts.map +1 -0
  118. package/lib/typescript/src/TipKit/types.d.ts +46 -0
  119. package/lib/typescript/src/TipKit/types.d.ts.map +1 -0
  120. package/lib/typescript/src/TipKit/validation.d.ts +4 -0
  121. package/lib/typescript/src/TipKit/validation.d.ts.map +1 -0
  122. package/lib/typescript/src/index.d.ts +10 -2
  123. package/lib/typescript/src/index.d.ts.map +1 -1
  124. package/lib/typescript/src/utils/getNativeTinyui.d.ts +1 -1
  125. package/lib/typescript/src/utils/getNativeTinyui.d.ts.map +1 -1
  126. package/package.json +37 -2
  127. package/{Tinyui.podspec → react-native-tinyui.podspec} +23 -2
  128. package/src/ConcentricView/ConcentricViewNativeComponent.ts +14 -0
  129. package/src/ConcentricView/index.tsx +34 -0
  130. package/src/ConcentricView/types.ts +6 -0
  131. package/src/LiquidGlassText/LiquidGlassTextNativeComponent.ts +2 -12
  132. package/src/LiquidGlassText/index.tsx +73 -39
  133. package/src/LiquidGlassText/types.ts +24 -50
  134. package/src/Menu/MenuNativeComponent.ts +7 -5
  135. package/src/Menu/index.tsx +6 -6
  136. package/src/Menu/options.ts +4 -18
  137. package/src/Menu/types.ts +18 -4
  138. package/src/NativeTinyui.ts +2 -0
  139. package/src/Popover/index.tsx +8 -3
  140. package/src/Popover/types.ts +2 -0
  141. package/src/SFSymbol/SFSymbolNativeComponent.ts +20 -0
  142. package/src/SFSymbol/index.tsx +89 -0
  143. package/src/SFSymbol/options.ts +116 -0
  144. package/src/SFSymbol/types.ts +158 -0
  145. package/src/Stepper/StepperNativeComponent.ts +29 -0
  146. package/src/Stepper/index.tsx +91 -0
  147. package/src/Stepper/types.ts +24 -0
  148. package/src/TipKit/Popover.tsx +87 -0
  149. package/src/TipKit/TipPopoverNativeComponent.ts +37 -0
  150. package/src/TipKit/index.ts +47 -0
  151. package/src/TipKit/types.ts +57 -0
  152. package/src/TipKit/validation.ts +39 -0
  153. package/src/index.tsx +32 -4
  154. package/src/utils/getNativeTinyui.ts +8 -1
  155. package/ios/Core/TinyuiHostingHelpers.swift +0 -74
  156. package/ios/Menu/TinyuiMenuViewSwift.swift +0 -408
  157. package/ios/Popover/TinyuiPopoverViewSwift.swift +0 -88
package/src/index.tsx CHANGED
@@ -1,6 +1,18 @@
1
1
  export { Menu } from './Menu';
2
+ export { TipKit } from './TipKit';
3
+ export type {
4
+ TipAction,
5
+ TipDisplayFrequency,
6
+ TipError,
7
+ TipInvalidationReason,
8
+ TipPopoverProps,
9
+ TipKitConfiguration,
10
+ TipStatus,
11
+ } from './TipKit';
2
12
  export type {
3
13
  MenuActionOption,
14
+ MenuActionPressEvent,
15
+ MenuActionPressEventData,
4
16
  MenuDividerOption,
5
17
  MenuItemState,
6
18
  MenuOption,
@@ -9,6 +21,24 @@ export type {
9
21
  MenuSubmenuOption,
10
22
  } from './Menu';
11
23
 
24
+ export { Stepper } from './Stepper';
25
+ export type { StepperProps } from './Stepper';
26
+
27
+ export { ConcentricView } from './ConcentricView';
28
+ export type { ConcentricViewProps } from './ConcentricView';
29
+
30
+ export { SFSymbol } from './SFSymbol';
31
+ export type {
32
+ SFSymbolContentTransition,
33
+ SFSymbolEffect,
34
+ SFSymbolEffectOptions,
35
+ SFSymbolProps,
36
+ SFSymbolRenderingMode,
37
+ SFSymbolScale,
38
+ SFSymbolTextStyle,
39
+ SFSymbolWeight,
40
+ } from './SFSymbol';
41
+
12
42
  export { Popover, PopoverClose } from './Popover';
13
43
  export type {
14
44
  PopoverArrowEdge,
@@ -24,10 +54,8 @@ export type { TinyuiComponent } from './utils';
24
54
 
25
55
  export { LiquidGlassText } from './LiquidGlassText';
26
56
  export type {
27
- LiquidGlassTextFont,
28
57
  LiquidGlassTextFontDesign,
29
- LiquidGlassTextFontStyle,
30
- LiquidGlassTextFontWeight,
31
- LiquidGlassTextGlass,
58
+ LiquidGlassTextEffect,
32
59
  LiquidGlassTextProps,
60
+ LiquidGlassTextStyle,
33
61
  } from './LiquidGlassText';
@@ -1,6 +1,13 @@
1
1
  import NativeTinyui, { type Spec } from '../NativeTinyui';
2
2
 
3
- export type TinyuiComponent = 'Menu' | 'Popover' | 'LiquidGlassText';
3
+ export type TinyuiComponent =
4
+ | 'Menu'
5
+ | 'Popover'
6
+ | 'LiquidGlassText'
7
+ | 'Stepper'
8
+ | 'ConcentricView'
9
+ | 'SFSymbol'
10
+ | 'TipKit';
4
11
 
5
12
  let nativeTinyui: Spec | null | undefined;
6
13
  let enabledComponents: ReadonlySet<string> | undefined;
@@ -1,74 +0,0 @@
1
- import SwiftUI
2
- import UIKit
3
-
4
- /**
5
- * Identifiable wrapper used to render React Native views (UIView) inside
6
- * SwiftUI. Modeled after react-native-pager-view's bridge.
7
- */
8
- struct IdentifiablePlatformView: Identifiable, Equatable {
9
- let id = UUID()
10
- let view: UIView
11
-
12
- init(_ view: UIView) {
13
- self.view = view
14
- }
15
- }
16
-
17
- /**
18
- * Renders a UIView inside SwiftUI. Uses UIViewControllerRepresentable so the
19
- * hosted React view keeps a valid view-controller hierarchy.
20
- */
21
- struct RepresentableView: UIViewControllerRepresentable {
22
- var view: UIView
23
-
24
- func makeUIViewController(context: Context) -> PlatformChildViewController {
25
- let viewController = PlatformChildViewController()
26
- viewController.wrappedView = view
27
- return viewController
28
- }
29
-
30
- func updateUIViewController(
31
- _ uiViewController: PlatformChildViewController,
32
- context: Context
33
- ) {
34
- if uiViewController.wrappedView !== view {
35
- uiViewController.wrappedView = view
36
- }
37
- }
38
- }
39
-
40
- class PlatformChildViewController: UIViewController {
41
- var wrappedView: UIView? {
42
- didSet {
43
- guard oldValue !== wrappedView else { return }
44
- oldValue?.removeFromSuperview()
45
- if isViewLoaded {
46
- mountWrappedView()
47
- }
48
- }
49
- }
50
-
51
- override func viewDidLoad() {
52
- super.viewDidLoad()
53
- view.backgroundColor = .clear
54
- mountWrappedView()
55
- }
56
-
57
- private func mountWrappedView() {
58
- guard let wrappedView else { return }
59
- view.addSubview(wrappedView)
60
- wrappedView.translatesAutoresizingMaskIntoConstraints = false
61
- wrappedView.pinEdges(to: view)
62
- }
63
- }
64
-
65
- extension UIView {
66
- func pinEdges(to other: UIView) {
67
- NSLayoutConstraint.activate([
68
- leadingAnchor.constraint(equalTo: other.leadingAnchor),
69
- trailingAnchor.constraint(equalTo: other.trailingAnchor),
70
- topAnchor.constraint(equalTo: other.topAnchor),
71
- bottomAnchor.constraint(equalTo: other.bottomAnchor),
72
- ])
73
- }
74
- }
@@ -1,408 +0,0 @@
1
- import SwiftUI
2
- import UIKit
3
-
4
- /**
5
- * Observable props shared between the Objective-C++ component view and the
6
- * SwiftUI `Menu` implementation. Mirror of `TinyuiMenuProps` in codegen.
7
- */
8
- class TinyuiMenuProps: ObservableObject {
9
- @Published var menuConfig: String = "{}"
10
- @Published var title: String = ""
11
- @Published var disabled: Bool = false
12
- @Published var hasPrimaryAction: Bool = false
13
- @Published var triggerView: UIView?
14
- }
15
-
16
- /**
17
- * SwiftUI `Menu` backed by UIKit's UIMenu, matching expo-ui's MenuView.
18
- *
19
- * The React Native trigger (a UIView) is embedded as the menu label through
20
- * `RepresentableView`, so the tap target remains the native RN subtree while
21
- * the menu content is built natively from the JSON config.
22
- *
23
- * Named distinctly from the Fabric component view `TinyuiMenuView`.
24
- */
25
- struct TinyuiMenuSwiftUIView: View {
26
- @ObservedObject var props: TinyuiMenuProps
27
- weak var delegate: TinyuiMenuViewDelegate?
28
-
29
- var body: some View {
30
- Group {
31
- if let triggerView = props.triggerView {
32
- if props.hasPrimaryAction {
33
- Menu {
34
- menuContent
35
- } label: {
36
- RepresentableView(view: triggerView)
37
- } primaryAction: {
38
- delegate?.onPrimaryAction()
39
- }
40
- .disabled(props.disabled)
41
- } else {
42
- Menu {
43
- menuContent
44
- } label: {
45
- RepresentableView(view: triggerView)
46
- }
47
- .disabled(props.disabled)
48
- }
49
- } else {
50
- // No trigger mounted yet; render an empty view until the RN child
51
- // is attached to the provider.
52
- Color.clear
53
- }
54
- }
55
- }
56
-
57
- @ViewBuilder
58
- private var menuContent: some View {
59
- if props.title.isEmpty {
60
- elementsContent
61
- } else {
62
- Section {
63
- elementsContent
64
- } header: {
65
- Text(props.title)
66
- }
67
- }
68
- }
69
-
70
- @ViewBuilder
71
- private var elementsContent: some View {
72
- ForEach(buildElements(from: props.menuConfig)) { element in
73
- element.buildView(delegate: delegate)
74
- }
75
- }
76
-
77
- /// Parses the JSON menu config into a list of SwiftUI element builders.
78
- private func buildElements(from json: String) -> [TinyuiMenuElement] {
79
- guard
80
- let data = json.data(using: .utf8),
81
- let root = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
82
- let items = root["items"] as? [[String: Any]]
83
- else {
84
- return []
85
- }
86
- return items.map { TinyuiMenuElement(dictionary: $0) }
87
- }
88
- }
89
-
90
- /**
91
- * Lightweight representation of a single UIMenu element parsed from the JSON
92
- * config. Used to build SwiftUI menu content without resolving all elements
93
- * eagerly. Returns `AnyView` to avoid recursive opaque return type errors.
94
- */
95
- struct TinyuiMenuElement: Identifiable {
96
- let id = UUID()
97
- private let dictionary: [String: Any]
98
-
99
- init(dictionary: [String: Any]) {
100
- self.dictionary = dictionary
101
- }
102
-
103
- func buildView(
104
- delegate: TinyuiMenuViewDelegate?,
105
- inheritedDestructive: Bool = false
106
- ) -> AnyView {
107
- if dictionary["hidden"] as? Bool ?? false {
108
- return AnyView(EmptyView())
109
- }
110
-
111
- switch dictionary["type"] as? String {
112
- case "action":
113
- let destructive = inheritedDestructive || (dictionary["destructive"] as? Bool ?? false)
114
- let keepOpen = dictionary["keepOpen"] as? Bool ?? false
115
- let id = dictionary["id"] as? String ?? ""
116
- let state = dictionary["state"] as? String ?? "off"
117
-
118
- // `on`/`off` use SwiftUI's native `Toggle` support inside `Menu`,
119
- // which renders the system checkmark. `mixed` has no native SwiftUI
120
- // equivalent, so it falls back to the minus indicator.
121
- if state == "on" || state == "off" {
122
- let isOn = Binding<Bool>(
123
- get: { state == "on" },
124
- set: { _ in delegate?.onItemPress(id: id) }
125
- )
126
- return AnyView(
127
- Toggle(isOn: isOn) {
128
- TinyuiMenuLabel(
129
- dictionary: dictionary,
130
- destructive: destructive,
131
- showsState: false
132
- )
133
- }
134
- .disabled(dictionary["disabled"] as? Bool ?? false)
135
- )
136
- }
137
-
138
- return AnyView(
139
- Button(role: destructive ? .destructive : nil) {
140
- delegate?.onItemPress(id: id)
141
- } label: {
142
- TinyuiMenuLabel(
143
- dictionary: dictionary,
144
- destructive: destructive,
145
- showsState: true
146
- )
147
- }
148
- .disabled(dictionary["disabled"] as? Bool ?? false)
149
- .menuActionDismissBehavior(keepOpen ? .disabled : .automatic)
150
- )
151
-
152
- case "submenu":
153
- let children = (dictionary["children"] as? [[String: Any]] ?? [])
154
- .map { TinyuiMenuElement(dictionary: $0) }
155
- let destructive = inheritedDestructive || (dictionary["destructive"] as? Bool ?? false)
156
- let disabled = dictionary["disabled"] as? Bool ?? false
157
-
158
- if dictionary["displayInline"] as? Bool ?? false {
159
- return AnyView(
160
- Section {
161
- ForEach(children) { child in
162
- child.buildView(
163
- delegate: delegate,
164
- inheritedDestructive: destructive
165
- )
166
- }
167
- } header: {
168
- TinyuiMenuLabel(
169
- dictionary: dictionary,
170
- destructive: destructive,
171
- showsState: false
172
- )
173
- }
174
- .disabled(disabled)
175
- )
176
- }
177
-
178
- return AnyView(
179
- Menu {
180
- ForEach(children) { child in
181
- child.buildView(
182
- delegate: delegate,
183
- inheritedDestructive: destructive
184
- )
185
- }
186
- } label: {
187
- TinyuiMenuLabel(
188
- dictionary: dictionary,
189
- destructive: destructive,
190
- showsState: false
191
- )
192
- }
193
- .disabled(disabled)
194
- )
195
-
196
- case "section":
197
- let children = (dictionary["children"] as? [[String: Any]] ?? [])
198
- .map { TinyuiMenuElement(dictionary: $0) }
199
- return AnyView(
200
- Section {
201
- ForEach(children) { child in
202
- child.buildView(
203
- delegate: delegate,
204
- inheritedDestructive: inheritedDestructive
205
- )
206
- }
207
- } header: {
208
- if let title = dictionary["title"] as? String, !title.isEmpty {
209
- Text(title)
210
- }
211
- }
212
- )
213
-
214
- case "separator":
215
- return AnyView(Divider())
216
-
217
- default:
218
- return AnyView(EmptyView())
219
- }
220
- }
221
- }
222
-
223
- /** Label content shared by actions and submenus. */
224
- private struct TinyuiMenuLabel: View {
225
- let dictionary: [String: Any]
226
- let destructive: Bool
227
- let showsState: Bool
228
-
229
- var body: some View {
230
- // SwiftUI only extracts a proper title from a menu Button's label when
231
- // the label is a single `Text` (optionally with an icon). Rendering the
232
- // title through a computed @ViewBuilder property or wrapping it in
233
- // nested containers breaks that extraction, so the title must be a
234
- // direct `Text` in the label hierarchy.
235
- HStack(spacing: 6) {
236
- menuIcon
237
- title
238
- if showsState {
239
- stateIndicator
240
- }
241
- }
242
- }
243
-
244
- private var stateIndicator: some View {
245
- // Only `mixed` reaches here; `on`/`off` are handled natively by `Toggle`.
246
- // UIKit's menu system renders "mixed" state with a minus/dash mark.
247
- if dictionary["state"] as? String == "mixed" {
248
- return AnyView(
249
- Image(systemName: "minus")
250
- .font(.system(size: 13, weight: .semibold))
251
- )
252
- }
253
- return AnyView(EmptyView())
254
- }
255
-
256
- private var title: Text {
257
- let title = dictionary["title"] as? String ?? ""
258
- let subtitle = (dictionary["subtitle"] as? String).flatMap {
259
- $0.isEmpty ? nil : $0
260
- }
261
- let text = subtitle.map { subtitle in
262
- Text("\(title)\n\(subtitle)")
263
- } ?? Text(title)
264
-
265
- if destructive {
266
- return text.foregroundColor(.red)
267
- } else if let color = TinyuiMenuColor.uiColor(from: dictionary["titleColor"]) {
268
- return text.foregroundColor(Color(uiColor: color))
269
- } else {
270
- return text
271
- }
272
- }
273
-
274
- @ViewBuilder
275
- private var menuIcon: some View {
276
- let iconColor = TinyuiMenuColor.uiColor(from: dictionary["iconColor"])
277
-
278
- if let icon = dictionary["icon"] as? String, !icon.isEmpty {
279
- if let image = UIImage(named: icon) {
280
- if let iconColor {
281
- Image(
282
- uiImage: image.withTintColor(
283
- iconColor,
284
- renderingMode: .alwaysOriginal
285
- )
286
- )
287
- } else {
288
- Image(uiImage: image.withRenderingMode(.alwaysTemplate))
289
- }
290
- }
291
- } else if let systemImage = dictionary["systemImage"] as? String,
292
- !systemImage.isEmpty {
293
- if let iconColor {
294
- Image(systemName: systemImage)
295
- .foregroundStyle(Color(uiColor: iconColor))
296
- } else {
297
- Image(systemName: systemImage)
298
- }
299
- }
300
- }
301
- }
302
-
303
- /** Converts React Native's processed ColorValue JSON into a dynamic UIColor. */
304
- private enum TinyuiMenuColor {
305
- static func uiColor(from value: Any?) -> UIColor? {
306
- if let number = value as? NSNumber {
307
- let argb = number.uint32Value
308
- return UIColor(
309
- red: CGFloat((argb >> 16) & 0xff) / 255,
310
- green: CGFloat((argb >> 8) & 0xff) / 255,
311
- blue: CGFloat(argb & 0xff) / 255,
312
- alpha: CGFloat((argb >> 24) & 0xff) / 255
313
- )
314
- }
315
-
316
- guard let object = value as? [String: Any] else {
317
- return nil
318
- }
319
-
320
- if let semanticNames = object["semantic"] as? [String] {
321
- for name in semanticNames {
322
- if let color = semanticColor(named: name) {
323
- return color
324
- }
325
- }
326
- }
327
-
328
- if let dynamic = object["dynamic"] as? [String: Any] {
329
- return UIColor { traits in
330
- let highContrast = traits.accessibilityContrast == .high
331
- let dark = traits.userInterfaceStyle == .dark
332
- let preferredKey: String
333
-
334
- switch (dark, highContrast) {
335
- case (true, true):
336
- preferredKey = "highContrastDark"
337
- case (false, true):
338
- preferredKey = "highContrastLight"
339
- case (true, false):
340
- preferredKey = "dark"
341
- case (false, false):
342
- preferredKey = "light"
343
- }
344
-
345
- return uiColor(from: dynamic[preferredKey])
346
- ?? uiColor(from: dynamic[dark ? "dark" : "light"])
347
- ?? UIColor.clear
348
- }
349
- }
350
-
351
- return nil
352
- }
353
-
354
- private static func semanticColor(named name: String) -> UIColor? {
355
- if let color = UIColor(named: name) {
356
- return color
357
- }
358
-
359
- let normalizedName = name.hasSuffix("Color")
360
- ? String(name.dropLast("Color".count))
361
- : name
362
-
363
- switch normalizedName {
364
- case "label": return .label
365
- case "secondaryLabel": return .secondaryLabel
366
- case "tertiaryLabel": return .tertiaryLabel
367
- case "quaternaryLabel": return .quaternaryLabel
368
- case "placeholderText": return .placeholderText
369
- case "separator": return .separator
370
- case "opaqueSeparator": return .opaqueSeparator
371
- case "link": return .link
372
- case "systemBackground": return .systemBackground
373
- case "secondarySystemBackground": return .secondarySystemBackground
374
- case "tertiarySystemBackground": return .tertiarySystemBackground
375
- case "systemGroupedBackground": return .systemGroupedBackground
376
- case "secondarySystemGroupedBackground": return .secondarySystemGroupedBackground
377
- case "tertiarySystemGroupedBackground": return .tertiarySystemGroupedBackground
378
- case "systemFill": return .systemFill
379
- case "secondarySystemFill": return .secondarySystemFill
380
- case "tertiarySystemFill": return .tertiarySystemFill
381
- case "quaternarySystemFill": return .quaternarySystemFill
382
- case "systemRed": return .systemRed
383
- case "systemOrange": return .systemOrange
384
- case "systemYellow": return .systemYellow
385
- case "systemGreen": return .systemGreen
386
- case "systemMint": return .systemMint
387
- case "systemTeal": return .systemTeal
388
- case "systemCyan": return .systemCyan
389
- case "systemBlue": return .systemBlue
390
- case "systemIndigo": return .systemIndigo
391
- case "systemPurple": return .systemPurple
392
- case "systemPink": return .systemPink
393
- case "systemBrown": return .systemBrown
394
- case "systemGray": return .systemGray
395
- case "systemGray2": return .systemGray2
396
- case "systemGray3": return .systemGray3
397
- case "systemGray4": return .systemGray4
398
- case "systemGray5": return .systemGray5
399
- case "systemGray6": return .systemGray6
400
- default: return nil
401
- }
402
- }
403
- }
404
-
405
- @objc public protocol TinyuiMenuViewDelegate {
406
- func onItemPress(id: String)
407
- func onPrimaryAction()
408
- }
@@ -1,88 +0,0 @@
1
- import SwiftUI
2
- import UIKit
3
-
4
- /**
5
- * Observable props shared between the Objective-C++ component view and the
6
- * SwiftUI `Popover` implementation. Mirror of `TinyuiPopoverProps` in codegen.
7
- */
8
- class TinyuiPopoverProps: ObservableObject {
9
- @Published var isPresented: Bool = false
10
- @Published var attachmentAnchor: String = "center"
11
- @Published var arrowEdge: String = "none"
12
- @Published var triggerView: UIView?
13
- @Published var contentView: UIView?
14
- }
15
-
16
- /**
17
- * SwiftUI `popover` backed by UIPopoverPresentationController, matching
18
- * expo-ui's PopoverView. The RN trigger is embedded as the anchor view and the
19
- * RN content view is presented inside the popover.
20
- *
21
- * Named distinctly from the Fabric component view `TinyuiPopoverView`.
22
- */
23
- struct TinyuiPopoverSwiftUIView: View {
24
- @ObservedObject var props: TinyuiPopoverProps
25
- weak var delegate: TinyuiPopoverViewDelegate?
26
-
27
- @State private var isPresented: Bool = false
28
-
29
- var body: some View {
30
- if let triggerView = props.triggerView {
31
- RepresentableView(view: triggerView)
32
- .popover(
33
- isPresented: $isPresented,
34
- attachmentAnchor: attachmentAnchorValue,
35
- arrowEdge: arrowEdgeValue
36
- ) {
37
- if #available(iOS 16.4, *) {
38
- popoverContent
39
- .presentationCompactAdaptation(.popover)
40
- } else {
41
- popoverContent
42
- }
43
- }
44
- .onChange(of: isPresented) { newValue in
45
- delegate?.onIsPresentedChange(isPresented: newValue)
46
- }
47
- .onChange(of: props.isPresented) { newValue in
48
- isPresented = newValue
49
- }
50
- .onAppear {
51
- isPresented = props.isPresented
52
- }
53
- } else {
54
- Color.clear
55
- }
56
- }
57
-
58
- private var attachmentAnchorValue: PopoverAttachmentAnchor {
59
- switch props.attachmentAnchor {
60
- case "top": return .point(.top)
61
- case "bottom": return .point(.bottom)
62
- case "leading": return .point(.leading)
63
- case "trailing": return .point(.trailing)
64
- default: return .point(.center)
65
- }
66
- }
67
-
68
- private var arrowEdgeValue: Edge? {
69
- switch props.arrowEdge {
70
- case "top": return .top
71
- case "bottom": return .bottom
72
- case "leading": return .leading
73
- case "trailing": return .trailing
74
- default: return nil
75
- }
76
- }
77
-
78
- @ViewBuilder
79
- private var popoverContent: some View {
80
- if let contentView = props.contentView {
81
- RepresentableView(view: contentView)
82
- }
83
- }
84
- }
85
-
86
- @objc public protocol TinyuiPopoverViewDelegate {
87
- func onIsPresentedChange(isPresented: Bool)
88
- }