@ankhorage/zora 25.8.0 → 25.8.2

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,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 25.8.2
4
+
5
+ ### Patch Changes
6
+
7
+ - ed63654: Reconcile ZORA's published native peer contract with the released Surface Expo SDK 57 tuple.
8
+
9
+ ## 25.8.1
10
+
11
+ ### Patch Changes
12
+
13
+ - 7a83559: Keep controlled GridInteractions move, resize, and pan gestures anchored to their start state.
14
+
3
15
  ## 25.8.0
4
16
 
5
17
  ### Minor Changes
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
 
4
4
  # @ankhorage/zora
5
5
 
6
- ![license: MIT](././paradox/badges/license.svg) ![npm: v25.8.0](././paradox/badges/npm.svg) ![runtime: bun](././paradox/badges/runtime.svg) ![typescript: strict](././paradox/badges/typescript.svg) ![eslint: checked](././paradox/badges/eslint.svg) ![prettier: checked](././paradox/badges/prettier.svg) ![build: checked](././paradox/badges/build.svg) ![tests: checked](././paradox/badges/tests.svg) ![paradox: warnings](././paradox/badges/docs.svg)
6
+ ![license: MIT](././paradox/badges/license.svg) ![npm: v25.8.2](././paradox/badges/npm.svg) ![runtime: bun](././paradox/badges/runtime.svg) ![typescript: strict](././paradox/badges/typescript.svg) ![eslint: checked](././paradox/badges/eslint.svg) ![prettier: checked](././paradox/badges/prettier.svg) ![build: checked](././paradox/badges/build.svg) ![tests: checked](././paradox/badges/tests.svg) ![paradox: warnings](././paradox/badges/docs.svg)
7
7
 
8
8
  Opinionated React Native and React Native Web UI kit built on @ankhorage/surface.
9
9
 
