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
@@ -0,0 +1,141 @@
1
+ #if TINYUI_FEATURE_SF_SYMBOL
2
+
3
+ #import "TinyuiSFSymbolImage.h"
4
+ #import <React/RCTConvert.h>
5
+ #include <cmath>
6
+
7
+ static UIImageSymbolWeight symbolWeight(NSString *value)
8
+ {
9
+ NSDictionary *weights = @{
10
+ @"ultraLight": @(UIImageSymbolWeightUltraLight), @"thin": @(UIImageSymbolWeightThin),
11
+ @"light": @(UIImageSymbolWeightLight), @"regular": @(UIImageSymbolWeightRegular),
12
+ @"medium": @(UIImageSymbolWeightMedium), @"semibold": @(UIImageSymbolWeightSemibold),
13
+ @"bold": @(UIImageSymbolWeightBold), @"heavy": @(UIImageSymbolWeightHeavy),
14
+ @"black": @(UIImageSymbolWeightBlack),
15
+ };
16
+ return (UIImageSymbolWeight)[weights[value ?: @""] integerValue];
17
+ }
18
+
19
+ static UIImageSymbolScale symbolScale(NSString *value)
20
+ {
21
+ if ([value isEqual:@"small"]) return UIImageSymbolScaleSmall;
22
+ if ([value isEqual:@"medium"]) return UIImageSymbolScaleMedium;
23
+ if ([value isEqual:@"large"]) return UIImageSymbolScaleLarge;
24
+ if ([value isEqual:@"unspecified"]) return UIImageSymbolScaleUnspecified;
25
+ return UIImageSymbolScaleDefault;
26
+ }
27
+
28
+ static UIFontTextStyle textStyle(NSString *value)
29
+ {
30
+ return @{
31
+ @"extraLargeTitle": UIFontTextStyleExtraLargeTitle,
32
+ @"extraLargeTitle2": UIFontTextStyleExtraLargeTitle2,
33
+ @"largeTitle": UIFontTextStyleLargeTitle, @"title1": UIFontTextStyleTitle1,
34
+ @"title2": UIFontTextStyleTitle2, @"title3": UIFontTextStyleTitle3,
35
+ @"headline": UIFontTextStyleHeadline, @"subheadline": UIFontTextStyleSubheadline,
36
+ @"body": UIFontTextStyleBody, @"callout": UIFontTextStyleCallout,
37
+ @"footnote": UIFontTextStyleFootnote, @"caption1": UIFontTextStyleCaption1,
38
+ @"caption2": UIFontTextStyleCaption2,
39
+ }[value ?: @""];
40
+ }
41
+
42
+ UIImage *TinyuiSFSymbolImage(NSDictionary *configuration, BOOL includeColors)
43
+ {
44
+ if (![configuration isKindOfClass:NSDictionary.class]) return nil;
45
+ NSString *name = configuration[@"name"];
46
+ if (![name isKindOfClass:NSString.class] || name.length == 0) return nil;
47
+
48
+ CGFloat size = [configuration[@"size"] doubleValue];
49
+ if (!std::isfinite(size) || size <= 0) size = 17;
50
+ UIImageSymbolWeight weight = symbolWeight(configuration[@"weight"]);
51
+ UIImageSymbolScale scale = symbolScale(configuration[@"scale"]);
52
+ UIFontTextStyle style = textStyle(configuration[@"textStyle"]);
53
+ if (style != nil) {
54
+ // RN supplies the scale explicitly; use unscaled metrics here to avoid
55
+ // applying Dynamic Type twice and to keep Yoga and UIKit in agreement.
56
+ UITraitCollection *traits = [UITraitCollection
57
+ traitCollectionWithPreferredContentSizeCategory:UIContentSizeCategoryLarge];
58
+ UIFont *font = [UIFont preferredFontForTextStyle:style compatibleWithTraitCollection:traits];
59
+ size = font.pointSize;
60
+ if (weight == UIImageSymbolWeightUnspecified) {
61
+ NSNumber *fontWeight = font.fontDescriptor.fontAttributes[UIFontDescriptorTraitsAttribute][UIFontWeightTrait];
62
+ weight = UIImageSymbolWeightForFontWeight(fontWeight.doubleValue);
63
+ }
64
+ }
65
+ double fontScale = [configuration[@"fontScale"] doubleValue];
66
+ if (std::isfinite(fontScale) && fontScale > 0) size *= fontScale;
67
+
68
+ UIImageSymbolConfiguration *symbol = [UIImageSymbolConfiguration
69
+ configurationWithPointSize:size weight:weight scale:scale];
70
+ NSString *family = configuration[@"fontFamily"];
71
+ if ([family isKindOfClass:NSString.class] && family.length > 0) {
72
+ UIFont *font = [UIFont fontWithName:family size:size];
73
+ if (font != nil) {
74
+ symbol = [UIImageSymbolConfiguration configurationWithFont:font scale:scale];
75
+ if (weight != UIImageSymbolWeightUnspecified) {
76
+ symbol = [symbol configurationByApplyingConfiguration:
77
+ [UIImageSymbolConfiguration configurationWithWeight:weight]];
78
+ }
79
+ }
80
+ }
81
+
82
+ if (includeColors) {
83
+ NSString *mode = configuration[@"renderingMode"];
84
+ UIColor *color = [RCTConvert UIColor:configuration[@"color"]] ?: UIColor.labelColor;
85
+ UIImageSymbolConfiguration *colors = nil;
86
+ if ([mode isEqual:@"monochrome"]) {
87
+ colors = UIImageSymbolConfiguration.configurationPreferringMonochrome;
88
+ } else if ([mode isEqual:@"multicolor"]) {
89
+ colors = UIImageSymbolConfiguration.configurationPreferringMulticolor;
90
+ } else if ([mode isEqual:@"hierarchical"]) {
91
+ colors = [UIImageSymbolConfiguration configurationWithHierarchicalColor:color];
92
+ } else if ([mode isEqual:@"palette"]) {
93
+ NSMutableArray<UIColor *> *palette = [NSMutableArray array];
94
+ id values = configuration[@"paletteColors"];
95
+ if ([values isKindOfClass:NSArray.class]) {
96
+ for (id value in values) {
97
+ UIColor *paletteColor = [RCTConvert UIColor:value];
98
+ if (paletteColor != nil && palette.count < 3) [palette addObject:paletteColor];
99
+ }
100
+ }
101
+ if (palette.count == 0) [palette addObject:color];
102
+ colors = [UIImageSymbolConfiguration configurationWithPaletteColors:palette];
103
+ }
104
+ if (colors != nil) symbol = [symbol configurationByApplyingConfiguration:colors];
105
+ }
106
+
107
+ if (@available(iOS 26.0, *)) {
108
+ NSString *variableMode = configuration[@"variableValueMode"];
109
+ UIImageSymbolVariableValueMode variable = UIImageSymbolVariableValueModeAutomatic;
110
+ if ([variableMode isEqual:@"color"]) variable = UIImageSymbolVariableValueModeColor;
111
+ if ([variableMode isEqual:@"draw"]) variable = UIImageSymbolVariableValueModeDraw;
112
+ symbol = [symbol configurationByApplyingConfiguration:
113
+ [UIImageSymbolConfiguration configurationWithVariableValueMode:variable]];
114
+
115
+ NSString *colorMode = configuration[@"colorRenderingMode"];
116
+ UIImageSymbolColorRenderingMode rendering = UIImageSymbolColorRenderingModeAutomatic;
117
+ if ([colorMode isEqual:@"flat"]) rendering = UIImageSymbolColorRenderingModeFlat;
118
+ if ([colorMode isEqual:@"gradient"]) rendering = UIImageSymbolColorRenderingModeGradient;
119
+ symbol = [symbol configurationByApplyingConfiguration:
120
+ [UIImageSymbolConfiguration configurationWithColorRenderingMode:rendering]];
121
+ }
122
+
123
+ BOOL asset = [configuration[@"source"] isEqual:@"asset"];
124
+ id variableValue = configuration[@"variableValue"];
125
+ UIImage *image;
126
+ if ([variableValue isKindOfClass:NSNumber.class]) {
127
+ double value = [variableValue doubleValue];
128
+ value = std::isfinite(value) ? fmin(1, fmax(0, value)) : 0;
129
+ image = asset
130
+ ? [UIImage imageNamed:name inBundle:NSBundle.mainBundle variableValue:value withConfiguration:symbol]
131
+ : [UIImage systemImageNamed:name variableValue:value withConfiguration:symbol];
132
+ } else {
133
+ image = asset
134
+ ? [UIImage imageNamed:name inBundle:NSBundle.mainBundle withConfiguration:symbol]
135
+ : [UIImage systemImageNamed:name withConfiguration:symbol];
136
+ }
137
+ // Symbol content transitions are undefined for ordinary bitmap assets.
138
+ return image.isSymbolImage ? image : nil;
139
+ }
140
+
141
+ #endif
@@ -0,0 +1,31 @@
1
+ #pragma once
2
+
3
+ #include <react/renderer/components/TinyuiSpec/ShadowNodes.h>
4
+ #include <react/renderer/core/ConcreteComponentDescriptor.h>
5
+
6
+ namespace facebook::react {
7
+
8
+ class TinyuiSFSymbolShadowNode final
9
+ : public ConcreteViewShadowNode<
10
+ TinyuiSFSymbolViewComponentName,
11
+ TinyuiSFSymbolViewProps,
12
+ TinyuiSFSymbolViewEventEmitter> {
13
+ public:
14
+ using ConcreteViewShadowNode::ConcreteViewShadowNode;
15
+
16
+ static ShadowNodeTraits BaseTraits()
17
+ {
18
+ auto traits = ConcreteViewShadowNode::BaseTraits();
19
+ traits.set(ShadowNodeTraits::Trait::LeafYogaNode);
20
+ traits.set(ShadowNodeTraits::Trait::MeasurableYogaNode);
21
+ return traits;
22
+ }
23
+
24
+ Size measureContent(
25
+ const LayoutContext &layoutContext,
26
+ const LayoutConstraints &layoutConstraints) const override;
27
+ };
28
+
29
+ using TinyuiSFSymbolComponentDescriptor = ConcreteComponentDescriptor<TinyuiSFSymbolShadowNode>;
30
+
31
+ } // namespace facebook::react
@@ -0,0 +1,30 @@
1
+ #if TINYUI_FEATURE_SF_SYMBOL
2
+
3
+ #import "TinyuiSFSymbolShadowNode.h"
4
+ #import "TinyuiSFSymbolImage.h"
5
+ #import <react/utils/FollyConvert.h>
6
+ #include <react/renderer/core/LayoutConstraints.h>
7
+ #include <react/renderer/core/LayoutContext.h>
8
+ #include <cmath>
9
+
10
+ namespace facebook::react {
11
+
12
+ Size TinyuiSFSymbolShadowNode::measureContent(
13
+ const LayoutContext &layoutContext,
14
+ const LayoutConstraints &layoutConstraints) const
15
+ {
16
+ @autoreleasepool {
17
+ // UIImage and UIFont creation are safe off the main thread. No UIView or
18
+ // main-queue sync is needed during concurrent Fabric layout.
19
+ UIImage *image = TinyuiSFSymbolImage(convertFollyDynamicToId(getConcreteProps().configuration), NO);
20
+ CGSize size = image == nil ? CGSizeZero : image.size;
21
+ auto scale = layoutContext.pointScaleFactor > 0 ? layoutContext.pointScaleFactor : 1;
22
+ return layoutConstraints.clamp({
23
+ static_cast<Float>(std::ceil(size.width * scale) / scale),
24
+ static_cast<Float>(std::ceil(size.height * scale) / scale)});
25
+ }
26
+ }
27
+
28
+ } // namespace facebook::react
29
+
30
+ #endif
@@ -0,0 +1,4 @@
1
+ #import <React/RCTViewComponentView.h>
2
+
3
+ @interface TinyuiSFSymbolView : RCTViewComponentView
4
+ @end
@@ -0,0 +1,173 @@
1
+ #if TINYUI_FEATURE_SF_SYMBOL
2
+
3
+ #import "TinyuiSFSymbolView.h"
4
+ #import "TinyuiSFSymbolEffects.h"
5
+ #import "TinyuiSFSymbolImage.h"
6
+ #import "TinyuiSFSymbolShadowNode.h"
7
+ #import <React/RCTConvert.h>
8
+ #import <react/utils/FollyConvert.h>
9
+ #import <react/renderer/components/TinyuiSpec/Props.h>
10
+ #import <react/renderer/components/TinyuiSpec/RCTComponentViewHelpers.h>
11
+
12
+ using namespace facebook::react;
13
+
14
+ @interface TinyuiSFSymbolView () <RCTTinyuiSFSymbolViewViewProtocol>
15
+ @end
16
+
17
+ @implementation TinyuiSFSymbolView {
18
+ UIImageView *_imageView;
19
+ NSDictionary *_configuration;
20
+ NSArray<NSDictionary *> *_effects;
21
+ NSMutableDictionary<NSString *, NSDictionary *> *_appliedEffects;
22
+ BOOL _respectReduceMotion;
23
+ }
24
+
25
+ + (ComponentDescriptorProvider)componentDescriptorProvider
26
+ {
27
+ return concreteComponentDescriptorProvider<TinyuiSFSymbolComponentDescriptor>();
28
+ }
29
+
30
+ - (instancetype)initWithFrame:(CGRect)frame
31
+ {
32
+ if (self = [super initWithFrame:frame]) {
33
+ _props = std::make_shared<const TinyuiSFSymbolViewProps>();
34
+ [self createImageView];
35
+ [NSNotificationCenter.defaultCenter addObserver:self selector:@selector(reduceMotionChanged:)
36
+ name:UIAccessibilityReduceMotionStatusDidChangeNotification object:nil];
37
+ }
38
+ return self;
39
+ }
40
+
41
+ - (void)createImageView
42
+ {
43
+ _imageView = [UIImageView new];
44
+ _imageView.contentMode = UIViewContentModeCenter;
45
+ _imageView.isAccessibilityElement = NO;
46
+ self.contentView = _imageView;
47
+ _configuration = nil;
48
+ _effects = @[];
49
+ _appliedEffects = [NSMutableDictionary dictionary];
50
+ _respectReduceMotion = YES;
51
+ }
52
+
53
+ - (BOOL)reduceMotion
54
+ {
55
+ return _respectReduceMotion && UIAccessibilityIsReduceMotionEnabled();
56
+ }
57
+
58
+ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps
59
+ {
60
+ const auto &next = *std::static_pointer_cast<const TinyuiSFSymbolViewProps>(props);
61
+ BOOL changedMotion = _respectReduceMotion != next.respectReduceMotion;
62
+ _respectReduceMotion = next.respectReduceMotion;
63
+ if (changedMotion) [self clearEffects];
64
+
65
+ NSDictionary *configuration = convertFollyDynamicToId(next.configuration);
66
+ if (![configuration isKindOfClass:NSDictionary.class]) configuration = @{};
67
+ if (![_configuration isEqual:configuration]) {
68
+ UIImage *image = TinyuiSFSymbolImage(configuration, YES);
69
+ _imageView.tintColor = [RCTConvert UIColor:configuration[@"color"]] ?: UIColor.labelColor;
70
+ NSDictionary *transitionConfig = convertFollyDynamicToId(next.contentTransition);
71
+ if (![transitionConfig isKindOfClass:NSDictionary.class]) transitionConfig = @{};
72
+ NSSymbolContentTransition *transition = TinyuiSFSymbolTransition(transitionConfig);
73
+ if (image != nil && _imageView.image != nil && self.window != nil && transition != nil && !self.reduceMotion) {
74
+ [_imageView setSymbolImage:image withContentTransition:transition
75
+ options:TinyuiSFSymbolOptions(transitionConfig)];
76
+ } else {
77
+ _imageView.image = image;
78
+ }
79
+ _configuration = configuration;
80
+ if (image == nil) [self clearEffects];
81
+ }
82
+
83
+ if (next.resizeMode == "contain") _imageView.contentMode = UIViewContentModeScaleAspectFit;
84
+ else if (next.resizeMode == "cover") _imageView.contentMode = UIViewContentModeScaleAspectFill;
85
+ else if (next.resizeMode == "stretch") _imageView.contentMode = UIViewContentModeScaleToFill;
86
+ else _imageView.contentMode = UIViewContentModeCenter;
87
+
88
+ id effects = convertFollyDynamicToId(next.effects);
89
+ _effects = [effects isKindOfClass:NSArray.class] ? effects : @[];
90
+ [super updateProps:props oldProps:oldProps];
91
+ }
92
+
93
+ - (void)finalizeUpdates:(RNComponentViewUpdateMask)updateMask
94
+ {
95
+ [super finalizeUpdates:updateMask];
96
+ [self applyEffects];
97
+ }
98
+
99
+ - (void)clearEffects
100
+ {
101
+ [_imageView removeAllSymbolEffectsWithOptions:NSSymbolEffectOptions.options animated:NO];
102
+ [_appliedEffects removeAllObjects];
103
+ }
104
+
105
+ - (void)applyEffects
106
+ {
107
+ if (self.window == nil || _imageView.image == nil) return;
108
+ NSMutableDictionary<NSString *, NSDictionary *> *requested = [NSMutableDictionary dictionary];
109
+ for (NSDictionary *configuration in _effects) {
110
+ if (![configuration isKindOfClass:NSDictionary.class]) continue;
111
+ NSString *type = configuration[@"type"];
112
+ if (![type isKindOfClass:NSString.class]) continue;
113
+ requested[type] = configuration;
114
+ }
115
+ for (NSString *type in _appliedEffects.allKeys) {
116
+ NSDictionary *old = _appliedEffects[type];
117
+ NSDictionary *next = requested[type];
118
+ if (![old isEqual:next] || [next[@"active"] isEqual:@NO]) {
119
+ NSSymbolEffect *effect = TinyuiSFSymbolEffect(old);
120
+ // Reset immediately when replaying/reconfiguring, so an animated removal
121
+ // cannot race the replacement effect. Animate only actual deactivation.
122
+ BOOL reapplying = next != nil && ![next[@"active"] isEqual:@NO];
123
+ BOOL animated = !reapplying && !self.reduceMotion && ![(next ?: old)[@"animated"] isEqual:@NO];
124
+ if (effect != nil) {
125
+ [_imageView removeSymbolEffectOfType:effect
126
+ options:TinyuiSFSymbolOptions((next ?: old)[@"options"]) animated:animated];
127
+ }
128
+ [_appliedEffects removeObjectForKey:type];
129
+ }
130
+ }
131
+ for (NSString *type in requested) {
132
+ NSDictionary *configuration = requested[type];
133
+ if ([configuration[@"active"] isEqual:@NO] || [_appliedEffects[type] isEqual:configuration]) continue;
134
+ BOOL stateful = [@[@"scale", @"appear", @"disappear", @"drawOn", @"drawOff"] containsObject:type];
135
+ // Keep visibility/scale state under Reduce Motion, without continuous or
136
+ // discrete animation. Clearing those effects must still restore the state.
137
+ if (self.reduceMotion && !stateful) continue;
138
+ NSSymbolEffect *effect = TinyuiSFSymbolEffect(configuration);
139
+ if (effect == nil) continue;
140
+ [_imageView addSymbolEffect:effect options:TinyuiSFSymbolOptions(configuration[@"options"])
141
+ animated:!self.reduceMotion && ![configuration[@"animated"] isEqual:@NO]];
142
+ _appliedEffects[type] = configuration;
143
+ }
144
+ }
145
+
146
+ - (void)didMoveToWindow
147
+ {
148
+ [super didMoveToWindow];
149
+ if (self.window == nil) [self clearEffects];
150
+ else [self applyEffects];
151
+ }
152
+
153
+ - (void)reduceMotionChanged:(NSNotification *)notification
154
+ {
155
+ [self clearEffects];
156
+ [self applyEffects];
157
+ }
158
+
159
+ - (void)prepareForRecycle
160
+ {
161
+ [self clearEffects];
162
+ [super prepareForRecycle];
163
+ [self createImageView];
164
+ }
165
+
166
+ - (void)dealloc
167
+ {
168
+ [NSNotificationCenter.defaultCenter removeObserver:self];
169
+ }
170
+
171
+ @end
172
+
173
+ #endif
@@ -0,0 +1,32 @@
1
+ #pragma once
2
+
3
+ #include <react/renderer/components/TinyuiSpec/ShadowNodes.h>
4
+ #include <react/renderer/core/ConcreteComponentDescriptor.h>
5
+ #include <react/renderer/core/LayoutConstraints.h>
6
+
7
+ namespace facebook::react {
8
+
9
+ class TinyuiStepperShadowNode final
10
+ : public ConcreteViewShadowNode<
11
+ TinyuiStepperViewComponentName,
12
+ TinyuiStepperViewProps,
13
+ TinyuiStepperViewEventEmitter> {
14
+ public:
15
+ using ConcreteViewShadowNode::ConcreteViewShadowNode;
16
+
17
+ static ShadowNodeTraits BaseTraits()
18
+ {
19
+ auto traits = ConcreteViewShadowNode::BaseTraits();
20
+ traits.set(ShadowNodeTraits::Trait::LeafYogaNode);
21
+ traits.set(ShadowNodeTraits::Trait::MeasurableYogaNode);
22
+ return traits;
23
+ }
24
+
25
+ Size measureContent(
26
+ const LayoutContext &layoutContext,
27
+ const LayoutConstraints &layoutConstraints) const override;
28
+ };
29
+
30
+ using TinyuiStepperComponentDescriptor = ConcreteComponentDescriptor<TinyuiStepperShadowNode>;
31
+
32
+ } // namespace facebook::react
@@ -0,0 +1,28 @@
1
+ #if TINYUI_FEATURE_STEPPER
2
+
3
+ #import "TinyuiStepperShadowNode.h"
4
+
5
+ #import <React/RCTUtils.h>
6
+ #import <UIKit/UIKit.h>
7
+
8
+ namespace facebook::react {
9
+
10
+ Size TinyuiStepperShadowNode::measureContent(
11
+ const LayoutContext &layoutContext,
12
+ const LayoutConstraints &layoutConstraints) const
13
+ {
14
+ // Match React Native's RCTSwitchSize: cache the native size and create the
15
+ // UIKit control on the main queue, even when Yoga measures on a worker.
16
+ static CGSize size;
17
+ static dispatch_once_t onceToken;
18
+ dispatch_once(&onceToken, ^{
19
+ RCTUnsafeExecuteOnMainQueueSync(^{
20
+ size = [UIStepper new].intrinsicContentSize;
21
+ });
22
+ });
23
+ return layoutConstraints.clamp({size.width, size.height});
24
+ }
25
+
26
+ } // namespace facebook::react
27
+
28
+ #endif
@@ -0,0 +1,12 @@
1
+ #if TINYUI_FEATURE_STEPPER
2
+
3
+ #import <React/RCTViewComponentView.h>
4
+
5
+ NS_ASSUME_NONNULL_BEGIN
6
+
7
+ @interface TinyuiStepperView : RCTViewComponentView
8
+ @end
9
+
10
+ NS_ASSUME_NONNULL_END
11
+
12
+ #endif
@@ -0,0 +1,128 @@
1
+ #if TINYUI_FEATURE_STEPPER
2
+
3
+ #import "TinyuiStepperView.h"
4
+ #import "TinyuiStepperShadowNode.h"
5
+
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
+
10
+ #include <algorithm>
11
+ #include <cmath>
12
+
13
+ using namespace facebook::react;
14
+
15
+ @interface TinyuiStepperView () <RCTTinyuiStepperViewViewProtocol>
16
+ @end
17
+
18
+ @implementation TinyuiStepperView {
19
+ UIStepper *_stepper;
20
+ int _nativeEventCount;
21
+ BOOL _hasInitialValue;
22
+ }
23
+
24
+ + (ComponentDescriptorProvider)componentDescriptorProvider
25
+ {
26
+ return concreteComponentDescriptorProvider<TinyuiStepperComponentDescriptor>();
27
+ }
28
+
29
+ - (instancetype)initWithFrame:(CGRect)frame
30
+ {
31
+ if (self = [super initWithFrame:frame]) {
32
+ static const auto defaultProps = std::make_shared<const TinyuiStepperViewProps>();
33
+ _props = defaultProps;
34
+ [self createStepper];
35
+ }
36
+ return self;
37
+ }
38
+
39
+ - (void)createStepper
40
+ {
41
+ _stepper = [[UIStepper alloc] initWithFrame:CGRectZero];
42
+ [_stepper addTarget:self action:@selector(onChange:) forControlEvents:UIControlEventValueChanged];
43
+ self.contentView = _stepper;
44
+ }
45
+
46
+ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps
47
+ {
48
+ const auto &newProps = *std::static_pointer_cast<TinyuiStepperViewProps const>(props);
49
+
50
+ // The public wrapper validates these; also guard the native boundary against
51
+ // invalid numbers, since UIStepper raises for nonpositive increments.
52
+ double minimum = std::isfinite(newProps.minimumValue) ? newProps.minimumValue : 0;
53
+ double maximum = std::isfinite(newProps.maximumValue) ? newProps.maximumValue : 100;
54
+ maximum = std::max(minimum, maximum);
55
+ double previousValue = _stepper.value;
56
+
57
+ // Expand the old range before shrinking it. Setting disjoint bounds in the
58
+ // wrong order lets UIKit change the other bound and prematurely clamp value.
59
+ _stepper.minimumValue = std::min(_stepper.minimumValue, minimum);
60
+ _stepper.maximumValue = std::max(_stepper.maximumValue, maximum);
61
+ _stepper.minimumValue = minimum;
62
+ _stepper.maximumValue = maximum;
63
+ _stepper.stepValue = std::isfinite(newProps.stepValue) && newProps.stepValue > 0
64
+ ? newProps.stepValue : 1;
65
+ _stepper.continuous = newProps.isContinuous;
66
+ _stepper.autorepeat = newProps.autorepeat;
67
+ _stepper.wraps = newProps.wraps;
68
+ _stepper.enabled = !newProps.disabled;
69
+
70
+ // Uncontrolled values stay in UIKit. Controlled values are applied only once
71
+ // JS has acknowledged the newest native event, avoiding autorepeat rollback.
72
+ double value = previousValue;
73
+ if (!_hasInitialValue ||
74
+ (newProps.controlled && newProps.mostRecentEventCount == _nativeEventCount)) {
75
+ value = std::isfinite(newProps.value) ? newProps.value : minimum;
76
+ }
77
+ _stepper.value = std::clamp(value, minimum, maximum);
78
+ _hasInitialValue = YES;
79
+
80
+ [super updateProps:props oldProps:oldProps];
81
+ }
82
+
83
+ - (void)layoutSubviews
84
+ {
85
+ [super layoutSubviews];
86
+ // UIKit controls keep their natural size; a larger React frame centers them.
87
+ CGSize size = [_stepper sizeThatFits:self.bounds.size];
88
+ _stepper.frame = CGRectMake(
89
+ (CGRectGetWidth(self.bounds) - size.width) / 2,
90
+ (CGRectGetHeight(self.bounds) - size.height) / 2,
91
+ size.width,
92
+ size.height);
93
+ }
94
+
95
+ - (void)onChange:(UIStepper *)sender
96
+ {
97
+ if (_eventEmitter == nullptr) {
98
+ return;
99
+ }
100
+ _nativeEventCount += 1;
101
+ std::static_pointer_cast<const TinyuiStepperViewEventEmitter>(_eventEmitter)
102
+ ->onChange(TinyuiStepperViewEventEmitter::OnChange{
103
+ .value = sender.value,
104
+ .eventCount = _nativeEventCount});
105
+ }
106
+
107
+ // Let UIStepper expose its native accessibility controls and actions.
108
+ - (BOOL)isAccessibilityElement
109
+ {
110
+ return NO;
111
+ }
112
+
113
+ - (NSObject *)accessibilityElement
114
+ {
115
+ return _stepper;
116
+ }
117
+
118
+ - (void)prepareForRecycle
119
+ {
120
+ [super prepareForRecycle];
121
+ _nativeEventCount = 0;
122
+ _hasInitialValue = NO;
123
+ [self createStepper];
124
+ }
125
+
126
+ @end
127
+
128
+ #endif
@@ -0,0 +1,31 @@
1
+ import SwiftUI
2
+ import TipKit
3
+
4
+ struct TinyuiTip: Tip, Sendable {
5
+ struct ActionContent: Equatable, Sendable {
6
+ let id: String
7
+ let title: String
8
+ }
9
+
10
+ let id: String
11
+ var titleString = ""
12
+ var messageString = ""
13
+ var symbolName = ""
14
+ var actionContents: [ActionContent] = []
15
+ var maximumDisplayCount = 0
16
+ var ignoresFrequency = false
17
+
18
+ var title: Text { Text(verbatim: titleString) }
19
+ var message: Text? { messageString.isEmpty ? nil : Text(verbatim: messageString) }
20
+ var image: Image? { symbolName.isEmpty ? nil : Image(systemName: symbolName) }
21
+ var actions: [Action] {
22
+ return actionContents.map { Action(id: $0.id, title: $0.title) }
23
+ }
24
+ var options: [any TipOption] {
25
+ var result: [any TipOption] = [Tips.IgnoresDisplayFrequency(ignoresFrequency)]
26
+ if maximumDisplayCount > 0 {
27
+ result.append(Tips.MaxDisplayCount(maximumDisplayCount))
28
+ }
29
+ return result
30
+ }
31
+ }