@object-ui/mobile 16.1.0 → 17.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,57 @@
1
1
  # @object-ui/mobile
2
2
 
3
+ ## 17.1.0
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [9e7349e]
8
+ - Updated dependencies [8864971]
9
+ - Updated dependencies [b41f401]
10
+ - Updated dependencies [19e9fa0]
11
+ - Updated dependencies [38ca8be]
12
+ - Updated dependencies [4952edf]
13
+ - Updated dependencies [7f0252e]
14
+ - Updated dependencies [7639a61]
15
+ - Updated dependencies [94e63ef]
16
+ - Updated dependencies [c4db402]
17
+ - Updated dependencies [5319bf1]
18
+ - Updated dependencies [49e5671]
19
+ - Updated dependencies [b5b97e2]
20
+ - Updated dependencies [f59f2c1]
21
+ - Updated dependencies [4874117]
22
+ - Updated dependencies [ce08d55]
23
+ - Updated dependencies [2374a49]
24
+ - Updated dependencies [ea7f477]
25
+ - Updated dependencies [7f23cd0]
26
+ - Updated dependencies [24e0e0a]
27
+ - Updated dependencies [3a6cf24]
28
+ - Updated dependencies [aa35561]
29
+ - Updated dependencies [03bd53b]
30
+ - Updated dependencies [3c1f321]
31
+ - Updated dependencies [a045a32]
32
+ - Updated dependencies [912496d]
33
+ - Updated dependencies [9867281]
34
+ - @object-ui/types@17.1.0
35
+
36
+ ## 17.0.0
37
+
38
+ ### Patch Changes
39
+
40
+ - Updated dependencies [1767124]
41
+ - Updated dependencies [8ecf5a6]
42
+ - Updated dependencies [dfd3705]
43
+ - Updated dependencies [6dee2cb]
44
+ - Updated dependencies [c7cff19]
45
+ - Updated dependencies [cd09a7b]
46
+ - Updated dependencies [f1abf0e]
47
+ - Updated dependencies [f05b84e]
48
+ - Updated dependencies [662bdf9]
49
+ - Updated dependencies [059a052]
50
+ - Updated dependencies [53642d4]
51
+ - Updated dependencies [8aae006]
52
+ - Updated dependencies [d147a13]
53
+ - @object-ui/types@17.0.0
54
+
3
55
  ## 16.1.0
4
56
 
5
57
  ### Patch Changes
