@sigx/lynx-gestures 0.1.2 → 0.4.1

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.
@@ -0,0 +1,106 @@
1
+ import { signal } from '@sigx/lynx';
2
+ import { distance, midpoint } from './utils.js';
3
+ /**
4
+ * Two-finger pinch/zoom gesture.
5
+ *
6
+ * Tracks fingers manually since Lynx fires separate touchstart events per
7
+ * finger (each with touches.length=1). Uses proximity-based matching on
8
+ * touchmove since Lynx identifiers are unreliable across events.
9
+ *
10
+ * NOTE: Requires a device/environment that delivers multi-touch events to
11
+ * the same element. Some Lynx hosts (e.g. Lynx Explorer on emulator) may
12
+ * not support this — test on a physical device.
13
+ */
14
+ export function usePinch(options = {}) {
15
+ const { onPinch } = options;
16
+ const state = signal({
17
+ phase: 'idle',
18
+ scale: 1,
19
+ focalX: 0,
20
+ focalY: 0,
21
+ });
22
+ let baseDistance = 0;
23
+ let active = false;
24
+ let finger1 = null;
25
+ let finger2 = null;
26
+ function onTouchStart(e) {
27
+ const t = e.touches[0];
28
+ if (!t)
29
+ return;
30
+ if (!finger1) {
31
+ finger1 = { ...t };
32
+ }
33
+ else if (!finger2) {
34
+ finger2 = { ...t };
35
+ active = true;
36
+ baseDistance = distance(finger1.pageX, finger1.pageY, finger2.pageX, finger2.pageY);
37
+ const [fx, fy] = midpoint(finger1.pageX, finger1.pageY, finger2.pageX, finger2.pageY);
38
+ state.phase = 'began';
39
+ state.scale = 1;
40
+ state.focalX = fx;
41
+ state.focalY = fy;
42
+ }
43
+ }
44
+ function onTouchMove(e) {
45
+ if (!active || !finger1 || !finger2)
46
+ return;
47
+ const t = e.changedTouches[0];
48
+ if (!t)
49
+ return;
50
+ // Determine which finger moved by proximity
51
+ const dist1 = distance(t.pageX, t.pageY, finger1.pageX, finger1.pageY);
52
+ const dist2 = distance(t.pageX, t.pageY, finger2.pageX, finger2.pageY);
53
+ if (dist1 < dist2) {
54
+ finger1 = { ...t };
55
+ }
56
+ else {
57
+ finger2 = { ...t };
58
+ }
59
+ const currentDist = distance(finger1.pageX, finger1.pageY, finger2.pageX, finger2.pageY);
60
+ const scale = baseDistance > 0 ? currentDist / baseDistance : 1;
61
+ const [fx, fy] = midpoint(finger1.pageX, finger1.pageY, finger2.pageX, finger2.pageY);
62
+ state.phase = 'active';
63
+ state.scale = scale;
64
+ state.focalX = fx;
65
+ state.focalY = fy;
66
+ onPinch?.(state);
67
+ }
68
+ function onTouchEnd() {
69
+ if (active) {
70
+ state.phase = 'ended';
71
+ onPinch?.(state);
72
+ }
73
+ active = false;
74
+ finger1 = null;
75
+ finger2 = null;
76
+ }
77
+ function onTouchCancel() {
78
+ if (active) {
79
+ state.phase = 'cancelled';
80
+ onPinch?.(state);
81
+ }
82
+ active = false;
83
+ finger1 = null;
84
+ finger2 = null;
85
+ }
86
+ function reset() {
87
+ active = false;
88
+ finger1 = null;
89
+ finger2 = null;
90
+ state.phase = 'idle';
91
+ state.scale = 1;
92
+ state.focalX = 0;
93
+ state.focalY = 0;
94
+ }
95
+ return {
96
+ state,
97
+ handlers: {
98
+ bindtouchstart: onTouchStart,
99
+ bindtouchmove: onTouchMove,
100
+ bindtouchend: onTouchEnd,
101
+ bindtouchcancel: onTouchCancel,
102
+ },
103
+ reset,
104
+ };
105
+ }
106
+ //# sourceMappingURL=use-pinch.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-pinch.js","sourceRoot":"","sources":["../src/use-pinch.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,117 @@
1
+ import { signal } from '@sigx/lynx';
2
+ import { angle, angleDelta, distance, midpoint } from './utils.js';
3
+ /**
4
+ * Two-finger rotation gesture.
5
+ *
6
+ * Tracks the angle of the line between two fingers; reports cumulative rotation
7
+ * in radians from gesture start. Like usePinch, uses proximity-based finger
8
+ * matching on touchmove (Lynx touch identifiers are not stable across events).
9
+ *
10
+ * NOTE: requires multi-touch delivery to the same element. Some Lynx hosts
11
+ * (Lynx Explorer on emulator) may not support this — test on a physical device.
12
+ */
13
+ export function useRotation(options = {}) {
14
+ const { onRotation } = options;
15
+ const state = signal({
16
+ phase: 'idle',
17
+ rotation: 0,
18
+ velocity: 0,
19
+ focalX: 0,
20
+ focalY: 0,
21
+ });
22
+ let baseAngle = 0;
23
+ let prevAngle = 0;
24
+ let prevTime = 0;
25
+ let active = false;
26
+ let finger1 = null;
27
+ let finger2 = null;
28
+ function onTouchStart(e) {
29
+ const t = e.touches[0];
30
+ if (!t)
31
+ return;
32
+ if (!finger1) {
33
+ finger1 = { ...t };
34
+ }
35
+ else if (!finger2) {
36
+ finger2 = { ...t };
37
+ active = true;
38
+ baseAngle = angle(finger1.pageX, finger1.pageY, finger2.pageX, finger2.pageY);
39
+ prevAngle = baseAngle;
40
+ prevTime = Date.now();
41
+ const [fx, fy] = midpoint(finger1.pageX, finger1.pageY, finger2.pageX, finger2.pageY);
42
+ state.phase = 'began';
43
+ state.rotation = 0;
44
+ state.velocity = 0;
45
+ state.focalX = fx;
46
+ state.focalY = fy;
47
+ }
48
+ }
49
+ function onTouchMove(e) {
50
+ if (!active || !finger1 || !finger2)
51
+ return;
52
+ const t = e.changedTouches[0];
53
+ if (!t)
54
+ return;
55
+ const dist1 = distance(t.pageX, t.pageY, finger1.pageX, finger1.pageY);
56
+ const dist2 = distance(t.pageX, t.pageY, finger2.pageX, finger2.pageY);
57
+ if (dist1 < dist2) {
58
+ finger1 = { ...t };
59
+ }
60
+ else {
61
+ finger2 = { ...t };
62
+ }
63
+ const now = Date.now();
64
+ const dt = Math.max(now - prevTime, 1);
65
+ const currentAngle = angle(finger1.pageX, finger1.pageY, finger2.pageX, finger2.pageY);
66
+ const rotation = angleDelta(baseAngle, currentAngle);
67
+ const velocity = angleDelta(prevAngle, currentAngle) / dt;
68
+ const [fx, fy] = midpoint(finger1.pageX, finger1.pageY, finger2.pageX, finger2.pageY);
69
+ prevAngle = currentAngle;
70
+ prevTime = now;
71
+ state.phase = 'active';
72
+ state.rotation = rotation;
73
+ state.velocity = velocity;
74
+ state.focalX = fx;
75
+ state.focalY = fy;
76
+ onRotation?.(state);
77
+ }
78
+ function onTouchEnd() {
79
+ if (active) {
80
+ state.phase = 'ended';
81
+ onRotation?.(state);
82
+ }
83
+ active = false;
84
+ finger1 = null;
85
+ finger2 = null;
86
+ }
87
+ function onTouchCancel() {
88
+ if (active) {
89
+ state.phase = 'cancelled';
90
+ onRotation?.(state);
91
+ }
92
+ active = false;
93
+ finger1 = null;
94
+ finger2 = null;
95
+ }
96
+ function reset() {
97
+ active = false;
98
+ finger1 = null;
99
+ finger2 = null;
100
+ state.phase = 'idle';
101
+ state.rotation = 0;
102
+ state.velocity = 0;
103
+ state.focalX = 0;
104
+ state.focalY = 0;
105
+ }
106
+ return {
107
+ state,
108
+ handlers: {
109
+ bindtouchstart: onTouchStart,
110
+ bindtouchmove: onTouchMove,
111
+ bindtouchend: onTouchEnd,
112
+ bindtouchcancel: onTouchCancel,
113
+ },
114
+ reset,
115
+ };
116
+ }
117
+ //# sourceMappingURL=use-rotation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-rotation.js","sourceRoot":"","sources":["../src/use-rotation.ts"],"names":[],"mappings":"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"}
package/dist/utils.js ADDED
@@ -0,0 +1,25 @@
1
+ // Geometry helpers used by the multi-touch JS-only fallback hooks
2
+ // (`usePinch`, `useRotation`). The arena-driven gesture surface
3
+ // (`Gesture.*` + `useGestureDetector`) computes its own deltas natively;
4
+ // this file is only relevant while the platform's pinch/rotation handlers
5
+ // are unfinished.
6
+ export function distance(x1, y1, x2, y2) {
7
+ return Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2);
8
+ }
9
+ export function midpoint(x1, y1, x2, y2) {
10
+ return [(x1 + x2) / 2, (y1 + y2) / 2];
11
+ }
12
+ /** Signed angle in radians from p1 to p2, range (-π, π]. */
13
+ export function angle(x1, y1, x2, y2) {
14
+ return Math.atan2(y2 - y1, x2 - x1);
15
+ }
16
+ /** Shortest signed angular delta between two radians, range (-π, π]. */
17
+ export function angleDelta(from, to) {
18
+ let d = to - from;
19
+ while (d > Math.PI)
20
+ d -= 2 * Math.PI;
21
+ while (d <= -Math.PI)
22
+ d += 2 * Math.PI;
23
+ return d;
24
+ }
25
+ //# sourceMappingURL=utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA,kEAAkE;AAClE,gEAAgE;AAChE,yEAAyE;AACzE,0EAA0E;AAC1E,kBAAkB;AAElB,MAAM,UAAU,QAAQ,CAAC,EAAU,EAAE,EAAU,EAAE,EAAU,EAAE,EAAU;IACrE,OAAO,IAAI,CAAC,IAAI,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC;AACpD,CAAC;AAED,MAAM,UAAU,QAAQ,CAAC,EAAU,EAAE,EAAU,EAAE,EAAU,EAAE,EAAU;IACrE,OAAO,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;AACxC,CAAC;AAED,4DAA4D;AAC5D,MAAM,UAAU,KAAK,CAAC,EAAU,EAAE,EAAU,EAAE,EAAU,EAAE,EAAU;IAClE,OAAO,IAAI,CAAC,KAAK,CAAC,EAAE,GAAG,EAAE,EAAE,EAAE,GAAG,EAAE,CAAC,CAAC;AACtC,CAAC;AAED,wEAAwE;AACxE,MAAM,UAAU,UAAU,CAAC,IAAY,EAAE,EAAU;IACjD,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC;IAClB,OAAO,CAAC,GAAG,IAAI,CAAC,EAAE;QAAE,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,EAAE,CAAC;IACrC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE;QAAE,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,EAAE,CAAC;IACvC,OAAO,CAAC,CAAC;AACX,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sigx/lynx-gestures",
3
- "version": "0.1.2",
3
+ "version": "0.4.1",
4
4
  "description": "Gesture system for sigx-lynx - declarative composables for tap, pan, pinch, swipe, long press",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -28,15 +28,15 @@
