@rafaelaugusto/jolt 0.1.0-beta.1 → 0.1.0-beta.10

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 (302) hide show
  1. package/README.md +367 -18
  2. package/dist/animate.d.ts +19 -3
  3. package/dist/animate.d.ts.map +1 -1
  4. package/dist/animate.js +186 -14
  5. package/dist/animate.js.map +1 -1
  6. package/dist/color.d.ts +11 -0
  7. package/dist/color.d.ts.map +1 -0
  8. package/dist/color.js +73 -0
  9. package/dist/color.js.map +1 -0
  10. package/dist/config.d.ts +31 -0
  11. package/dist/config.d.ts.map +1 -0
  12. package/dist/config.js +73 -0
  13. package/dist/config.js.map +1 -0
  14. package/dist/decay.d.ts +11 -0
  15. package/dist/decay.d.ts.map +1 -0
  16. package/dist/decay.js +22 -0
  17. package/dist/decay.js.map +1 -0
  18. package/dist/decoration.d.ts +18 -0
  19. package/dist/decoration.d.ts.map +1 -0
  20. package/dist/decoration.js +155 -0
  21. package/dist/decoration.js.map +1 -0
  22. package/dist/drag-input.d.ts +15 -0
  23. package/dist/drag-input.d.ts.map +1 -0
  24. package/dist/drag-input.js +66 -0
  25. package/dist/drag-input.js.map +1 -0
  26. package/dist/drag-physics.d.ts +20 -0
  27. package/dist/drag-physics.d.ts.map +1 -0
  28. package/dist/drag-physics.js +75 -0
  29. package/dist/drag-physics.js.map +1 -0
  30. package/dist/drag.d.ts +58 -0
  31. package/dist/drag.d.ts.map +1 -0
  32. package/dist/drag.js +324 -0
  33. package/dist/drag.js.map +1 -0
  34. package/dist/driver/android-color.d.ts +20 -0
  35. package/dist/driver/android-color.d.ts.map +1 -0
  36. package/dist/driver/android-color.js +136 -0
  37. package/dist/driver/android-color.js.map +1 -0
  38. package/dist/driver/android-decoration.d.ts +22 -0
  39. package/dist/driver/android-decoration.d.ts.map +1 -0
  40. package/dist/driver/android-decoration.js +139 -0
  41. package/dist/driver/android-decoration.js.map +1 -0
  42. package/dist/driver/android-playback.d.ts +13 -0
  43. package/dist/driver/android-playback.d.ts.map +1 -0
  44. package/dist/driver/android-playback.js +141 -0
  45. package/dist/driver/android-playback.js.map +1 -0
  46. package/dist/driver/android.d.ts +1 -1
  47. package/dist/driver/android.d.ts.map +1 -1
  48. package/dist/driver/android.js +165 -13
  49. package/dist/driver/android.js.map +1 -1
  50. package/dist/driver/index.d.ts +25 -2
  51. package/dist/driver/index.d.ts.map +1 -1
  52. package/dist/driver/index.js.map +1 -1
  53. package/dist/driver/ios-color.d.ts +16 -0
  54. package/dist/driver/ios-color.d.ts.map +1 -0
  55. package/dist/driver/ios-color.js +392 -0
  56. package/dist/driver/ios-color.js.map +1 -0
  57. package/dist/driver/ios-decoration.d.ts +17 -0
  58. package/dist/driver/ios-decoration.d.ts.map +1 -0
  59. package/dist/driver/ios-decoration.js +271 -0
  60. package/dist/driver/ios-decoration.js.map +1 -0
  61. package/dist/driver/ios-layout.d.ts +9 -0
  62. package/dist/driver/ios-layout.d.ts.map +1 -0
  63. package/dist/driver/ios-layout.js +52 -0
  64. package/dist/driver/ios-layout.js.map +1 -0
  65. package/dist/driver/ios-playback.d.ts +10 -0
  66. package/dist/driver/ios-playback.d.ts.map +1 -0
  67. package/dist/driver/ios-playback.js +47 -0
  68. package/dist/driver/ios-playback.js.map +1 -0
  69. package/dist/driver/ios-shadow.d.ts +6 -0
  70. package/dist/driver/ios-shadow.d.ts.map +1 -0
  71. package/dist/driver/ios-shadow.js +52 -0
  72. package/dist/driver/ios-shadow.js.map +1 -0
  73. package/dist/driver/ios.d.ts.map +1 -1
  74. package/dist/driver/ios.js +163 -28
  75. package/dist/driver/ios.js.map +1 -1
  76. package/dist/engine.d.ts +75 -5
  77. package/dist/engine.d.ts.map +1 -1
  78. package/dist/engine.js +293 -23
  79. package/dist/engine.js.map +1 -1
  80. package/dist/haptics.d.ts +21 -0
  81. package/dist/haptics.d.ts.map +1 -0
  82. package/dist/haptics.js +120 -0
  83. package/dist/haptics.js.map +1 -0
  84. package/dist/image-effect-native.d.ts +26 -0
  85. package/dist/image-effect-native.d.ts.map +1 -0
  86. package/dist/image-effect-native.js +73 -0
  87. package/dist/image-effect-native.js.map +1 -0
  88. package/dist/image-effect.d.ts +50 -0
  89. package/dist/image-effect.d.ts.map +1 -0
  90. package/dist/image-effect.js +290 -0
  91. package/dist/image-effect.js.map +1 -0
  92. package/dist/image-owner.d.ts +3 -0
  93. package/dist/image-owner.d.ts.map +1 -0
  94. package/dist/image-owner.js +11 -0
  95. package/dist/image-owner.js.map +1 -0
  96. package/dist/index.d.ts +43 -2
  97. package/dist/index.d.ts.map +1 -1
  98. package/dist/index.js +20 -1
  99. package/dist/index.js.map +1 -1
  100. package/dist/interaction-surface.d.ts +12 -0
  101. package/dist/interaction-surface.d.ts.map +1 -0
  102. package/dist/interaction-surface.js +36 -0
  103. package/dist/interaction-surface.js.map +1 -0
  104. package/dist/items.d.ts +20 -0
  105. package/dist/items.d.ts.map +1 -0
  106. package/dist/items.js +109 -0
  107. package/dist/items.js.map +1 -0
  108. package/dist/keyboard.d.ts +41 -0
  109. package/dist/keyboard.d.ts.map +1 -0
  110. package/dist/keyboard.js +256 -0
  111. package/dist/keyboard.js.map +1 -0
  112. package/dist/label.d.ts +7 -0
  113. package/dist/label.d.ts.map +1 -0
  114. package/dist/label.js +60 -0
  115. package/dist/label.js.map +1 -0
  116. package/dist/layout.d.ts +13 -0
  117. package/dist/layout.d.ts.map +1 -0
  118. package/dist/layout.js +348 -0
  119. package/dist/layout.js.map +1 -0
  120. package/dist/liquid-grid.d.ts +3 -0
  121. package/dist/liquid-grid.d.ts.map +1 -0
  122. package/dist/liquid-grid.js +24 -0
  123. package/dist/liquid-grid.js.map +1 -0
  124. package/dist/liquid.d.ts +18 -0
  125. package/dist/liquid.d.ts.map +1 -0
  126. package/dist/liquid.js +234 -0
  127. package/dist/liquid.js.map +1 -0
  128. package/dist/loader.d.ts +19 -0
  129. package/dist/loader.d.ts.map +1 -0
  130. package/dist/loader.js +173 -0
  131. package/dist/loader.js.map +1 -0
  132. package/dist/motion-scope.d.ts +39 -0
  133. package/dist/motion-scope.d.ts.map +1 -0
  134. package/dist/motion-scope.js +238 -0
  135. package/dist/motion-scope.js.map +1 -0
  136. package/dist/motion-states.d.ts +36 -0
  137. package/dist/motion-states.d.ts.map +1 -0
  138. package/dist/motion-states.js +165 -0
  139. package/dist/motion-states.js.map +1 -0
  140. package/dist/particles.d.ts +39 -0
  141. package/dist/particles.d.ts.map +1 -0
  142. package/dist/particles.js +265 -0
  143. package/dist/particles.js.map +1 -0
  144. package/dist/path.d.ts +12 -0
  145. package/dist/path.d.ts.map +1 -0
  146. package/dist/path.js +90 -0
  147. package/dist/path.js.map +1 -0
  148. package/dist/playback-group.d.ts +6 -0
  149. package/dist/playback-group.d.ts.map +1 -0
  150. package/dist/playback-group.js +141 -0
  151. package/dist/playback-group.js.map +1 -0
  152. package/dist/playback.d.ts +25 -0
  153. package/dist/playback.d.ts.map +1 -0
  154. package/dist/playback.js +35 -0
  155. package/dist/playback.js.map +1 -0
  156. package/dist/press.d.ts.map +1 -1
  157. package/dist/press.js +23 -7
  158. package/dist/press.js.map +1 -1
  159. package/dist/properties.d.ts +6 -3
  160. package/dist/properties.d.ts.map +1 -1
  161. package/dist/properties.js +26 -2
  162. package/dist/properties.js.map +1 -1
  163. package/dist/reveal.d.ts +25 -0
  164. package/dist/reveal.d.ts.map +1 -0
  165. package/dist/reveal.js +294 -0
  166. package/dist/reveal.js.map +1 -0
  167. package/dist/roll-diff.d.ts +22 -0
  168. package/dist/roll-diff.d.ts.map +1 -0
  169. package/dist/roll-diff.js +36 -0
  170. package/dist/roll-diff.js.map +1 -0
  171. package/dist/roll.d.ts +29 -0
  172. package/dist/roll.d.ts.map +1 -0
  173. package/dist/roll.js +442 -0
  174. package/dist/roll.js.map +1 -0
  175. package/dist/scroll-linked.d.ts +10 -0
  176. package/dist/scroll-linked.d.ts.map +1 -0
  177. package/dist/scroll-linked.js +58 -0
  178. package/dist/scroll-linked.js.map +1 -0
  179. package/dist/sequence.d.ts +10 -5
  180. package/dist/sequence.d.ts.map +1 -1
  181. package/dist/sequence.js +208 -14
  182. package/dist/sequence.js.map +1 -1
  183. package/dist/shimmer.d.ts +12 -0
  184. package/dist/shimmer.d.ts.map +1 -0
  185. package/dist/shimmer.js +161 -0
  186. package/dist/shimmer.js.map +1 -0
  187. package/dist/svg-path.d.ts +14 -0
  188. package/dist/svg-path.d.ts.map +1 -0
  189. package/dist/svg-path.js +281 -0
  190. package/dist/svg-path.js.map +1 -0
  191. package/dist/symbol-effect.d.ts +23 -0
  192. package/dist/symbol-effect.d.ts.map +1 -0
  193. package/dist/symbol-effect.js +187 -0
  194. package/dist/symbol-effect.js.map +1 -0
  195. package/dist/text-layout.d.ts +30 -0
  196. package/dist/text-layout.d.ts.map +1 -0
  197. package/dist/text-layout.js +185 -0
  198. package/dist/text-layout.js.map +1 -0
  199. package/dist/text.d.ts +17 -0
  200. package/dist/text.d.ts.map +1 -0
  201. package/dist/text.js +263 -0
  202. package/dist/text.js.map +1 -0
  203. package/dist/track.d.ts +3 -1
  204. package/dist/track.d.ts.map +1 -1
  205. package/dist/track.js +266 -13
  206. package/dist/track.js.map +1 -1
  207. package/dist/transition.d.ts +17 -1
  208. package/dist/transition.d.ts.map +1 -1
  209. package/dist/transition.js +41 -6
  210. package/dist/transition.js.map +1 -1
  211. package/dist/types.d.ts +48 -1
  212. package/dist/types.d.ts.map +1 -1
  213. package/dist/vue/index.d.ts +95 -0
  214. package/dist/vue/index.d.ts.map +1 -0
  215. package/dist/vue/index.js +163 -0
  216. package/dist/vue/index.js.map +1 -0
  217. package/dist/zoom-native.d.ts +30 -0
  218. package/dist/zoom-native.d.ts.map +1 -0
  219. package/dist/zoom-native.js +56 -0
  220. package/dist/zoom-native.js.map +1 -0
  221. package/dist/zoomable.d.ts +48 -0
  222. package/dist/zoomable.d.ts.map +1 -0
  223. package/dist/zoomable.js +249 -0
  224. package/dist/zoomable.js.map +1 -0
  225. package/package.json +29 -5
  226. package/platforms/android/include.gradle +7 -0
  227. package/platforms/android/java/org/rafaelaugusto/jolt/ColorTarget.java +102 -0
  228. package/platforms/android/java/org/rafaelaugusto/jolt/DecorationTarget.java +154 -0
  229. package/platforms/android/java/org/rafaelaugusto/jolt/ImageEffect.java +288 -0
  230. package/platforms/android/java/org/rafaelaugusto/jolt/KeyboardFollower.java +91 -0
  231. package/platforms/android/java/org/rafaelaugusto/jolt/KeyboardObserver.java +97 -0
  232. package/platforms/android/java/org/rafaelaugusto/jolt/NumberRoll.java +190 -0
  233. package/platforms/android/java/org/rafaelaugusto/jolt/PhotoZoom.java +250 -0
  234. package/platforms/android/java/org/rafaelaugusto/jolt/TextReveal.java +180 -0
  235. package/platforms/android/proguard-rules.pro +19 -0
  236. package/platforms/ios/build.xcconfig +1 -0
  237. package/platforms/ios/src/JoltImageEffect.h +25 -0
  238. package/platforms/ios/src/JoltImageEffect.mm +242 -0
  239. package/platforms/ios/src/JoltInteractiveAnimation.h +18 -0
  240. package/platforms/ios/src/JoltInteractiveAnimation.m +123 -0
  241. package/platforms/ios/src/JoltPhotoZoom.h +23 -0
  242. package/platforms/ios/src/JoltPhotoZoom.m +229 -0
  243. package/platforms/ios/src/module.modulemap +6 -0
  244. package/src/android-plugin.d.ts +155 -0
  245. package/src/animate.ts +249 -36
  246. package/src/color.ts +101 -0
  247. package/src/config.ts +84 -0
  248. package/src/decay.ts +45 -0
  249. package/src/decoration.ts +180 -0
  250. package/src/drag-input.ts +74 -0
  251. package/src/drag-physics.ts +110 -0
  252. package/src/drag.ts +395 -0
  253. package/src/driver/android-color.ts +170 -0
  254. package/src/driver/android-decoration.ts +182 -0
  255. package/src/driver/android-playback.ts +157 -0
  256. package/src/driver/android.ts +208 -20
  257. package/src/driver/index.ts +37 -2
  258. package/src/driver/ios-color.ts +441 -0
  259. package/src/driver/ios-decoration.ts +332 -0
  260. package/src/driver/ios-layout.ts +63 -0
  261. package/src/driver/ios-playback.ts +51 -0
  262. package/src/driver/ios-shadow.ts +50 -0
  263. package/src/driver/ios.ts +193 -33
  264. package/src/engine.ts +321 -27
  265. package/src/haptics.ts +148 -0
  266. package/src/image-effect-native.ts +129 -0
  267. package/src/image-effect.ts +336 -0
  268. package/src/image-owner.ts +12 -0
  269. package/src/index.ts +59 -2
  270. package/src/interaction-surface.ts +41 -0
  271. package/src/ios-plugin.d.ts +81 -0
  272. package/src/items.ts +140 -0
  273. package/src/keyboard.ts +291 -0
  274. package/src/label.ts +76 -0
  275. package/src/layout.ts +408 -0
  276. package/src/liquid-grid.ts +28 -0
  277. package/src/liquid.ts +287 -0
  278. package/src/loader.ts +202 -0
  279. package/src/motion-scope.ts +279 -0
  280. package/src/motion-states.ts +215 -0
  281. package/src/particles.ts +323 -0
  282. package/src/path.ts +124 -0
  283. package/src/playback-group.ts +143 -0
  284. package/src/playback.ts +62 -0
  285. package/src/press.ts +26 -10
  286. package/src/properties.ts +32 -6
  287. package/src/reveal.ts +374 -0
  288. package/src/roll-diff.ts +57 -0
  289. package/src/roll.ts +515 -0
  290. package/src/scroll-linked.ts +73 -0
  291. package/src/sequence.ts +204 -14
  292. package/src/shimmer.ts +180 -0
  293. package/src/svg-path.ts +355 -0
  294. package/src/symbol-effect.ts +226 -0
  295. package/src/text-layout.ts +228 -0
  296. package/src/text.ts +279 -0
  297. package/src/track.ts +347 -12
  298. package/src/transition.ts +63 -6
  299. package/src/types.ts +49 -1
  300. package/src/vue/index.ts +231 -0
  301. package/src/zoom-native.ts +124 -0
  302. package/src/zoomable.ts +330 -0
