react-native-layout-dnd 0.1.0-alpha.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/LICENSE +20 -0
- package/README.md +262 -0
- package/lib/module/adapters/coordinates.js +135 -0
- package/lib/module/adapters/coordinates.js.map +1 -0
- package/lib/module/adapters/scroll.js +81 -0
- package/lib/module/adapters/scroll.js.map +1 -0
- package/lib/module/adapters/virtualizedList.js +69 -0
- package/lib/module/adapters/virtualizedList.js.map +1 -0
- package/lib/module/components/DndProvider.js +1144 -0
- package/lib/module/components/DndProvider.js.map +1 -0
- package/lib/module/components/DragHandle.js +65 -0
- package/lib/module/components/DragHandle.js.map +1 -0
- package/lib/module/components/GridController.js +151 -0
- package/lib/module/components/GridController.js.map +1 -0
- package/lib/module/components/GridDragHandle.js +15 -0
- package/lib/module/components/GridDragHandle.js.map +1 -0
- package/lib/module/components/GridZoneView.js +59 -0
- package/lib/module/components/GridZoneView.js.map +1 -0
- package/lib/module/components/SortableGrid.js +305 -0
- package/lib/module/components/SortableGrid.js.map +1 -0
- package/lib/module/components/SortableList.js +714 -0
- package/lib/module/components/SortableList.js.map +1 -0
- package/lib/module/components/dndTypes.js +4 -0
- package/lib/module/components/dndTypes.js.map +1 -0
- package/lib/module/components/gridTypes.js +4 -0
- package/lib/module/components/gridTypes.js.map +1 -0
- package/lib/module/components/handleViewProps.js +13 -0
- package/lib/module/components/handleViewProps.js.map +1 -0
- package/lib/module/contracts.js +4 -0
- package/lib/module/contracts.js.map +1 -0
- package/lib/module/controller/approval.js +98 -0
- package/lib/module/controller/approval.js.map +1 -0
- package/lib/module/controller/coordinatorBase.js +93 -0
- package/lib/module/controller/coordinatorBase.js.map +1 -0
- package/lib/module/controller/dndSession.js +621 -0
- package/lib/module/controller/dndSession.js.map +1 -0
- package/lib/module/controller/dndState.js +103 -0
- package/lib/module/controller/dndState.js.map +1 -0
- package/lib/module/controller/gridAdapter.js +191 -0
- package/lib/module/controller/gridAdapter.js.map +1 -0
- package/lib/module/controller/gridState.js +101 -0
- package/lib/module/controller/gridState.js.map +1 -0
- package/lib/module/controller/locations.js +48 -0
- package/lib/module/controller/locations.js.map +1 -0
- package/lib/module/controller/previewPath.js +73 -0
- package/lib/module/controller/previewPath.js.map +1 -0
- package/lib/module/controller/sessionCore.js +222 -0
- package/lib/module/controller/sessionCore.js.map +1 -0
- package/lib/module/controller/useDndState.js +9 -0
- package/lib/module/controller/useDndState.js.map +1 -0
- package/lib/module/controller/useGridState.js +9 -0
- package/lib/module/controller/useGridState.js.map +1 -0
- package/lib/module/controller/useStoreState.js +15 -0
- package/lib/module/controller/useStoreState.js.map +1 -0
- package/lib/module/defaults.js +7 -0
- package/lib/module/defaults.js.map +1 -0
- package/lib/module/diagnostics.js +16 -0
- package/lib/module/diagnostics.js.map +1 -0
- package/lib/module/engine/gridPlacements.js +54 -0
- package/lib/module/engine/gridPlacements.js.map +1 -0
- package/lib/module/engine/index.js +19 -0
- package/lib/module/engine/index.js.map +1 -0
- package/lib/module/engine/layout.js +36 -0
- package/lib/module/engine/layout.js.map +1 -0
- package/lib/module/engine/layoutMove.js +326 -0
- package/lib/module/engine/layoutMove.js.map +1 -0
- package/lib/module/engine/layoutState.js +344 -0
- package/lib/module/engine/layoutState.js.map +1 -0
- package/lib/module/engine/list.js +172 -0
- package/lib/module/engine/list.js.map +1 -0
- package/lib/module/engine/listIndex.js +125 -0
- package/lib/module/engine/listIndex.js.map +1 -0
- package/lib/module/engine/move.js +216 -0
- package/lib/module/engine/move.js.map +1 -0
- package/lib/module/engine/movementPolicy.js +77 -0
- package/lib/module/engine/movementPolicy.js.map +1 -0
- package/lib/module/engine/orderedTarget.js +71 -0
- package/lib/module/engine/orderedTarget.js.map +1 -0
- package/lib/module/engine/pack.js +30 -0
- package/lib/module/engine/pack.js.map +1 -0
- package/lib/module/engine/rectangles.js +12 -0
- package/lib/module/engine/rectangles.js.map +1 -0
- package/lib/module/engine/sameLayout.js +29 -0
- package/lib/module/engine/sameLayout.js.map +1 -0
- package/lib/module/engine/search.js +159 -0
- package/lib/module/engine/search.js.map +1 -0
- package/lib/module/engine/spatial.js +27 -0
- package/lib/module/engine/spatial.js.map +1 -0
- package/lib/module/engine/spatialAppRotation.js +39 -0
- package/lib/module/engine/spatialAppRotation.js.map +1 -0
- package/lib/module/engine/spatialExchange.js +164 -0
- package/lib/module/engine/spatialExchange.js.map +1 -0
- package/lib/module/engine/spatialInsertion.js +135 -0
- package/lib/module/engine/spatialInsertion.js.map +1 -0
- package/lib/module/engine/spatialPolicy.js +41 -0
- package/lib/module/engine/spatialPolicy.js.map +1 -0
- package/lib/module/engine/spatialRotation.js +48 -0
- package/lib/module/engine/spatialRotation.js.map +1 -0
- package/lib/module/engine/validation.js +205 -0
- package/lib/module/engine/validation.js.map +1 -0
- package/lib/module/gesture/DndGestureGroup.js +268 -0
- package/lib/module/gesture/DndGestureGroup.js.map +1 -0
- package/lib/module/gesture/DndGestureHost.js +32 -0
- package/lib/module/gesture/DndGestureHost.js.map +1 -0
- package/lib/module/gesture/gestureConfig.js +81 -0
- package/lib/module/gesture/gestureConfig.js.map +1 -0
- package/lib/module/gesture/gestureGrouping.js +122 -0
- package/lib/module/gesture/gestureGrouping.js.map +1 -0
- package/lib/module/gesture/gestureHost.js +106 -0
- package/lib/module/gesture/gestureHost.js.map +1 -0
- package/lib/module/index.js +23 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/pointer/dndTarget.js +101 -0
- package/lib/module/pointer/dndTarget.js.map +1 -0
- package/lib/module/pointer/geometry.js +363 -0
- package/lib/module/pointer/geometry.js.map +1 -0
- package/lib/module/pointer/layoutTarget.js +307 -0
- package/lib/module/pointer/layoutTarget.js.map +1 -0
- package/lib/module/pointer/listCellMotion.js +99 -0
- package/lib/module/pointer/listCellMotion.js.map +1 -0
- package/lib/module/pointer/listTarget.js +42 -0
- package/lib/module/pointer/listTarget.js.map +1 -0
- package/lib/module/pointer/motion.js +37 -0
- package/lib/module/pointer/motion.js.map +1 -0
- package/lib/module/runtime/dndPresentation.js +81 -0
- package/lib/module/runtime/dndPresentation.js.map +1 -0
- package/lib/module/runtime/dndRenderStore.js +129 -0
- package/lib/module/runtime/dndRenderStore.js.map +1 -0
- package/lib/module/runtime/dndRuntime.js +135 -0
- package/lib/module/runtime/dndRuntime.js.map +1 -0
- package/lib/module/runtime/dndSettlement.js +77 -0
- package/lib/module/runtime/dndSettlement.js.map +1 -0
- package/lib/module/runtime/gridRuntime.js +37 -0
- package/lib/module/runtime/gridRuntime.js.map +1 -0
- package/lib/module/types.js +4 -0
- package/lib/module/types.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/adapters/coordinates.d.ts +9 -0
- package/lib/typescript/src/adapters/coordinates.d.ts.map +1 -0
- package/lib/typescript/src/adapters/scroll.d.ts +49 -0
- package/lib/typescript/src/adapters/scroll.d.ts.map +1 -0
- package/lib/typescript/src/adapters/virtualizedList.d.ts +22 -0
- package/lib/typescript/src/adapters/virtualizedList.d.ts.map +1 -0
- package/lib/typescript/src/components/DndProvider.d.ts +4 -0
- package/lib/typescript/src/components/DndProvider.d.ts.map +1 -0
- package/lib/typescript/src/components/DragHandle.d.ts +4 -0
- package/lib/typescript/src/components/DragHandle.d.ts.map +1 -0
- package/lib/typescript/src/components/GridController.d.ts +21 -0
- package/lib/typescript/src/components/GridController.d.ts.map +1 -0
- package/lib/typescript/src/components/GridDragHandle.d.ts +8 -0
- package/lib/typescript/src/components/GridDragHandle.d.ts.map +1 -0
- package/lib/typescript/src/components/GridZoneView.d.ts +8 -0
- package/lib/typescript/src/components/GridZoneView.d.ts.map +1 -0
- package/lib/typescript/src/components/SortableGrid.d.ts +4 -0
- package/lib/typescript/src/components/SortableGrid.d.ts.map +1 -0
- package/lib/typescript/src/components/SortableList.d.ts +4 -0
- package/lib/typescript/src/components/SortableList.d.ts.map +1 -0
- package/lib/typescript/src/components/dndTypes.d.ts +94 -0
- package/lib/typescript/src/components/dndTypes.d.ts.map +1 -0
- package/lib/typescript/src/components/gridTypes.d.ts +84 -0
- package/lib/typescript/src/components/gridTypes.d.ts.map +1 -0
- package/lib/typescript/src/components/handleViewProps.d.ts +6 -0
- package/lib/typescript/src/components/handleViewProps.d.ts.map +1 -0
- package/lib/typescript/src/contracts.d.ts +145 -0
- package/lib/typescript/src/contracts.d.ts.map +1 -0
- package/lib/typescript/src/controller/approval.d.ts +66 -0
- package/lib/typescript/src/controller/approval.d.ts.map +1 -0
- package/lib/typescript/src/controller/coordinatorBase.d.ts +37 -0
- package/lib/typescript/src/controller/coordinatorBase.d.ts.map +1 -0
- package/lib/typescript/src/controller/dndSession.d.ts +152 -0
- package/lib/typescript/src/controller/dndSession.d.ts.map +1 -0
- package/lib/typescript/src/controller/dndState.d.ts +27 -0
- package/lib/typescript/src/controller/dndState.d.ts.map +1 -0
- package/lib/typescript/src/controller/gridAdapter.d.ts +44 -0
- package/lib/typescript/src/controller/gridAdapter.d.ts.map +1 -0
- package/lib/typescript/src/controller/gridState.d.ts +18 -0
- package/lib/typescript/src/controller/gridState.d.ts.map +1 -0
- package/lib/typescript/src/controller/locations.d.ts +9 -0
- package/lib/typescript/src/controller/locations.d.ts.map +1 -0
- package/lib/typescript/src/controller/previewPath.d.ts +49 -0
- package/lib/typescript/src/controller/previewPath.d.ts.map +1 -0
- package/lib/typescript/src/controller/sessionCore.d.ts +106 -0
- package/lib/typescript/src/controller/sessionCore.d.ts.map +1 -0
- package/lib/typescript/src/controller/useDndState.d.ts +8 -0
- package/lib/typescript/src/controller/useDndState.d.ts.map +1 -0
- package/lib/typescript/src/controller/useGridState.d.ts +8 -0
- package/lib/typescript/src/controller/useGridState.d.ts.map +1 -0
- package/lib/typescript/src/controller/useStoreState.d.ts +16 -0
- package/lib/typescript/src/controller/useStoreState.d.ts.map +1 -0
- package/lib/typescript/src/defaults.d.ts +5 -0
- package/lib/typescript/src/defaults.d.ts.map +1 -0
- package/lib/typescript/src/diagnostics.d.ts +9 -0
- package/lib/typescript/src/diagnostics.d.ts.map +1 -0
- package/lib/typescript/src/engine/gridPlacements.d.ts +22 -0
- package/lib/typescript/src/engine/gridPlacements.d.ts.map +1 -0
- package/lib/typescript/src/engine/index.d.ts +28 -0
- package/lib/typescript/src/engine/index.d.ts.map +1 -0
- package/lib/typescript/src/engine/layout.d.ts +4 -0
- package/lib/typescript/src/engine/layout.d.ts.map +1 -0
- package/lib/typescript/src/engine/layoutMove.d.ts +46 -0
- package/lib/typescript/src/engine/layoutMove.d.ts.map +1 -0
- package/lib/typescript/src/engine/layoutState.d.ts +22 -0
- package/lib/typescript/src/engine/layoutState.d.ts.map +1 -0
- package/lib/typescript/src/engine/list.d.ts +68 -0
- package/lib/typescript/src/engine/list.d.ts.map +1 -0
- package/lib/typescript/src/engine/listIndex.d.ts +32 -0
- package/lib/typescript/src/engine/listIndex.d.ts.map +1 -0
- package/lib/typescript/src/engine/move.d.ts +10 -0
- package/lib/typescript/src/engine/move.d.ts.map +1 -0
- package/lib/typescript/src/engine/movementPolicy.d.ts +34 -0
- package/lib/typescript/src/engine/movementPolicy.d.ts.map +1 -0
- package/lib/typescript/src/engine/orderedTarget.d.ts +32 -0
- package/lib/typescript/src/engine/orderedTarget.d.ts.map +1 -0
- package/lib/typescript/src/engine/pack.d.ts +3 -0
- package/lib/typescript/src/engine/pack.d.ts.map +1 -0
- package/lib/typescript/src/engine/rectangles.d.ts +9 -0
- package/lib/typescript/src/engine/rectangles.d.ts.map +1 -0
- package/lib/typescript/src/engine/sameLayout.d.ts +4 -0
- package/lib/typescript/src/engine/sameLayout.d.ts.map +1 -0
- package/lib/typescript/src/engine/search.d.ts +28 -0
- package/lib/typescript/src/engine/search.d.ts.map +1 -0
- package/lib/typescript/src/engine/spatial.d.ts +16 -0
- package/lib/typescript/src/engine/spatial.d.ts.map +1 -0
- package/lib/typescript/src/engine/spatialAppRotation.d.ts +4 -0
- package/lib/typescript/src/engine/spatialAppRotation.d.ts.map +1 -0
- package/lib/typescript/src/engine/spatialExchange.d.ts +5 -0
- package/lib/typescript/src/engine/spatialExchange.d.ts.map +1 -0
- package/lib/typescript/src/engine/spatialInsertion.d.ts +5 -0
- package/lib/typescript/src/engine/spatialInsertion.d.ts.map +1 -0
- package/lib/typescript/src/engine/spatialPolicy.d.ts +9 -0
- package/lib/typescript/src/engine/spatialPolicy.d.ts.map +1 -0
- package/lib/typescript/src/engine/spatialRotation.d.ts +8 -0
- package/lib/typescript/src/engine/spatialRotation.d.ts.map +1 -0
- package/lib/typescript/src/engine/validation.d.ts +9 -0
- package/lib/typescript/src/engine/validation.d.ts.map +1 -0
- package/lib/typescript/src/gesture/DndGestureGroup.d.ts +33 -0
- package/lib/typescript/src/gesture/DndGestureGroup.d.ts.map +1 -0
- package/lib/typescript/src/gesture/DndGestureHost.d.ts +12 -0
- package/lib/typescript/src/gesture/DndGestureHost.d.ts.map +1 -0
- package/lib/typescript/src/gesture/gestureConfig.d.ts +15 -0
- package/lib/typescript/src/gesture/gestureConfig.d.ts.map +1 -0
- package/lib/typescript/src/gesture/gestureGrouping.d.ts +66 -0
- package/lib/typescript/src/gesture/gestureGrouping.d.ts.map +1 -0
- package/lib/typescript/src/gesture/gestureHost.d.ts +23 -0
- package/lib/typescript/src/gesture/gestureHost.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +38 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/pointer/dndTarget.d.ts +14 -0
- package/lib/typescript/src/pointer/dndTarget.d.ts.map +1 -0
- package/lib/typescript/src/pointer/geometry.d.ts +28 -0
- package/lib/typescript/src/pointer/geometry.d.ts.map +1 -0
- package/lib/typescript/src/pointer/layoutTarget.d.ts +43 -0
- package/lib/typescript/src/pointer/layoutTarget.d.ts.map +1 -0
- package/lib/typescript/src/pointer/listCellMotion.d.ts +43 -0
- package/lib/typescript/src/pointer/listCellMotion.d.ts.map +1 -0
- package/lib/typescript/src/pointer/listTarget.d.ts +7 -0
- package/lib/typescript/src/pointer/listTarget.d.ts.map +1 -0
- package/lib/typescript/src/pointer/motion.d.ts +20 -0
- package/lib/typescript/src/pointer/motion.d.ts.map +1 -0
- package/lib/typescript/src/runtime/dndPresentation.d.ts +37 -0
- package/lib/typescript/src/runtime/dndPresentation.d.ts.map +1 -0
- package/lib/typescript/src/runtime/dndRenderStore.d.ts +46 -0
- package/lib/typescript/src/runtime/dndRenderStore.d.ts.map +1 -0
- package/lib/typescript/src/runtime/dndRuntime.d.ts +138 -0
- package/lib/typescript/src/runtime/dndRuntime.d.ts.map +1 -0
- package/lib/typescript/src/runtime/dndSettlement.d.ts +18 -0
- package/lib/typescript/src/runtime/dndSettlement.d.ts.map +1 -0
- package/lib/typescript/src/runtime/gridRuntime.d.ts +54 -0
- package/lib/typescript/src/runtime/gridRuntime.d.ts.map +1 -0
- package/lib/typescript/src/types.d.ts +180 -0
- package/lib/typescript/src/types.d.ts.map +1 -0
- package/package.json +136 -0
- package/src/adapters/coordinates.ts +181 -0
- package/src/adapters/scroll.ts +147 -0
- package/src/adapters/virtualizedList.ts +112 -0
- package/src/components/DndProvider.tsx +1836 -0
- package/src/components/DragHandle.tsx +89 -0
- package/src/components/GridController.tsx +213 -0
- package/src/components/GridDragHandle.tsx +11 -0
- package/src/components/GridZoneView.tsx +57 -0
- package/src/components/SortableGrid.tsx +449 -0
- package/src/components/SortableList.tsx +1028 -0
- package/src/components/dndTypes.ts +102 -0
- package/src/components/gridTypes.ts +90 -0
- package/src/components/handleViewProps.ts +35 -0
- package/src/contracts.ts +149 -0
- package/src/controller/approval.ts +179 -0
- package/src/controller/coordinatorBase.ts +118 -0
- package/src/controller/dndSession.ts +1058 -0
- package/src/controller/dndState.ts +198 -0
- package/src/controller/gridAdapter.ts +268 -0
- package/src/controller/gridState.ts +187 -0
- package/src/controller/locations.ts +67 -0
- package/src/controller/previewPath.ts +90 -0
- package/src/controller/sessionCore.ts +390 -0
- package/src/controller/useDndState.ts +26 -0
- package/src/controller/useGridState.ts +22 -0
- package/src/controller/useStoreState.ts +27 -0
- package/src/defaults.ts +4 -0
- package/src/diagnostics.ts +23 -0
- package/src/engine/gridPlacements.ts +85 -0
- package/src/engine/index.ts +99 -0
- package/src/engine/layout.ts +29 -0
- package/src/engine/layoutMove.ts +401 -0
- package/src/engine/layoutState.ts +490 -0
- package/src/engine/list.ts +307 -0
- package/src/engine/listIndex.ts +174 -0
- package/src/engine/move.ts +274 -0
- package/src/engine/movementPolicy.ts +151 -0
- package/src/engine/orderedTarget.ts +131 -0
- package/src/engine/pack.ts +24 -0
- package/src/engine/rectangles.ts +18 -0
- package/src/engine/sameLayout.ts +50 -0
- package/src/engine/search.ts +174 -0
- package/src/engine/spatial.ts +36 -0
- package/src/engine/spatialAppRotation.ts +53 -0
- package/src/engine/spatialExchange.ts +212 -0
- package/src/engine/spatialInsertion.ts +157 -0
- package/src/engine/spatialPolicy.ts +48 -0
- package/src/engine/spatialRotation.ts +56 -0
- package/src/engine/validation.ts +358 -0
- package/src/gesture/DndGestureGroup.tsx +357 -0
- package/src/gesture/DndGestureHost.tsx +50 -0
- package/src/gesture/gestureConfig.ts +123 -0
- package/src/gesture/gestureGrouping.ts +210 -0
- package/src/gesture/gestureHost.tsx +163 -0
- package/src/index.ts +162 -0
- package/src/pointer/dndTarget.ts +213 -0
- package/src/pointer/geometry.ts +584 -0
- package/src/pointer/layoutTarget.ts +515 -0
- package/src/pointer/listCellMotion.ts +174 -0
- package/src/pointer/listTarget.ts +56 -0
- package/src/pointer/motion.ts +66 -0
- package/src/runtime/dndPresentation.ts +113 -0
- package/src/runtime/dndRenderStore.ts +246 -0
- package/src/runtime/dndRuntime.ts +311 -0
- package/src/runtime/dndSettlement.ts +117 -0
- package/src/runtime/gridRuntime.ts +73 -0
- package/src/types.ts +189 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import type { ReactNode, Ref } from 'react';
|
|
2
|
+
import type { StyleProp, ViewProps, ViewStyle } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import type { AutoScrollOptions, DragAxis } from '../adapters/scroll';
|
|
5
|
+
import type { ListVirtualizationOptions } from '../adapters/virtualizedList';
|
|
6
|
+
import type { DndItem, GridPlacement } from '../contracts';
|
|
7
|
+
import type {
|
|
8
|
+
DndDragEndEvent,
|
|
9
|
+
DndSessionOptions,
|
|
10
|
+
} from '../controller/dndSession';
|
|
11
|
+
import type { HandleViewProps } from './handleViewProps';
|
|
12
|
+
import type { GridMotionConfig } from '../pointer/motion';
|
|
13
|
+
import type { GestureRelations, ZoneGeometry } from './gridTypes';
|
|
14
|
+
|
|
15
|
+
export type DndMotionConfig = GridMotionConfig;
|
|
16
|
+
|
|
17
|
+
export type ListItemRenderArgs<T> = {
|
|
18
|
+
item: DndItem<T>;
|
|
19
|
+
index: number;
|
|
20
|
+
zoneId: string;
|
|
21
|
+
isDragging: boolean;
|
|
22
|
+
};
|
|
23
|
+
export type GridItemRenderArgs<T> = ListItemRenderArgs<T> & {
|
|
24
|
+
position: GridPlacement['position'];
|
|
25
|
+
span: GridPlacement['span'];
|
|
26
|
+
placement?: GridPlacement['placement'];
|
|
27
|
+
width: number;
|
|
28
|
+
height: number;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/** Spatial grid under the shared provider; cells use the existing geometry contract. */
|
|
32
|
+
export type SortableGridProps<T> = Omit<ViewProps, 'children'> & {
|
|
33
|
+
zoneId: string;
|
|
34
|
+
geometry: ZoneGeometry;
|
|
35
|
+
renderItem(args: GridItemRenderArgs<T>): ReactNode;
|
|
36
|
+
dropIndicatorStyle?: StyleProp<ViewStyle>;
|
|
37
|
+
children?: ReactNode;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export type DndPreviewArgs<T> = ListItemRenderArgs<T> & {
|
|
41
|
+
sessionId: string;
|
|
42
|
+
targetZoneId: string | null;
|
|
43
|
+
width: number;
|
|
44
|
+
height: number;
|
|
45
|
+
validity: 'pending' | 'valid' | 'invalid';
|
|
46
|
+
/** Call after the custom preview's drawable content has committed. Late sessions are ignored. */
|
|
47
|
+
onReady(): void;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export type DndProviderHandle = {
|
|
51
|
+
/** Cancel the current drag because zone geometry moved under it. */
|
|
52
|
+
invalidateGeometry(): void;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export type DndProviderProps<T> = Omit<ViewProps, 'children'> &
|
|
56
|
+
DndSessionOptions<T> & {
|
|
57
|
+
children: ReactNode;
|
|
58
|
+
activation?: { delayMs: number };
|
|
59
|
+
/** Independent of list orientation. Defaults to both screen axes. */
|
|
60
|
+
dragAxis?: DragAxis;
|
|
61
|
+
motion?: DndMotionConfig;
|
|
62
|
+
renderDragPreview?(args: DndPreviewArgs<T>): ReactNode;
|
|
63
|
+
/** Keep the original visible until the custom preview calls onReady. Defaults to false. */
|
|
64
|
+
waitForDragPreviewReady?: boolean;
|
|
65
|
+
dragPreviewStyle?: StyleProp<ViewStyle>;
|
|
66
|
+
/**
|
|
67
|
+
* The `onDragEnd` event again once the drag preview has finished settling
|
|
68
|
+
* into its cell, or right after `onDragEnd` when nothing animates.
|
|
69
|
+
* `GridController.onDragEnd` uses this timing.
|
|
70
|
+
*/
|
|
71
|
+
onDragSettled?(event: DndDragEndEvent<T>): void;
|
|
72
|
+
ref?: Ref<DndProviderHandle>;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/** ScrollView by default; enable virtualization for FlatList. Sizes are logical pixels. */
|
|
76
|
+
export type SortableListProps<T> = Omit<ViewProps, 'children'> & {
|
|
77
|
+
zoneId: string;
|
|
78
|
+
renderItem(args: ListItemRenderArgs<T>): ReactNode;
|
|
79
|
+
/** Main-axis estimate before a cell has been measured. Defaults to 48. */
|
|
80
|
+
estimatedItemSize?: number;
|
|
81
|
+
/** Optional authoritative main-axis size, constant or per item. */
|
|
82
|
+
itemSize?: number | ((itemId: string, index: number) => number);
|
|
83
|
+
gap?: number;
|
|
84
|
+
paddingStart?: number;
|
|
85
|
+
paddingEnd?: number;
|
|
86
|
+
autoScroll?: AutoScrollOptions;
|
|
87
|
+
scrollEnabled?: boolean;
|
|
88
|
+
showsScrollIndicator?: boolean;
|
|
89
|
+
onScrollOffsetChange?(offset: { x: number; y: number }): void;
|
|
90
|
+
/** Use FlatList windowing. True selects defaults; false is the default. */
|
|
91
|
+
virtualization?: boolean | ListVirtualizationOptions;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
/** Accessibility and test attributes forwarded to the handle's native view. */
|
|
95
|
+
export type DragHandleViewProps = HandleViewProps;
|
|
96
|
+
|
|
97
|
+
export type DragHandleProps = DragHandleViewProps & {
|
|
98
|
+
children: ReactNode;
|
|
99
|
+
itemId?: string;
|
|
100
|
+
disabled?: boolean;
|
|
101
|
+
gestureRelations?: GestureRelations;
|
|
102
|
+
};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type { ReactNode, Ref } from 'react';
|
|
2
|
+
import type { StyleProp, ViewProps, ViewStyle } from 'react-native';
|
|
3
|
+
import type { PanGestureConfig } from 'react-native-gesture-handler';
|
|
4
|
+
import type { GridMovementPolicy } from '../engine/movementPolicy';
|
|
5
|
+
import type { HandleViewProps } from './handleViewProps';
|
|
6
|
+
import type { GridMotionConfig } from '../pointer/motion';
|
|
7
|
+
|
|
8
|
+
import type {
|
|
9
|
+
CanDropArgs,
|
|
10
|
+
DragEndEvent,
|
|
11
|
+
DragStartEvent,
|
|
12
|
+
DropDecision,
|
|
13
|
+
GridChange,
|
|
14
|
+
GridDiagnosticEvent,
|
|
15
|
+
GridItem,
|
|
16
|
+
GridValue,
|
|
17
|
+
ValidationIssue,
|
|
18
|
+
} from '../types';
|
|
19
|
+
|
|
20
|
+
export type GestureRelations = Pick<
|
|
21
|
+
PanGestureConfig,
|
|
22
|
+
'block' | 'requireToFail' | 'simultaneousWith'
|
|
23
|
+
>;
|
|
24
|
+
export type GridInsets = {
|
|
25
|
+
top: number;
|
|
26
|
+
right: number;
|
|
27
|
+
bottom: number;
|
|
28
|
+
left: number;
|
|
29
|
+
};
|
|
30
|
+
export type ZoneGeometry = {
|
|
31
|
+
rowGap: number;
|
|
32
|
+
columnGap: number;
|
|
33
|
+
padding: GridInsets;
|
|
34
|
+
} & (
|
|
35
|
+
{ mode: 'fixed'; cellWidth: number; cellHeight: number } | { mode: 'fit' }
|
|
36
|
+
);
|
|
37
|
+
export type ItemRenderArgs<T> = {
|
|
38
|
+
item: GridItem<T>;
|
|
39
|
+
zoneId: string;
|
|
40
|
+
width: number;
|
|
41
|
+
height: number;
|
|
42
|
+
isDragging: boolean;
|
|
43
|
+
};
|
|
44
|
+
export type DragPreviewArgs<T> = Omit<ItemRenderArgs<T>, 'zoneId'> & {
|
|
45
|
+
sessionId: string;
|
|
46
|
+
/** Call after the custom preview's drawable content has committed. Late sessions are ignored. */
|
|
47
|
+
onReady(): void;
|
|
48
|
+
sourceZoneId: string;
|
|
49
|
+
targetZoneId: string | null;
|
|
50
|
+
validity: 'pending' | 'valid' | 'invalid';
|
|
51
|
+
};
|
|
52
|
+
export type GridControllerHandle = { invalidateGeometry(): void };
|
|
53
|
+
export type GridControllerProps<T> = Omit<ViewProps, 'children'> & {
|
|
54
|
+
value: GridValue<T>;
|
|
55
|
+
onChange(nextValue: GridValue<T>, change: GridChange): void;
|
|
56
|
+
renderItem(args: ItemRenderArgs<T>): ReactNode;
|
|
57
|
+
renderDragPreview?(args: DragPreviewArgs<T>): ReactNode;
|
|
58
|
+
/** Keep the original visible until the custom preview calls onReady. Defaults to false. */
|
|
59
|
+
waitForDragPreviewReady?: boolean;
|
|
60
|
+
/** Optional preview surface styling (background, border, clipping, shadow). */
|
|
61
|
+
dragPreviewStyle?: StyleProp<ViewStyle>;
|
|
62
|
+
canDrop?(args: CanDropArgs<T>): DropDecision;
|
|
63
|
+
onDragStart?(event: DragStartEvent): void;
|
|
64
|
+
/** Fires once the drag preview has settled into its cell, or at once when nothing animates. */
|
|
65
|
+
onDragEnd?(event: DragEndEvent): void;
|
|
66
|
+
onValidationError?(issues: ValidationIssue[]): void;
|
|
67
|
+
/** Optional JS-only observation. Exceptions are ignored; omit to disable timing. */
|
|
68
|
+
onDiagnostic?(event: GridDiagnosticEvent): void;
|
|
69
|
+
/** Defaults to 10,000 bounded search steps. */
|
|
70
|
+
searchBudget?: number;
|
|
71
|
+
/** Defaults to a 250ms long press. */
|
|
72
|
+
activation?: { delayMs: number };
|
|
73
|
+
/** Generic rules by default; opt into HOME_GRID_MOVEMENT_POLICY for home compatibility. */
|
|
74
|
+
movementPolicy?: GridMovementPolicy;
|
|
75
|
+
motion?: GridMotionConfig;
|
|
76
|
+
responseTimeoutMs?: number;
|
|
77
|
+
disabled?: boolean;
|
|
78
|
+
ref?: Ref<GridControllerHandle>;
|
|
79
|
+
children: ReactNode;
|
|
80
|
+
};
|
|
81
|
+
export type GridZoneViewProps = ViewProps & {
|
|
82
|
+
zoneId: string;
|
|
83
|
+
geometry: ZoneGeometry;
|
|
84
|
+
};
|
|
85
|
+
export type GridDragHandleProps = HandleViewProps & {
|
|
86
|
+
itemId: string;
|
|
87
|
+
gestureRelations?: GestureRelations;
|
|
88
|
+
disabled?: boolean;
|
|
89
|
+
children: ReactNode;
|
|
90
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { ViewProps } from 'react-native';
|
|
2
|
+
|
|
3
|
+
/** Accessibility and test attributes forwarded to a handle's native view. */
|
|
4
|
+
export type HandleViewProps = Pick<
|
|
5
|
+
ViewProps,
|
|
6
|
+
| 'accessible'
|
|
7
|
+
| 'accessibilityLabel'
|
|
8
|
+
| 'accessibilityHint'
|
|
9
|
+
| 'accessibilityRole'
|
|
10
|
+
| 'accessibilityState'
|
|
11
|
+
| 'accessibilityActions'
|
|
12
|
+
| 'onAccessibilityAction'
|
|
13
|
+
| 'importantForAccessibility'
|
|
14
|
+
| 'testID'
|
|
15
|
+
>;
|
|
16
|
+
|
|
17
|
+
const HANDLE_VIEW_PROP_KEYS = [
|
|
18
|
+
'accessible',
|
|
19
|
+
'accessibilityLabel',
|
|
20
|
+
'accessibilityHint',
|
|
21
|
+
'accessibilityRole',
|
|
22
|
+
'accessibilityState',
|
|
23
|
+
'accessibilityActions',
|
|
24
|
+
'onAccessibilityAction',
|
|
25
|
+
'importantForAccessibility',
|
|
26
|
+
'testID',
|
|
27
|
+
] as const satisfies ReadonlyArray<keyof HandleViewProps>;
|
|
28
|
+
|
|
29
|
+
/** Copies only the defined view attributes so handle-specific props never reach the native view. */
|
|
30
|
+
export function pickHandleViewProps(props: HandleViewProps): HandleViewProps {
|
|
31
|
+
const forwarded: Record<string, unknown> = {};
|
|
32
|
+
for (const key of HANDLE_VIEW_PROP_KEYS)
|
|
33
|
+
if (props[key] !== undefined) forwarded[key] = props[key];
|
|
34
|
+
return forwarded as HandleViewProps;
|
|
35
|
+
}
|
package/src/contracts.ts
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CellPosition,
|
|
3
|
+
CellSpan,
|
|
4
|
+
ItemPlacement,
|
|
5
|
+
ValidationCode,
|
|
6
|
+
} from './types';
|
|
7
|
+
|
|
8
|
+
export type ItemId = string;
|
|
9
|
+
export type ZoneId = string;
|
|
10
|
+
|
|
11
|
+
/** Application data has no layout meaning and remains owned by the caller. */
|
|
12
|
+
export type DndItem<T> = { id: ItemId; data: T };
|
|
13
|
+
|
|
14
|
+
export type GridPlacement = {
|
|
15
|
+
itemId: ItemId;
|
|
16
|
+
position: CellPosition;
|
|
17
|
+
span: CellSpan;
|
|
18
|
+
placement?: ItemPlacement;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type GridLayoutZone = {
|
|
22
|
+
id: ZoneId;
|
|
23
|
+
kind: 'grid';
|
|
24
|
+
rows: number;
|
|
25
|
+
columns: number;
|
|
26
|
+
placements: GridPlacement[];
|
|
27
|
+
/**
|
|
28
|
+
* Ordered packing (a dock, for example): every placement has this span,
|
|
29
|
+
* the array order is the item order and positions are the reading-order
|
|
30
|
+
* slots of that order. A move inserts at the slot the target addresses and
|
|
31
|
+
* re-packs; removing an item closes the gap; capacity is rows × columns
|
|
32
|
+
* in slots. Omitted for spatial grids.
|
|
33
|
+
*/
|
|
34
|
+
itemSpan?: CellSpan;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/** A one-dimensional order, independent of item sizes and the drag axis. */
|
|
38
|
+
export type ListLayoutZone = {
|
|
39
|
+
id: ZoneId;
|
|
40
|
+
kind: 'list';
|
|
41
|
+
orientation: 'vertical' | 'horizontal';
|
|
42
|
+
itemIds: ItemId[];
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export type LayoutZone = GridLayoutZone | ListLayoutZone;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Every item is referenced by exactly one zone. Revisions are nonnegative safe
|
|
49
|
+
* integers; the owner increments the revision for each accepted/external change.
|
|
50
|
+
* Shared by the list adapter and the grid conversion helpers.
|
|
51
|
+
*/
|
|
52
|
+
export type LayoutState<T> = {
|
|
53
|
+
revision: number;
|
|
54
|
+
items: DndItem<T>[];
|
|
55
|
+
zones: LayoutZone[];
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export type GridLocation = {
|
|
59
|
+
kind: 'grid';
|
|
60
|
+
zoneId: ZoneId;
|
|
61
|
+
position: CellPosition;
|
|
62
|
+
};
|
|
63
|
+
export type ListLocation = { kind: 'list'; zoneId: ZoneId; index: number };
|
|
64
|
+
export type LayoutLocation = GridLocation | ListLocation;
|
|
65
|
+
|
|
66
|
+
/** The completed candidate is approved atomically against baseRevision. */
|
|
67
|
+
export type MoveProposal<State, Location = LayoutLocation> = {
|
|
68
|
+
sessionId: string;
|
|
69
|
+
itemId: ItemId;
|
|
70
|
+
baseRevision: number;
|
|
71
|
+
from: Location;
|
|
72
|
+
to: Location;
|
|
73
|
+
value: State;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/** A response applies only to the matching session AND base revision. */
|
|
77
|
+
export type ProposalResponse = {
|
|
78
|
+
sessionId: string;
|
|
79
|
+
baseRevision: number;
|
|
80
|
+
accepted: boolean;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
export type LayoutValidationCode =
|
|
84
|
+
| ValidationCode
|
|
85
|
+
| 'invalid-revision'
|
|
86
|
+
| 'unknown-item'
|
|
87
|
+
| 'unplaced-item'
|
|
88
|
+
| 'duplicate-reference'
|
|
89
|
+
| 'invalid-orientation';
|
|
90
|
+
export type LayoutValidationIssue = {
|
|
91
|
+
code: LayoutValidationCode;
|
|
92
|
+
zoneId?: ZoneId;
|
|
93
|
+
itemId?: ItemId;
|
|
94
|
+
message: string;
|
|
95
|
+
};
|
|
96
|
+
export type LayoutValidationResult =
|
|
97
|
+
{ valid: true } | { valid: false; issues: LayoutValidationIssue[] };
|
|
98
|
+
|
|
99
|
+
export type LayoutConversionIssue = Omit<LayoutValidationIssue, 'code'> & {
|
|
100
|
+
code: LayoutValidationCode | 'unsupported-zone';
|
|
101
|
+
};
|
|
102
|
+
export type LayoutConversionResult<Value> =
|
|
103
|
+
| { valid: true; value: Value }
|
|
104
|
+
| { valid: false; issues: LayoutConversionIssue[] };
|
|
105
|
+
|
|
106
|
+
/** Logical pixels in the coordinate space named by the containing contract. */
|
|
107
|
+
export type LayoutPoint = { x: number; y: number };
|
|
108
|
+
export type LayoutRect = LayoutPoint & { width: number; height: number };
|
|
109
|
+
export type CoordinateSpace = 'screen' | 'content';
|
|
110
|
+
|
|
111
|
+
type MeasurementBase<Space extends CoordinateSpace> = {
|
|
112
|
+
space: Space;
|
|
113
|
+
rect: LayoutRect;
|
|
114
|
+
revision: number;
|
|
115
|
+
/** Monotonic milliseconds in the same clock domain as MeasurementFreshness.nowMs. */
|
|
116
|
+
timestampMs: number;
|
|
117
|
+
};
|
|
118
|
+
export type MeasuredRect<Space extends CoordinateSpace = CoordinateSpace> =
|
|
119
|
+
MeasurementBase<Space> & { source: 'measured' };
|
|
120
|
+
export type EstimatedRect<Space extends CoordinateSpace = CoordinateSpace> =
|
|
121
|
+
MeasurementBase<Space> & { source: 'estimated' };
|
|
122
|
+
export type RectMeasurement<Space extends CoordinateSpace = CoordinateSpace> =
|
|
123
|
+
MeasuredRect<Space> | EstimatedRect<Space>;
|
|
124
|
+
|
|
125
|
+
export type MeasurementFreshness = {
|
|
126
|
+
revision: number;
|
|
127
|
+
/** Supply both nowMs and maxAgeMs to impose an age limit. */
|
|
128
|
+
nowMs?: number;
|
|
129
|
+
maxAgeMs?: number;
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Viewport is the measured screen origin of content at scroll offset zero.
|
|
134
|
+
* Offset changes do not change layout revision. Negative offsets support bounce
|
|
135
|
+
* and insets. Padding is part of content coordinates, not another offset here.
|
|
136
|
+
*/
|
|
137
|
+
export type ZoneCoordinateContext = {
|
|
138
|
+
viewport: MeasuredRect<'screen'>;
|
|
139
|
+
scrollOffset: LayoutPoint;
|
|
140
|
+
};
|
|
141
|
+
export type CoordinateFailureReason =
|
|
142
|
+
| 'invalid-coordinate'
|
|
143
|
+
| 'invalid-context'
|
|
144
|
+
| 'invalid-measurement'
|
|
145
|
+
| 'invalid-freshness'
|
|
146
|
+
| 'stale-measurement';
|
|
147
|
+
export type CoordinateResult<Value> =
|
|
148
|
+
| { valid: true; value: Value }
|
|
149
|
+
| { valid: false; reason: CoordinateFailureReason };
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import type { MoveProposal, ProposalResponse } from '../contracts';
|
|
2
|
+
import { isRecord } from '../engine/validation';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Shared revisioned approval for both value shapes (`GridValue` and
|
|
6
|
+
* `LayoutState`). The adapter supplies the shape-specific checks; the order of
|
|
7
|
+
* checks, the stale rules and the response protocol are the same for both.
|
|
8
|
+
*/
|
|
9
|
+
export type ApprovalResult<Value, Issue> =
|
|
10
|
+
| { status: 'accepted' | 'rejected' | 'stale'; value: Value }
|
|
11
|
+
| { status: 'invalid'; value: Value; issues: Issue[] };
|
|
12
|
+
|
|
13
|
+
/** Legacy grid values may carry a response without a base revision. */
|
|
14
|
+
type Revisioned = {
|
|
15
|
+
revision: number;
|
|
16
|
+
proposalResponse?: {
|
|
17
|
+
sessionId: string;
|
|
18
|
+
baseRevision?: number;
|
|
19
|
+
accepted: boolean;
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export type ApprovalAdapter<Value extends Revisioned, Location, Issue> = {
|
|
24
|
+
/** Issues of the current value, or null when it is usable as a baseline. */
|
|
25
|
+
validateCurrent(current: Value): Issue[] | null;
|
|
26
|
+
/** Issues of the complete candidate, or null when it is valid. */
|
|
27
|
+
validateCandidate(candidate: Value): Issue[] | null;
|
|
28
|
+
issue(message: string): Issue;
|
|
29
|
+
isLocation(value: unknown): value is Location;
|
|
30
|
+
location(value: Value, itemId: string): Location | null;
|
|
31
|
+
sameLocation(first: Location, second: Location | null): boolean;
|
|
32
|
+
hasDestinationZone(value: Value, to: Location): boolean;
|
|
33
|
+
zonesPreserved(current: Value, candidate: Value): boolean;
|
|
34
|
+
itemsPreserved(current: Value, candidate: Value): boolean;
|
|
35
|
+
messages: { zones: string; items: string; destination: string };
|
|
36
|
+
/** Attach the response (and any retained caller objects) to the candidate. */
|
|
37
|
+
accept(current: Value, candidate: Value, response: ProposalResponse): Value;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/** A response only changes the exact revision on which the complete proposal was based. */
|
|
41
|
+
export function respondToProposal<Value extends Revisioned, Location, Issue>(
|
|
42
|
+
adapter: ApprovalAdapter<Value, Location, Issue>,
|
|
43
|
+
current: Value,
|
|
44
|
+
proposal: MoveProposal<Value, Location>,
|
|
45
|
+
accepted: boolean,
|
|
46
|
+
checkCurrent: boolean,
|
|
47
|
+
): ApprovalResult<Value, Issue> {
|
|
48
|
+
const invalid = (message: string): ApprovalResult<Value, Issue> => ({
|
|
49
|
+
status: 'invalid',
|
|
50
|
+
value: current,
|
|
51
|
+
issues: [adapter.issue(message)],
|
|
52
|
+
});
|
|
53
|
+
if (checkCurrent) {
|
|
54
|
+
const issues = adapter.validateCurrent(current);
|
|
55
|
+
if (issues) return { status: 'invalid', value: current, issues };
|
|
56
|
+
}
|
|
57
|
+
if (
|
|
58
|
+
!isRecord(proposal) ||
|
|
59
|
+
typeof proposal.sessionId !== 'string' ||
|
|
60
|
+
typeof proposal.itemId !== 'string' ||
|
|
61
|
+
!Number.isSafeInteger(proposal.baseRevision) ||
|
|
62
|
+
proposal.baseRevision < 0 ||
|
|
63
|
+
typeof accepted !== 'boolean'
|
|
64
|
+
)
|
|
65
|
+
return invalid(
|
|
66
|
+
'A proposal requires a session ID, item ID, base revision and boolean response.',
|
|
67
|
+
);
|
|
68
|
+
if (
|
|
69
|
+
proposal.baseRevision !== current.revision ||
|
|
70
|
+
(current.proposalResponse?.sessionId === proposal.sessionId &&
|
|
71
|
+
current.proposalResponse.baseRevision === proposal.baseRevision)
|
|
72
|
+
)
|
|
73
|
+
return { status: 'stale', value: current };
|
|
74
|
+
if (
|
|
75
|
+
!adapter.isLocation(proposal.from) ||
|
|
76
|
+
!adapter.isLocation(proposal.to) ||
|
|
77
|
+
!adapter.sameLocation(
|
|
78
|
+
proposal.from,
|
|
79
|
+
adapter.location(current, proposal.itemId),
|
|
80
|
+
) ||
|
|
81
|
+
!adapter.hasDestinationZone(current, proposal.to)
|
|
82
|
+
)
|
|
83
|
+
return invalid(
|
|
84
|
+
'Proposal locations must identify the current item and an existing destination zone.',
|
|
85
|
+
);
|
|
86
|
+
const response: ProposalResponse = {
|
|
87
|
+
sessionId: proposal.sessionId,
|
|
88
|
+
baseRevision: proposal.baseRevision,
|
|
89
|
+
accepted,
|
|
90
|
+
};
|
|
91
|
+
if (!accepted)
|
|
92
|
+
return {
|
|
93
|
+
status: 'rejected',
|
|
94
|
+
value: { ...current, proposalResponse: response },
|
|
95
|
+
};
|
|
96
|
+
const issues = adapter.validateCandidate(proposal.value);
|
|
97
|
+
if (issues) return { status: 'invalid', value: current, issues };
|
|
98
|
+
if (
|
|
99
|
+
!Number.isSafeInteger(proposal.value.revision) ||
|
|
100
|
+
proposal.value.revision !== current.revision + 1
|
|
101
|
+
)
|
|
102
|
+
return invalid(
|
|
103
|
+
'An accepted proposal must advance its base revision by one.',
|
|
104
|
+
);
|
|
105
|
+
if (!adapter.zonesPreserved(current, proposal.value))
|
|
106
|
+
return invalid(adapter.messages.zones);
|
|
107
|
+
if (!adapter.itemsPreserved(current, proposal.value))
|
|
108
|
+
return invalid(adapter.messages.items);
|
|
109
|
+
if (
|
|
110
|
+
!adapter.sameLocation(
|
|
111
|
+
proposal.to,
|
|
112
|
+
adapter.location(proposal.value, proposal.itemId),
|
|
113
|
+
)
|
|
114
|
+
)
|
|
115
|
+
return invalid(adapter.messages.destination);
|
|
116
|
+
return {
|
|
117
|
+
status: 'accepted',
|
|
118
|
+
value: adapter.accept(current, proposal.value, response),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export type ApprovalStore<Value, Proposal, Issue> = {
|
|
123
|
+
getSnapshot(): Value;
|
|
124
|
+
subscribe(listener: () => void): () => void;
|
|
125
|
+
respond(proposal: Proposal, accepted: boolean): ApprovalResult<Value, Issue>;
|
|
126
|
+
/** Publish a value produced outside the reducer, such as an external edit. */
|
|
127
|
+
publish(next: Value): void;
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Listeners, closed-session memory and publication shared by the grid and
|
|
132
|
+
* layout stores. Rejections leave the revision unchanged; every closed session
|
|
133
|
+
* is remembered until the next revision so a delayed response cannot reopen an
|
|
134
|
+
* earlier rejection.
|
|
135
|
+
*/
|
|
136
|
+
export function createApprovalStore<
|
|
137
|
+
Value extends { revision: number },
|
|
138
|
+
Proposal extends { sessionId: string; baseRevision: number },
|
|
139
|
+
Issue,
|
|
140
|
+
>(
|
|
141
|
+
initial: Value,
|
|
142
|
+
respond: (
|
|
143
|
+
current: Value,
|
|
144
|
+
proposal: Proposal,
|
|
145
|
+
accepted: boolean,
|
|
146
|
+
) => ApprovalResult<Value, Issue>,
|
|
147
|
+
): ApprovalStore<Value, Proposal, Issue> {
|
|
148
|
+
let value = initial;
|
|
149
|
+
const listeners = new Set<() => void>();
|
|
150
|
+
const closedSessions = new Set<string>();
|
|
151
|
+
const publish = (next: Value) => {
|
|
152
|
+
if (next === value) return;
|
|
153
|
+
if (next.revision !== value.revision) closedSessions.clear();
|
|
154
|
+
value = next;
|
|
155
|
+
for (const listener of listeners) listener();
|
|
156
|
+
};
|
|
157
|
+
return {
|
|
158
|
+
getSnapshot: () => value,
|
|
159
|
+
subscribe: listener => {
|
|
160
|
+
listeners.add(listener);
|
|
161
|
+
return () => {
|
|
162
|
+
listeners.delete(listener);
|
|
163
|
+
};
|
|
164
|
+
},
|
|
165
|
+
respond: (proposal, accepted) => {
|
|
166
|
+
if (
|
|
167
|
+
isRecord(proposal) &&
|
|
168
|
+
proposal.baseRevision === value.revision &&
|
|
169
|
+
closedSessions.has(proposal.sessionId)
|
|
170
|
+
)
|
|
171
|
+
return { status: 'stale', value };
|
|
172
|
+
const result = respond(value, proposal, accepted);
|
|
173
|
+
if (result.status === 'rejected') closedSessions.add(proposal.sessionId);
|
|
174
|
+
publish(result.value);
|
|
175
|
+
return result;
|
|
176
|
+
},
|
|
177
|
+
publish,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Snapshot ownership shared by both session coordinators: subscribers,
|
|
3
|
+
* batched notification, shallow patching with `Object.is` change detection
|
|
4
|
+
* and the response deadline timer. The subclasses own their snapshot shapes,
|
|
5
|
+
* candidate calculation and lifecycle events.
|
|
6
|
+
*/
|
|
7
|
+
export abstract class CoordinatorBase<Snapshot extends object> {
|
|
8
|
+
protected snapshot: Snapshot;
|
|
9
|
+
protected changed = false;
|
|
10
|
+
private listeners = new Set<() => void>();
|
|
11
|
+
private depth = 0;
|
|
12
|
+
private responseTimer: ReturnType<typeof setTimeout> | undefined;
|
|
13
|
+
|
|
14
|
+
protected constructor(initial: Snapshot) {
|
|
15
|
+
this.snapshot = initial;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
getSnapshot = (): Snapshot => this.snapshot;
|
|
19
|
+
|
|
20
|
+
subscribe = (listener: () => void): (() => void) => {
|
|
21
|
+
this.listeners.add(listener);
|
|
22
|
+
return () => {
|
|
23
|
+
this.listeners.delete(listener);
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/** Nested batches notify subscribers once, after the outermost one ends. */
|
|
28
|
+
protected batch<R>(work: () => R): R {
|
|
29
|
+
this.batchStarted();
|
|
30
|
+
this.depth++;
|
|
31
|
+
try {
|
|
32
|
+
return work();
|
|
33
|
+
} finally {
|
|
34
|
+
this.depth--;
|
|
35
|
+
if (this.depth === 0) {
|
|
36
|
+
try {
|
|
37
|
+
if (this.changed) {
|
|
38
|
+
this.changed = false;
|
|
39
|
+
for (const listener of this.listeners) listener();
|
|
40
|
+
}
|
|
41
|
+
} finally {
|
|
42
|
+
this.batchFinished();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
protected batchStarted(): void {}
|
|
49
|
+
|
|
50
|
+
/** Runs after subscribers were notified, even when one of them threw. */
|
|
51
|
+
protected batchFinished(): void {}
|
|
52
|
+
|
|
53
|
+
protected get batchDepth(): number {
|
|
54
|
+
return this.depth;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
protected update(patch: Partial<Snapshot>): void {
|
|
58
|
+
if (
|
|
59
|
+
!Object.keys(patch).some(
|
|
60
|
+
key =>
|
|
61
|
+
!Object.is(
|
|
62
|
+
this.snapshot[key as keyof Snapshot],
|
|
63
|
+
patch[key as keyof Snapshot],
|
|
64
|
+
),
|
|
65
|
+
)
|
|
66
|
+
)
|
|
67
|
+
return;
|
|
68
|
+
this.snapshot = { ...this.snapshot, ...patch };
|
|
69
|
+
this.changed = true;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Put back an earlier snapshot and its pending-notification flag. */
|
|
73
|
+
protected restore(snapshot: Snapshot, changed: boolean): void {
|
|
74
|
+
this.snapshot = snapshot;
|
|
75
|
+
this.changed = changed;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Wait for a proposal response until `deadline`. The timer re-arms while the
|
|
80
|
+
* clock has not reached the deadline (fake timers, long sleeps) and calls
|
|
81
|
+
* `onExpire` inside a batch once it has, unless `isPending` says the
|
|
82
|
+
* proposal was already settled.
|
|
83
|
+
*/
|
|
84
|
+
protected armDeadline(
|
|
85
|
+
deadline: number | undefined,
|
|
86
|
+
isPending: () => boolean,
|
|
87
|
+
onExpire: () => void,
|
|
88
|
+
): void {
|
|
89
|
+
const remaining = Math.max(0, (deadline ?? now()) - now());
|
|
90
|
+
this.responseTimer = setTimeout(
|
|
91
|
+
() =>
|
|
92
|
+
this.batch(() => {
|
|
93
|
+
this.responseTimer = undefined;
|
|
94
|
+
if (!isPending()) return;
|
|
95
|
+
if (deadline !== undefined && now() < deadline)
|
|
96
|
+
this.armDeadline(deadline, isPending, onExpire);
|
|
97
|
+
else onExpire();
|
|
98
|
+
}),
|
|
99
|
+
Math.min(remaining, 2147483647),
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
protected clearDeadline(): void {
|
|
104
|
+
clearTimeout(this.responseTimer);
|
|
105
|
+
this.responseTimer = undefined;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** RN provides this monotonic clock; its generated ambient declarations omit it. */
|
|
110
|
+
export function now(): number {
|
|
111
|
+
return (
|
|
112
|
+
globalThis as typeof globalThis & { performance: { now(): number } }
|
|
113
|
+
).performance.now();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function reportCallbackError(scope: string, error: unknown): void {
|
|
117
|
+
console.error(`[${scope}] Callback failed`, error);
|
|
118
|
+
}
|