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/README.md CHANGED
@@ -1,15 +1,16 @@
1
1
  # react-native-tinyui
2
2
 
3
- Dependency-free, native iOS `Menu`, `Popover` and `LiquidGlassText` components for React Native's
3
+ Dependency-free, native iOS `Menu`, `Popover`, `TipKit`, `Stepper`, `ConcentricView`, `SFSymbol` and `LiquidGlassText` components for React Native's
4
4
  New Architecture. The public API follows normal React composition patterns;
5
- the native UI is implemented with SwiftUI behind Fabric components.
5
+ the native UI is exposed through Fabric components. `Menu`, `Popover`, `Stepper`, `ConcentricView` and `SFSymbol` are
6
+ implemented directly with UIKit; `LiquidGlassText` uses SwiftUI where needed.
6
7
 
7
8
  The initial component behavior is inspired by
8
9
  [`@expo/ui`](https://github.com/expo/expo/tree/main/packages/expo-ui), but this
9
10
  package does not require Expo Modules or any other runtime package. The
10
- React Native ↔ SwiftUI bridge follows the approach used by
11
+ React Native ↔ native view integration follows the approach used by
11
12
  [`react-native-pager-view`](https://github.com/callstack/react-native-pager-view):
12
- a Fabric component view hosts the SwiftUI view through a `UIHostingController`.
13
+ a Fabric component owns the platform view that renders its content.
13
14
 
14
15
  ## Requirements
15
16
 
@@ -45,6 +46,10 @@ Available components:
45
46
  - `Menu`
46
47
  - `Popover`
47
48
  - `LiquidGlassText`
49
+ - `Stepper`
50
+ - `ConcentricView`
51
+ - `SFSymbol`
52
+ - `TipKit` (enables `TipKit.Popover`, `TipKit.configure` and `TipKit.invalidate`)
48
53
 
49
54
  All components are enabled by default. Use an empty `components` array to
50
55
  compile only the TurboModule core. Run `pod install` again whenever this list
@@ -57,6 +62,10 @@ Component-specific JavaScript entry points are also available:
57
62
  import { Menu } from 'react-native-tinyui/menu';
58
63
  import { Popover } from 'react-native-tinyui/popover';
59
64
  import { LiquidGlassText } from 'react-native-tinyui/liquid-glass-text';
65
+ import { Stepper } from 'react-native-tinyui/stepper';
66
+ import { ConcentricView } from 'react-native-tinyui/concentric-view';
67
+ import { SFSymbol } from 'react-native-tinyui/sf-symbol';
68
+ import { TipKit } from 'react-native-tinyui/tip-kit';
60
69
  ```
61
70
 
62
71
  The root `react-native-tinyui` imports remain supported for backwards
@@ -66,9 +75,8 @@ compatibility. JavaScript entry points provide cleaner dependency boundaries;
66
75
  ## Menu
67
76
 
68
77
  `Menu` uses `children` as its trigger and receives its native menu entries
69
- through the `options` prop. Without `onPrimaryAction`, a tap opens the menu.
70
- When `onPrimaryAction` is present, a tap invokes it and a long press opens the
71
- menu.
78
+ through the `options` prop. A tap opens the native menu; `onActionPress`
79
+ receives every selected action's stable id and displayed title.
72
80
 
73
81
  ```tsx
74
82
  import { Menu } from 'react-native-tinyui';
@@ -76,34 +84,40 @@ import { Menu } from 'react-native-tinyui';
76
84
  <Menu
77
85
  accessibilityLabel="Document actions"
78
86
  title="Document actions"
79
- onPrimaryAction={() => openDocument()}
87
+ onActionPress={({ nativeEvent }) => {
88
+ if (nativeEvent.id === 'rename') rename();
89
+ if (nativeEvent.id === 'toggle-pinned') togglePinned();
90
+ if (nativeEvent.id === 'copy-link') copyLink();
91
+ if (nativeEvent.id === 'invite-people') invitePeople();
92
+ if (nativeEvent.id === 'remove') remove();
93
+ }}
80
94
  options={[
81
95
  {
96
+ id: 'rename',
82
97
  title: 'Rename',
83
98
  systemImage: 'square.and.pencil',
84
- onSelect: rename,
85
99
  },
86
100
  {
101
+ id: 'toggle-pinned',
87
102
  title: 'Pinned',
88
103
  state: 'on',
89
104
  systemImage: 'pin',
90
- onSelect: togglePinned,
91
105
  },
92
106
  {
93
107
  type: 'submenu',
94
108
  title: 'Share',
95
109
  systemImage: 'square.and.arrow.up',
96
110
  options: [
97
- { title: 'Copy link', onSelect: copyLink },
98
- { title: 'Invite people', onSelect: invitePeople },
111
+ { id: 'copy-link', title: 'Copy link' },
112
+ { id: 'invite-people', title: 'Invite people' },
99
113
  ],
100
114
  },
101
115
  { type: 'divider' },
102
116
  {
117
+ id: 'remove',
103
118
  title: 'Delete',
104
119
  destructive: true,
105
120
  systemImage: 'trash',
106
- onSelect: remove,
107
121
  },
108
122
  ]}
109
123
  >
@@ -119,11 +133,14 @@ or `type: 'divider'` for structural entries; nested entries use their own
119
133
  simply render no image.
120
134
 
121
135
  Action options support `subtitle`, `state`, `destructive`, `disabled`, `hidden`,
122
- and `keepOpen`. Use `titleColor` to customize the title, or `icon` to load an
123
- image from the containing app's asset catalog; `iconColor` tints either an asset
124
- image or an SF Symbol. These colors accept React Native `ColorValue`s, including
125
- dynamic and semantic iOS colors. Submenus support the same label fields plus
126
- `destructive`, `disabled`, `hidden`, and `displayInline`.
136
+ and `keepOpen`. Set explicit `id` values when handling `onActionPress`; actions
137
+ without one receive a generated menu-path id. Use `icon` to load an image from
138
+ the containing app's asset catalog; `iconColor` tints either an asset image or
139
+ an SF Symbol and accepts React Native `ColorValue`s, including dynamic and
140
+ semantic iOS colors. UIKit doesn't expose custom menu title colors, so the
141
+ deprecated `titleColor` field is retained only for source compatibility; use
142
+ `destructive` for system red styling. Submenus support the same label fields
143
+ plus `destructive`, `disabled`, `hidden`, and `displayInline`.
127
144
 
128
145
  ## Popover
129
146
 
@@ -138,10 +155,9 @@ import { Popover, PopoverClose } from 'react-native-tinyui';
138
155
  defaultOpen={false}
139
156
  attachmentAnchor="bottom"
140
157
  arrowEdge="top"
141
- contentSize={{ width: 320, height: 220 }}
142
158
  onOpenChange={(open) => console.log({ open })}
143
159
  content={({ close }) => (
144
- <View style={styles.content}>
160
+ <View style={{ width: 280, padding: 20 }}>
145
161
  <Text>Any React Native content can be rendered here.</Text>
146
162
  <PopoverClose close={close} style={styles.doneButton}>
147
163
  <Text>Done</Text>
@@ -161,14 +177,120 @@ the `content` prop, which can be a node or a render function receiving
161
177
  as `Popover.Close`) renders a `Pressable` that calls `close` when pressed.
162
178
 
163
179
  Use `open` with `onOpenChange` for a controlled popover, or `defaultOpen` for an
164
- uncontrolled one. `contentSize` defaults to 320 × 240 points and can be updated
165
- while the popover is visible.
180
+ uncontrolled one. The layout of `content` determines the popover's preferred
181
+ size: set `width` and optionally `height` on its root view, or let its children
182
+ determine the height. Size changes are applied while the popover is visible.
183
+ UIKit may limit the displayed size to the available space on screen.
184
+
185
+ Keep the content background transparent to show the native popover material:
186
+ Liquid Glass on iOS 26+ when built with Xcode 26+, and the system popover
187
+ appearance on earlier iOS versions. `style` on `Popover` styles the trigger's
188
+ outer container, not the presented content.
189
+
190
+ ## TipKit
191
+
192
+ `TipKit.Popover` anchors a native TipKit `TipUIPopoverViewController` to ordinary
193
+ React Native children on iOS 17+. The children retain their own touch handlers;
194
+ the tip appears automatically when both `enabled` and TipKit's eligibility allow
195
+ it. Its text, SF Symbol, action buttons, display history and invalidation use
196
+ Apple's TipKit framework. No additional package is required.
197
+
198
+ Configure TipKit once, **before mounting tips** (for example, during app startup):
199
+
200
+ ```tsx
201
+ import { TipKit, SFSymbol } from 'react-native-tinyui';
202
+ import { Pressable } from 'react-native';
203
+
204
+ await TipKit.configure({ displayFrequency: 'daily' });
205
+
206
+ // Inside your screen:
207
+ <TipKit.Popover
208
+ tipId="favorite-feature-v1"
209
+ title="Keep your favorites close"
210
+ message="Save an item to find it quickly next time."
211
+ systemImage="star"
212
+ enabled={isScreenFocused && !hasFavorites}
213
+ maxDisplayCount={3}
214
+ actions={[{ id: 'learn-more', title: 'Learn more' }]}
215
+ onActionPress={({ id }) => {
216
+ if (id === 'learn-more') openHelp();
217
+ }}
218
+ onStatusChange={(status) => console.log(status)}
219
+ onVisibleChange={(visible) => console.log({ visible })}
220
+ onError={(error) => console.warn(error.message)}
221
+ >
222
+ <Pressable onPress={async () => {
223
+ await saveFavorite();
224
+ await TipKit.invalidate('favorite-feature-v1', 'actionPerformed');
225
+ }}>
226
+ <SFSymbol name="star" size={24} />
227
+ </Pressable>
228
+ </TipKit.Popover>;
229
+ ```
230
+
231
+ | Prop | Description | Default |
232
+ | --- | --- | --- |
233
+ | `tipId` | Required stable identity for persistent history | — |
234
+ | `title` | Required nonempty title, already localized by the app | — |
235
+ | `message` | Supporting plain text | — |
236
+ | `systemImage` | SF Symbol name | — |
237
+ | `actions` | Buttons with unique nonempty `{ id, title }` values | `[]` |
238
+ | `enabled` | Allows presentation; does not override TipKit eligibility | `true` |
239
+ | `maxDisplayCount` | Positive integer; automatically invalidates after this many displays | Unlimited |
240
+ | `ignoresDisplayFrequency` | Exempts this tip from the app-wide display interval | `false` |
241
+ | `arrowEdge` | Preferred bubble edge: `top`, `bottom`, `leading`, `trailing`, `auto` | `auto` |
242
+ | `onStatusChange` | `{ status: 'pending' \| 'available' }` or `{ status: 'invalidated', reason }` | — |
243
+ | `onVisibleChange` | Whether the native bubble is actually visible | — |
244
+ | `onActionPress` | Receives the selected `{ id, title }`; does not automatically invalidate | — |
245
+ | `onError` | Receives `{ code, message }`; otherwise a warning is logged | — |
246
+
247
+ The component accepts standard `ViewProps` and a native view ref. `style` lays
248
+ out the anchor, not the bubble. UIKit chooses the bubble's size and may adapt
249
+ the arrow to available space; leading/trailing follow layout direction. Bubble
250
+ content is native text/images/actions; `children` supplies only the anchor.
251
+ Content edits apply to the next presentation; the currently visible bubble
252
+ retains the content and action labels it was presented with.
253
+
254
+ ### Configuration and lifetime
255
+
256
+ `TipKit.configure({ displayFrequency })` returns a promise. Supported frequencies
257
+ are `immediate`, `hourly`, `daily` (default), `weekly` and `monthly`. This is an
258
+ app-wide TipKit setting. Repeating the same configuration is safe, including
259
+ after Fast Refresh; changing it later rejects with `E_TIPS_ALREADY_CONFIGURED`.
260
+ Coordinate this startup configuration with any other native TipKit integration.
261
+ Initialization errors reject the promise; an enabled view mounted too early
262
+ reports `E_TIPS_NOT_CONFIGURED` and can recover after configuration succeeds.
263
+
264
+ `TipKit.invalidate(tipId, reason = 'actionPerformed')` also returns a promise and
265
+ requires configuration first. It persistently invalidates the ID even when no
266
+ view for that tip is mounted. `reason` may be `actionPerformed` or `tipClosed`.
267
+ System invalidations can additionally report `displayCountExceeded`,
268
+ `displayDurationExceeded`, or `unknown` through `onStatusChange`.
269
+
270
+ Keep IDs stable across renders, app launches and translations. All instances
271
+ with the same ID share their TipKit history. Keep `maxDisplayCount` and
272
+ `ignoresDisplayFrequency` consistent for each ID; conflicting values report
273
+ `E_CONFLICTING_TIP_OPTIONS`. Use a deliberate new versioned ID for a new feature
274
+ tip. Hiding a tip, unmounting it or reconfiguring TinyUI never resets history.
275
+
276
+ Use `enabled={isScreenFocused && businessCondition}` with your navigation
277
+ library. TinyUI also waits for a visible anchor and a free presenter, and
278
+ dismisses on detachment or when the anchor leaves the visible area. It presents
279
+ at most one TinyUI tip at a time and waits while another modal occupies the
280
+ presenter. An outside dismissal suppresses immediate reopening for that mount;
281
+ toggle `enabled` off and on to allow another attempt, subject to TipKit's state.
282
+ The system close button can permanently invalidate a tip.
283
+
284
+ This first version exposes popover tips and JS eligibility conditions. Inline
285
+ `TipUIView`, native rule/event builders, TipGroup and datastore reset APIs are
286
+ not exposed. The example's **New demo tip** button deliberately creates another
287
+ ID for trying the behavior again without clearing the app's TipKit datastore.
166
288
 
167
289
  ## LiquidGlassText
168
290
 
169
291
  `LiquidGlassText` renders native glass inside the glyph outlines, based on
170
- [GlassText](https://github.com/ailtonvivaz/GlassText). It supports the reference
171
- library's text, glass, font and alignment APIs through React props.
292
+ [GlassText](https://github.com/ailtonvivaz/GlassText). It exposes the effect and
293
+ typography through React props.
172
294
  Background image loading is not part of this component.
173
295
 
174
296
  Building this component requires **Xcode 26+**. Glass is rendered on **iOS 26+**;
@@ -180,48 +302,49 @@ import { LiquidGlassText } from 'react-native-tinyui';
180
302
 
181
303
  <LiquidGlassText
182
304
  text="Liquid Glass"
183
- glass={{ effect: 'regular', tint: '#00BBDD' }}
184
- font={{ size: 48, weight: 'bold', design: 'rounded' }}
305
+ effect="regular"
306
+ tint="#00BBDD"
307
+ interactive
308
+ fontDesign="rounded"
309
+ textStyle={{ fontSize: 48, fontWeight: '700' }}
185
310
  />;
186
311
 
187
312
  <LiquidGlassText
188
313
  text={'Multi-line\nGlass Text\nEffect'}
189
- glass="clear"
190
- font="largeTitle"
191
- fontWeight="heavy"
192
314
  fontDesign="serif"
315
+ textStyle={{ fontSize: 34, fontWeight: '800' }}
193
316
  multilineTextAlignment="center"
194
317
  />;
195
318
  ```
196
319
 
197
- | Prop | Values | Default |
198
- | ------------------------ | ---------------------------------------------------------------------------------------- | ------------- |
199
- | `text` | String (including text translated in JavaScript) | Required |
200
- | `glass` | `'clear'`, `'regular'`, `'identity'`, or `{ effect?, tint?, interactive? }` | `'clear'` |
201
- | `font` | A semantic text style, or `{ size, weight?, design?, family? }` | `'body'` |
202
- | `fontWeight` | `ultraLight`, `thin`, `light`, `regular`, `medium`, `semibold`, `bold`, `heavy`, `black` | Font's weight |
203
- | `fontDesign` | `default`, `serif`, `monospaced`, `rounded` | Font's design |
204
- | `multilineTextAlignment` | `leading`, `center`, `trailing` | `leading` |
205
-
206
- Semantic font styles are `largeTitle`, `title`, `title2`, `title3`, `headline`,
207
- `subheadline`, `body`, `callout`, `footnote`, `caption`, and `caption2`. They
208
- follow SwiftUI Dynamic Type. Explicit `size` values use fixed point sizes;
209
- `family` accepts the PostScript name of a font installed in the app.
210
- `fontWeight` and `fontDesign` override the corresponding system font options.
211
- A custom font family determines its own design.
212
-
213
- Glass options accept `tint` as a React Native `ColorValue`, including
214
- `PlatformColor` and `DynamicColorIOS`. The object form defaults to the clear
215
- effect, with no tint or interactivity. `identity` applies no glass effect.
320
+ | Prop | Values | Default |
321
+ | ------------------------ | ------------------------------------------------ | --------- |
322
+ | `text` | String (including text translated in JavaScript) | Required |
323
+ | `effect` | `clear`, `regular`, `identity` | `clear` |
324
+ | `tint` | React Native `ColorValue` | — |
325
+ | `interactive` | Whether the glass responds to interaction | `false` |
326
+ | `fontDesign` | `default`, `serif`, `monospaced`, `rounded` | `default` |
327
+ | `textStyle` | Supported React Native text properties | — |
328
+ | `multilineTextAlignment` | `leading`, `center`, `trailing` | `leading` |
329
+
330
+ `textStyle` accepts arrays and `StyleSheet.create` references. Explicit
331
+ `multilineTextAlignment` overrides `textStyle.textAlign`. `textStyle.color`
332
+ supplies the glass tint and fallback text color; an explicit `tint` takes
333
+ precedence. `fontFamily` accepts the PostScript name of a font installed in the
334
+ app, and `fontDesign` only applies to system fonts.
335
+
336
+ `tint` accepts a React Native `ColorValue`, including `PlatformColor` and
337
+ `DynamicColorIOS`. `identity` applies no glass effect.
216
338
  The component also accepts standard `ViewProps`, including `style`, `testID`,
217
339
  accessibility props and a native view ref. The supplied text is used for
218
340
  the default accessibility label; pass `accessibilityLabel` to override it.
219
341
 
220
- Native content measurements supply the default width and height. Layout styles
221
- can override these dimensions, but do not resize the font or wrap text. As in
222
- GlassText, use explicit `\n` characters for multiple lines. Keep padding on a
223
- surrounding `View`. Color emoji and other glyphs without vector outlines cannot
224
- produce a glass shape.
342
+ Fabric measures the native text during layout and supplies its default width and
343
+ height without a JavaScript measurement pass. Layout styles can override these
344
+ dimensions, but do not resize the font or wrap text. As in GlassText, use
345
+ explicit `\n` characters for multiple lines. Keep padding on a surrounding
346
+ `View`. Color emoji and other glyphs without vector outlines cannot produce a
347
+ glass shape.
225
348
 
226
349
  `text` accepts a string. Applications using JavaScript localization can pass
227
350
  their translated string directly.
@@ -229,6 +352,213 @@ their translated string directly.
229
352
  The adapted Core Text outline implementation retains the upstream MIT notice in
230
353
  [`ios/LiquidGlassText/GlassText-LICENSE`](ios/LiquidGlassText/GlassText-LICENSE).
231
354
 
355
+ ## Stepper
356
+
357
+ `Stepper` bridges UIKit's
358
+ [`UIStepper`](https://developer.apple.com/documentation/uikit/uistepper) through
359
+ Fabric. It supports fractional steps, press-and-hold repeat, and wrapping at
360
+ the bounds on iOS 17+.
361
+
362
+ ```tsx
363
+ import { useState } from 'react';
364
+ import { Stepper } from 'react-native-tinyui';
365
+
366
+ function Quantity() {
367
+ const [quantity, setQuantity] = useState(1);
368
+ return (
369
+ <Stepper
370
+ accessibilityLabel="Quantity"
371
+ value={quantity}
372
+ minimumValue={1}
373
+ maximumValue={10}
374
+ onValueChange={setQuantity}
375
+ />
376
+ );
377
+ }
378
+
379
+ // Uncontrolled: UIKit owns the value after initialization.
380
+ <Stepper defaultValue={2.5} stepValue={0.5} maximumValue={5} wraps />;
381
+ ```
382
+
383
+ | Prop | Description | Default |
384
+ | --- | --- | --- |
385
+ | `value` | Controlled numeric value | — |
386
+ | `defaultValue` | Initial value when uncontrolled; later changes are ignored | `0` |
387
+ | `minimumValue` | Lower bound | `0` |
388
+ | `maximumValue` | Upper bound, at least `minimumValue` | `100` |
389
+ | `stepValue` | Positive increment/decrement amount | `1` |
390
+ | `isContinuous` | Report changes during interaction; otherwise report on release | `true` |
391
+ | `autorepeat` | Repeatedly step while holding a button | `true` |
392
+ | `wraps` | Continue from the opposite bound when stepping past an end | `false` |
393
+ | `disabled` | Prevent user interaction | `false` |
394
+ | `onValueChange` | Callback receiving the new number after user interaction | — |
395
+
396
+ When `value` is supplied, update it in `onValueChange` to accept changes. Keeping
397
+ the same `value` restores the native control to that value. Omit `value` to let
398
+ UIKit manage the state. Programmatic value or range changes do not call
399
+ `onValueChange`.
400
+
401
+ All numeric props must be finite, `stepValue` must be positive, and
402
+ `maximumValue` must be at least `minimumValue`; invalid inputs throw a descriptive
403
+ error. Values are clamped to the current range. Equal bounds produce a fixed
404
+ value. Changing an uncontrolled range also clamps its current native value.
405
+
406
+ The component accepts standard `ViewProps` (except `children`) and a native view
407
+ ref. Native accessibility behavior is preserved; use `accessibilityLabel` to
408
+ name the value being adjusted. Fabric measures the native control's intrinsic
409
+ size. A larger `style` frame centers the native control without stretching
410
+ its buttons. Put padding on a surrounding `View`. Like UIKit's control, the
411
+ stepper shows the minus and plus buttons; render a separate `Text` for the value.
412
+
413
+ ## ConcentricView
414
+
415
+ `ConcentricView` is a Fabric container that lets UIKit resolve its corner radii
416
+ relative to its containing view. On iOS 26+, it applies
417
+ [`UICornerRadius.containerConcentricRadius`](https://developer.apple.com/documentation/uikit/uicornerradius-c.class/containerconcentricradius)
418
+ through `UIView.cornerConfiguration`. UIKit handles geometry and layout changes.
419
+ Building this component requires **Xcode 26+**; on iOS 17–25 it uses
420
+ `minimumRadius` as a fixed corner radius.
421
+
422
+ ```tsx
423
+ import { Text } from 'react-native';
424
+ import { ConcentricView } from 'react-native-tinyui';
425
+
426
+ <ConcentricView
427
+ minimumRadius={32}
428
+ style={{ padding: 12, backgroundColor: '#DCEBFF' }}
429
+ >
430
+ <ConcentricView
431
+ minimumRadius={12}
432
+ style={{ padding: 20, backgroundColor: '#3875D5' }}
433
+ >
434
+ <Text>UIKit resolves each corner.</Text>
435
+ </ConcentricView>
436
+ </ConcentricView>;
437
+ ```
438
+
439
+ `minimumRadius` defaults to `0` and must be finite and nonnegative. It only
440
+ controls the fixed fallback radius before iOS 26; iOS 26+ resolves corners
441
+ entirely through UIKit, without a minimum radius. The component accepts
442
+ `children`, standard `ViewProps`, and a native view ref. It clips content
443
+ by default; use `style={{ overflow: 'visible' }}` to allow overflow. Avoid
444
+ `style.borderRadius` and individual corner radii on this component.
445
+ React Native's custom border, outline, and shadow drawing does not calculate
446
+ concentric radii; keep those decorations on a surrounding view when needed.
447
+
448
+ ## SFSymbol
449
+
450
+ `SFSymbol` uses **UIKit `UIImageView` + Apple's Symbols framework**, exposed
451
+ through Fabric. It does not embed a SwiftUI hosting view. Xcode 26+ is required
452
+ to build; the deployment target remains iOS 17. Yoga measures the configured
453
+ symbol's natural size, so `style.width` and `style.height` are optional.
454
+
455
+ ```tsx
456
+ import { useState } from 'react';
457
+ import { Button, PlatformColor, View } from 'react-native';
458
+ import { SFSymbol } from 'react-native-tinyui';
459
+
460
+ function Favorite() {
461
+ const [selected, setSelected] = useState(false);
462
+ const [trigger, setTrigger] = useState(0);
463
+ return (
464
+ <View>
465
+ <SFSymbol
466
+ name={selected ? 'heart.fill' : 'heart'}
467
+ size={32}
468
+ weight="semibold"
469
+ renderingMode="hierarchical"
470
+ color={PlatformColor('systemPinkColor')}
471
+ effect={{ type: 'bounce', trigger, options: { repeat: false } }}
472
+ contentTransition={{ type: 'magicReplace', direction: 'downUp' }}
473
+ />
474
+ <Button title="Favorite" onPress={() => {
475
+ setSelected(!selected);
476
+ setTrigger(trigger + 1);
477
+ }} />
478
+ </View>
479
+ );
480
+ }
481
+ ```
482
+
483
+ ### Image configuration
484
+
485
+ | Prop | Values / behavior |
486
+ | --- | --- |
487
+ | `name` | Required complete symbol name, including variants such as `square.fill`. Missing symbols render empty. |
488
+ | `source` | `system` (default), or `asset` for a custom SF Symbol in the app's asset catalog. Ordinary bitmap assets are not accepted. |
489
+ | `size` | Positive point size; default `17`. |
490
+ | `weight` | `unspecified` (default), `ultraLight`, `thin`, `light`, `regular`, `medium`, `semibold`, `bold`, `heavy`, `black`. |
491
+ | `scale` | `default`, `unspecified`, `small`, `medium`, `large`. |
492
+ | `textStyle` | `extraLargeTitle`, `extraLargeTitle2`, `largeTitle`, `title1`–`title3`, `headline`, `subheadline`, `body`, `callout`, `footnote`, `caption1`, `caption2`. Overrides `size` and supplies the style's weight unless explicitly set. |
493
+ | `fontFamily` | Font/PostScript name used to derive symbol metrics; unknown fonts use system metrics. |
494
+ | `allowFontScaling` | Default `true`. Scales the base point size by React Native's `fontScale`, including when using `textStyle`. |
495
+ | `maxFontSizeMultiplier` | Cap on that scale, >= 1. Omitted or `0` means unlimited. |
496
+ | `renderingMode` | `automatic` (default), `monochrome`, `hierarchical`, `palette`, `multicolor`. |
497
+ | `color` | React Native `ColorValue`, including `PlatformColor` and `DynamicColorIOS`. Defaults to semantic label color. |
498
+ | `paletteColors` | One to three `ColorValue`s in primary, secondary, tertiary order, used in palette mode. If omitted, the palette uses `color`. |
499
+ | `variableValue` | Progress from `0` to `1`; omitted uses the symbol default. Requires a symbol with variable annotations. |
500
+ | `variableValueMode` | `automatic` (default), `color`, `draw` (iOS 26+). |
501
+ | `colorRenderingMode` | `automatic` (default), `flat`, `gradient` (iOS 26+). |
502
+ | `resizeMode` | `center` (default, natural point size), `contain`, `cover`, `stretch`, for explicitly sized frames. Use `style.overflow: 'hidden'` to clip. |
503
+
504
+ Standard `ViewProps`, styles and refs are supported. Symbols are decorative
505
+ (`accessible={false}`) by default; supply `accessible` and a meaningful
506
+ `accessibilityLabel` for standalone images. Use a `Pressable` for interactive icons.
507
+
508
+ ### Preset effects
509
+
510
+ Pass one object to `effect`, or an array of **distinct** effect types to combine
511
+ compatible effects. UIKit determines how combined effects interact.
512
+
513
+ | `type` | Configuration | Minimum iOS |
514
+ | --- | --- | --- |
515
+ | `bounce`, `scale`, `appear`, `disappear` | `direction: 'up' \| 'down'`, `scope` | 17 |
516
+ | `pulse` | `scope` | 17 |
517
+ | `variableColor` | `iteration: 'iterative' \| 'cumulative'`, `reversing: boolean`, `inactiveLayers: 'hide' \| 'dim'` | 17 |
518
+ | `wiggle` | `direction: 'up' \| 'down' \| 'left' \| 'right' \| 'forward' \| 'backward' \| 'clockwise' \| 'counterClockwise'`, or `angle` in degrees clockwise from +x; `scope` | 18 |
519
+ | `rotate` | `direction: 'clockwise' \| 'counterClockwise'`, `scope` | 18 |
520
+ | `breathe` | `style: 'plain' \| 'pulse'`, `scope` | 18 |
521
+ | `drawOn` | `scope`, also accepting `individually` | 26 |
522
+ | `drawOff` | `scope`, also accepting `individually`; `reversed: boolean` | 26 |
523
+
524
+ `scope` is `byLayer` or `wholeSymbol`; omit any modifier to keep Apple's default.
525
+ Each preset also accepts:
526
+
527
+ - `active` (default `true`): apply/remove the effect. Scale and visibility
528
+ effects hold their state until removed. Disappear/Draw Off can hide the symbol.
529
+ - `trigger`: a number or string. Change it to replay an effect. Mounting or
530
+ changing effect configuration also applies it; unrelated rerenders do not.
531
+ - `animated` (default `true`): animate application/removal of the effect.
532
+ - `options.speed`: positive speed multiplier, default system speed.
533
+ - `options.repeat`: a positive integer play count, `'forever'`, or `false` for
534
+ one play. Omit to use the preset's native repetition behavior.
535
+ - `options.repeatBehavior`: `periodic` or `continuous` (iOS 18+).
536
+ Continuous repeats forever and cannot be combined with a count or delay.
537
+ - `options.repeatDelay`: nonnegative delay in seconds between periodic plays
538
+ (iOS 18+). Without a count, periodic/delayed repetition continues indefinitely.
539
+
540
+ Repetition options only affect presets that support repeating. For example,
541
+ `scale` is a held state, not a looping animation. Removing `effect` clears all
542
+ effects. Detached/recycled views stop animations; reattaching applies them again.
543
+
544
+ ### Replacement transitions and compatibility
545
+
546
+ `contentTransition` animates changes to the name or image configuration. It
547
+ accepts `type: 'automatic' | 'replace' | 'magicReplace'`, optional
548
+ `direction: 'downUp' | 'upUp' | 'offUp'`, `scope: 'byLayer' | 'wholeSymbol'`, and
549
+ a positive `speed`. Direction and scope configure Replace and the Magic Replace
550
+ fallback; Automatic uses the system transition. The initial image is not transitioned.
551
+
552
+ Unsupported presets are ignored on earlier iOS versions. Magic Replace falls
553
+ back to the configured Replace before iOS 18. Continuous repetition falls back
554
+ to periodic repetition on iOS 17; custom repeat delay is ignored. Before iOS 26,
555
+ variable mode and gradient configuration are ignored. Actual animation, palette,
556
+ multicolor and variable/draw support depend on each symbol's native annotations.
557
+
558
+ `respectReduceMotion` defaults to `true`: repeating/discrete effects and content
559
+ transitions are suppressed while Reduce Motion is enabled, while scale and
560
+ visibility state changes apply instantly. Set it to `false` to opt out.
561
+
232
562
  ## Contributing
233
563
 
234
564
  - [Development workflow](CONTRIBUTING.md#development-workflow)
@@ -0,0 +1,12 @@
1
+ #if TINYUI_FEATURE_CONCENTRIC_VIEW
2
+
3
+ #import <React/RCTViewComponentView.h>
4
+
5
+ NS_ASSUME_NONNULL_BEGIN
6
+
7
+ @interface TinyuiConcentricView : RCTViewComponentView
8
+ @end
9
+
10
+ NS_ASSUME_NONNULL_END
11
+
12
+ #endif
@@ -0,0 +1,60 @@
1
+ #if TINYUI_FEATURE_CONCENTRIC_VIEW
2
+
3
+ #import "TinyuiConcentricView.h"
4
+
5
+ #import <react/renderer/components/TinyuiSpec/ComponentDescriptors.h>
6
+ #import <react/renderer/components/TinyuiSpec/Props.h>
7
+ #import <react/renderer/components/TinyuiSpec/RCTComponentViewHelpers.h>
8
+
9
+ using namespace facebook::react;
10
+
11
+ @interface TinyuiConcentricView () <RCTTinyuiConcentricViewViewProtocol>
12
+ @end
13
+
14
+ @implementation TinyuiConcentricView
15
+
16
+ + (ComponentDescriptorProvider)componentDescriptorProvider
17
+ {
18
+ return concreteComponentDescriptorProvider<TinyuiConcentricViewComponentDescriptor>();
19
+ }
20
+
21
+ - (instancetype)initWithFrame:(CGRect)frame
22
+ {
23
+ if (self = [super initWithFrame:frame]) {
24
+ static const auto defaultProps = std::make_shared<const TinyuiConcentricViewProps>();
25
+ _props = defaultProps;
26
+ }
27
+ return self;
28
+ }
29
+
30
+ - (void)finalizeUpdates:(RNComponentViewUpdateMask)updateMask
31
+ {
32
+ [super finalizeUpdates:updateMask];
33
+ [self applyCornerConfiguration];
34
+ }
35
+
36
+ - (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection
37
+ {
38
+ [super traitCollectionDidChange:previousTraitCollection];
39
+ if ([self.traitCollection hasDifferentColorAppearanceComparedToTraitCollection:previousTraitCollection]) {
40
+ [self applyCornerConfiguration];
41
+ }
42
+ }
43
+
44
+ - (void)applyCornerConfiguration
45
+ {
46
+ // Fabric updates layer.cornerRadius while applying view styles. Configure
47
+ // UIKit afterwards so it owns the resolved radii, including on layout changes.
48
+ if (@available(iOS 26.0, *)) {
49
+ self.cornerConfiguration = [UICornerConfiguration configurationWithRadius:
50
+ [UICornerRadius containerConcentricRadius]];
51
+ } else {
52
+ const auto &props = *std::static_pointer_cast<const TinyuiConcentricViewProps>(_props);
53
+ self.layer.cornerRadius = props.minimumRadius;
54
+ self.layer.cornerCurve = kCACornerCurveContinuous;
55
+ }
56
+ }
57
+
58
+ @end
59
+
60
+ #endif