@gridui/react-utils 0.0.0-stage → 1.0.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/README.md CHANGED
@@ -1,3 +1,49 @@
1
- # Temporary Holding Version
1
+ # React utils
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ `@gridui/react-utils` owns React interaction lifecycle hooks shared by independent
4
+ component families. It depends on React and the React-free `@gridui/interaction`
5
+ helpers. It has no stylesheet or Theme dependency.
6
+
7
+ ## Pointer drag
8
+
9
+ `usePointerDrag<Data>(onCancel)` returns a stable `PointerDrag<Data>` controller.
10
+ Each hook instance owns at most one session. Call its methods from events or
11
+ effects; session changes do not trigger renders. Keep visible state in the owner.
12
+
13
+ - `start(event, data)` accepts a primary, left-button pointer when no session is
14
+ active. It captures that pointer on `event.currentTarget`, records caller data,
15
+ and subscribes to Escape and blur on the element's owning window. It returns
16
+ whether it started; a native capture error propagates without creating a session.
17
+ - `get(pointerId?)` returns the active caller data, or `null` for an absent or
18
+ different pointer. Treat the data as the drag's starting snapshot.
19
+ - `finish(pointerId)` releases resources and returns that pointer's data without
20
+ invoking `onCancel`. The owner decides what to commit.
21
+ - `cancel(pointerId?)` releases resources and invokes the latest committed
22
+ `onCancel(data)` once. Omitting the ID cancels whichever session is active.
23
+
24
+ Wire `pointercancel` and `lostpointercapture` to `cancel(event.pointerId)`.
25
+ Escape, window blur, and effect disconnection cancel automatically, including
26
+ React Activity hiding retained content. Cleanup clears ownership before releasing
27
+ capture and notifying the owner, so capture-loss events cannot cancel twice.
28
+ The next drag can start after effects reconnect. IME-owned and already handled
29
+ Escape events are ignored; active Escape is claimed before modal dismissal.
30
+
31
+ The owner handles focus, preventDefault, touch-action styles, geometry, bounds,
32
+ controlled state, preview, commit, and rollback. `Resizable` rolls back continuous
33
+ size notifications on cancel; `BottomSheet` discards its preview and commits snap
34
+ points only after `finish`. There is no shared cache, persistence, or product state.
35
+
36
+ ```tsx
37
+ import { usePointerDrag } from "@gridui/react-utils";
38
+
39
+ // Inside a component, where preview is caller-owned React state:
40
+ const drag = usePointerDrag<{ startY: number }>(() => setPreview(null));
41
+ // Pointer down: drag.start(event, { startY: event.clientY });
42
+ // Pointer move: const start = drag.get(event.pointerId);
43
+ // Pointer up: if (drag.finish(event.pointerId)) commitPreview();
44
+ // Pointer cancel / lost capture: drag.cancel(event.pointerId);
45
+ ```
46
+
47
+ ## Release history
48
+
49
+ Package-owned records live in [.changeset/history](https://github.com/sudo-ui/GridUI-React-Monorepo/tree/main/packages-core/react-utils/.changeset/history).
@@ -0,0 +1,8 @@
1
+ /**
2
+ * @author WMXPY
3
+ * @package ReactUtils
4
+ * @namespace Root
5
+ * @description Index
6
+ */
7
+ export type { PointerDrag, PointerDragStart } from "./pointer-drag";
8
+ export { usePointerDrag } from "./use-pointer-drag";
package/dist/index.js ADDED
@@ -0,0 +1,73 @@
1
+ // src/pointer-drag.ts
2
+ import { listenForDragCancellation } from "@gridui/interaction";
3
+ var createPointerDrag = (onCancel) => {
4
+ let active = null;
5
+ const get = (pointerId) => {
6
+ return active && (pointerId === void 0 || active.pointerId === pointerId) ? active.data : null;
7
+ };
8
+ const detach = (pointerId) => {
9
+ if (!active || pointerId !== void 0 && active.pointerId !== pointerId) {
10
+ return null;
11
+ }
12
+ const session = active;
13
+ active = null;
14
+ session.cleanup();
15
+ if (session.element.hasPointerCapture(session.pointerId)) {
16
+ session.element.releasePointerCapture(session.pointerId);
17
+ }
18
+ return session.data;
19
+ };
20
+ const cancel = (pointerId) => {
21
+ const data = detach(pointerId);
22
+ if (data) {
23
+ onCancel(data);
24
+ }
25
+ };
26
+ return {
27
+ get,
28
+ cancel,
29
+ finish: (pointerId) => {
30
+ return detach(pointerId);
31
+ },
32
+ start: (event, data) => {
33
+ if (active || event.button !== 0 || !event.isPrimary) {
34
+ return false;
35
+ }
36
+ const element = event.currentTarget;
37
+ element.setPointerCapture(event.pointerId);
38
+ active = {
39
+ pointerId: event.pointerId,
40
+ element,
41
+ data,
42
+ cleanup: listenForDragCancellation(element, () => {
43
+ cancel();
44
+ })
45
+ };
46
+ return true;
47
+ }
48
+ };
49
+ };
50
+
51
+ // src/use-pointer-drag.ts
52
+ import { useEffect, useLayoutEffect, useRef, useState } from "react";
53
+ var usePointerDrag = (onCancel) => {
54
+ const cancelRef = useRef(onCancel);
55
+ useLayoutEffect(() => {
56
+ cancelRef.current = onCancel;
57
+ });
58
+ const [drag] = useState(() => {
59
+ return createPointerDrag((data) => {
60
+ cancelRef.current(data);
61
+ });
62
+ });
63
+ useEffect(() => {
64
+ return () => {
65
+ drag.cancel();
66
+ };
67
+ }, [drag]);
68
+ return drag;
69
+ };
70
+ export {
71
+ usePointerDrag
72
+ };
73
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/pointer-drag.ts", "../src/use-pointer-drag.ts"],
4
+ "sourcesContent": ["/**\n * @author WMXPY\n * @package ReactUtils\n * @namespace Root\n * @description Pointer Drag\n */\n\nimport { listenForDragCancellation } from \"@gridui/interaction\";\nimport type { PointerEvent } from \"react\";\n\nexport type PointerDragStart = Pick<PointerEvent<HTMLElement>, \"button\" | \"isPrimary\" | \"pointerId\" | \"currentTarget\">;\n\nexport type PointerDrag<Data extends object> = {\n readonly start: (event: PointerDragStart, data: Data) => boolean;\n readonly get: (pointerId?: number) => Data | null;\n readonly finish: (pointerId: number) => Data | null;\n readonly cancel: (pointerId?: number) => void;\n};\n\ntype Session<Data> = {\n readonly pointerId: number;\n readonly element: HTMLElement;\n readonly data: Data;\n readonly cleanup: () => void;\n};\n\nexport const createPointerDrag = <Data extends object>(onCancel: (data: Data) => void): PointerDrag<Data> => {\n let active: Session<Data> | null = null;\n const get = (pointerId?: number) => {\n return active && (pointerId === undefined || active.pointerId === pointerId) ? active.data : null;\n };\n const detach = (pointerId?: number) => {\n if (!active || (pointerId !== undefined && active.pointerId !== pointerId)) {\n return null;\n }\n const session = active;\n active = null;\n session.cleanup();\n if (session.element.hasPointerCapture(session.pointerId)) {\n session.element.releasePointerCapture(session.pointerId);\n }\n return session.data;\n };\n const cancel = (pointerId?: number) => {\n const data = detach(pointerId);\n if (data) {\n onCancel(data);\n }\n };\n return {\n get,\n cancel,\n finish: (pointerId) => {\n return detach(pointerId);\n },\n start: (event, data) => {\n if (active || event.button !== 0 || !event.isPrimary) {\n return false;\n }\n const element = event.currentTarget;\n element.setPointerCapture(event.pointerId);\n active = {\n pointerId: event.pointerId,\n element,\n data,\n cleanup: listenForDragCancellation(element, () => {\n cancel();\n }),\n };\n return true;\n },\n };\n};\n", "/**\n * @author WMXPY\n * @package ReactUtils\n * @namespace Root\n * @description Use Pointer Drag\n */\n\nimport { createPointerDrag, type PointerDrag } from \"./pointer-drag\";\nimport { useEffect, useLayoutEffect, useRef, useState } from \"react\";\n\n/** Own one pointer session; the caller owns geometry, rendering, commit and rollback. */\nexport const usePointerDrag = <Data extends object>(onCancel: (data: Data) => void): PointerDrag<Data> => {\n const cancelRef = useRef(onCancel);\n useLayoutEffect(() => {\n cancelRef.current = onCancel;\n });\n const [drag] = useState(() => {\n return createPointerDrag<Data>((data) => {\n cancelRef.current(data);\n });\n });\n useEffect(() => {\n return () => {\n drag.cancel();\n };\n }, [drag]);\n return drag;\n};\n"],
5
+ "mappings": ";AAOA,SAAS,iCAAiC;AAmBnC,IAAM,oBAAoB,CAAsB,aAAsD;AACzG,MAAI,SAA+B;AACnC,QAAM,MAAM,CAAC,cAAuB;AAChC,WAAO,WAAW,cAAc,UAAa,OAAO,cAAc,aAAa,OAAO,OAAO;AAAA,EACjG;AACA,QAAM,SAAS,CAAC,cAAuB;AACnC,QAAI,CAAC,UAAW,cAAc,UAAa,OAAO,cAAc,WAAY;AACxE,aAAO;AAAA,IACX;AACA,UAAM,UAAU;AAChB,aAAS;AACT,YAAQ,QAAQ;AAChB,QAAI,QAAQ,QAAQ,kBAAkB,QAAQ,SAAS,GAAG;AACtD,cAAQ,QAAQ,sBAAsB,QAAQ,SAAS;AAAA,IAC3D;AACA,WAAO,QAAQ;AAAA,EACnB;AACA,QAAM,SAAS,CAAC,cAAuB;AACnC,UAAM,OAAO,OAAO,SAAS;AAC7B,QAAI,MAAM;AACN,eAAS,IAAI;AAAA,IACjB;AAAA,EACJ;AACA,SAAO;AAAA,IACH;AAAA,IACA;AAAA,IACA,QAAQ,CAAC,cAAc;AACnB,aAAO,OAAO,SAAS;AAAA,IAC3B;AAAA,IACA,OAAO,CAAC,OAAO,SAAS;AACpB,UAAI,UAAU,MAAM,WAAW,KAAK,CAAC,MAAM,WAAW;AAClD,eAAO;AAAA,MACX;AACA,YAAM,UAAU,MAAM;AACtB,cAAQ,kBAAkB,MAAM,SAAS;AACzC,eAAS;AAAA,QACL,WAAW,MAAM;AAAA,QACjB;AAAA,QACA;AAAA,QACA,SAAS,0BAA0B,SAAS,MAAM;AAC9C,iBAAO;AAAA,QACX,CAAC;AAAA,MACL;AACA,aAAO;AAAA,IACX;AAAA,EACJ;AACJ;;;AChEA,SAAS,WAAW,iBAAiB,QAAQ,gBAAgB;AAGtD,IAAM,iBAAiB,CAAsB,aAAsD;AACtG,QAAM,YAAY,OAAO,QAAQ;AACjC,kBAAgB,MAAM;AAClB,cAAU,UAAU;AAAA,EACxB,CAAC;AACD,QAAM,CAAC,IAAI,IAAI,SAAS,MAAM;AAC1B,WAAO,kBAAwB,CAAC,SAAS;AACrC,gBAAU,QAAQ,IAAI;AAAA,IAC1B,CAAC;AAAA,EACL,CAAC;AACD,YAAU,MAAM;AACZ,WAAO,MAAM;AACT,WAAK,OAAO;AAAA,IAChB;AAAA,EACJ,GAAG,CAAC,IAAI,CAAC;AACT,SAAO;AACX;",
6
+ "names": []
7
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * @author WMXPY
3
+ * @package ReactUtils
4
+ * @namespace Root
5
+ * @description Pointer Drag
6
+ */
7
+ import type { PointerEvent } from "react";
8
+ export type PointerDragStart = Pick<PointerEvent<HTMLElement>, "button" | "isPrimary" | "pointerId" | "currentTarget">;
9
+ export type PointerDrag<Data extends object> = {
10
+ readonly start: (event: PointerDragStart, data: Data) => boolean;
11
+ readonly get: (pointerId?: number) => Data | null;
12
+ readonly finish: (pointerId: number) => Data | null;
13
+ readonly cancel: (pointerId?: number) => void;
14
+ };
15
+ export declare const createPointerDrag: <Data extends object>(onCancel: (data: Data) => void) => PointerDrag<Data>;
@@ -0,0 +1,9 @@
1
+ /**
2
+ * @author WMXPY
3
+ * @package ReactUtils
4
+ * @namespace Root
5
+ * @description Use Pointer Drag
6
+ */
7
+ import { type PointerDrag } from "./pointer-drag";
8
+ /** Own one pointer session; the caller owns geometry, rendering, commit and rollback. */
9
+ export declare const usePointerDrag: <Data extends object>(onCancel: (data: Data) => void) => PointerDrag<Data>;
@@ -0,0 +1,33 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "name": "@gridui/react-utils",
4
+ "version": "1.0.0",
5
+ "category": "core",
6
+ "description": "Shared React interaction lifecycle hooks",
7
+ "usage": "`@gridui/react-utils` owns React interaction lifecycle hooks shared by independent component families. It depends on React and the React-free `@gridui/interaction` helpers. It has no stylesheet or Theme dependency.",
8
+ "documentation": [
9
+ "README.md"
10
+ ],
11
+ "exports": [
12
+ {
13
+ "name": "PointerDrag",
14
+ "kind": "type",
15
+ "importPath": "@gridui/react-utils"
16
+ },
17
+ {
18
+ "name": "PointerDragStart",
19
+ "kind": "type",
20
+ "importPath": "@gridui/react-utils"
21
+ },
22
+ {
23
+ "name": "usePointerDrag",
24
+ "kind": "function",
25
+ "importPath": "@gridui/react-utils"
26
+ }
27
+ ],
28
+ "styles": [],
29
+ "examples": [
30
+ "https://github.com/sudo-ui/GridUI-React-Monorepo/blob/main/packages-core/react-utils/stories/getting-started.mdx",
31
+ "https://github.com/sudo-ui/GridUI-React-Monorepo/blob/main/packages-core/react-utils/stories/pointer-drag-lifecycle.mdx"
32
+ ]
33
+ }
package/package.json CHANGED
@@ -1,6 +1,46 @@
1
1
  {
2
2
  "name": "@gridui/react-utils",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "1.0.0",
4
+ "description": "Shared React interaction lifecycle hooks",
5
+ "license": "UNLICENSED",
6
+ "type": "module",
7
+ "files": [
8
+ "dist",
9
+ "README.md",
10
+ "docs"
11
+ ],
12
+ "sideEffects": false,
13
+ "exports": {
14
+ ".": {
15
+ "types": "./dist/index.d.ts",
16
+ "import": "./dist/index.js"
17
+ }
18
+ },
19
+ "publishConfig": {
20
+ "access": "public"
21
+ },
22
+ "dependencies": {
23
+ "@gridui/interaction": "^1.2.1"
24
+ },
25
+ "devDependencies": {
26
+ "@gridui-internal/configuration": "0.0.0-internal",
27
+ "@jest/globals": "30.5.2",
28
+ "@types/jest": "30.0.0",
29
+ "@types/node": "26.6.6",
30
+ "@types/react": "19.3.0",
31
+ "@types/react-dom": "19.3.0",
32
+ "jest": "30.5.2",
33
+ "react": "19.3.1",
34
+ "react-dom": "19.3.1",
35
+ "typescript": "6.0.3"
36
+ },
37
+ "peerDependencies": {
38
+ "react": "^19.2.0"
39
+ },
40
+ "scripts": {
41
+ "build": "node ../../scripts/compilation/package.ts",
42
+ "compile": "tsc --project tsconfig.json",
43
+ "test": "jest",
44
+ "coverage": "jest --coverage"
45
+ }
6
46
  }