package/dist/index.d.ts CHANGED
@@ -33,5 +33,5 @@ export { createOfflineDataSource, type OfflineDataSource, type OfflineDataSource
33
33
  export { useOfflineSync, type OfflineSyncState } from './useOfflineSync';
34
34
  export { getServiceWorkerSource, requestBackgroundSync, type ServiceWorkerSourceOptions, } from './serviceWorkerSource';
35
35
  export { BREAKPOINTS, resolveResponsiveValue } from './breakpoints';
36
- export type { BreakpointName, ResponsiveValue, ResponsiveConfig, MobileOverrides, PWAConfig, PWAIcon, CacheStrategy, OfflineConfig, OfflineRoute, GestureType, GestureConfig, GestureContext, MobileComponentConfig, SpecGestureConfig, SwipeGestureConfig, PinchGestureConfig, LongPressGestureConfig, TouchInteraction, TouchTargetConfig, } from '@object-ui/types';
36
+ export type { BreakpointName, ResponsiveValue, ResponsiveConfig, MobileOverrides, PWAConfig, PWAIcon, FetchCacheStrategy, OfflineConfig, OfflineRoute, GestureType, GestureConfig, GestureContext, MobileComponentConfig, SpecGestureConfig, SwipeGestureConfig, PinchGestureConfig, LongPressGestureConfig, TouchInteraction, TouchTargetConfig, } from '@object-ui/types';
37
37
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,aAAa,EAAE,KAAK,eAAe,EAAE,MAAM,iBAAiB,CAAC;AACtE,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,OAAO,EAAE,mBAAmB,EAAE,KAAK,oBAAoB,EAAE,KAAK,uBAAuB,EAAE,MAAM,uBAAuB,CAAC;AACrH,OAAO,EAAE,UAAU,EAAE,KAAK,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAClE,OAAO,EAAE,cAAc,EAAE,KAAK,qBAAqB,EAAE,MAAM,kBAAkB,CAAC;AAC9E,OAAO,EAAE,cAAc,EAAE,KAAK,qBAAqB,EAAE,KAAK,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AACtG,OAAO,EAAE,gBAAgB,EAAE,KAAK,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AACjF,OAAO,EAAE,cAAc,EAAE,KAAK,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AAC5E,OAAO,EAAE,mBAAmB,EAAE,KAAK,wBAAwB,EAAE,MAAM,uBAAuB,CAAC;AAC3F,OAAO,EAAE,mBAAmB,EAAE,MAAM,OAAO,CAAC;AAC5C,OAAO,EAAE,qBAAqB,EAAE,KAAK,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AAClF,OAAO,EACL,kBAAkB,EAClB,qBAAqB,EACrB,kBAAkB,EAClB,YAAY,EACZ,KAAK,gBAAgB,EACrB,KAAK,mBAAmB,GACzB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACL,uBAAuB,EACvB,KAAK,iBAAiB,EACtB,KAAK,wBAAwB,EAC7B,KAAK,mBAAmB,GACzB,MAAM,2BAA2B,CAAC;AACnC,OAAO,EAAE,cAAc,EAAE,KAAK,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACzE,OAAO,EACL,sBAAsB,EACtB,qBAAqB,EACrB,KAAK,0BAA0B,GAChC,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,WAAW,EAAE,sBAAsB,EAAE,MAAM,eAAe,CAAC;AAGpE,YAAY,EACV,cAAc,EACd,eAAe,EACf,gBAAgB,EAChB,eAAe,EACf,SAAS,EACT,OAAO,EACP,aAAa,EACb,aAAa,EACb,YAAY,EACZ,WAAW,EACX,aAAa,EACb,cAAc,EACd,qBAAqB,EACrB,iBAAiB,EACjB,kBAAkB,EAClB,kBAAkB,EAClB,sBAAsB,EACtB,gBAAgB,EAChB,iBAAiB,GAClB,MAAM,kBAAkB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,aAAa,EAAE,KAAK,eAAe,EAAE,MAAM,iBAAiB,CAAC;AACtE,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,OAAO,EAAE,mBAAmB,EAAE,KAAK,oBAAoB,EAAE,KAAK,uBAAuB,EAAE,MAAM,uBAAuB,CAAC;AACrH,OAAO,EAAE,UAAU,EAAE,KAAK,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAClE,OAAO,EAAE,cAAc,EAAE,KAAK,qBAAqB,EAAE,MAAM,kBAAkB,CAAC;AAC9E,OAAO,EAAE,cAAc,EAAE,KAAK,qBAAqB,EAAE,KAAK,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AACtG,OAAO,EAAE,gBAAgB,EAAE,KAAK,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AACjF,OAAO,EAAE,cAAc,EAAE,KAAK,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AAC5E,OAAO,EAAE,mBAAmB,EAAE,KAAK,wBAAwB,EAAE,MAAM,uBAAuB,CAAC;AAC3F,OAAO,EAAE,mBAAmB,EAAE,MAAM,OAAO,CAAC;AAC5C,OAAO,EAAE,qBAAqB,EAAE,KAAK,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AAClF,OAAO,EACL,kBAAkB,EAClB,qBAAqB,EACrB,kBAAkB,EAClB,YAAY,EACZ,KAAK,gBAAgB,EACrB,KAAK,mBAAmB,GACzB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACL,uBAAuB,EACvB,KAAK,iBAAiB,EACtB,KAAK,wBAAwB,EAC7B,KAAK,mBAAmB,GACzB,MAAM,2BAA2B,CAAC;AACnC,OAAO,EAAE,cAAc,EAAE,KAAK,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACzE,OAAO,EACL,sBAAsB,EACtB,qBAAqB,EACrB,KAAK,0BAA0B,GAChC,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,WAAW,EAAE,sBAAsB,EAAE,MAAM,eAAe,CAAC;AAGpE,YAAY,EACV,cAAc,EACd,eAAe,EACf,gBAAgB,EAChB,eAAe,EACf,SAAS,EACT,OAAO,EACP,kBAAkB,EAClB,aAAa,EACb,YAAY,EACZ,WAAW,EACX,aAAa,EACb,cAAc,EACd,qBAAqB,EACrB,iBAAiB,EACjB,kBAAkB,EAClB,kBAAkB,EAClB,sBAAsB,EACtB,gBAAgB,EAChB,iBAAiB,GAClB,MAAM,kBAAkB,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"useGesture.d.ts","sourceRoot":"","sources":["../src/useGesture.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,KAAK,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAEpE,MAAM,WAAW,iBAAiB;IAChC,6BAA6B;IAC7B,IAAI,EAAE,WAAW,CAAC;IAClB,wCAAwC;IACxC,SAAS,EAAE,CAAC,OAAO,EAAE,cAAc,KAAK,IAAI,CAAC;IAC7C,oDAAoD;IACpD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,uDAAuD;IACvD,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,2CAA2C;IAC3C,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,UAAU,CAAC,CAAC,SAAS,WAAW,GAAG,WAAW,EAC5D,OAAO,EAAE,iBAAiB,uCAkH3B"}
1
+ {"version":3,"file":"useGesture.d.ts","sourceRoot":"","sources":["../src/useGesture.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,KAAK,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAEpE,MAAM,WAAW,iBAAiB;IAChC,6BAA6B;IAC7B,IAAI,EAAE,WAAW,CAAC;IAClB,wCAAwC;IACxC,SAAS,EAAE,CAAC,OAAO,EAAE,cAAc,KAAK,IAAI,CAAC;IAC7C,oDAAoD;IACpD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,uDAAuD;IACvD,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,2CAA2C;IAC3C,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,UAAU,CAAC,CAAC,SAAS,WAAW,GAAG,WAAW,EAC5D,OAAO,EAAE,iBAAiB,uCAqK3B"}
@@ -23,12 +23,27 @@ export function useGesture(options) {
23
23
  const ref = useRef(null);
24
24
  const startRef = useRef(null);
25
25
  const longPressTimerRef = useRef(undefined);
26
+ // Real double-tap needs the PREVIOUS tap's timestamp — the old
27
+ // implementation fired on every single qualifying tap (#2942).
28
+ const lastTapRef = useRef(0);
29
+ // Two-touch tracking for pinch/rotate: initial + latest distance/angle
30
+ // between the two touch points.
31
+ const twoTouchRef = useRef(null);
26
32
  const { type, onGesture, threshold = 50, longPressDuration = 500, enabled = true } = options;
27
33
  const handleTouchStart = useCallback((e) => {
28
34
  if (!enabled)
29
35
  return;
30
36
  const touch = e.touches[0];
31
37
  startRef.current = { x: touch.clientX, y: touch.clientY, time: Date.now() };
38
+ // Pinch/rotate baseline — distance and angle between the two touches.
39
+ if ((type === 'pinch' || type === 'rotate') && e.touches.length >= 2) {
40
+ const [t1, t2] = [e.touches[0], e.touches[1]];
41
+ const dx2 = t2.clientX - t1.clientX;
42
+ const dy2 = t2.clientY - t1.clientY;
43
+ const d = Math.hypot(dx2, dy2);
44
+ const a = (Math.atan2(dy2, dx2) * 180) / Math.PI;
45
+ twoTouchRef.current = { d0: d, a0: a, d1: d, a1: a };
46
+ }
32
47
  if (type === 'long-press') {
33
48
  longPressTimerRef.current = setTimeout(() => {
34
49
  if (startRef.current) {
@@ -62,14 +77,45 @@ export function useGesture(options) {
62
77
  duration,
63
78
  velocity,
64
79
  };
80
+ // Pinch / rotate resolve from the two-touch delta, not the single-touch
81
+ // path below. Fired once on release, like the other discrete gestures.
82
+ if (type === 'pinch' || type === 'rotate') {
83
+ const t2 = twoTouchRef.current;
84
+ twoTouchRef.current = null;
85
+ startRef.current = null;
86
+ if (!t2 || t2.d0 <= 0)
87
+ return;
88
+ const scale = t2.d1 / t2.d0;
89
+ // Normalize the angle delta into (-180, 180].
90
+ let rotation = t2.a1 - t2.a0;
91
+ if (rotation > 180)
92
+ rotation -= 360;
93
+ if (rotation <= -180)
94
+ rotation += 360;
95
+ if (type === 'pinch' && Math.abs(scale - 1) >= 0.1) {
96
+ onGesture({ ...context, scale });
97
+ }
98
+ else if (type === 'rotate' && Math.abs(rotation) >= 15) {
99
+ onGesture({ ...context, rotation });
100
+ }
101
+ return;
102
+ }
65
103
  // Detect gesture type
66
104
  if (type === 'tap' && distance < 10 && duration < 300) {
67
105
  onGesture(context);
68
106
  }
69
107
  else if (type === 'double-tap') {
70
- // Double-tap handled separately (simplified)
108
+ // Two qualifying taps within 350ms — a single tap must NOT fire
109
+ // (it used to, collapsing double-tap into tap, #2942).
71
110
  if (distance < 10 && duration < 300) {
72
- onGesture(context);
111
+ const now = Date.now();
112
+ if (now - lastTapRef.current <= 350) {
113
+ lastTapRef.current = 0;
114
+ onGesture(context);
115
+ }
116
+ else {
117
+ lastTapRef.current = now;
118
+ }
73
119
  }
74
120
  }
75
121
  else if (distance >= threshold) {
@@ -93,11 +139,19 @@ export function useGesture(options) {
93
139
  }
94
140
  startRef.current = null;
95
141
  }, [type, onGesture, threshold, enabled]);
96
- const handleTouchMove = useCallback(() => {
142
+ const handleTouchMove = useCallback((e) => {
97
143
  // Cancel long-press if finger moves
98
144
  if (type === 'long-press') {
99
145
  clearTimeout(longPressTimerRef.current);
100
146
  }
147
+ // Track the latest two-touch distance/angle for pinch/rotate.
148
+ if ((type === 'pinch' || type === 'rotate') && twoTouchRef.current && e.touches.length >= 2) {
149
+ const [t1, t2] = [e.touches[0], e.touches[1]];
150
+ const dx2 = t2.clientX - t1.clientX;
151
+ const dy2 = t2.clientY - t1.clientY;
152
+ twoTouchRef.current.d1 = Math.hypot(dx2, dy2);
153
+ twoTouchRef.current.a1 = (Math.atan2(dy2, dx2) * 180) / Math.PI;
154
+ }
101
155
  }, [type]);
102
156
  useEffect(() => {
103
157
  const el = ref.current;
@@ -5,7 +5,7 @@
5
5
  * This source code is licensed under the MIT license found in the
6
6
  * LICENSE file in the root directory of this source tree.
7
7
  */
8
- import type { SpecGestureConfig } from '@object-ui/types';
8
+ import type { GestureType, SpecGestureConfig } from '@object-ui/types';
9
9
  export interface UseSpecGestureOptions {
10
10
  /** Spec gesture configuration */
11
11
  config: SpecGestureConfig;
@@ -15,7 +15,33 @@ export interface UseSpecGestureOptions {
15
15
  onPinch?: (scale: number) => void;
16
16
  /** Callback when a long-press gesture is detected */
17
17
  onLongPress?: () => void;
18
+ /** Callback when a double-tap gesture is detected */
19
+ onDoubleTap?: () => void;
20
+ /** Callback when a pan/drag gesture is detected (any-direction move) */
21
+ onPan?: (direction: string) => void;
22
+ /** Callback when a rotate gesture is detected (degrees, CW positive) */
23
+ onRotate?: (rotation: number) => void;
24
+ /** Fallback for gestures without a dedicated callback */
25
+ onGesture?: (context: {
26
+ type: string;
27
+ direction?: string;
28
+ scale?: number;
29
+ rotation?: number;
30
+ }) => void;
18
31
  }
32
+ /**
33
+ * Spec `GestureTypeSchema` (`ui/touch.zod.ts`) → the recognizer type
34
+ * `useGesture` implements. The spec's `drag` and `pan` are one recognizer
35
+ * (any-direction move past the threshold); `swipe` resolves per configured
36
+ * direction, so it maps through {@link SWIPE_DIRECTION_MAP} instead.
37
+ * Exported for the spec-parity test.
38
+ *
39
+ * Before #2942 the hook never read `config.type` at all — it branched on
40
+ * which SUB-OBJECT was present (`swipe` / `pinch` / `longPress`), so
41
+ * `pan` / `drag` / `rotate` / `double_tap` (types with no sub-object) all
42
+ * fell through to the `'tap'` initializer and fired on a tap.
43
+ */
44
+ export declare const SPEC_GESTURE_TYPE_MAP: Record<string, GestureType>;
19
45
  /**
20
46
  * Spec-aware gesture hook that maps an @objectstack/spec GestureConfig
21
47
  * to the existing useGesture hook.
@@ -1 +1 @@
1
- {"version":3,"file":"useSpecGesture.d.ts","sourceRoot":"","sources":["../src/useSpecGesture.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,KAAK,EAAe,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAEvE,MAAM,WAAW,qBAAqB;IACpC,iCAAiC;IACjC,MAAM,EAAE,iBAAiB,CAAC;IAC1B,gDAAgD;IAChD,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;IACtC,gDAAgD;IAChD,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,qDAAqD;IACrD,WAAW,CAAC,EAAE,MAAM,IAAI,CAAC;CAC1B;AASD;;;;;;;;;;;;GAYG;AACH,wBAAgB,cAAc,CAAC,CAAC,SAAS,WAAW,GAAG,WAAW,EAChE,OAAO,EAAE,qBAAqB,uCAiC/B"}
1
+ {"version":3,"file":"useSpecGesture.d.ts","sourceRoot":"","sources":["../src/useSpecGesture.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,KAAK,EAAE,WAAW,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAEvE,MAAM,WAAW,qBAAqB;IACpC,iCAAiC;IACjC,MAAM,EAAE,iBAAiB,CAAC;IAC1B,gDAAgD;IAChD,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;IACtC,gDAAgD;IAChD,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,qDAAqD;IACrD,WAAW,CAAC,EAAE,MAAM,IAAI,CAAC;IACzB,qDAAqD;IACrD,WAAW,CAAC,EAAE,MAAM,IAAI,CAAC;IACzB,wEAAwE;IACxE,KAAK,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC,wEAAwE;IACxE,QAAQ,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IACtC,yDAAyD;IACzD,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;CACxG;AASD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,qBAAqB,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAQ7D,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,wBAAgB,cAAc,CAAC,CAAC,SAAS,WAAW,GAAG,WAAW,EAChE,OAAO,EAAE,qBAAqB,uCAoE/B"}
@@ -12,6 +12,27 @@ const SWIPE_DIRECTION_MAP = {
12
12
  up: 'swipe-up',
13
13
  down: 'swipe-down',
14
14
  };
15
+ /**
16
+ * Spec `GestureTypeSchema` (`ui/touch.zod.ts`) → the recognizer type
17
+ * `useGesture` implements. The spec's `drag` and `pan` are one recognizer
18
+ * (any-direction move past the threshold); `swipe` resolves per configured
19
+ * direction, so it maps through {@link SWIPE_DIRECTION_MAP} instead.
20
+ * Exported for the spec-parity test.
21
+ *
22
+ * Before #2942 the hook never read `config.type` at all — it branched on
23
+ * which SUB-OBJECT was present (`swipe` / `pinch` / `longPress`), so
24
+ * `pan` / `drag` / `rotate` / `double_tap` (types with no sub-object) all
25
+ * fell through to the `'tap'` initializer and fired on a tap.
26
+ */
27
+ export const SPEC_GESTURE_TYPE_MAP = {
28
+ swipe: 'swipe-left', // per-direction; resolved via SWIPE_DIRECTION_MAP
29
+ pinch: 'pinch',
30
+ long_press: 'long-press',
31
+ double_tap: 'double-tap',
32
+ drag: 'pan',
33
+ pan: 'pan',
34
+ rotate: 'rotate',
35
+ };
15
36
  /**
16
37
  * Spec-aware gesture hook that maps an @objectstack/spec GestureConfig
17
38
  * to the existing useGesture hook.
@@ -26,28 +47,59 @@ const SWIPE_DIRECTION_MAP = {
26
47
  * ```
27
48
  */
28
49
  export function useSpecGesture(options) {
29
- const { config, onSwipe, onPinch, onLongPress } = options;
50
+ const { config, onSwipe, onPinch, onLongPress, onDoubleTap, onPan, onRotate, onGesture: onAny } = options;
30
51
  const enabled = config.enabled ?? true;
52
+ // The DECLARED type drives recognition (#2942) — `config.type` is required
53
+ // by the spec's `GestureConfigSchema`. The sub-object presence checks below
54
+ // only back-fill legacy configs that predate the type field.
55
+ const declared = typeof config.type === 'string'
56
+ ? config.type
57
+ : config.swipe
58
+ ? 'swipe'
59
+ : config.longPress
60
+ ? 'long_press'
61
+ : config.pinch
62
+ ? 'pinch'
63
+ : undefined;
31
64
  let gestureType = 'tap';
32
65
  let threshold;
33
66
  let longPressDuration;
34
67
  let onGesture = () => { };
35
- if (config.swipe && onSwipe) {
36
- const dir = Array.isArray(config.swipe.direction)
37
- ? config.swipe.direction[0]
38
- : config.swipe.direction;
39
- gestureType = (dir && SWIPE_DIRECTION_MAP[dir]) ?? 'swipe-left';
40
- threshold = config.swipe.threshold;
41
- onGesture = (ctx) => onSwipe(ctx.direction ?? dir ?? 'left');
42
- }
43
- else if (config.longPress && onLongPress) {
44
- gestureType = 'long-press';
45
- longPressDuration = config.longPress.duration;
46
- onGesture = () => onLongPress();
47
- }
48
- else if (config.pinch && onPinch) {
49
- gestureType = 'pinch';
50
- onGesture = (ctx) => onPinch(ctx.scale ?? 1);
68
+ const fallback = (ctx) => onAny?.(ctx);
69
+ switch (declared) {
70
+ case 'swipe': {
71
+ const dir = Array.isArray(config.swipe?.direction)
72
+ ? config.swipe?.direction[0]
73
+ : config.swipe?.direction;
74
+ gestureType = (dir ? SWIPE_DIRECTION_MAP[dir] : undefined) ?? 'swipe-left';
75
+ threshold = config.swipe?.threshold;
76
+ onGesture = (ctx) => (onSwipe ? onSwipe(ctx.direction ?? dir ?? 'left') : fallback({ type: 'swipe', ...ctx }));
77
+ break;
78
+ }
79
+ case 'long_press':
80
+ gestureType = 'long-press';
81
+ longPressDuration = config.longPress?.duration;
82
+ onGesture = () => (onLongPress ? onLongPress() : fallback({ type: 'long_press' }));
83
+ break;
84
+ case 'pinch':
85
+ gestureType = 'pinch';
86
+ onGesture = (ctx) => (onPinch ? onPinch(ctx.scale ?? 1) : fallback({ type: 'pinch', ...ctx }));
87
+ break;
88
+ case 'double_tap':
89
+ gestureType = 'double-tap';
90
+ onGesture = () => (onDoubleTap ? onDoubleTap() : fallback({ type: 'double_tap' }));
91
+ break;
92
+ case 'pan':
93
+ case 'drag':
94
+ gestureType = 'pan';
95
+ onGesture = (ctx) => (onPan ? onPan(ctx.direction ?? 'left') : fallback({ type: declared, ...ctx }));
96
+ break;
97
+ case 'rotate':
98
+ gestureType = 'rotate';
99
+ onGesture = (ctx) => (onRotate ? onRotate(ctx.rotation ?? 0) : fallback({ type: 'rotate', ...ctx }));
100
+ break;
101
+ default:
102
+ break;
51
103
  }
52
104
  return useGesture({
53
105
  type: gestureType,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@object-ui/mobile",
3
- "version": "16.1.0",
3
+ "version": "17.1.0",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "description": "Mobile optimization for Object UI with responsive components, PWA support, and touch gesture handling.",
@@ -30,11 +30,12 @@
30
30
  "react": "^18.0.0 || ^19.0.0"
31
31
  },
32
32
  "dependencies": {
33
- "@object-ui/types": "16.1.0"
33
+ "@object-ui/types": "17.1.0"
34
34
  },
35
35
  "devDependencies": {
36
+ "@objectstack/spec": "^17.0.0-rc.0",
36
37
  "@types/react": "19.2.17",
37
- "react": "19.2.7",
38
+ "react": "19.2.8",
38
39
  "typescript": "^6.0.3",
39
40
  "vitest": "^4.1.10"
40
41
  },
@@ -63,7 +64,7 @@
63
64
  "build": "tsc",
64
65
  "clean": "rm -rf dist",
65
66
  "test": "vitest run",
66
- "type-check": "tsc --noEmit",
67
+ "type-check": "tsc --noEmit && tsc -p tsconfig.test.json",
67
68
  "lint": "eslint ."
68
69
  }
69
70
  }