@object-ui/mobile 16.1.0 → 17.2.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,132 @@
1
1
  # @object-ui/mobile
2
2
 
3
+ ## 17.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 726b89c: `@object-ui/types` stops declaring sixteen symbols under names `@objectstack/spec` owns (objectui#3156, objectstack#4115).
8
+
9
+ Seven are now **derived** from the spec, nine are **renamed** to the local
10
+ dialect they always were. Both halves remove the same hazard: a local
11
+ declaration under a spec export's name reads as the spec's own definition to
12
+ the next reader, so a copy that is merely _correct today_ is a planted premise
13
+ tomorrow.
14
+
15
+ **Derived** — the spec now supplies the keys, by reference:
16
+
17
+ | symbol | derivation |
18
+ | :----------------------- | :-------------------------------------------------------------------------------- |
19
+ | `ActionParam` | `z.input<typeof ActionParamSchema>`, `type` widened to the local legacy spellings |
20
+ | `CreateExportJobRequest` | `Omit<CreateExportJobInput, 'object'>` (`object` is the method argument) |
21
+ | `CreateExportJobResult` | re-export from `@objectstack/spec/contracts` |
22
+ | `ImportRowResult` | re-export from `@objectstack/spec/api` |
23
+ | `NavigationArea` | spec keys, with `navigation` / `visible` pinned locally |
24
+ | `NavigationAreaSchema` | `specFieldsExcept(NavigationAreaSchema.shape, …)` |
25
+ | `Theme` | re-export of the spec's `ThemeInput` (the authoring shape) |
26
+ | `ExportJobFormat` | re-export of the spec's `ExportFormat` |
27
+
28
+ Four of these close real gaps rather than tidy names. `ActionParam` never
29
+ declared `reference` — the key `resolveActionParams()` actually reads for an
30
+ inline lookup target — nor `defaultFromRow`, which the metadata designer's own
31
+ inspector writes; it also narrowed `visible` to a bare string although the
32
+ resolver has always accepted the `{ dialect, source }` envelope too.
33
+ `CreateExportJobResult.createdAt` and `ImportRowResult.action` were optional
34
+ here and required by the server, leaving every consumer a branch that could
35
+ never run. And `NavigationArea`'s `id` now carries the spec's own length rule
36
+ instead of accepting any string.
37
+
38
+ **Renamed** — same word, different concept:
39
+
40
+ | was | now | why |
41
+ | :----------------- | :----------------------- | :----------------------------------------------------------------------------------------------------------------------------- |
42
+ | `FileMetadata` | `UploadedFileMetadata` | field-VALUE payload (`url`, `original_name`), not the storage file record |
43
+ | `GestureType` | `TouchGestureType` | direction-fused (`swipe-left`), not the spec's type+direction pair |
44
+ | `GestureConfig` | `TouchGestureConfig` | gesture→`action` binding, not per-gesture tuning |
45
+ | `OfflineConfig` | `PWAOfflineConfig` | service-worker route caching, not the offline data/sync model |
46
+ | `PageRegion` | `PageNodeRegion` | region of the renderer page NODE, holding `SchemaNode`s |
47
+ | `PageRegionSchema` | `PageNodeRegionSchema` | zod twin of the above |
48
+ | `ResponsiveConfig` | `MobileResponsiveConfig` | mobile box config, not the spec's SDUI grid contract |
49
+ | `WidgetManifest` | `RuntimeWidgetManifest` | SDUI component manifest, not the field-widget plugin manifest |
50
+ | `WidgetSource` | `RuntimeWidgetSource` | `module`/`inline`/`registry` loader union — and its `inline` carries a resolved component where the spec's carries source code |
51
+
52
+ **Migration**: the old names are gone, not deprecated — an alias would preserve
53
+ exactly the ambiguity being removed. Import the new name; nothing about the
54
+ shapes changed. `@object-ui/types` already re-exports the spec's own
55
+ `SpecResponsiveConfig`, and `@object-ui/react`'s `useOffline` config remains the
56
+ spec-shaped `OfflineConfig`, so both concepts stay reachable under
57
+ distinguishable names.
58
+
59
+ Each rename carries a bidirectional tripwire
60
+ (`packages/types/src/__tests__/page-nav-misc-spec-parity.test.ts`): it fails if
61
+ the spec ever claims the new name, and also if the spec retires the old one —
62
+ at which point the natural name can be taken back rather than the workaround
63
+ outliving its reason.
64
+
65
+ ### Patch Changes
66
+
67
+ - Updated dependencies [4ae0ac4]
68
+ - Updated dependencies [696e3c1]
69
+ - Updated dependencies [4bf612c]
70
+ - Updated dependencies [cb82705]
71
+ - Updated dependencies [f572849]
72
+ - Updated dependencies [444457c]
73
+ - Updated dependencies [022e4c3]
74
+ - Updated dependencies [009e25d]
75
+ - Updated dependencies [726b89c]
76
+ - @object-ui/types@17.2.0
77
+
78
+ ## 17.1.0
79
+
80
+ ### Patch Changes
81
+
82
+ - Updated dependencies [9e7349e]
83
+ - Updated dependencies [8864971]
84
+ - Updated dependencies [b41f401]
85
+ - Updated dependencies [19e9fa0]
86
+ - Updated dependencies [38ca8be]
87
+ - Updated dependencies [4952edf]
88
+ - Updated dependencies [7f0252e]
89
+ - Updated dependencies [7639a61]
90
+ - Updated dependencies [94e63ef]
91
+ - Updated dependencies [c4db402]
92
+ - Updated dependencies [5319bf1]
93
+ - Updated dependencies [49e5671]
94
+ - Updated dependencies [b5b97e2]
95
+ - Updated dependencies [f59f2c1]
96
+ - Updated dependencies [4874117]
97
+ - Updated dependencies [ce08d55]
98
+ - Updated dependencies [2374a49]
99
+ - Updated dependencies [ea7f477]
100
+ - Updated dependencies [7f23cd0]
101
+ - Updated dependencies [24e0e0a]
102
+ - Updated dependencies [3a6cf24]
103
+ - Updated dependencies [aa35561]
104
+ - Updated dependencies [03bd53b]
105
+ - Updated dependencies [3c1f321]
106
+ - Updated dependencies [a045a32]
107
+ - Updated dependencies [912496d]
108
+ - Updated dependencies [9867281]
109
+ - @object-ui/types@17.1.0
110
+
111
+ ## 17.0.0
112
+
113
+ ### Patch Changes
114
+
115
+ - Updated dependencies [1767124]
116
+ - Updated dependencies [8ecf5a6]
117
+ - Updated dependencies [dfd3705]
118
+ - Updated dependencies [6dee2cb]
119
+ - Updated dependencies [c7cff19]
120
+ - Updated dependencies [cd09a7b]
121
+ - Updated dependencies [f1abf0e]
122
+ - Updated dependencies [f05b84e]
123
+ - Updated dependencies [662bdf9]
124
+ - Updated dependencies [059a052]
125
+ - Updated dependencies [53642d4]
126
+ - Updated dependencies [8aae006]
127
+ - Updated dependencies [d147a13]
128
+ - @object-ui/types@17.0.0
129
+
3
130
  ## 16.1.0