28
28
  "author": "Andreas Ekdahl",
29
29
  "license": "MIT",
30
30
  "dependencies": {
31
- "@sigx/lynx": "^0.1.4"
31
+ "@sigx/lynx": "^0.4.1"
32
32
  },
33
33
  "devDependencies": {
34
- "@lynx-js/react": "^0.119.0",
34
+ "@lynx-js/react": "^0.121.0",
35
+ "@typescript/native-preview": "7.0.0-dev.20260511.1",
35
36
  "typescript": "^6.0.3",
36
- "vite": "^8.0.12",
37
- "@sigx/lynx-plugin": "^0.2.7",
38
- "@sigx/lynx-runtime-main": "^0.2.7",
39
- "@sigx/lynx-testing": "^0.2.6"
37
+ "@sigx/lynx-runtime-main": "^0.4.1",
38
+ "@sigx/lynx-plugin": "^0.4.1",
39
+ "@sigx/lynx-testing": "^0.4.1"
40
40
  },
41
41
  "repository": {
42
42
  "type": "git",
@@ -51,7 +51,8 @@
51
51
  "access": "public"
52
52
  },
53
53
  "scripts": {
54
- "build": "vite build && tsgo --emitDeclarationOnly",
55
- "dev": "vite build --watch"
54
+ "build": "node ../../scripts/clean.mjs dist && tsgo",
55
+ "dev": "tsgo --watch",
56
+ "clean": "node ../../scripts/clean.mjs dist .turbo"
56
57
  }