@@ -1 +1 @@
1
- {"version":3,"file":"createGridInteractionsController.d.ts","sourceRoot":"","sources":["../../../../src/features/grid-interactions/application/createGridInteractionsController.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAIV,0BAA0B,EAC1B,+BAA+B,EAChC,MAAM,kCAAkC,CAAC;AAc1C,kGAAkG;AAClG,wBAAgB,gCAAgC,CAC9C,KAAK,EAAE,+BAA+B,GACrC,0BAA0B,CA8E5B"}
1
+ {"version":3,"file":"createGridInteractionsController.d.ts","sourceRoot":"","sources":["../../../../src/features/grid-interactions/application/createGridInteractionsController.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAIV,0BAA0B,EAC1B,+BAA+B,EAChC,MAAM,kCAAkC,CAAC;AAqB1C,kGAAkG;AAClG,wBAAgB,gCAAgC,CAC9C,KAAK,EAAE,+BAA+B,GACrC,0BAA0B,CAoF5B"}
@@ -4,8 +4,8 @@ export function createGridInteractionsController(props) {
4
4
  let currentProps = props;
5
5
  let state = { kind: 'idle' };
6
6
  let previousIntent;
7
- const point = (pointer) => Number.isFinite(pointer.x) && Number.isFinite(pointer.y)
8
- ? viewportToWorld(pointer, currentProps.viewport)
7
+ const point = (pointer, viewport = currentProps.viewport) => Number.isFinite(pointer.x) && Number.isFinite(pointer.y)
8
+ ? viewportToWorld(pointer, viewport)
9
9
  : undefined;
10
10
  const emit = (intent) => {
11
11
  if (JSON.stringify(previousIntent) === JSON.stringify(intent))
@@ -21,27 +21,33 @@ export function createGridInteractionsController(props) {
21
21
  if (start === undefined)
22
22
  return;
23
23
  if (pointer.spaceKey === true || pointer.altKey === true) {
24
- state = { kind: 'pan', start };
24
+ state = { kind: 'pan', start, viewport: currentProps.viewport };
25
25
  return;
26
26
  }
27
27
  const target = hitTestWorldRects(currentProps.items, start);
28
28
  if (target === undefined || isProtected(target)) {
29
- state = { kind: 'marquee', start };
29
+ state = { kind: 'marquee', start, viewport: currentProps.viewport };
30
30
  return;
31
31
  }
32
32
  if (handle !== undefined && target.resizable !== false) {
33
- state = { kind: 'resize', start, item: target, handle };
33
+ state = { kind: 'resize', start, viewport: currentProps.viewport, item: target, handle };
34
34
  return;
35
35
  }
36
36
  const itemIds = currentProps.selectedIds?.includes(target.id) === true
37
37
  ? currentProps.selectedIds
38
38
  : [target.id];
39
- state = { kind: 'move', start, itemIds };
39
+ state = {
40
+ kind: 'move',
41
+ start,
42
+ viewport: currentProps.viewport,
43
+ itemIds,
44
+ items: currentProps.items.filter((item) => itemIds.includes(item.id)),
45
+ };
40
46
  },
41
47
  move: (pointer) => {
42
48
  if (!canEdit(currentProps))
43
49
  return;
44
- const current = point(pointer);
50
+ const current = point(pointer, viewportForState(state, currentProps.viewport));
45
51
  if (current !== undefined)
46
52
  emitPreview(state, current, currentProps, emit);
47
53
  },
@@ -50,7 +56,7 @@ export function createGridInteractionsController(props) {
50
56
  state = { kind: 'idle' };
51
57
  return;
52
58
  }
53
- const current = point(pointer);
59
+ const current = point(pointer, viewportForState(state, currentProps.viewport));
54
60
  if (current !== undefined)
55
61
  emitPreview(state, current, currentProps, emit);
56
62
  state = { kind: 'idle' };
@@ -97,7 +103,7 @@ function emitPreview(state, current, props, emit) {
97
103
  emit({
98
104
  type: 'pan',
99
105
  itemIds: [],
100
- viewport: panViewport(props.viewport, {
106
+ viewport: panViewport(state.viewport, {
101
107
  x: state.start.x - current.x,
102
108
  y: state.start.y - current.y,
103
109
  }),
@@ -117,8 +123,7 @@ function emitPreview(state, current, props, emit) {
117
123
  }
118
124
  const delta = resolveSnapDelta(state, current, props);
119
125
  if (state.kind === 'move') {
120
- const selected = props.items.filter((item) => state.itemIds.includes(item.id));
121
- emit({ type: 'move', itemIds: state.itemIds, rects: moveWorldRects(selected, delta) });
126
+ emit({ type: 'move', itemIds: state.itemIds, rects: moveWorldRects(state.items, delta) });
122
127
  return;
123
128
  }
124
129
  emit({
@@ -135,7 +140,7 @@ function resolveSnapDelta(state, current, props) {
135
140
  const raw = { x: current.x - state.start.x, y: current.y - state.start.y };
136
141
  if (props.snap?.enabled !== true)
137
142
  return raw;
138
- const anchor = state.kind === 'resize' ? state.item : props.items.find((item) => item.id === state.itemIds[0]);
143
+ const anchor = state.kind === 'resize' ? state.item : state.items.at(0);
139
144
  if (anchor === undefined)
140
145
  return raw;
141
146
  const options = {
@@ -154,6 +159,10 @@ function resolveSnapDelta(state, current, props) {
154
159
  }) - anchor.y,
155
160
  };
156
161
  }
162
+ /*** Keep pointer coordinates in the original gesture coordinate space across controlled updates. */
163
+ function viewportForState(state, fallback) {
164
+ return state.kind === 'idle' ? fallback : state.viewport;
165
+ }
157
166
  /*** Keep all caller-declared protected items out of selection and edit intents. */
158
167
  function isProtected(item) {
159
168
  return (item.disabled === true ||
@@ -1 +1 @@
1
- {"version":3,"file":"createGridInteractionsController.js","sourceRoot":"","sources":["../../../../src/features/grid-interactions/application/createGridInteractionsController.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n type GridPoint,\n type GridRect,\n type GridResizeHandle,\n hitTestWorldRects,\n moveWorldRects,\n panViewport,\n resizeWorldRect,\n resolveWorldSnapCandidate,\n selectWorldRects,\n viewportToWorld,\n} from '@ankhorage/grid-view';\n\nimport type {\n GridInteractionIntent,\n GridInteractionItem,\n GridInteractionPointer,\n GridInteractionsController,\n GridInteractionsControllerProps,\n} from '../../../types/grid-interactions';\n\ntype InteractionState =\n | { readonly kind: 'idle' }\n | { readonly kind: 'marquee'; readonly start: GridPoint }\n | { readonly kind: 'pan'; readonly start: GridPoint }\n | { readonly kind: 'move'; readonly start: GridPoint; readonly itemIds: readonly string[] }\n | {\n readonly kind: 'resize';\n readonly start: GridPoint;\n readonly item: GridInteractionItem;\n readonly handle: GridResizeHandle;\n };\n\n/*** Create a platform-neutral controlled interaction controller using grid-view geometry only. */\nexport function createGridInteractionsController(\n props: GridInteractionsControllerProps,\n): GridInteractionsController {\n let currentProps = props;\n let state: InteractionState = { kind: 'idle' };\n let previousIntent: GridInteractionIntent | undefined;\n const point = (pointer: GridInteractionPointer) =>\n Number.isFinite(pointer.x) && Number.isFinite(pointer.y)\n ? viewportToWorld(pointer, currentProps.viewport)\n : undefined;\n const emit = (intent: GridInteractionIntent) => {\n if (JSON.stringify(previousIntent) === JSON.stringify(intent)) return;\n previousIntent = intent;\n currentProps.onIntent(intent);\n };\n\n return {\n begin: (pointer, handle) => {\n if (!canEdit(currentProps)) return;\n const start = point(pointer);\n if (start === undefined) return;\n if (pointer.spaceKey === true || pointer.altKey === true) {\n state = { kind: 'pan', start };\n return;\n }\n const target = hitTestWorldRects(currentProps.items, start);\n if (target === undefined || isProtected(target)) {\n state = { kind: 'marquee', start };\n return;\n }\n if (handle !== undefined && target.resizable !== false) {\n state = { kind: 'resize', start, item: target, handle };\n return;\n }\n const itemIds =\n currentProps.selectedIds?.includes(target.id) === true\n ? currentProps.selectedIds\n : [target.id];\n state = { kind: 'move', start, itemIds };\n },\n move: (pointer) => {\n if (!canEdit(currentProps)) return;\n const current = point(pointer);\n if (current !== undefined) emitPreview(state, current, currentProps, emit);\n },\n end: (pointer) => {\n if (!canEdit(currentProps)) {\n state = { kind: 'idle' };\n return;\n }\n const current = point(pointer);\n if (current !== undefined) emitPreview(state, current, currentProps, emit);\n state = { kind: 'idle' };\n },\n cancel: () => {\n state = { kind: 'idle' };\n },\n keyDown: (key, pointer = { x: 0, y: 0 }) => {\n if (!canEdit(currentProps)) return false;\n const focusedId = currentProps.selectedIds?.at(-1);\n const focused = currentProps.items.find((item) => item.id === focusedId);\n if (!focused || isProtected(focused)) return false;\n const increment = pointer.shiftKey === true ? 10 : 1;\n const delta = keyToDelta(key, increment);\n if (delta === undefined) return false;\n if (pointer.altKey === true) {\n emit({\n type: 'resize',\n itemIds: [focused.id],\n rects: [resizeWorldRect(focused, resizeHandleForDelta(delta), delta)],\n });\n return true;\n }\n emit({ type: 'move', itemIds: [focused.id], rects: moveWorldRects([focused], delta) });\n return true;\n },\n update: (nextProps) => {\n currentProps = nextProps;\n },\n };\n}\n\n/*** Determines whether the component-wide interaction policy permits edit intents. */\nfunction canEdit(props: GridInteractionsControllerProps): boolean {\n return props.interactionPolicy !== 'passive';\n}\n\n/*** Emit one controlled preview/intention without retaining or mutating caller-owned item data. */\nfunction emitPreview(\n state: InteractionState,\n current: GridPoint,\n props: GridInteractionsControllerProps,\n emit: (intent: GridInteractionIntent) => void,\n) {\n if (state.kind === 'idle') return;\n if (state.kind === 'pan') {\n emit({\n type: 'pan',\n itemIds: [],\n viewport: panViewport(props.viewport, {\n x: state.start.x - current.x,\n y: state.start.y - current.y,\n }),\n });\n return;\n }\n if (state.kind === 'marquee') {\n const marquee = normalizeRect(state.start, current);\n const itemIds = selectWorldRects(props.items, marquee, {\n mode: props.marqueeMode ?? 'intersect',\n }).filter((id) => {\n const item = props.items.find((candidate) => candidate.id === id);\n return item !== undefined && !isProtected(item);\n });\n emit({ type: 'marquee', itemIds, marquee });\n return;\n }\n const delta = resolveSnapDelta(state, current, props);\n if (state.kind === 'move') {\n const selected = props.items.filter((item) => state.itemIds.includes(item.id));\n emit({ type: 'move', itemIds: state.itemIds, rects: moveWorldRects(selected, delta) });\n return;\n }\n emit({\n type: 'resize',\n itemIds: [state.item.id],\n rects: [resizeWorldRect(state.item, state.handle, delta)],\n });\n}\n\n/*** Resolve explicit per-axis candidate snapping without inventing geometry outside grid-view. */\nfunction resolveSnapDelta(\n state: InteractionState,\n current: GridPoint,\n props: GridInteractionsControllerProps,\n): GridPoint {\n if (state.kind === 'idle' || state.kind === 'marquee' || state.kind === 'pan') {\n return { x: 0, y: 0 };\n }\n const raw = { x: current.x - state.start.x, y: current.y - state.start.y };\n if (props.snap?.enabled !== true) return raw;\n const anchor =\n state.kind === 'resize' ? state.item : props.items.find((item) => item.id === state.itemIds[0]);\n if (anchor === undefined) return raw;\n const options = {\n enabled: true,\n tolerancePixels: props.snap.tolerancePixels,\n priorities: props.snap.priorities,\n };\n return {\n x:\n resolveWorldSnapCandidate(anchor.x + raw.x, props.snap.x ?? [], {\n ...options,\n pixelsPerUnit: props.viewport.pixelsPerUnitX,\n }) - anchor.x,\n y:\n resolveWorldSnapCandidate(anchor.y + raw.y, props.snap.y ?? [], {\n ...options,\n pixelsPerUnit: props.viewport.pixelsPerUnitY,\n }) - anchor.y,\n };\n}\n\n/*** Keep all caller-declared protected items out of selection and edit intents. */\nfunction isProtected(item: GridInteractionItem): boolean {\n return (\n item.disabled === true ||\n item.locked === true ||\n item.readOnly === true ||\n item.passive === true\n );\n}\n\n/*** Normalize an arbitrary-direction drag into the world rectangle expected by grid-view selection. */\nfunction normalizeRect(start: GridPoint, end: GridPoint): GridRect {\n return {\n x: Math.min(start.x, end.x),\n y: Math.min(start.y, end.y),\n width: Math.abs(end.x - start.x),\n height: Math.abs(end.y - start.y),\n };\n}\n\n/** Maps accessible arrow keys to caller-controlled world-space edit increments. */\nfunction keyToDelta(key: string, increment: number): GridPoint | undefined {\n if (key === 'ArrowUp') return { x: 0, y: -increment };\n if (key === 'ArrowRight') return { x: increment, y: 0 };\n if (key === 'ArrowDown') return { x: 0, y: increment };\n if (key === 'ArrowLeft') return { x: -increment, y: 0 };\n return undefined;\n}\n\n/** Chooses the physical edge matching an accessible resize direction. */\nfunction resizeHandleForDelta(delta: GridPoint): GridResizeHandle {\n if (delta.x < 0) return delta.y < 0 ? 'top-left' : delta.y > 0 ? 'bottom-left' : 'left';\n return delta.y < 0 ? 'top-right' : delta.y > 0 ? 'bottom-right' : 'right';\n}\n"]}
1
+ {"version":3,"file":"createGridInteractionsController.js","sourceRoot":"","sources":["../../../../src/features/grid-interactions/application/createGridInteractionsController.ts"],"names":[],"mappings":"AAAA,OAAO,EAKL,iBAAiB,EACjB,cAAc,EACd,WAAW,EACX,eAAe,EACf,yBAAyB,EACzB,gBAAgB,EAChB,eAAe,GAChB,MAAM,sBAAsB,CAAC;AA6B9B,kGAAkG;AAClG,MAAM,UAAU,gCAAgC,CAC9C,KAAsC;IAEtC,IAAI,YAAY,GAAG,KAAK,CAAC;IACzB,IAAI,KAAK,GAAqB,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;IAC/C,IAAI,cAAiD,CAAC;IACtD,MAAM,KAAK,GAAG,CAAC,OAA+B,EAAE,QAAQ,GAAG,YAAY,CAAC,QAAQ,EAAE,EAAE,CAClF,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC;QACtD,CAAC,CAAC,eAAe,CAAC,OAAO,EAAE,QAAQ,CAAC;QACpC,CAAC,CAAC,SAAS,CAAC;IAChB,MAAM,IAAI,GAAG,CAAC,MAA6B,EAAE,EAAE;QAC7C,IAAI,IAAI,CAAC,SAAS,CAAC,cAAc,CAAC,KAAK,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC;YAAE,OAAO;QACtE,cAAc,GAAG,MAAM,CAAC;QACxB,YAAY,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;IAChC,CAAC,CAAC;IAEF,OAAO;QACL,KAAK,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;YACzB,IAAI,CAAC,OAAO,CAAC,YAAY,CAAC;gBAAE,OAAO;YACnC,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,CAAC;YAC7B,IAAI,KAAK,KAAK,SAAS;gBAAE,OAAO;YAChC,IAAI,OAAO,CAAC,QAAQ,KAAK,IAAI,IAAI,OAAO,CAAC,MAAM,KAAK,IAAI,EAAE,CAAC;gBACzD,KAAK,GAAG,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,YAAY,CAAC,QAAQ,EAAE,CAAC;gBAChE,OAAO;YACT,CAAC;YACD,MAAM,MAAM,GAAG,iBAAiB,CAAC,YAAY,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;YAC5D,IAAI,MAAM,KAAK,SAAS,IAAI,WAAW,CAAC,MAAM,CAAC,EAAE,CAAC;gBAChD,KAAK,GAAG,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,QAAQ,EAAE,YAAY,CAAC,QAAQ,EAAE,CAAC;gBACpE,OAAO;YACT,CAAC;YACD,IAAI,MAAM,KAAK,SAAS,IAAI,MAAM,CAAC,SAAS,KAAK,KAAK,EAAE,CAAC;gBACvD,KAAK,GAAG,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,QAAQ,EAAE,YAAY,CAAC,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;gBACzF,OAAO;YACT,CAAC;YACD,MAAM,OAAO,GACX,YAAY,CAAC,WAAW,EAAE,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC,KAAK,IAAI;gBACpD,CAAC,CAAC,YAAY,CAAC,WAAW;gBAC1B,CAAC,CAAC,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;YAClB,KAAK,GAAG;gBACN,IAAI,EAAE,MAAM;gBACZ,KAAK;gBACL,QAAQ,EAAE,YAAY,CAAC,QAAQ;gBAC/B,OAAO;gBACP,KAAK,EAAE,YAAY,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;aACtE,CAAC;QACJ,CAAC;QACD,IAAI,EAAE,CAAC,OAAO,EAAE,EAAE;YAChB,IAAI,CAAC,OAAO,CAAC,YAAY,CAAC;gBAAE,OAAO;YACnC,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,EAAE,gBAAgB,CAAC,KAAK,EAAE,YAAY,CAAC,QAAQ,CAAC,CAAC,CAAC;YAC/E,IAAI,OAAO,KAAK,SAAS;gBAAE,WAAW,CAAC,KAAK,EAAE,OAAO,EAAE,YAAY,EAAE,IAAI,CAAC,CAAC;QAC7E,CAAC;QACD,GAAG,EAAE,CAAC,OAAO,EAAE,EAAE;YACf,IAAI,CAAC,OAAO,CAAC,YAAY,CAAC,EAAE,CAAC;gBAC3B,KAAK,GAAG,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;gBACzB,OAAO;YACT,CAAC;YACD,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,EAAE,gBAAgB,CAAC,KAAK,EAAE,YAAY,CAAC,QAAQ,CAAC,CAAC,CAAC;YAC/E,IAAI,OAAO,KAAK,SAAS;gBAAE,WAAW,CAAC,KAAK,EAAE,OAAO,EAAE,YAAY,EAAE,IAAI,CAAC,CAAC;YAC3E,KAAK,GAAG,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;QAC3B,CAAC;QACD,MAAM,EAAE,GAAG,EAAE;YACX,KAAK,GAAG,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;QAC3B,CAAC;QACD,OAAO,EAAE,CAAC,GAAG,EAAE,OAAO,GAAG,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,EAAE;YACzC,IAAI,CAAC,OAAO,CAAC,YAAY,CAAC;gBAAE,OAAO,KAAK,CAAC;YACzC,MAAM,SAAS,GAAG,YAAY,CAAC,WAAW,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;YACnD,MAAM,OAAO,GAAG,YAAY,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,EAAE,KAAK,SAAS,CAAC,CAAC;YACzE,IAAI,CAAC,OAAO,IAAI,WAAW,CAAC,OAAO,CAAC;gBAAE,OAAO,KAAK,CAAC;YACnD,MAAM,SAAS,GAAG,OAAO,CAAC,QAAQ,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;YACrD,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE,SAAS,CAAC,CAAC;YACzC,IAAI,KAAK,KAAK,SAAS;gBAAE,OAAO,KAAK,CAAC;YACtC,IAAI,OAAO,CAAC,MAAM,KAAK,IAAI,EAAE,CAAC;gBAC5B,IAAI,CAAC;oBACH,IAAI,EAAE,QAAQ;oBACd,OAAO,EAAE,CAAC,OAAO,CAAC,EAAE,CAAC;oBACrB,KAAK,EAAE,CAAC,eAAe,CAAC,OAAO,EAAE,oBAAoB,CAAC,KAAK,CAAC,EAAE,KAAK,CAAC,CAAC;iBACtE,CAAC,CAAC;gBACH,OAAO,IAAI,CAAC;YACd,CAAC;YACD,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,OAAO,CAAC,EAAE,CAAC,EAAE,KAAK,EAAE,cAAc,CAAC,CAAC,OAAO,CAAC,EAAE,KAAK,CAAC,EAAE,CAAC,CAAC;YACvF,OAAO,IAAI,CAAC;QACd,CAAC;QACD,MAAM,EAAE,CAAC,SAAS,EAAE,EAAE;YACpB,YAAY,GAAG,SAAS,CAAC;QAC3B,CAAC;KACF,CAAC;AACJ,CAAC;AAED,sFAAsF;AACtF,SAAS,OAAO,CAAC,KAAsC;IACrD,OAAO,KAAK,CAAC,iBAAiB,KAAK,SAAS,CAAC;AAC/C,CAAC;AAED,mGAAmG;AACnG,SAAS,WAAW,CAClB,KAAuB,EACvB,OAAkB,EAClB,KAAsC,EACtC,IAA6C;IAE7C,IAAI,KAAK,CAAC,IAAI,KAAK,MAAM;QAAE,OAAO;IAClC,IAAI,KAAK,CAAC,IAAI,KAAK,KAAK,EAAE,CAAC;QACzB,IAAI,CAAC;YACH,IAAI,EAAE,KAAK;YACX,OAAO,EAAE,EAAE;YACX,QAAQ,EAAE,WAAW,CAAC,KAAK,CAAC,QAAQ,EAAE;gBACpC,CAAC,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC;gBAC5B,CAAC,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC;aAC7B,CAAC;SACH,CAAC,CAAC;QACH,OAAO;IACT,CAAC;IACD,IAAI,KAAK,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;QAC7B,MAAM,OAAO,GAAG,aAAa,CAAC,KAAK,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;QACpD,MAAM,OAAO,GAAG,gBAAgB,CAAC,KAAK,CAAC,KAAK,EAAE,OAAO,EAAE;YACrD,IAAI,EAAE,KAAK,CAAC,WAAW,IAAI,WAAW;SACvC,CAAC,CAAC,MAAM,CAAC,CAAC,EAAE,EAAE,EAAE;YACf,MAAM,IAAI,GAAG,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC;YAClE,OAAO,IAAI,KAAK,SAAS,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC;QAClD,CAAC,CAAC,CAAC;QACH,IAAI,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,CAAC;QAC5C,OAAO;IACT,CAAC;IACD,MAAM,KAAK,GAAG,gBAAgB,CAAC,KAAK,EAAE,OAAO,EAAE,KAAK,CAAC,CAAC;IACtD,IAAI,KAAK,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;QAC1B,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,KAAK,CAAC,OAAO,EAAE,KAAK,EAAE,cAAc,CAAC,KAAK,CAAC,KAAK,EAAE,KAAK,CAAC,EAAE,CAAC,CAAC;QAC1F,OAAO;IACT,CAAC;IACD,IAAI,CAAC;QACH,IAAI,EAAE,QAAQ;QACd,OAAO,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;QACxB,KAAK,EAAE,CAAC,eAAe,CAAC,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;KAC1D,CAAC,CAAC;AACL,CAAC;AAED,kGAAkG;AAClG,SAAS,gBAAgB,CACvB,KAAuB,EACvB,OAAkB,EAClB,KAAsC;IAEtC,IAAI,KAAK,CAAC,IAAI,KAAK,MAAM,IAAI,KAAK,CAAC,IAAI,KAAK,SAAS,IAAI,KAAK,CAAC,IAAI,KAAK,KAAK,EAAE,CAAC;QAC9E,OAAO,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC;IACxB,CAAC;IACD,MAAM,GAAG,GAAG,EAAE,CAAC,EAAE,OAAO,CAAC,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,EAAE,OAAO,CAAC,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC;IAC3E,IAAI,KAAK,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI;QAAE,OAAO,GAAG,CAAC;IAC7C,MAAM,MAAM,GAAG,KAAK,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;IACxE,IAAI,MAAM,KAAK,SAAS;QAAE,OAAO,GAAG,CAAC;IACrC,MAAM,OAAO,GAAG;QACd,OAAO,EAAE,IAAI;QACb,eAAe,EAAE,KAAK,CAAC,IAAI,CAAC,eAAe;QAC3C,UAAU,EAAE,KAAK,CAAC,IAAI,CAAC,UAAU;KAClC,CAAC;IACF,OAAO;QACL,CAAC,EACC,yBAAyB,CAAC,MAAM,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE;YAC9D,GAAG,OAAO;YACV,aAAa,EAAE,KAAK,CAAC,QAAQ,CAAC,cAAc;SAC7C,CAAC,GAAG,MAAM,CAAC,CAAC;QACf,CAAC,EACC,yBAAyB,CAAC,MAAM,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE;YAC9D,GAAG,OAAO;YACV,aAAa,EAAE,KAAK,CAAC,QAAQ,CAAC,cAAc;SAC7C,CAAC,GAAG,MAAM,CAAC,CAAC;KAChB,CAAC;AACJ,CAAC;AAED,oGAAoG;AACpG,SAAS,gBAAgB,CAAC,KAAuB,EAAE,QAAsB;IACvE,OAAO,KAAK,CAAC,IAAI,KAAK,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,QAAQ,CAAC;AAC3D,CAAC;AAED,mFAAmF;AACnF,SAAS,WAAW,CAAC,IAAyB;IAC5C,OAAO,CACL,IAAI,CAAC,QAAQ,KAAK,IAAI;QACtB,IAAI,CAAC,MAAM,KAAK,IAAI;QACpB,IAAI,CAAC,QAAQ,KAAK,IAAI;QACtB,IAAI,CAAC,OAAO,KAAK,IAAI,CACtB,CAAC;AACJ,CAAC;AAED,uGAAuG;AACvG,SAAS,aAAa,CAAC,KAAgB,EAAE,GAAc;IACrD,OAAO;QACL,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC;QAC3B,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC;QAC3B,KAAK,EAAE,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC;QAChC,MAAM,EAAE,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC;KAClC,CAAC;AACJ,CAAC;AAED,mFAAmF;AACnF,SAAS,UAAU,CAAC,GAAW,EAAE,SAAiB;IAChD,IAAI,GAAG,KAAK,SAAS;QAAE,OAAO,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,SAAS,EAAE,CAAC;IACtD,IAAI,GAAG,KAAK,YAAY;QAAE,OAAO,EAAE,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC;IACxD,IAAI,GAAG,KAAK,WAAW;QAAE,OAAO,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,SAAS,EAAE,CAAC;IACvD,IAAI,GAAG,KAAK,WAAW;QAAE,OAAO,EAAE,CAAC,EAAE,CAAC,SAAS,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC;IACxD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,yEAAyE;AACzE,SAAS,oBAAoB,CAAC,KAAgB;IAC5C,IAAI,KAAK,CAAC,CAAC,GAAG,CAAC;QAAE,OAAO,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,MAAM,CAAC;IACxF,OAAO,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC,OAAO,CAAC;AAC5E,CAAC","sourcesContent":["import {\n type GridPoint,\n type GridRect,\n type GridResizeHandle,\n type GridViewport,\n hitTestWorldRects,\n moveWorldRects,\n panViewport,\n resizeWorldRect,\n resolveWorldSnapCandidate,\n selectWorldRects,\n viewportToWorld,\n} from '@ankhorage/grid-view';\n\nimport type {\n GridInteractionIntent,\n GridInteractionItem,\n GridInteractionPointer,\n GridInteractionsController,\n GridInteractionsControllerProps,\n} from '../../../types/grid-interactions';\n\ntype InteractionState =\n | { readonly kind: 'idle' }\n | { readonly kind: 'marquee'; readonly start: GridPoint; readonly viewport: GridViewport }\n | { readonly kind: 'pan'; readonly start: GridPoint; readonly viewport: GridViewport }\n | {\n readonly kind: 'move';\n readonly start: GridPoint;\n readonly viewport: GridViewport;\n readonly itemIds: readonly string[];\n readonly items: readonly GridInteractionItem[];\n }\n | {\n readonly kind: 'resize';\n readonly start: GridPoint;\n readonly viewport: GridViewport;\n readonly item: GridInteractionItem;\n readonly handle: GridResizeHandle;\n };\n\n/*** Create a platform-neutral controlled interaction controller using grid-view geometry only. */\nexport function createGridInteractionsController(\n props: GridInteractionsControllerProps,\n): GridInteractionsController {\n let currentProps = props;\n let state: InteractionState = { kind: 'idle' };\n let previousIntent: GridInteractionIntent | undefined;\n const point = (pointer: GridInteractionPointer, viewport = currentProps.viewport) =>\n Number.isFinite(pointer.x) && Number.isFinite(pointer.y)\n ? viewportToWorld(pointer, viewport)\n : undefined;\n const emit = (intent: GridInteractionIntent) => {\n if (JSON.stringify(previousIntent) === JSON.stringify(intent)) return;\n previousIntent = intent;\n currentProps.onIntent(intent);\n };\n\n return {\n begin: (pointer, handle) => {\n if (!canEdit(currentProps)) return;\n const start = point(pointer);\n if (start === undefined) return;\n if (pointer.spaceKey === true || pointer.altKey === true) {\n state = { kind: 'pan', start, viewport: currentProps.viewport };\n return;\n }\n const target = hitTestWorldRects(currentProps.items, start);\n if (target === undefined || isProtected(target)) {\n state = { kind: 'marquee', start, viewport: currentProps.viewport };\n return;\n }\n if (handle !== undefined && target.resizable !== false) {\n state = { kind: 'resize', start, viewport: currentProps.viewport, item: target, handle };\n return;\n }\n const itemIds =\n currentProps.selectedIds?.includes(target.id) === true\n ? currentProps.selectedIds\n : [target.id];\n state = {\n kind: 'move',\n start,\n viewport: currentProps.viewport,\n itemIds,\n items: currentProps.items.filter((item) => itemIds.includes(item.id)),\n };\n },\n move: (pointer) => {\n if (!canEdit(currentProps)) return;\n const current = point(pointer, viewportForState(state, currentProps.viewport));\n if (current !== undefined) emitPreview(state, current, currentProps, emit);\n },\n end: (pointer) => {\n if (!canEdit(currentProps)) {\n state = { kind: 'idle' };\n return;\n }\n const current = point(pointer, viewportForState(state, currentProps.viewport));\n if (current !== undefined) emitPreview(state, current, currentProps, emit);\n state = { kind: 'idle' };\n },\n cancel: () => {\n state = { kind: 'idle' };\n },\n keyDown: (key, pointer = { x: 0, y: 0 }) => {\n if (!canEdit(currentProps)) return false;\n const focusedId = currentProps.selectedIds?.at(-1);\n const focused = currentProps.items.find((item) => item.id === focusedId);\n if (!focused || isProtected(focused)) return false;\n const increment = pointer.shiftKey === true ? 10 : 1;\n const delta = keyToDelta(key, increment);\n if (delta === undefined) return false;\n if (pointer.altKey === true) {\n emit({\n type: 'resize',\n itemIds: [focused.id],\n rects: [resizeWorldRect(focused, resizeHandleForDelta(delta), delta)],\n });\n return true;\n }\n emit({ type: 'move', itemIds: [focused.id], rects: moveWorldRects([focused], delta) });\n return true;\n },\n update: (nextProps) => {\n currentProps = nextProps;\n },\n };\n}\n\n/*** Determines whether the component-wide interaction policy permits edit intents. */\nfunction canEdit(props: GridInteractionsControllerProps): boolean {\n return props.interactionPolicy !== 'passive';\n}\n\n/*** Emit one controlled preview/intention without retaining or mutating caller-owned item data. */\nfunction emitPreview(\n state: InteractionState,\n current: GridPoint,\n props: GridInteractionsControllerProps,\n emit: (intent: GridInteractionIntent) => void,\n) {\n if (state.kind === 'idle') return;\n if (state.kind === 'pan') {\n emit({\n type: 'pan',\n itemIds: [],\n viewport: panViewport(state.viewport, {\n x: state.start.x - current.x,\n y: state.start.y - current.y,\n }),\n });\n return;\n }\n if (state.kind === 'marquee') {\n const marquee = normalizeRect(state.start, current);\n const itemIds = selectWorldRects(props.items, marquee, {\n mode: props.marqueeMode ?? 'intersect',\n }).filter((id) => {\n const item = props.items.find((candidate) => candidate.id === id);\n return item !== undefined && !isProtected(item);\n });\n emit({ type: 'marquee', itemIds, marquee });\n return;\n }\n const delta = resolveSnapDelta(state, current, props);\n if (state.kind === 'move') {\n emit({ type: 'move', itemIds: state.itemIds, rects: moveWorldRects(state.items, delta) });\n return;\n }\n emit({\n type: 'resize',\n itemIds: [state.item.id],\n rects: [resizeWorldRect(state.item, state.handle, delta)],\n });\n}\n\n/*** Resolve explicit per-axis candidate snapping without inventing geometry outside grid-view. */\nfunction resolveSnapDelta(\n state: InteractionState,\n current: GridPoint,\n props: GridInteractionsControllerProps,\n): GridPoint {\n if (state.kind === 'idle' || state.kind === 'marquee' || state.kind === 'pan') {\n return { x: 0, y: 0 };\n }\n const raw = { x: current.x - state.start.x, y: current.y - state.start.y };\n if (props.snap?.enabled !== true) return raw;\n const anchor = state.kind === 'resize' ? state.item : state.items.at(0);\n if (anchor === undefined) return raw;\n const options = {\n enabled: true,\n tolerancePixels: props.snap.tolerancePixels,\n priorities: props.snap.priorities,\n };\n return {\n x:\n resolveWorldSnapCandidate(anchor.x + raw.x, props.snap.x ?? [], {\n ...options,\n pixelsPerUnit: props.viewport.pixelsPerUnitX,\n }) - anchor.x,\n y:\n resolveWorldSnapCandidate(anchor.y + raw.y, props.snap.y ?? [], {\n ...options,\n pixelsPerUnit: props.viewport.pixelsPerUnitY,\n }) - anchor.y,\n };\n}\n\n/*** Keep pointer coordinates in the original gesture coordinate space across controlled updates. */\nfunction viewportForState(state: InteractionState, fallback: GridViewport): GridViewport {\n return state.kind === 'idle' ? fallback : state.viewport;\n}\n\n/*** Keep all caller-declared protected items out of selection and edit intents. */\nfunction isProtected(item: GridInteractionItem): boolean {\n return (\n item.disabled === true ||\n item.locked === true ||\n item.readOnly === true ||\n item.passive === true\n );\n}\n\n/*** Normalize an arbitrary-direction drag into the world rectangle expected by grid-view selection. */\nfunction normalizeRect(start: GridPoint, end: GridPoint): GridRect {\n return {\n x: Math.min(start.x, end.x),\n y: Math.min(start.y, end.y),\n width: Math.abs(end.x - start.x),\n height: Math.abs(end.y - start.y),\n };\n}\n\n/** Maps accessible arrow keys to caller-controlled world-space edit increments. */\nfunction keyToDelta(key: string, increment: number): GridPoint | undefined {\n if (key === 'ArrowUp') return { x: 0, y: -increment };\n if (key === 'ArrowRight') return { x: increment, y: 0 };\n if (key === 'ArrowDown') return { x: 0, y: increment };\n if (key === 'ArrowLeft') return { x: -increment, y: 0 };\n return undefined;\n}\n\n/** Chooses the physical edge matching an accessible resize direction. */\nfunction resizeHandleForDelta(delta: GridPoint): GridResizeHandle {\n if (delta.x < 0) return delta.y < 0 ? 'top-left' : delta.y > 0 ? 'bottom-left' : 'left';\n return delta.y < 0 ? 'top-right' : delta.y > 0 ? 'bottom-right' : 'right';\n}\n"]}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ankhorage/zora",
3
3
  "type": "module",
4
- "version": "25.8.0",
4
+ "version": "25.8.2",
5
5
  "description": "Opinionated React Native and React Native Web UI kit built on @ankhorage/surface.",
6
6
  "homepage": "https://github.com/ankhorage/zora#readme",
7
7
  "bugs": {
@@ -30,6 +30,7 @@
30
30
  "@react-native-vector-icons/fontawesome5": "^13.1.3",
31
31
  "@react-native-vector-icons/fontawesome6": "^13.1.4",
32
32
  "@react-native-vector-icons/ionicons": "^13.1.3",
33
+ "@react-native-vector-icons/material-design-icons": "^13.1.3",
33
34
  "react": "19.2.3",
34
35
  "react-native": "0.86.x",
35
36
  "react-native-gesture-handler": "~2.32.0",
@@ -48,7 +49,7 @@
48
49
  "@ankhorage/grid-view": "^0.5.0",
49
50
  "@ankhorage/reader": "^0.1.0",
50
51
  "@ankhorage/runtime": "^4.1.1",
51
- "@ankhorage/surface": "^9.4.2",
52
+ "@ankhorage/surface": "^9.4.5",
52
53
  "@ankhorage/utility": "^1.9.85",
53
54
  "chess.js": "^1.4.0",
54
55
  "expo-document-picker": "~57.0.3",
@@ -139,6 +140,7 @@
139
140
  "@react-native-vector-icons/fontawesome5": "^13.1.4",
140
141
  "@react-native-vector-icons/fontawesome6": "^13.1.4",
141
142
  "@react-native-vector-icons/ionicons": "^13.1.4",
143
+ "@react-native-vector-icons/material-design-icons": "^13.1.4",
142
144
  "@types/bun": "^1.4.2",
143
145
  "@types/node": "^26.6.5",
144
146
  "@types/react": "^19.3.0",
@@ -112,4 +112,118 @@ describe('createGridInteractionsController', () => {
112
112
  { type: 'resize', itemIds: ['a'], rects: [{ id: 'a', x: 1, y: 1, width: 5, height: 4 }] },
113
113
  ]);
114
114
  });
115
+
116
+ test('does not compound controlled move previews or reapply the release endpoint', () => {
117
+ const intents: unknown[] = [];
118
+ const initialItems = [{ id: 'a', x: 40, y: 40, width: 20, height: 20 }];
119
+ const controller = createGridInteractionsController({
120
+ viewport,
121
+ items: initialItems,
122
+ onIntent: (intent) => intents.push(intent),
123
+ });
124
+
125
+ controller.begin({ x: 80, y: 160 });
126
+ controller.move({ x: 100, y: 160 });
127
+ controller.update({
128
+ viewport,
129
+ items: [{ id: 'a', x: 50, y: 40, width: 20, height: 20 }],
130
+ onIntent: (intent) => intents.push(intent),
131
+ });
132
+ controller.move({ x: 120, y: 160 });
133
+ controller.update({
134
+ viewport,
135
+ items: [{ id: 'a', x: 60, y: 40, width: 20, height: 20 }],
136
+ onIntent: (intent) => intents.push(intent),
137
+ });
138
+ controller.end({ x: 120, y: 160 });
139
+
140
+ expect(intents).toEqual([
141
+ { type: 'move', itemIds: ['a'], rects: [{ id: 'a', x: 50, y: 40, width: 20, height: 20 }] },
142
+ { type: 'move', itemIds: ['a'], rects: [{ id: 'a', x: 60, y: 40, width: 20, height: 20 }] },
143
+ ]);
144
+ });
145
+
146
+ test('keeps controlled resize, negative multi-item movement, cancel, and pan non-compounding', () => {
147
+ const resizeIntents: unknown[] = [];
148
+ const resizeController = createGridInteractionsController({
149
+ viewport,
150
+ items: [{ id: 'a', x: 40, y: 40, width: 20, height: 20 }],
151
+ onIntent: (intent) => resizeIntents.push(intent),
152
+ });
153
+
154
+ resizeController.begin({ x: 120, y: 240 }, 'bottom-right');
155
+ resizeController.move({ x: 140, y: 280 });
156
+ resizeController.update({
157
+ viewport,
158
+ items: [{ id: 'a', x: 40, y: 40, width: 30, height: 30 }],
159
+ onIntent: (intent) => resizeIntents.push(intent),
160
+ });
161
+ resizeController.move({ x: 160, y: 320 });
162
+ resizeController.update({
163
+ viewport,
164
+ items: [{ id: 'a', x: 40, y: 40, width: 40, height: 40 }],
165
+ onIntent: (intent) => resizeIntents.push(intent),
166
+ });
167
+ resizeController.end({ x: 160, y: 320 });
168
+
169
+ const moveIntents: unknown[] = [];
170
+ const moveController = createGridInteractionsController({
171
+ viewport,
172
+ items: [
173
+ { id: 'a', x: 40, y: 40, width: 20, height: 20 },
174
+ { id: 'b', x: 80, y: 40, width: 20, height: 20 },
175
+ ],
176
+ selectedIds: ['a', 'b'],
177
+ onIntent: (intent) => moveIntents.push(intent),
178
+ });
179
+
180
+ moveController.begin({ x: 80, y: 160 });
181
+ moveController.move({ x: 60, y: 120 });
182
+ moveController.cancel();
183
+ moveController.end({ x: 40, y: 80 });
184
+
185
+ const panIntents: unknown[] = [];
186
+ const panController = createGridInteractionsController({
187
+ viewport,
188
+ items: [],
189
+ onIntent: (intent) => panIntents.push(intent),
190
+ });
191
+
192
+ panController.begin({ x: 20, y: 40, spaceKey: true });
193
+ panController.move({ x: 40, y: 80 });
194
+ panController.update({
195
+ viewport: { ...viewport, offsetX: 5, offsetY: 2.5 },
196
+ items: [],
197
+ onIntent: (intent) => panIntents.push(intent),
198
+ });
199
+ panController.move({ x: 60, y: 120 });
200
+ panController.end({ x: 60, y: 120 });
201
+
202
+ expect(resizeIntents).toEqual([
203
+ { type: 'resize', itemIds: ['a'], rects: [{ id: 'a', x: 40, y: 40, width: 30, height: 30 }] },
204
+ { type: 'resize', itemIds: ['a'], rects: [{ id: 'a', x: 40, y: 40, width: 40, height: 40 }] },
205
+ ]);
206
+ expect(moveIntents).toEqual([
207
+ {
208
+ type: 'move',
209
+ itemIds: ['a', 'b'],
210
+ rects: [
211
+ { id: 'a', x: 30, y: 30, width: 20, height: 20 },
212
+ { id: 'b', x: 70, y: 30, width: 20, height: 20 },
213
+ ],
214
+ },
215
+ ]);
216
+ expect(panIntents).toEqual([
217
+ {
218
+ type: 'pan',
219
+ itemIds: [],
220
+ viewport: { ...viewport, offsetX: 5, offsetY: 2.5 },
221
+ },
222
+ {
223
+ type: 'pan',
224
+ itemIds: [],
225
+ viewport: { ...viewport, offsetX: 10, offsetY: 5 },
226
+ },
227
+ ]);
228
+ });
115
229
  });
@@ -2,6 +2,7 @@ import {
2
2
  type GridPoint,
3
3
  type GridRect,
4
4
  type GridResizeHandle,
5
+ type GridViewport,
5
6
  hitTestWorldRects,
6
7
  moveWorldRects,
7
8
  panViewport,
@@ -21,12 +22,19 @@ import type {
21
22
 
22
23
  type InteractionState =
23
24
  | { readonly kind: 'idle' }
24
- | { readonly kind: 'marquee'; readonly start: GridPoint }
25
- | { readonly kind: 'pan'; readonly start: GridPoint }
26
- | { readonly kind: 'move'; readonly start: GridPoint; readonly itemIds: readonly string[] }
25
+ | { readonly kind: 'marquee'; readonly start: GridPoint; readonly viewport: GridViewport }
26
+ | { readonly kind: 'pan'; readonly start: GridPoint; readonly viewport: GridViewport }
27
+ | {
28
+ readonly kind: 'move';
29
+ readonly start: GridPoint;
30
+ readonly viewport: GridViewport;
31
+ readonly itemIds: readonly string[];
32
+ readonly items: readonly GridInteractionItem[];
33
+ }
27
34
  | {
28
35
  readonly kind: 'resize';
29
36
  readonly start: GridPoint;
37
+ readonly viewport: GridViewport;
30
38
  readonly item: GridInteractionItem;
31
39
  readonly handle: GridResizeHandle;
32
40
  };
@@ -38,9 +46,9 @@ export function createGridInteractionsController(
38
46
  let currentProps = props;
39
47
  let state: InteractionState = { kind: 'idle' };
40
48
  let previousIntent: GridInteractionIntent | undefined;
41
- const point = (pointer: GridInteractionPointer) =>
49
+ const point = (pointer: GridInteractionPointer, viewport = currentProps.viewport) =>
42
50
  Number.isFinite(pointer.x) && Number.isFinite(pointer.y)
43
- ? viewportToWorld(pointer, currentProps.viewport)
51
+ ? viewportToWorld(pointer, viewport)
44
52
  : undefined;
45
53
  const emit = (intent: GridInteractionIntent) => {
46
54
  if (JSON.stringify(previousIntent) === JSON.stringify(intent)) return;
@@ -54,27 +62,33 @@ export function createGridInteractionsController(
54
62
  const start = point(pointer);
55
63
  if (start === undefined) return;
56
64
  if (pointer.spaceKey === true || pointer.altKey === true) {
57
- state = { kind: 'pan', start };
65
+ state = { kind: 'pan', start, viewport: currentProps.viewport };
58
66
  return;
59
67
  }
60
68
  const target = hitTestWorldRects(currentProps.items, start);
61
69
  if (target === undefined || isProtected(target)) {
62
- state = { kind: 'marquee', start };
70
+ state = { kind: 'marquee', start, viewport: currentProps.viewport };
63
71
  return;
64
72
  }
65
73
  if (handle !== undefined && target.resizable !== false) {
66
- state = { kind: 'resize', start, item: target, handle };
74
+ state = { kind: 'resize', start, viewport: currentProps.viewport, item: target, handle };
67
75
  return;
68
76
  }
69
77
  const itemIds =
70
78
  currentProps.selectedIds?.includes(target.id) === true
71
79
  ? currentProps.selectedIds
72
80
  : [target.id];
73
- state = { kind: 'move', start, itemIds };
81
+ state = {
82
+ kind: 'move',
83
+ start,
84
+ viewport: currentProps.viewport,
85
+ itemIds,
86
+ items: currentProps.items.filter((item) => itemIds.includes(item.id)),
87
+ };
74
88
  },
75
89
  move: (pointer) => {
76
90
  if (!canEdit(currentProps)) return;
77
- const current = point(pointer);
91
+ const current = point(pointer, viewportForState(state, currentProps.viewport));
78
92
  if (current !== undefined) emitPreview(state, current, currentProps, emit);
79
93
  },
80
94
  end: (pointer) => {
@@ -82,7 +96,7 @@ export function createGridInteractionsController(
82
96
  state = { kind: 'idle' };
83
97
  return;
84
98
  }
85
- const current = point(pointer);
99
+ const current = point(pointer, viewportForState(state, currentProps.viewport));
86
100
  if (current !== undefined) emitPreview(state, current, currentProps, emit);
87
101
  state = { kind: 'idle' };
88
102
  },
@@ -131,7 +145,7 @@ function emitPreview(
131
145
  emit({
132
146
  type: 'pan',
133
147
  itemIds: [],
134
- viewport: panViewport(props.viewport, {
148
+ viewport: panViewport(state.viewport, {
135
149
  x: state.start.x - current.x,
136
150
  y: state.start.y - current.y,
137
151
  }),
@@ -151,8 +165,7 @@ function emitPreview(
151
165
  }
152
166
  const delta = resolveSnapDelta(state, current, props);
153
167
  if (state.kind === 'move') {
154
- const selected = props.items.filter((item) => state.itemIds.includes(item.id));
155
- emit({ type: 'move', itemIds: state.itemIds, rects: moveWorldRects(selected, delta) });
168
+ emit({ type: 'move', itemIds: state.itemIds, rects: moveWorldRects(state.items, delta) });
156
169
  return;
157
170
  }
158
171
  emit({
@@ -173,8 +186,7 @@ function resolveSnapDelta(
173
186
  }
174
187
  const raw = { x: current.x - state.start.x, y: current.y - state.start.y };
175
188
  if (props.snap?.enabled !== true) return raw;
176
- const anchor =
177
- state.kind === 'resize' ? state.item : props.items.find((item) => item.id === state.itemIds[0]);
189
+ const anchor = state.kind === 'resize' ? state.item : state.items.at(0);
178
190
  if (anchor === undefined) return raw;
179
191
  const options = {
180
192
  enabled: true,
@@ -195,6 +207,11 @@ function resolveSnapDelta(
195
207
  };
196
208
  }
197
209
 
210
+ /*** Keep pointer coordinates in the original gesture coordinate space across controlled updates. */
211
+ function viewportForState(state: InteractionState, fallback: GridViewport): GridViewport {
212
+ return state.kind === 'idle' ? fallback : state.viewport;
213
+ }
214
+
198
215
  /*** Keep all caller-declared protected items out of selection and edit intents. */
199
216
  function isProtected(item: GridInteractionItem): boolean {
200
217
  return (
@@ -2,6 +2,22 @@ import { readFile } from 'node:fs/promises';
2
2
 
3
3
  import { expect, test } from 'bun:test';
4
4
 
5
+ const EXPO_57_NATIVE_PEER_VERSIONS = {
6
+ '@react-native-vector-icons/fontawesome': '13.1.4',
7
+ '@react-native-vector-icons/fontawesome5': '13.1.4',
8
+ '@react-native-vector-icons/fontawesome6': '13.1.4',
9
+ '@react-native-vector-icons/ionicons': '13.1.4',
10
+ '@react-native-vector-icons/material-design-icons': '13.1.4',
11
+ react: '19.2.3',
12
+ 'react-native': '0.86.3',
13
+ 'react-native-gesture-handler': '2.32.0',
14
+ 'react-native-reanimated': '4.5.1',
15
+ 'react-native-safe-area-context': '5.7.0',
16
+ 'react-native-svg': '15.15.4',
17
+ 'react-native-web': '0.21.3',
18
+ 'react-native-worklets': '0.10.1',
19
+ } as const;
20
+
5
21
  function isRecord(value: unknown): value is Record<string, unknown> {
6
22
  return typeof value === 'object' && value !== null && !Array.isArray(value);
7
23
  }
@@ -20,3 +36,40 @@ test('root-entry native imports expose only required portable peers', async () =
20
36
  expect(isRecord(peerDependencies) && peerDependencies['expo-font']).toBeUndefined();
21
37
  expect(isRecord(peerDependencies) && peerDependencies['@expo/vector-icons']).toBeUndefined();
22
38
  });
39
+
40
+ test('declares a jointly satisfiable Expo 57 native peer contract with Surface', async () => {
41
+ const packageJson = JSON.parse(
42
+ await readFile(new URL('../package.json', import.meta.url), 'utf8'),
43
+ ) as unknown;
44
+ if (!isRecord(packageJson)) throw new Error('Expected package.json to contain an object.');
45
+
46
+ const { dependencies, peerDependencies } = packageJson;
47
+ if (!isRecord(dependencies) || !isRecord(peerDependencies)) {
48
+ throw new Error('Expected package.json dependency sections to contain objects.');
49
+ }
50
+
51
+ expect(dependencies['@ankhorage/surface']).toBe('^9.4.5');
52
+
53
+ const surfacePackageJson = JSON.parse(
54
+ await readFile(
55
+ new URL('../node_modules/@ankhorage/surface/package.json', import.meta.url),
56
+ 'utf8',
57
+ ),
58
+ ) as unknown;
59
+ if (!isRecord(surfacePackageJson) || !isRecord(surfacePackageJson.peerDependencies)) {
60
+ throw new Error('Expected Surface to expose peer dependencies.');
61
+ }
62
+
63
+ const zoraPeers = new Map(Object.entries(peerDependencies));
64
+ const surfacePeers = new Map(Object.entries(surfacePackageJson.peerDependencies));
65
+
66
+ for (const [name, version] of Object.entries(EXPO_57_NATIVE_PEER_VERSIONS)) {
67
+ const zoraRange = zoraPeers.get(name);
68
+ const surfaceRange = surfacePeers.get(name);
69
+
70
+ expect(typeof zoraRange).toBe('string');
71
+ expect(typeof surfaceRange).toBe('string');
72
+ expect(Bun.semver.satisfies(version, zoraRange as string)).toBe(true);
73
+ expect(Bun.semver.satisfies(version, surfaceRange as string)).toBe(true);
74
+ }
75
+ });
@@ -24,7 +24,7 @@ function createGridInteractionsController(props) {
24
24
  let currentProps = props;
25
25
  let state = { kind: "idle" };
26
26
  let previousIntent;
27
- const point = (pointer) => Number.isFinite(pointer.x) && Number.isFinite(pointer.y) ? viewportToWorld(pointer, currentProps.viewport) : undefined;
27
+ const point = (pointer, viewport = currentProps.viewport) => Number.isFinite(pointer.x) && Number.isFinite(pointer.y) ? viewportToWorld(pointer, viewport) : undefined;
28
28
  const emit = (intent) => {
29
29
  if (JSON.stringify(previousIntent) === JSON.stringify(intent))
30
30
  return;
@@ -39,25 +39,31 @@ function createGridInteractionsController(props) {
39
39
  if (start === undefined)
40
40
  return;
41
41
  if (pointer.spaceKey === true || pointer.altKey === true) {
42
- state = { kind: "pan", start };
42
+ state = { kind: "pan", start, viewport: currentProps.viewport };
43
43
  return;
44
44
  }
45
45
  const target = hitTestWorldRects(currentProps.items, start);
46
46
  if (target === undefined || isProtected(target)) {
47
- state = { kind: "marquee", start };
47
+ state = { kind: "marquee", start, viewport: currentProps.viewport };
48
48
  return;
49
49
  }
50
50
  if (handle !== undefined && target.resizable !== false) {
51
- state = { kind: "resize", start, item: target, handle };
51
+ state = { kind: "resize", start, viewport: currentProps.viewport, item: target, handle };
52
52
  return;
53
53
  }
54
54
  const itemIds = currentProps.selectedIds?.includes(target.id) === true ? currentProps.selectedIds : [target.id];
55
- state = { kind: "move", start, itemIds };
55
+ state = {
56
+ kind: "move",
57
+ start,
58
+ viewport: currentProps.viewport,
59
+ itemIds,
60
+ items: currentProps.items.filter((item) => itemIds.includes(item.id))
61
+ };
56
62
  },
57
63
  move: (pointer) => {
58
64
  if (!canEdit(currentProps))
59
65
  return;
60
- const current = point(pointer);
66
+ const current = point(pointer, viewportForState(state, currentProps.viewport));
61
67
  if (current !== undefined)
62
68
  emitPreview(state, current, currentProps, emit);
63
69
  },
@@ -66,7 +72,7 @@ function createGridInteractionsController(props) {
66
72
  state = { kind: "idle" };
67
73
  return;
68
74
  }
69
- const current = point(pointer);
75
+ const current = point(pointer, viewportForState(state, currentProps.viewport));
70
76
  if (current !== undefined)
71
77
  emitPreview(state, current, currentProps, emit);
72
78
  state = { kind: "idle" };
@@ -111,7 +117,7 @@ function emitPreview(state, current, props, emit) {
111
117
  emit({
112
118
  type: "pan",
113
119
  itemIds: [],
114
- viewport: panViewport(props.viewport, {
120
+ viewport: panViewport(state.viewport, {
115
121
  x: state.start.x - current.x,
116
122
  y: state.start.y - current.y
117
123
  })
@@ -131,8 +137,7 @@ function emitPreview(state, current, props, emit) {
131
137
  }
132
138
  const delta = resolveSnapDelta(state, current, props);
133
139
  if (state.kind === "move") {
134
- const selected = props.items.filter((item) => state.itemIds.includes(item.id));
135
- emit({ type: "move", itemIds: state.itemIds, rects: moveWorldRects(selected, delta) });
140
+ emit({ type: "move", itemIds: state.itemIds, rects: moveWorldRects(state.items, delta) });
136
141
  return;
137
142
  }
138
143
  emit({
@@ -148,7 +153,7 @@ function resolveSnapDelta(state, current, props) {
148
153
  const raw = { x: current.x - state.start.x, y: current.y - state.start.y };
149
154
  if (props.snap?.enabled !== true)
150
155
  return raw;
151
- const anchor = state.kind === "resize" ? state.item : props.items.find((item) => item.id === state.itemIds[0]);
156
+ const anchor = state.kind === "resize" ? state.item : state.items.at(0);
152
157
  if (anchor === undefined)
153
158
  return raw;
154
159
  const options = {
@@ -167,6 +172,9 @@ function resolveSnapDelta(state, current, props) {
167
172
  }) - anchor.y
168
173
  };
169
174
  }
175
+ function viewportForState(state, fallback) {
176
+ return state.kind === "idle" ? fallback : state.viewport;
177
+ }
170
178
  function isProtected(item) {
171
179
  return item.disabled === true || item.locked === true || item.readOnly === true || item.passive === true;
172
180
  }