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.
- package/README.md +383 -53
- package/ios/ConcentricView/TinyuiConcentricView.h +12 -0
- package/ios/ConcentricView/TinyuiConcentricView.mm +60 -0
- package/ios/Core/Tinyui.mm +54 -0
- package/ios/Core/TinyuiComponentClasses.mm +44 -0
- package/ios/LiquidGlassText/TinyuiLiquidGlassTextProvider.swift +64 -79
- package/ios/LiquidGlassText/TinyuiLiquidGlassTextShadowNode.h +46 -0
- package/ios/LiquidGlassText/TinyuiLiquidGlassTextShadowNode.mm +159 -0
- package/ios/LiquidGlassText/TinyuiLiquidGlassTextView.mm +6 -27
- package/ios/LiquidGlassText/TinyuiTextOutlineShape.swift +5 -2
- package/ios/Menu/TinyuiMenuBuilder.swift +238 -0
- package/ios/Menu/TinyuiMenuProvider.swift +51 -85
- package/ios/Menu/TinyuiMenuView.mm +13 -35
- package/ios/Popover/TinyuiPopoverProvider.swift +198 -44
- package/ios/Popover/TinyuiPopoverView.mm +26 -1
- package/ios/SFSymbol/TinyuiSFSymbolEffects.h +9 -0
- package/ios/SFSymbol/TinyuiSFSymbolEffects.mm +180 -0
- package/ios/SFSymbol/TinyuiSFSymbolImage.h +8 -0
- package/ios/SFSymbol/TinyuiSFSymbolImage.mm +141 -0
- package/ios/SFSymbol/TinyuiSFSymbolShadowNode.h +31 -0
- package/ios/SFSymbol/TinyuiSFSymbolShadowNode.mm +30 -0
- package/ios/SFSymbol/TinyuiSFSymbolView.h +4 -0
- package/ios/SFSymbol/TinyuiSFSymbolView.mm +173 -0
- package/ios/Stepper/TinyuiStepperShadowNode.h +32 -0
- package/ios/Stepper/TinyuiStepperShadowNode.mm +28 -0
- package/ios/Stepper/TinyuiStepperView.h +12 -0
- package/ios/Stepper/TinyuiStepperView.mm +128 -0
- package/ios/TipKit/TinyuiTip.swift +31 -0
- package/ios/TipKit/TinyuiTipPopoverProvider.swift +276 -0
- package/ios/TipKit/TinyuiTipPopoverView.h +8 -0
- package/ios/TipKit/TinyuiTipPopoverView.mm +114 -0
- package/ios/TipKit/TinyuiTipsRuntime.swift +148 -0
- package/lib/module/ConcentricView/ConcentricViewNativeComponent.ts +14 -0
- package/lib/module/ConcentricView/index.js +29 -0
- package/lib/module/ConcentricView/index.js.map +1 -0
- package/lib/module/ConcentricView/types.js +4 -0
- package/lib/module/ConcentricView/types.js.map +1 -0
- package/lib/module/LiquidGlassText/LiquidGlassTextNativeComponent.ts +2 -12
- package/lib/module/LiquidGlassText/index.js +60 -35
- package/lib/module/LiquidGlassText/index.js.map +1 -1
- package/lib/module/Menu/MenuNativeComponent.ts +7 -5
- package/lib/module/Menu/index.js +4 -9
- package/lib/module/Menu/index.js.map +1 -1
- package/lib/module/Menu/options.js +4 -12
- package/lib/module/Menu/options.js.map +1 -1
- package/lib/module/NativeTinyui.js.map +1 -1
- package/lib/module/Popover/index.js +4 -2
- package/lib/module/Popover/index.js.map +1 -1
- package/lib/module/SFSymbol/SFSymbolNativeComponent.ts +20 -0
- package/lib/module/SFSymbol/index.js +73 -0
- package/lib/module/SFSymbol/index.js.map +1 -0
- package/lib/module/SFSymbol/options.js +79 -0
- package/lib/module/SFSymbol/options.js.map +1 -0
- package/lib/module/SFSymbol/types.js +4 -0
- package/lib/module/SFSymbol/types.js.map +1 -0
- package/lib/module/Stepper/StepperNativeComponent.ts +29 -0
- package/lib/module/Stepper/index.js +75 -0
- package/lib/module/Stepper/index.js.map +1 -0
- package/lib/module/Stepper/types.js +4 -0
- package/lib/module/Stepper/types.js.map +1 -0
- package/lib/module/TipKit/Popover.js +89 -0
- package/lib/module/TipKit/Popover.js.map +1 -0
- package/lib/module/TipKit/TipPopoverNativeComponent.ts +37 -0
- package/lib/module/TipKit/index.js +28 -0
- package/lib/module/TipKit/index.js.map +1 -0
- package/lib/module/TipKit/types.js +4 -0
- package/lib/module/TipKit/types.js.map +1 -0
- package/lib/module/TipKit/validation.js +24 -0
- package/lib/module/TipKit/validation.js.map +1 -0
- package/lib/module/index.js +4 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/utils/getNativeTinyui.js.map +1 -1
- package/lib/typescript/src/ConcentricView/ConcentricViewNativeComponent.d.ts +7 -0
- package/lib/typescript/src/ConcentricView/ConcentricViewNativeComponent.d.ts.map +1 -0
- package/lib/typescript/src/ConcentricView/index.d.ts +4 -0
- package/lib/typescript/src/ConcentricView/index.d.ts.map +1 -0
- package/lib/typescript/src/ConcentricView/types.d.ts +6 -0
- package/lib/typescript/src/ConcentricView/types.d.ts.map +1 -0
- package/lib/typescript/src/LiquidGlassText/LiquidGlassTextNativeComponent.d.ts +2 -8
- package/lib/typescript/src/LiquidGlassText/LiquidGlassTextNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/LiquidGlassText/index.d.ts +1 -1
- package/lib/typescript/src/LiquidGlassText/index.d.ts.map +1 -1
- package/lib/typescript/src/LiquidGlassText/types.d.ts +13 -22
- package/lib/typescript/src/LiquidGlassText/types.d.ts.map +1 -1
- package/lib/typescript/src/Menu/MenuNativeComponent.d.ts +4 -5
- package/lib/typescript/src/Menu/MenuNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/Menu/index.d.ts +1 -1
- package/lib/typescript/src/Menu/index.d.ts.map +1 -1
- package/lib/typescript/src/Menu/options.d.ts +40 -4
- package/lib/typescript/src/Menu/options.d.ts.map +1 -1
- package/lib/typescript/src/Menu/types.d.ts +16 -4
- package/lib/typescript/src/Menu/types.d.ts.map +1 -1
- package/lib/typescript/src/NativeTinyui.d.ts +2 -0
- package/lib/typescript/src/NativeTinyui.d.ts.map +1 -1
- package/lib/typescript/src/Popover/index.d.ts.map +1 -1
- package/lib/typescript/src/Popover/types.d.ts +2 -0
- package/lib/typescript/src/Popover/types.d.ts.map +1 -1
- package/lib/typescript/src/SFSymbol/SFSymbolNativeComponent.d.ts +11 -0
- package/lib/typescript/src/SFSymbol/SFSymbolNativeComponent.d.ts.map +1 -0
- package/lib/typescript/src/SFSymbol/index.d.ts +4 -0
- package/lib/typescript/src/SFSymbol/index.d.ts.map +1 -0
- package/lib/typescript/src/SFSymbol/options.d.ts +86 -0
- package/lib/typescript/src/SFSymbol/options.d.ts.map +1 -0
- package/lib/typescript/src/SFSymbol/types.d.ts +113 -0
- package/lib/typescript/src/SFSymbol/types.d.ts.map +1 -0
- package/lib/typescript/src/Stepper/StepperNativeComponent.d.ts +21 -0
- package/lib/typescript/src/Stepper/StepperNativeComponent.d.ts.map +1 -0
- package/lib/typescript/src/Stepper/index.d.ts +4 -0
- package/lib/typescript/src/Stepper/index.d.ts.map +1 -0
- package/lib/typescript/src/Stepper/types.d.ts +24 -0
- package/lib/typescript/src/Stepper/types.d.ts.map +1 -0
- package/lib/typescript/src/TipKit/Popover.d.ts +3 -0
- package/lib/typescript/src/TipKit/Popover.d.ts.map +1 -0
- package/lib/typescript/src/TipKit/TipPopoverNativeComponent.d.ts +38 -0
- package/lib/typescript/src/TipKit/TipPopoverNativeComponent.d.ts.map +1 -0
- package/lib/typescript/src/TipKit/index.d.ts +10 -0
- package/lib/typescript/src/TipKit/index.d.ts.map +1 -0
- package/lib/typescript/src/TipKit/types.d.ts +46 -0
- package/lib/typescript/src/TipKit/types.d.ts.map +1 -0
- package/lib/typescript/src/TipKit/validation.d.ts +4 -0
- package/lib/typescript/src/TipKit/validation.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +10 -2
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/utils/getNativeTinyui.d.ts +1 -1
- package/lib/typescript/src/utils/getNativeTinyui.d.ts.map +1 -1
- package/package.json +37 -2
- package/{Tinyui.podspec → react-native-tinyui.podspec} +23 -2
- package/src/ConcentricView/ConcentricViewNativeComponent.ts +14 -0
- package/src/ConcentricView/index.tsx +34 -0
- package/src/ConcentricView/types.ts +6 -0
- package/src/LiquidGlassText/LiquidGlassTextNativeComponent.ts +2 -12
- package/src/LiquidGlassText/index.tsx +73 -39
- package/src/LiquidGlassText/types.ts +24 -50
- package/src/Menu/MenuNativeComponent.ts +7 -5
- package/src/Menu/index.tsx +6 -6
- package/src/Menu/options.ts +4 -18
- package/src/Menu/types.ts +18 -4
- package/src/NativeTinyui.ts +2 -0
- package/src/Popover/index.tsx +8 -3
- package/src/Popover/types.ts +2 -0
- package/src/SFSymbol/SFSymbolNativeComponent.ts +20 -0
- package/src/SFSymbol/index.tsx +89 -0
- package/src/SFSymbol/options.ts +116 -0
- package/src/SFSymbol/types.ts +158 -0
- package/src/Stepper/StepperNativeComponent.ts +29 -0
- package/src/Stepper/index.tsx +91 -0
- package/src/Stepper/types.ts +24 -0
- package/src/TipKit/Popover.tsx +87 -0
- package/src/TipKit/TipPopoverNativeComponent.ts +37 -0
- package/src/TipKit/index.ts +47 -0
- package/src/TipKit/types.ts +57 -0
- package/src/TipKit/validation.ts +39 -0
- package/src/index.tsx +32 -4
- package/src/utils/getNativeTinyui.ts +8 -1
- package/ios/Core/TinyuiHostingHelpers.swift +0 -74
- package/ios/Menu/TinyuiMenuViewSwift.swift +0 -408
- 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
|
-
|
|
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 =
|
|
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
|
-
}
|