57
58
  }
@@ -16,6 +16,17 @@ export type PressableProps =
16
16
  & Define.Prop<'disabled', boolean, false>
17
17
  & Define.Prop<'class', string, false>
18
18
  & Define.Prop<'style', Record<string, string | number>, false>
19
+ // Accessibility passthrough — forwarded onto the inner <view> so the
20
+ // interactive node (the one that owns the tap handler) is also the one
21
+ // screen readers activate. Without these, callers who want a11y on a
22
+ // Pressable have to wrap it in an outer accessibility-element <view>,
23
+ // which puts the metadata and the gesture handler on different nodes
24
+ // and breaks screen-reader activation.
25
+ & Define.Prop<'accessibility-element', boolean, false>
26
+ & Define.Prop<'accessibility-label', string, false>
27
+ & Define.Prop<'accessibility-role', string, false>
28
+ & Define.Prop<'accessibility-trait', string, false>
29
+ & Define.Prop<'accessibility-status', string, false>
19
30
  & Define.Slot<'default'>
20
31
  & Define.Event<'press', void>
21
32
  & Define.Event<'longPress', void>;
@@ -57,9 +68,10 @@ interface PressableMTState {
57
68
  * `LongPress.onEnd` skips press emission when the touch drifted past
58
69
  * the threshold (matching Tap's success criteria).
59
70
  *
60
- * Disabled is captured at setup; runtime toggling won't update an active
61
- * gesture's behavior. Wrap the parent in conditional rendering for now if
62
- * dynamic disable is needed.
71
+ * `disabled` reads live from a `MainThreadRef` so flipping it after mount
72
+ * (e.g. a `<Button loading>` toggling on) immediately suppresses both
73
+ * visual feedback and emit, without remounting the component or the
74
+ * underlying gesture registration.
63
75
  */
64
76
  export const Pressable = component<PressableProps>(({ props, slots, emit }) => {
65
77
  const elRef = useMainThreadRef<MainThread.Element | null>(null);
@@ -74,7 +86,11 @@ export const Pressable = component<PressableProps>(({ props, slots, emit }) => {
74
86
  const minDuration = longPressDuration > 0 ? longPressDuration : 1_000_000;
75
87
  const maxDistance = props.maxDistance ?? 10;
76
88
  const maxDistanceSq = maxDistance * maxDistance;
77
- const disabled = props.disabled ?? false;
89
+
90
+ // Reactive `disabled` — worklets read `disabledRef.current` so prop
91
+ // changes after mount take effect without re-registering the gesture.
92
+ // The render fn below keeps this ref in sync each pass.
93
+ const disabledRef = useMainThreadRef<boolean>(!!props.disabled);
78
94
 
79
95
  const state = useMainThreadRef<PressableMTState>({
80
96
  longPressFired: false,
@@ -87,7 +103,7 @@ export const Pressable = component<PressableProps>(({ props, slots, emit }) => {
87
103
  .maxDistance(maxDistance)
88
104
  .onBegin((e: any) => {
89
105
  'main thread';
90
- if (disabled) return;
106
+ if (disabledRef.current) return;
91
107
  // Reset the cross-platform state on every fresh touch-down. Both
92
108
  // Tap.onBegin and LongPress.onBegin fire — first one wins, second
93
109
  // is a no-op because pressEmitted/longPressFired are already false.
@@ -103,7 +119,7 @@ export const Pressable = component<PressableProps>(({ props, slots, emit }) => {
103
119
  })
104
120
  .onStart(() => {
105
121
  'main thread';
106
- if (disabled) return;
122
+ if (disabledRef.current) return;
107
123
  // Android path: Tap.onStart fires on touchend within maxDuration;
108
124
  // emit press here. The LongPress.onEnd fallback below is gated on
109
125
  // !pressEmitted so it won't double-fire on Android.
@@ -129,7 +145,7 @@ export const Pressable = component<PressableProps>(({ props, slots, emit }) => {
129
145
  .maxDistance(maxDistance)
130
146
  .onBegin(() => {
131
147
  'main thread';
132
- if (disabled) return;
148
+ if (disabledRef.current) return;
133
149
  // Idempotent with Tap.onBegin — both fire on touch-down. State has
134
150
  // already been initialised by Tap.onBegin (whichever fires first).
135
151
  elRef.current?.setStyleProperties({
@@ -139,7 +155,7 @@ export const Pressable = component<PressableProps>(({ props, slots, emit }) => {
139
155
  })
140
156
  .onStart(() => {
141
157
  'main thread';
142
- if (disabled) return;
158
+ if (disabledRef.current) return;
143
159
  state.current.longPressFired = true;
144
160
  runOnBackground(() => { emit('longPress'); })();
145
161
  })
@@ -151,7 +167,7 @@ export const Pressable = component<PressableProps>(({ props, slots, emit }) => {
151
167
  opacity: 1,
152
168
  transform: 'scale(1)',
153
169
  });
154
- if (disabled) return;
170
+ if (disabledRef.current) return;
155
171
  // iOS fallback path. On iOS Tap.onStart never fires, so press would
156
172
  // never emit without this. On Android this is a no-op because
157
173
  // pressEmitted is already true (or longPressFired is true).
@@ -169,13 +185,23 @@ export const Pressable = component<PressableProps>(({ props, slots, emit }) => {
169
185
 
170
186
  useGestureDetector(elRef, gesture);
171
187
 
172
- return () => (
173
- <view
174
- class={props.class}
175
- style={props.style}
176
- main-thread:ref={elRef}
177
- >
178
- {slots.default?.()}
179
- </view>
180
- );
188
+ return () => {
189
+ // Keep the reactive-disabled ref in sync with the prop on every render.
190
+ // Worklets read `.current` at call time, so this is the only writer.
191
+ disabledRef.current = !!props.disabled;
192
+ return (
193
+ <view
194
+ class={props.class}
195
+ style={props.style}
196
+ main-thread:ref={elRef}
197
+ accessibility-element={props['accessibility-element']}
198
+ accessibility-label={props['accessibility-label']}
199
+ accessibility-role={props['accessibility-role']}
200
+ accessibility-trait={props['accessibility-trait']}
201
+ accessibility-status={props['accessibility-status']}
202
+ >
203
+ {slots.default?.()}
204
+ </view>
205
+ );
206
+ };
181
207
  });