4
131
 
5
132
  ### Patch Changes
@@ -6,19 +6,19 @@
6
6
  * LICENSE file in the root directory of this source tree.
7
7
  */
8
8
  import React from 'react';
9
- import type { PWAConfig, OfflineConfig } from '@object-ui/types';
9
+ import type { PWAConfig, PWAOfflineConfig } from '@object-ui/types';
10
10
  import { type BreakpointState } from './useBreakpoint';
11
11
  export interface MobileContextValue extends BreakpointState {
12
12
  /** PWA configuration */
13
13
  pwa?: PWAConfig;
14
14
  /** Offline configuration */
15
- offline?: OfflineConfig;
15
+ offline?: PWAOfflineConfig;
16
16
  }
17
17
  export interface MobileProviderProps {
18
18
  /** PWA configuration */
19
19
  pwa?: PWAConfig;
20
20
  /** Offline configuration */
21
- offline?: OfflineConfig;
21
+ offline?: PWAOfflineConfig;
22
22
  /** Children */
23
23
  children: React.ReactNode;
24
24
  }
@@ -1 +1 @@
1
- {"version":3,"file":"MobileProvider.d.ts","sourceRoot":"","sources":["../src/MobileProvider.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAA6C,MAAM,OAAO,CAAC;AAClE,OAAO,KAAK,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACjE,OAAO,EAAiB,KAAK,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAEtE,MAAM,WAAW,kBAAmB,SAAQ,eAAe;IACzD,wBAAwB;IACxB,GAAG,CAAC,EAAE,SAAS,CAAC;IAChB,4BAA4B;IAC5B,OAAO,CAAC,EAAE,aAAa,CAAC;CACzB;AAKD,MAAM,WAAW,mBAAmB;IAClC,wBAAwB;IACxB,GAAG,CAAC,EAAE,SAAS,CAAC;IAChB,4BAA4B;IAC5B,OAAO,CAAC,EAAE,aAAa,CAAC;IACxB,eAAe;IACf,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;CAC3B;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,EAAE,GAAG,EAAE,OAAO,EAAE,QAAQ,EAAE,EAAE,mBAAmB,qBAa7E;AAED;;GAEG;AACH,wBAAgB,gBAAgB,IAAI,kBAAkB,GAAG,IAAI,CAE5D"}
1
+ {"version":3,"file":"MobileProvider.d.ts","sourceRoot":"","sources":["../src/MobileProvider.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAA6C,MAAM,OAAO,CAAC;AAClE,OAAO,KAAK,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACpE,OAAO,EAAiB,KAAK,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAEtE,MAAM,WAAW,kBAAmB,SAAQ,eAAe;IACzD,wBAAwB;IACxB,GAAG,CAAC,EAAE,SAAS,CAAC;IAChB,4BAA4B;IAC5B,OAAO,CAAC,EAAE,gBAAgB,CAAC;CAC5B;AAKD,MAAM,WAAW,mBAAmB;IAClC,wBAAwB;IACxB,GAAG,CAAC,EAAE,SAAS,CAAC;IAChB,4BAA4B;IAC5B,OAAO,CAAC,EAAE,gBAAgB,CAAC;IAC3B,eAAe;IACf,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;CAC3B;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,EAAE,GAAG,EAAE,OAAO,EAAE,QAAQ,EAAE,EAAE,mBAAmB,qBAa7E;AAED;;GAEG;AACH,wBAAgB,gBAAgB,IAAI,kBAAkB,GAAG,IAAI,CAE5D"}
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, MobileResponsiveConfig, MobileOverrides, PWAConfig, PWAIcon, FetchCacheStrategy, PWAOfflineConfig, OfflineRoute, TouchGestureType, TouchGestureConfig, 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,sBAAsB,EACtB,eAAe,EACf,SAAS,EACT,OAAO,EACP,kBAAkB,EAClB,gBAAgB,EAChB,YAAY,EACZ,gBAAgB,EAChB,kBAAkB,EAClB,cAAc,EACd,qBAAqB,EACrB,iBAAiB,EACjB,kBAAkB,EAClB,kBAAkB,EAClB,sBAAsB,EACtB,gBAAgB,EAChB,iBAAiB,GAClB,MAAM,kBAAkB,CAAC"}
@@ -5,10 +5,10 @@
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 { GestureType, GestureContext } from '@object-ui/types';
8
+ import type { TouchGestureType, GestureContext } from '@object-ui/types';
9
9
  export interface UseGestureOptions {
10
10
  /** Gesture type to detect */
11
- type: GestureType;
11
+ type: TouchGestureType;
12
12
  /** Callback when gesture is detected */
13
13
  onGesture: (context: GestureContext) => void;
14
14
  /** Minimum distance for swipe detection (pixels) */
@@ -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,gBAAgB,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAEzE,MAAM,WAAW,iBAAiB;IAChC,6BAA6B;IAC7B,IAAI,EAAE,gBAAgB,CAAC;IACvB,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 { TouchGestureType, 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, TouchGestureType>;
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,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAE5E,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,gBAAgB,CAQlE,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.2.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.2.0"
34
34
  },
35
35
  "devDependencies": {
36
+ "@objectstack/spec": "^17.0.0-rc.1",
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
  }