@@ -0,0 +1,231 @@
1
+ import {
2
+ BaseTransition,
3
+ defineComponent,
4
+ h,
5
+ onUnmounted,
6
+ onActivated,
7
+ onDeactivated,
8
+ readonly,
9
+ shallowRef,
10
+ type App,
11
+ type BaseTransitionProps,
12
+ type NSVElement,
13
+ type ObjectDirective,
14
+ type PropType,
15
+ type Ref,
16
+ } from 'nativescript-vue'
17
+ import {
18
+ press,
19
+ motionScope,
20
+ onKeyboard,
21
+ onReducedMotion,
22
+ reducedMotion,
23
+ type KeyboardState,
24
+ type MotionScope,
25
+ type AnimateOptions,
26
+ type Keyframes,
27
+ type PlaybackAnimation,
28
+ type PresetName,
29
+ type SpringName,
30
+ } from '../index.js'
31
+ import { currentKeyboard } from '../keyboard.js'
32
+
33
+ type Host = NSVElement
34
+ type Options = AnimateOptions | SpringName
35
+
36
+ /** A preset name or keyframes, alone or paired with a transition. */
37
+ export type AnimateValue =
38
+ PresetName | Keyframes | readonly [animation: PresetName | Keyframes, options?: Options]
39
+
40
+ /** The pressed keyframes, alone or paired with a transition. */
41
+ export type PressValue = Keyframes | readonly [pressed: Keyframes, options?: Options]
42
+
43
+ export interface TransitionProps {
44
+ /** Played when the child is inserted. */
45
+ enter?: AnimateValue
46
+ /** Played before the child is removed. */
47
+ leave?: AnimateValue
48
+ /** Also play `enter` on the first render. */
49
+ appear?: boolean
50
+ mode?: 'in-out' | 'out-in' | 'default'
51
+ }
52
+
53
+ const animations = new WeakMap<object, PlaybackAnimation>()
54
+ const scopes = new WeakMap<object, MotionScope>()
55
+ const removers = new WeakMap<object, () => void>()
56
+
57
+ function split<T>(
58
+ value: T | readonly [T, Options?],
59
+ ): [animation: T, options: Options | undefined] {
60
+ return Array.isArray(value)
61
+ ? [value[0] as T, value[1] as Options | undefined]
62
+ : [value as T, undefined]
63
+ }
64
+
65
+ function same(a: unknown, b: unknown): boolean {
66
+ if (Object.is(a, b)) return true
67
+ if (typeof a !== 'object' || typeof b !== 'object' || a === null || b === null)
68
+ return false
69
+ if (Array.isArray(a) !== Array.isArray(b)) return false
70
+ const keys = Object.keys(a)
71
+ return (
72
+ keys.length === Object.keys(b).length &&
73
+ keys.every((key) =>
74
+ same((a as Record<string, unknown>)[key], (b as Record<string, unknown>)[key]),
75
+ )
76
+ )
77
+ }
78
+
79
+ function play(el: Host, value: AnimateValue) {
80
+ const [animation, options] = split<PresetName | Keyframes>(value)
81
+ let scope = scopes.get(el)
82
+ if (!scope) scopes.set(el, (scope = motionScope()))
83
+ const handle = scope.animate(el, animation, options)
84
+ animations.set(el, handle)
85
+ return handle
86
+ }
87
+
88
+ function cancel(el: Host) {
89
+ scopes.get(el)?.dispose()
90
+ scopes.delete(el)
91
+ animations.delete(el)
92
+ }
93
+
94
+ function attach(el: Host, value: PressValue | undefined) {
95
+ const [pressed, options] = value === undefined ? [] : split<Keyframes>(value)
96
+ removers.set(el, press(el, pressed, options))
97
+ }
98
+
99
+ /**
100
+ * Plays an animation when the element mounts, and again whenever the value changes.
101
+ *
102
+ * ```vue
103
+ * <Label v-animate="'fadeInUp'" />
104
+ * <Label v-animate="['popIn', { delay: 200 }]" />
105
+ * ```
106
+ */
107
+ export const vAnimate: ObjectDirective<Host, AnimateValue> = {
108
+ mounted(el, { value }) {
109
+ play(el, value)
110
+ },
111
+ updated(el, { value, oldValue }) {
112
+ if (!same(value, oldValue)) play(el, value)
113
+ },
114
+ unmounted: cancel,
115
+ }
116
+
117
+ /**
118
+ * Press feedback while a finger is on the element.
119
+ *
120
+ * ```vue
121
+ * <GridLayout v-press />
122
+ * <GridLayout v-press="{ scale: 0.95 }" />
123
+ * ```
124
+ */
125
+ export const vPress: ObjectDirective<Host, PressValue | undefined> = {
126
+ mounted(el, { value }) {
127
+ attach(el, value)
128
+ },
129
+ updated(el, { value, oldValue }) {
130
+ if (same(value, oldValue)) return
131
+ removers.get(el)?.()
132
+ attach(el, value)
133
+ },
134
+ unmounted(el) {
135
+ removers.get(el)?.()
136
+ removers.delete(el)
137
+ },
138
+ }
139
+
140
+ /** The hooks `JoltTransition` hands to Vue's `BaseTransition`. */
141
+ export function transitionHooks(props: TransitionProps): BaseTransitionProps<Host> {
142
+ const run = (value: AnimateValue | undefined) => (el: Host, done: () => void) => {
143
+ if (value === undefined) {
144
+ done()
145
+ return
146
+ }
147
+ const handle = play(el, value)
148
+ void handle.finished.then(() => {
149
+ if (animations.get(el) === handle) animations.delete(el)
150
+ done()
151
+ })
152
+ }
153
+ const hooks: BaseTransitionProps<Host> = {
154
+ onEnter: run(props.enter),
155
+ onEnterCancelled: cancel,
156
+ onLeave: run(props.leave),
157
+ onLeaveCancelled: cancel,
158
+ }
159
+ if (props.appear) {
160
+ hooks.onAppear = hooks.onEnter
161
+ hooks.onAppearCancelled = cancel
162
+ }
163
+ return hooks
164
+ }
165
+
166
+ const value = {
167
+ type: [String, Object, Array] as PropType<AnimateValue>,
168
+ default: undefined,
169
+ }
170
+
171
+ /**
172
+ * Animates a child in and out. Vue keeps the element until the leave animation settles.
173
+ *
174
+ * ```vue
175
+ * <JoltTransition enter="popIn" leave="fadeOut">
176
+ * <Label v-if="visible" text="Saved" />
177
+ * </JoltTransition>
178
+ * ```
179
+ */
180
+ export const JoltTransition = defineComponent({
181
+ name: 'JoltTransition',
182
+ props: {
183
+ enter: value,
184
+ leave: value,
185
+ appear: Boolean,
186
+ mode: { type: String as PropType<TransitionProps['mode']>, default: undefined },
187
+ },
188
+ setup(props, { slots }) {
189
+ return () =>
190
+ h(
191
+ BaseTransition,
192
+ { ...transitionHooks(props), mode: props.mode, appear: props.appear },
193
+ slots,
194
+ )
195
+ },
196
+ })
197
+
198
+ /** Registers `v-animate`, `v-press` and `JoltTransition` on the app. */
199
+ export function install(app: App) {
200
+ app.directive('animate', vAnimate)
201
+ app.directive('press', vPress)
202
+ app.component('JoltTransition', JoltTransition)
203
+ }
204
+
205
+ export default { install }
206
+
207
+ /** Call in setup(): suspend cached components and dispose only on actual unmount. */
208
+ export function useMotionScope(): MotionScope {
209
+ const scope = motionScope()
210
+ onUnmounted(() => scope.dispose())
211
+ onDeactivated(() => scope.suspend())
212
+ onActivated(() => scope.resume())
213
+ return scope
214
+ }
215
+
216
+ /** Call in setup(): whether motion is reduced, kept up to date until the component unmounts. */
217
+ export function useReducedMotion(): Readonly<Ref<boolean>> {
218
+ const reduced = shallowRef(reducedMotion())
219
+ const off = onReducedMotion((value) => (reduced.value = value))
220
+ onUnmounted(off)
221
+ return readonly(reduced)
222
+ }
223
+
224
+ /** Call in setup(): the keyboard's state, kept up to date until the component unmounts. */
225
+ export function useKeyboard(): Readonly<Ref<KeyboardState>> {
226
+ const keyboard = shallowRef<KeyboardState>(currentKeyboard())
227
+ const off = onKeyboard((next) => (keyboard.value = next))
228
+ keyboard.value = currentKeyboard()
229
+ onUnmounted(off)
230
+ return readonly(keyboard) as Readonly<Ref<KeyboardState>>
231
+ }
@@ -0,0 +1,124 @@
1
+ import { Utils, isIOS, type Image, type View } from '@nativescript/core'
2
+
3
+ export interface ZoomSnapshot {
4
+ x: number
5
+ y: number
6
+ scale: number
7
+ progress: number
8
+ pending: boolean
9
+ }
10
+ export interface ZoomDriver {
11
+ readonly native: object
12
+ readonly state: ZoomSnapshot
13
+ zoom(scale: number, x: number, y: number, animated: boolean): void
14
+ reset(animated: boolean): void
15
+ requestDismiss(): void
16
+ setReduced(value: boolean): void
17
+ setActive(value: boolean): void
18
+ layout(): void
19
+ stop(): void
20
+ }
21
+ export interface ZoomSettings {
22
+ minimum: number
23
+ maximum: number
24
+ doubleTap: number
25
+ dismiss: boolean
26
+ reduced: boolean
27
+ backdrop?: View
28
+ chrome?: View
29
+ }
30
+
31
+ export function createZoomDriver(
32
+ image: Image,
33
+ viewport: View,
34
+ options: ZoomSettings,
35
+ dismiss: (
36
+ x: number,
37
+ y: number,
38
+ scale: number,
39
+ velocity: number,
40
+ progress: number,
41
+ ) => void,
42
+ settled: (x: number, y: number, scale: number) => void,
43
+ ): ZoomDriver {
44
+ const { minimum, maximum, doubleTap, reduced, backdrop, chrome } = options
45
+ if (isIOS) {
46
+ const native =
47
+ JoltPhotoZoom.attachImageViewportMinimumMaximumDoubleTapDismissReducedBackdropChromeOnDismissOnSettled(
48
+ image.nativeViewProtected,
49
+ viewport.nativeViewProtected,
50
+ minimum,
51
+ maximum,
52
+ doubleTap,
53
+ options.dismiss,
54
+ reduced,
55
+ backdrop?.nativeViewProtected ?? null,
56
+ chrome?.nativeViewProtected ?? null,
57
+ dismiss,
58
+ settled,
59
+ )
60
+ return {
61
+ native,
62
+ get state() {
63
+ return {
64
+ x: native.x,
65
+ y: native.y,
66
+ scale: native.scale,
67
+ progress: native.dismissProgress,
68
+ pending: native.dismissPending,
69
+ }
70
+ },
71
+ zoom: (s, x, y, a) => native.zoomFocusXFocusYAnimated(s, x, y, a),
72
+ reset: (a) => native.reset(a),
73
+ requestDismiss: () => native.requestDismiss(),
74
+ setReduced: (r) => native.setReduced(r),
75
+ setActive: (a) => native.setActive(a),
76
+ layout: () => native.layout(),
77
+ stop: () => native.stop(),
78
+ }
79
+ }
80
+ const density = Utils.layout.getDisplayDensity()
81
+ const source = image.imageSource!
82
+ const angle = Math.abs((source.rotationAngle ?? 0) % 180)
83
+ const swapped = angle > 45 && angle < 135
84
+ const native = new org.rafaelaugusto.jolt.PhotoZoom(
85
+ image.nativeViewProtected,
86
+ viewport.nativeViewProtected,
87
+ swapped ? source.height : source.width,
88
+ swapped ? source.width : source.height,
89
+ minimum,
90
+ maximum,
91
+ doubleTap,
92
+ options.dismiss,
93
+ reduced,
94
+ backdrop?.nativeViewProtected ?? null,
95
+ chrome?.nativeViewProtected ?? null,
96
+ (viewport as View & { touchListener?: android.view.View.OnTouchListener })
97
+ .touchListener ?? null,
98
+ (image as View & { touchListener?: android.view.View.OnTouchListener })
99
+ .touchListener ?? null,
100
+ new org.rafaelaugusto.jolt.PhotoZoom.Listener({
101
+ onDismiss: dismiss,
102
+ onSettled: settled,
103
+ }),
104
+ )
105
+ return {
106
+ native,
107
+ get state() {
108
+ return {
109
+ x: native.getX(),
110
+ y: native.getY(),
111
+ scale: native.getScale(),
112
+ progress: native.getDismissProgress(),
113
+ pending: native.isDismissPending(),
114
+ }
115
+ },
116
+ zoom: (s, x, y, a) => native.zoom(s, x * density, y * density, a),
117
+ reset: (a) => native.reset(a),
118
+ requestDismiss: () => native.requestDismiss(),
119
+ setReduced: (r) => native.setReduced(r),
120
+ setActive: (a) => native.setActive(a),
121
+ layout: () => {},
122
+ stop: () => native.stop(),
123
+ }
124
+ }
@@ -0,0 +1,330 @@
1
+ import { Application, Image, isIOS, type View } from '@nativescript/core'
2
+ import { stop, toView } from './animate.js'
3
+ import { reducedMotion } from './config.js'
4
+ import { ownImage } from './image-owner.js'
5
+ import { createZoomDriver, type ZoomDriver, type ZoomSnapshot } from './zoom-native.js'
6
+ import type { Target } from './types.js'
7
+
8
+ export type ZoomState = Readonly<ZoomSnapshot>
9
+ export interface PhotoDismissal {
10
+ readonly state: ZoomState
11
+ /** Release velocity in viewport-local DIPs per second. */
12
+ readonly velocity: number
13
+ /** Return to the viewer. False means this intent is obsolete. */
14
+ cancel(): boolean
15
+ /** Release the controller. Navigate only when this returns true. */
16
+ complete(): boolean
17
+ }
18
+ export interface ZoomableOptions {
19
+ /** Direct parent of the Image. Its bounds are the clipping/pointer viewport. */
20
+ viewport?: Target
21
+ /** Relative zoom range. Defaults to [1, 5]; minimum must be >= 1. */
22
+ scale?: readonly [number, number]
23
+ /** Double-tap zoom, clamped into the configured range. Defaults to 2.5. */
24
+ doubleTap?: number
25
+ dismiss?: 'vertical' | false
26
+ /** These views follow dismissal natively; no JS progress callback is needed. */
27
+ backdrop?: Target
28
+ chrome?: Target
29
+ /** One terminal intent per drag/programmatic close. App/router owns navigation. */
30
+ onDismiss?: (dismissal: PhotoDismissal) => void
31
+ /** Called at rest, never on each pointer/frame update. */
32
+ onSettled?: (state: ZoomState) => void
33
+ /** Setup failed after loading; resources are disposed before this callback. */
34
+ onError?: (error: Error) => void
35
+ }
36
+ export interface Zoomable {
37
+ readonly ready: boolean
38
+ readonly error: Error | undefined
39
+ readonly state: ZoomState
40
+ /** Native adapter entry point for native navigation integrations. */
41
+ readonly nativeController: object | undefined
42
+ zoomTo(scale: number, focus?: { x: number; y: number }, animated?: boolean): void
43
+ reset(animated?: boolean): void
44
+ requestDismiss(): void
45
+ cancelDismiss(): void
46
+ dispose(): void
47
+ }
48
+
49
+ /** Makes an Image interactive without per-frame JS. Owns its transforms until disposed. */
50
+ export function zoomable(target: Target, options: ZoomableOptions = {}): Zoomable {
51
+ const image = toView(target)
52
+ if (!(image instanceof Image))
53
+ throw new TypeError('zoomable() expects a NativeScript Image.')
54
+ if (!options || typeof options !== 'object')
55
+ throw new TypeError('Zoom options must be an object.')
56
+ const viewport = options.viewport
57
+ ? toView(options.viewport)
58
+ : (image.parent as View | undefined)
59
+ if (!viewport)
60
+ throw new TypeError('A zoom viewport is required before the image has a parent.')
61
+ const scale = options.scale ?? [1, 5]
62
+ if (
63
+ !Array.isArray(scale) ||
64
+ scale.length !== 2 ||
65
+ !scale.every(Number.isFinite) ||
66
+ scale[0]! < 1 ||
67
+ scale[1]! < scale[0]!
68
+ )
69
+ throw new RangeError(
70
+ 'Zoom scale must be [minimum, maximum], with 1 <= minimum <= maximum.',
71
+ )
72
+ const doubleTap = options.doubleTap ?? 2.5
73
+ if (!Number.isFinite(doubleTap) || doubleTap < 1)
74
+ throw new RangeError('Double-tap zoom must be a finite number >= 1.')
75
+ if (
76
+ options.dismiss !== undefined &&
77
+ options.dismiss !== false &&
78
+ options.dismiss !== 'vertical'
79
+ )
80
+ throw new TypeError("Dismiss must be 'vertical' or false.")
81
+ for (const callback of [options.onDismiss, options.onSettled, options.onError])
82
+ if (callback !== undefined && typeof callback !== 'function')
83
+ throw new TypeError('Zoom callbacks must be functions.')
84
+ const backdrop = options.backdrop ? toView(options.backdrop) : undefined
85
+ const chrome = options.chrome ? toView(options.chrome) : undefined
86
+ if (
87
+ backdrop === image ||
88
+ chrome === image ||
89
+ backdrop === viewport ||
90
+ chrome === viewport
91
+ )
92
+ throw new TypeError(
93
+ 'Dismissal backdrop/chrome must be separate from the image and viewport.',
94
+ )
95
+ if (image.parent && image.parent !== viewport)
96
+ throw new TypeError('The zoom viewport must be the image’s direct parent.')
97
+ if (
98
+ image.rotate ||
99
+ image.rotateX ||
100
+ image.rotateY ||
101
+ image.scaleX <= 0 ||
102
+ image.scaleY <= 0
103
+ )
104
+ throw new TypeError(
105
+ 'Zoom supports positive axis-aligned image transforms; rotate an ancestor instead.',
106
+ )
107
+ return new Photo(image, viewport, { ...options, doubleTap, scale, backdrop, chrome })
108
+ }
109
+
110
+ class Photo implements Zoomable {
111
+ private failure: Error | undefined
112
+ private suspended = false
113
+ private driver: ZoomDriver | undefined
114
+ private stopped = false
115
+ private queued = false
116
+ private releaseOwnership: () => void = () => {}
117
+ private generation = 0
118
+ private reduceObserver:
119
+ ReturnType<typeof Application.ios.addNotificationObserver> | undefined
120
+ private latest: ZoomSnapshot
121
+ constructor(
122
+ private readonly image: Image,
123
+ private readonly viewport: View,
124
+ private readonly options: Omit<ZoomableOptions, 'backdrop' | 'chrome'> & {
125
+ scale: readonly [number, number]
126
+ doubleTap: number
127
+ backdrop?: View
128
+ chrome?: View
129
+ },
130
+ ) {
131
+ this.latest = { x: 0, y: 0, scale: options.scale[0], progress: 0, pending: false }
132
+ this.releaseOwnership = ownImage(image, this.dispose)
133
+ stop(image, ['x', 'y', 'scaleX', 'scaleY'])
134
+ for (const view of [image, viewport]) {
135
+ view.on('loaded', this.schedule)
136
+ view.on('layoutChanged', this.schedule)
137
+ view.on('unloaded', this.dispose)
138
+ }
139
+ image.on('imageSourceChange', this.sourceChanged)
140
+ image.on('stretchChange', this.sourceChanged)
141
+ Application.on(Application.suspendEvent, this.suspend)
142
+ Application.on(Application.resumeEvent, this.resume)
143
+ if (isIOS)
144
+ this.reduceObserver = Application.ios.addNotificationObserver(
145
+ UIAccessibilityReduceMotionStatusDidChangeNotification,
146
+ this.preferences,
147
+ )
148
+ this.schedule()
149
+ }
150
+ get ready() {
151
+ return !!this.driver
152
+ }
153
+ get error() {
154
+ return this.failure
155
+ }
156
+ get state(): ZoomState {
157
+ return { ...(this.driver?.state ?? this.latest) }
158
+ }
159
+ get nativeController() {
160
+ return this.driver?.native
161
+ }
162
+ zoomTo(value: number, focus?: { x: number; y: number }, animated = true) {
163
+ if (
164
+ !Number.isFinite(value) ||
165
+ value < this.options.scale[0] ||
166
+ value > this.options.scale[1]
167
+ )
168
+ throw new RangeError('Zoom must be within the configured scale range.')
169
+ if (focus && (!Number.isFinite(focus.x) || !Number.isFinite(focus.y)))
170
+ throw new RangeError('Zoom focus must be finite.')
171
+ if (this.stopped) return
172
+ const size = this.viewport.getActualSize()
173
+ this.driver?.zoom(
174
+ value,
175
+ focus?.x ?? size.width / 2,
176
+ focus?.y ?? size.height / 2,
177
+ animated,
178
+ )
179
+ }
180
+ reset(animated = true) {
181
+ this.generation++
182
+ this.driver?.reset(animated)
183
+ }
184
+ requestDismiss() {
185
+ this.driver?.requestDismiss()
186
+ }
187
+ cancelDismiss() {
188
+ this.reset(true)
189
+ }
190
+ dispose = () => {
191
+ if (this.stopped) return
192
+ this.stopped = true
193
+ this.generation++
194
+ this.latest = {
195
+ x: 0,
196
+ y: 0,
197
+ scale: this.options.scale[0],
198
+ progress: 0,
199
+ pending: false,
200
+ }
201
+ this.driver?.stop()
202
+ this.driver = undefined
203
+ this.releaseOwnership()
204
+ for (const view of [this.image, this.viewport]) {
205
+ view.off('loaded', this.schedule)
206
+ view.off('layoutChanged', this.schedule)
207
+ view.off('unloaded', this.dispose)
208
+ }
209
+ this.image.off('imageSourceChange', this.sourceChanged)
210
+ this.image.off('stretchChange', this.sourceChanged)
211
+ Application.off(Application.suspendEvent, this.suspend)
212
+ Application.off(Application.resumeEvent, this.resume)
213
+ if (this.reduceObserver)
214
+ Application.ios.removeNotificationObserver(
215
+ this.reduceObserver,
216
+ UIAccessibilityReduceMotionStatusDidChangeNotification,
217
+ )
218
+ this.reduceObserver = undefined
219
+ }
220
+ private dismissed = (
221
+ x: number,
222
+ y: number,
223
+ scale: number,
224
+ velocity: number,
225
+ progress: number,
226
+ ) => {
227
+ if (this.stopped) return
228
+ const generation = ++this.generation
229
+ const valid = () => !this.stopped && generation === this.generation
230
+ const dismissal: PhotoDismissal = {
231
+ state: { x, y, scale, progress, pending: true },
232
+ velocity,
233
+ cancel: () => {
234
+ if (!valid()) return false
235
+ this.cancelDismiss()
236
+ return true
237
+ },
238
+ complete: () => {
239
+ if (!valid()) return false
240
+ this.dispose()
241
+ return true
242
+ },
243
+ }
244
+ if (this.options.onDismiss) this.options.onDismiss(dismissal)
245
+ else dismissal.cancel()
246
+ }
247
+ private settled = (x: number, y: number, scale: number) => {
248
+ if (this.stopped) return
249
+ this.generation++
250
+ this.latest = { x, y, scale, progress: 0, pending: false }
251
+ this.options.onSettled?.({ ...this.latest })
252
+ }
253
+ private sourceChanged = () => {
254
+ this.generation++
255
+ this.driver?.stop()
256
+ this.driver = undefined
257
+ this.schedule()
258
+ }
259
+ private suspend = () => {
260
+ this.suspended = true
261
+ this.generation++
262
+ this.driver?.setActive(false)
263
+ }
264
+ private resume = () => {
265
+ this.suspended = false
266
+ this.preferences()
267
+ this.driver?.setActive(true)
268
+ this.schedule()
269
+ }
270
+ private preferences = () => this.driver?.setReduced(reducedMotion())
271
+ private schedule = () => {
272
+ if (this.stopped || this.queued) return
273
+ this.queued = true
274
+ void Promise.resolve()
275
+ .then(() => {
276
+ this.queued = false
277
+ if (
278
+ this.stopped ||
279
+ this.suspended ||
280
+ !this.image.isLoaded ||
281
+ !this.viewport.isLoaded
282
+ )
283
+ return
284
+ if (this.driver) {
285
+ this.driver.layout()
286
+ return
287
+ }
288
+ if (this.image.parent !== this.viewport)
289
+ throw new TypeError('Zoom viewport must remain the direct image parent.')
290
+ const size = this.viewport.getActualSize()
291
+ const imageSize = this.image.getActualSize(),
292
+ source = this.image.imageSource
293
+ if (
294
+ size.width <= 0 ||
295
+ size.height <= 0 ||
296
+ imageSize.width <= 0 ||
297
+ imageSize.height <= 0 ||
298
+ !source ||
299
+ !Number.isFinite(source.width) ||
300
+ !Number.isFinite(source.height) ||
301
+ source.width <= 0 ||
302
+ source.height <= 0
303
+ )
304
+ return
305
+ if (isIOS && typeof JoltPhotoZoom === 'undefined')
306
+ throw new Error('Rebuild the native app to install Jolt photo gestures.')
307
+ this.driver = createZoomDriver(
308
+ this.image,
309
+ this.viewport,
310
+ {
311
+ minimum: this.options.scale[0],
312
+ maximum: this.options.scale[1],
313
+ doubleTap: this.options.doubleTap,
314
+ dismiss: this.options.dismiss === 'vertical',
315
+ reduced: reducedMotion(),
316
+ backdrop: this.options.backdrop,
317
+ chrome: this.options.chrome,
318
+ },
319
+ this.dismissed,
320
+ this.settled,
321
+ )
322
+ })
323
+ .catch((error: unknown) => {
324
+ if (this.stopped) return
325
+ this.failure = error instanceof Error ? error : new Error(String(error))
326
+ this.dispose()
327
+ this.options.onError?.(this.failure)
328
+ })
329
+ }
330
+ }