@object-ui/collaboration 2.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.
@@ -0,0 +1,100 @@
1
+ /**
2
+ * ObjectUI
3
+ * Copyright (c) 2024-present ObjectStack Inc.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+ import React, { useMemo } from 'react';
9
+ const cursorStyles = {
10
+ container: {
11
+ position: 'absolute',
12
+ inset: 0,
13
+ pointerEvents: 'none',
14
+ overflow: 'hidden',
15
+ zIndex: 9999,
16
+ },
17
+ cursor: {
18
+ position: 'absolute',
19
+ pointerEvents: 'none',
20
+ transition: 'transform 120ms ease-out, opacity 300ms ease',
21
+ willChange: 'transform, opacity',
22
+ },
23
+ label: {
24
+ position: 'absolute',
25
+ left: '16px',
26
+ top: '16px',
27
+ fontSize: '11px',
28
+ fontWeight: 500,
29
+ fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
30
+ color: '#fff',
31
+ padding: '1px 6px',
32
+ borderRadius: '3px',
33
+ whiteSpace: 'nowrap',
34
+ lineHeight: '18px',
35
+ },
36
+ avatar: {
37
+ position: 'absolute',
38
+ left: '18px',
39
+ top: '-4px',
40
+ width: '18px',
41
+ height: '18px',
42
+ borderRadius: '50%',
43
+ border: '2px solid #fff',
44
+ objectFit: 'cover',
45
+ },
46
+ };
47
+ function CursorSvg({ color, size }) {
48
+ return React.createElement('svg', {
49
+ width: size,
50
+ height: size,
51
+ viewBox: '0 0 24 24',
52
+ fill: 'none',
53
+ xmlns: 'http://www.w3.org/2000/svg',
54
+ style: { display: 'block' },
55
+ }, React.createElement('path', {
56
+ d: 'M5.65376 12.3673H5.46026L5.31717 12.4976L0.500002 16.8829L0.500002 1.19841L11.7841 12.3673H5.65376Z',
57
+ fill: color,
58
+ stroke: '#fff',
59
+ strokeWidth: '1',
60
+ }));
61
+ }
62
+ /**
63
+ * Live cursors component displaying other users' cursor positions.
64
+ *
65
+ * Renders absolutely-positioned cursor SVGs with smooth CSS transitions,
66
+ * user name labels, and fade-out for idle users.
67
+ */
68
+ export function LiveCursors({ users, containerRef: _containerRef, showNames = true, showAvatars = false, cursorSize = 20, fadeIdle = true, className, }) {
69
+ const visibleUsers = useMemo(() => users.filter(u => u.cursor), [users]);
70
+ return React.createElement('div', {
71
+ style: cursorStyles.container,
72
+ className,
73
+ 'aria-hidden': 'true',
74
+ }, visibleUsers.map(user => {
75
+ const { cursor } = user;
76
+ if (!cursor)
77
+ return null;
78
+ const opacity = fadeIdle
79
+ ? user.status === 'active' ? 1 : user.status === 'idle' ? 0.5 : 0.2
80
+ : 1;
81
+ return React.createElement('div', {
82
+ key: user.userId,
83
+ style: {
84
+ ...cursorStyles.cursor,
85
+ transform: `translate(${cursor.x}px, ${cursor.y}px)`,
86
+ opacity,
87
+ },
88
+ 'data-user-id': user.userId,
89
+ }, React.createElement(CursorSvg, { color: user.color, size: cursorSize }), showAvatars && user.avatar && React.createElement('img', {
90
+ src: user.avatar,
91
+ alt: user.userName,
92
+ style: cursorStyles.avatar,
93
+ }), showNames && React.createElement('span', {
94
+ style: {
95
+ ...cursorStyles.label,
96
+ backgroundColor: user.color,
97
+ },
98
+ }, user.userName));
99
+ }));
100
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * ObjectUI
3
+ * Copyright (c) 2024-present ObjectStack Inc.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+ import React from 'react';
9
+ import type { PresenceUser } from './usePresence';
10
+ export interface PresenceAvatarsProps {
11
+ /** Present users */
12
+ users: PresenceUser[];
13
+ /** Max avatars to show before "+N" */
14
+ maxVisible?: number;
15
+ /** Avatar size */
16
+ size?: 'sm' | 'md' | 'lg';
17
+ /** Show status indicators */
18
+ showStatus?: boolean;
19
+ /** Additional className */
20
+ className?: string;
21
+ }
22
+ /**
23
+ * Avatar stack component showing active users with overflow indicator.
24
+ *
25
+ * Displays user avatars (or initials) in an overlapping stack,
26
+ * with optional status indicators and a "+N" overflow badge.
27
+ */
28
+ export declare function PresenceAvatars({ users, maxVisible, size, showStatus, className, }: PresenceAvatarsProps): React.ReactElement;
29
+ //# sourceMappingURL=PresenceAvatars.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PresenceAvatars.d.ts","sourceRoot":"","sources":["../src/PresenceAvatars.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAkB,MAAM,OAAO,CAAC;AACvC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAElD,MAAM,WAAW,oBAAoB;IACnC,oBAAoB;IACpB,KAAK,EAAE,YAAY,EAAE,CAAC;IACtB,sCAAsC;IACtC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,kBAAkB;IAClB,IAAI,CAAC,EAAE,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;IAC1B,6BAA6B;IAC7B,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,2BAA2B;IAC3B,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAuBD;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,EAC9B,KAAK,EACL,UAAc,EACd,IAAW,EACX,UAAiB,EACjB,SAAS,GACV,EAAE,oBAAoB,GAAG,KAAK,CAAC,YAAY,CA6F3C"}
@@ -0,0 +1,113 @@
1
+ /**
2
+ * ObjectUI
3
+ * Copyright (c) 2024-present ObjectStack Inc.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+ import React, { useMemo } from 'react';
9
+ const sizeMap = {
10
+ sm: 24,
11
+ md: 32,
12
+ lg: 40,
13
+ };
14
+ const statusColors = {
15
+ active: '#22c55e',
16
+ idle: '#f59e0b',
17
+ away: '#94a3b8',
18
+ };
19
+ function getInitials(name) {
20
+ return name
21
+ .split(' ')
22
+ .map(part => part[0])
23
+ .join('')
24
+ .toUpperCase()
25
+ .slice(0, 2);
26
+ }
27
+ /**
28
+ * Avatar stack component showing active users with overflow indicator.
29
+ *
30
+ * Displays user avatars (or initials) in an overlapping stack,
31
+ * with optional status indicators and a "+N" overflow badge.
32
+ */
33
+ export function PresenceAvatars({ users, maxVisible = 5, size = 'md', showStatus = true, className, }) {
34
+ const px = sizeMap[size];
35
+ const overlapOffset = Math.round(px * 0.3);
36
+ const fontSize = Math.round(px * 0.35);
37
+ const statusDotSize = Math.max(8, Math.round(px * 0.28));
38
+ const visible = useMemo(() => users.slice(0, maxVisible), [users, maxVisible]);
39
+ const overflowCount = Math.max(0, users.length - maxVisible);
40
+ const containerStyle = {
41
+ display: 'inline-flex',
42
+ alignItems: 'center',
43
+ flexDirection: 'row-reverse',
44
+ fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
45
+ };
46
+ const avatarBaseStyle = {
47
+ width: `${px}px`,
48
+ height: `${px}px`,
49
+ borderRadius: '50%',
50
+ border: '2px solid #fff',
51
+ display: 'flex',
52
+ alignItems: 'center',
53
+ justifyContent: 'center',
54
+ fontSize: `${fontSize}px`,
55
+ fontWeight: 600,
56
+ color: '#fff',
57
+ position: 'relative',
58
+ flexShrink: 0,
59
+ overflow: 'hidden',
60
+ boxSizing: 'border-box',
61
+ };
62
+ const overflowStyle = {
63
+ ...avatarBaseStyle,
64
+ backgroundColor: '#e2e8f0',
65
+ color: '#475569',
66
+ marginLeft: `-${overlapOffset}px`,
67
+ };
68
+ const statusDotStyle = {
69
+ position: 'absolute',
70
+ bottom: '-1px',
71
+ right: '-1px',
72
+ width: `${statusDotSize}px`,
73
+ height: `${statusDotSize}px`,
74
+ borderRadius: '50%',
75
+ border: '2px solid #fff',
76
+ boxSizing: 'border-box',
77
+ };
78
+ // Render in reverse order so the first user appears on top (z-index via DOM order with row-reverse)
79
+ const reversedVisible = useMemo(() => [...visible].reverse(), [visible]);
80
+ return React.createElement('div', {
81
+ style: containerStyle,
82
+ className,
83
+ role: 'group',
84
+ 'aria-label': `${users.length} user${users.length !== 1 ? 's' : ''} present`,
85
+ },
86
+ // Overflow badge (rendered first because of row-reverse)
87
+ overflowCount > 0 && React.createElement('div', {
88
+ key: 'overflow',
89
+ style: overflowStyle,
90
+ title: `${overflowCount} more user${overflowCount !== 1 ? 's' : ''}`,
91
+ }, `+${overflowCount}`),
92
+ // Avatars
93
+ reversedVisible.map((user, idx) => React.createElement('div', {
94
+ key: user.userId,
95
+ style: {
96
+ ...avatarBaseStyle,
97
+ backgroundColor: user.color,
98
+ marginLeft: idx > 0 || overflowCount > 0 ? `-${overlapOffset}px` : '0',
99
+ },
100
+ title: `${user.userName} (${user.status})`,
101
+ }, user.avatar
102
+ ? React.createElement('img', {
103
+ src: user.avatar,
104
+ alt: user.userName,
105
+ style: { width: '100%', height: '100%', objectFit: 'cover' },
106
+ })
107
+ : getInitials(user.userName), showStatus && React.createElement('span', {
108
+ style: {
109
+ ...statusDotStyle,
110
+ backgroundColor: statusColors[user.status],
111
+ },
112
+ }))));
113
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * ObjectUI
3
+ * Copyright (c) 2024-present ObjectStack Inc.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+ /**
9
+ * @object-ui/collaboration
10
+ *
11
+ * Real-time collaboration for Object UI providing:
12
+ * - useRealtimeSubscription hook for WebSocket data subscriptions
13
+ * - usePresence hook for live cursor and presence tracking
14
+ * - useConflictResolution hook for version history and conflict management
15
+ * - CommentThread component for threaded comments with @mentions
16
+ * - LiveCursors component for displaying remote user cursors
17
+ * - PresenceAvatars component for showing active user avatars
18
+ *
19
+ * @packageDocumentation
20
+ */
21
+ export { useRealtimeSubscription, type RealtimeConfig, type ConnectionState, type RealtimeMessage, type RealtimeResult, } from './useRealtimeSubscription';
22
+ export { usePresence, createPresenceUpdater, type PresenceUser, type PresenceConfig, type PresenceResult, } from './usePresence';
23
+ export { useConflictResolution, type VersionEntry, type ConflictInfo, type ConflictResolutionResult, } from './useConflictResolution';
24
+ export { CommentThread, type Comment, type CommentThreadProps, } from './CommentThread';
25
+ export { LiveCursors, type LiveCursorsProps, } from './LiveCursors';
26
+ export { PresenceAvatars, type PresenceAvatarsProps, } from './PresenceAvatars';
27
+ export type { CollaborationPresence, CollaborationOperation, CollaborationConfig, } from '@object-ui/types';
28
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH;;;;;;;;;;;;GAYG;AAEH,OAAO,EACL,uBAAuB,EACvB,KAAK,cAAc,EACnB,KAAK,eAAe,EACpB,KAAK,eAAe,EACpB,KAAK,cAAc,GACpB,MAAM,2BAA2B,CAAC;AAEnC,OAAO,EACL,WAAW,EACX,qBAAqB,EACrB,KAAK,YAAY,EACjB,KAAK,cAAc,EACnB,KAAK,cAAc,GACpB,MAAM,eAAe,CAAC;AAEvB,OAAO,EACL,qBAAqB,EACrB,KAAK,YAAY,EACjB,KAAK,YAAY,EACjB,KAAK,wBAAwB,GAC9B,MAAM,yBAAyB,CAAC;AAEjC,OAAO,EACL,aAAa,EACb,KAAK,OAAO,EACZ,KAAK,kBAAkB,GACxB,MAAM,iBAAiB,CAAC;AAEzB,OAAO,EACL,WAAW,EACX,KAAK,gBAAgB,GACtB,MAAM,eAAe,CAAC;AAEvB,OAAO,EACL,eAAe,EACf,KAAK,oBAAoB,GAC1B,MAAM,mBAAmB,CAAC;AAG3B,YAAY,EACV,qBAAqB,EACrB,sBAAsB,EACtB,mBAAmB,GACpB,MAAM,kBAAkB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,26 @@
1
+ /**
2
+ * ObjectUI
3
+ * Copyright (c) 2024-present ObjectStack Inc.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+ /**
9
+ * @object-ui/collaboration
10
+ *
11
+ * Real-time collaboration for Object UI providing:
12
+ * - useRealtimeSubscription hook for WebSocket data subscriptions
13
+ * - usePresence hook for live cursor and presence tracking
14
+ * - useConflictResolution hook for version history and conflict management
15
+ * - CommentThread component for threaded comments with @mentions
16
+ * - LiveCursors component for displaying remote user cursors
17
+ * - PresenceAvatars component for showing active user avatars
18
+ *
19
+ * @packageDocumentation
20
+ */
21
+ export { useRealtimeSubscription, } from './useRealtimeSubscription';
22
+ export { usePresence, createPresenceUpdater, } from './usePresence';
23
+ export { useConflictResolution, } from './useConflictResolution';
24
+ export { CommentThread, } from './CommentThread';
25
+ export { LiveCursors, } from './LiveCursors';
26
+ export { PresenceAvatars, } from './PresenceAvatars';
@@ -0,0 +1,72 @@
1
+ /**
2
+ * ObjectUI
3
+ * Copyright (c) 2024-present ObjectStack Inc.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+ import type { CollaborationConfig } from '@object-ui/types';
9
+ export interface VersionEntry {
10
+ id: string;
11
+ version: number;
12
+ timestamp: string;
13
+ userId: string;
14
+ userName?: string;
15
+ changes: Record<string, {
16
+ before: unknown;
17
+ after: unknown;
18
+ }>;
19
+ message?: string;
20
+ }
21
+ export interface ConflictInfo {
22
+ id: string;
23
+ field: string;
24
+ localValue: unknown;
25
+ remoteValue: unknown;
26
+ baseValue: unknown;
27
+ localTimestamp: string;
28
+ remoteTimestamp: string;
29
+ remoteUserId: string;
30
+ }
31
+ export interface ConflictResolutionResult {
32
+ /** Version history entries */
33
+ versions: VersionEntry[];
34
+ /** Current version number */
35
+ currentVersion: number;
36
+ /** Pending conflicts that need resolution */
37
+ conflicts: ConflictInfo[];
38
+ /** Whether there are unresolved conflicts */
39
+ hasConflicts: boolean;
40
+ /** Record a new version */
41
+ recordVersion: (changes: Record<string, {
42
+ before: unknown;
43
+ after: unknown;
44
+ }>, message?: string) => void;
45
+ /** Resolve a conflict */
46
+ resolveConflict: (conflictId: string, resolution: 'local' | 'remote' | 'merge', mergedValue?: unknown) => void;
47
+ /** Resolve all conflicts with a strategy */
48
+ resolveAllConflicts: (strategy: 'local' | 'remote') => void;
49
+ /** Revert to a previous version */
50
+ revertToVersion: (versionId: string) => Record<string, unknown> | null;
51
+ /** Compare two versions */
52
+ compareVersions: (versionA: string, versionB: string) => Record<string, {
53
+ before: unknown;
54
+ after: unknown;
55
+ }> | null;
56
+ /** Add a conflict */
57
+ addConflict: (conflict: Omit<ConflictInfo, 'id'>) => void;
58
+ /** Clear version history */
59
+ clearHistory: () => void;
60
+ }
61
+ /**
62
+ * Hook for conflict resolution with version history.
63
+ *
64
+ * Manages a local version history and conflict queue. Supports
65
+ * last-write-wins, manual merge, and server-wins resolution strategies.
66
+ *
67
+ * @param userId - Current user ID
68
+ * @param userName - Current user display name
69
+ * @param _config - Optional collaboration config (for future server-side integration)
70
+ */
71
+ export declare function useConflictResolution(userId: string, userName?: string, _config?: CollaborationConfig): ConflictResolutionResult;
72
+ //# sourceMappingURL=useConflictResolution.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useConflictResolution.d.ts","sourceRoot":"","sources":["../src/useConflictResolution.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AAE5D,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE;QAAE,MAAM,EAAE,OAAO,CAAC;QAAC,KAAK,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;IAC7D,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,OAAO,CAAC;IACpB,WAAW,EAAE,OAAO,CAAC;IACrB,SAAS,EAAE,OAAO,CAAC;IACnB,cAAc,EAAE,MAAM,CAAC;IACvB,eAAe,EAAE,MAAM,CAAC;IACxB,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,wBAAwB;IACvC,8BAA8B;IAC9B,QAAQ,EAAE,YAAY,EAAE,CAAC;IACzB,6BAA6B;IAC7B,cAAc,EAAE,MAAM,CAAC;IACvB,6CAA6C;IAC7C,SAAS,EAAE,YAAY,EAAE,CAAC;IAC1B,6CAA6C;IAC7C,YAAY,EAAE,OAAO,CAAC;IACtB,2BAA2B;IAC3B,aAAa,EAAE,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE;QAAE,MAAM,EAAE,OAAO,CAAC;QAAC,KAAK,EAAE,OAAO,CAAA;KAAE,CAAC,EAAE,OAAO,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;IACxG,yBAAyB;IACzB,eAAe,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,UAAU,EAAE,OAAO,GAAG,QAAQ,GAAG,OAAO,EAAE,WAAW,CAAC,EAAE,OAAO,KAAK,IAAI,CAAC;IAC/G,4CAA4C;IAC5C,mBAAmB,EAAE,CAAC,QAAQ,EAAE,OAAO,GAAG,QAAQ,KAAK,IAAI,CAAC;IAC5D,mCAAmC;IACnC,eAAe,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IACvE,2BAA2B;IAC3B,eAAe,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,MAAM,CAAC,MAAM,EAAE;QAAE,MAAM,EAAE,OAAO,CAAC;QAAC,KAAK,EAAE,OAAO,CAAA;KAAE,CAAC,GAAG,IAAI,CAAC;IACpH,qBAAqB;IACrB,WAAW,EAAE,CAAC,QAAQ,EAAE,IAAI,CAAC,YAAY,EAAE,IAAI,CAAC,KAAK,IAAI,CAAC;IAC1D,4BAA4B;IAC5B,YAAY,EAAE,MAAM,IAAI,CAAC;CAC1B;AAgBD;;;;;;;;;GASG;AACH,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,MAAM,EACd,QAAQ,CAAC,EAAE,MAAM,EACjB,OAAO,CAAC,EAAE,mBAAmB,GAC5B,wBAAwB,CAoN1B"}
@@ -0,0 +1,211 @@
1
+ /**
2
+ * ObjectUI
3
+ * Copyright (c) 2024-present ObjectStack Inc.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+ import { useState, useCallback, useMemo } from 'react';
9
+ let conflictCounter = 0;
10
+ function generateConflictId() {
11
+ conflictCounter += 1;
12
+ return `conflict-${Date.now()}-${conflictCounter}`;
13
+ }
14
+ let versionIdCounter = 0;
15
+ function generateVersionId() {
16
+ versionIdCounter += 1;
17
+ return `ver-${Date.now()}-${versionIdCounter}`;
18
+ }
19
+ /**
20
+ * Hook for conflict resolution with version history.
21
+ *
22
+ * Manages a local version history and conflict queue. Supports
23
+ * last-write-wins, manual merge, and server-wins resolution strategies.
24
+ *
25
+ * @param userId - Current user ID
26
+ * @param userName - Current user display name
27
+ * @param _config - Optional collaboration config (for future server-side integration)
28
+ */
29
+ export function useConflictResolution(userId, userName, _config) {
30
+ const [versions, setVersions] = useState([]);
31
+ const [conflicts, setConflicts] = useState([]);
32
+ const currentVersion = useMemo(() => {
33
+ if (versions.length === 0)
34
+ return 0;
35
+ return versions[versions.length - 1].version;
36
+ }, [versions]);
37
+ const hasConflicts = conflicts.length > 0;
38
+ const recordVersion = useCallback((changes, message) => {
39
+ setVersions(prev => {
40
+ const nextVersion = prev.length === 0 ? 1 : prev[prev.length - 1].version + 1;
41
+ const entry = {
42
+ id: generateVersionId(),
43
+ version: nextVersion,
44
+ timestamp: new Date().toISOString(),
45
+ userId,
46
+ userName,
47
+ changes,
48
+ message,
49
+ };
50
+ return [...prev, entry];
51
+ });
52
+ }, [userId, userName]);
53
+ const resolveConflict = useCallback((conflictId, resolution, mergedValue) => {
54
+ setConflicts(prev => {
55
+ const conflict = prev.find(c => c.id === conflictId);
56
+ if (!conflict)
57
+ return prev;
58
+ let resolvedValue;
59
+ switch (resolution) {
60
+ case 'local':
61
+ resolvedValue = conflict.localValue;
62
+ break;
63
+ case 'remote':
64
+ resolvedValue = conflict.remoteValue;
65
+ break;
66
+ case 'merge':
67
+ resolvedValue = mergedValue ?? conflict.localValue;
68
+ break;
69
+ }
70
+ // Record the resolution as a version entry
71
+ setVersions(vPrev => {
72
+ const nextVersion = vPrev.length === 0 ? 1 : vPrev[vPrev.length - 1].version + 1;
73
+ const entry = {
74
+ id: generateVersionId(),
75
+ version: nextVersion,
76
+ timestamp: new Date().toISOString(),
77
+ userId,
78
+ userName,
79
+ changes: {
80
+ [conflict.field]: { before: conflict.baseValue, after: resolvedValue },
81
+ },
82
+ message: `Resolved conflict on "${conflict.field}" using ${resolution} strategy`,
83
+ };
84
+ return [...vPrev, entry];
85
+ });
86
+ return prev.filter(c => c.id !== conflictId);
87
+ });
88
+ }, [userId, userName]);
89
+ const resolveAllConflicts = useCallback((strategy) => {
90
+ setConflicts(prev => {
91
+ if (prev.length === 0)
92
+ return prev;
93
+ const allChanges = {};
94
+ for (const conflict of prev) {
95
+ allChanges[conflict.field] = {
96
+ before: conflict.baseValue,
97
+ after: strategy === 'local' ? conflict.localValue : conflict.remoteValue,
98
+ };
99
+ }
100
+ setVersions(vPrev => {
101
+ const nextVersion = vPrev.length === 0 ? 1 : vPrev[vPrev.length - 1].version + 1;
102
+ const entry = {
103
+ id: generateVersionId(),
104
+ version: nextVersion,
105
+ timestamp: new Date().toISOString(),
106
+ userId,
107
+ userName,
108
+ changes: allChanges,
109
+ message: `Resolved all conflicts using ${strategy} strategy`,
110
+ };
111
+ return [...vPrev, entry];
112
+ });
113
+ return [];
114
+ });
115
+ }, [userId, userName]);
116
+ const revertToVersion = useCallback((versionId) => {
117
+ const targetIdx = versions.findIndex(v => v.id === versionId);
118
+ if (targetIdx === -1)
119
+ return null;
120
+ // Build the state at the target version by replaying changes
121
+ const state = {};
122
+ for (let i = 0; i <= targetIdx; i++) {
123
+ const entry = versions[i];
124
+ for (const [field, change] of Object.entries(entry.changes)) {
125
+ state[field] = change.after;
126
+ }
127
+ }
128
+ // Record the revert as a new version
129
+ const revertChanges = {};
130
+ const currentState = {};
131
+ for (const entry of versions) {
132
+ for (const [field, change] of Object.entries(entry.changes)) {
133
+ currentState[field] = change.after;
134
+ }
135
+ }
136
+ for (const [field, value] of Object.entries(state)) {
137
+ if (currentState[field] !== value) {
138
+ revertChanges[field] = { before: currentState[field], after: value };
139
+ }
140
+ }
141
+ if (Object.keys(revertChanges).length > 0) {
142
+ setVersions(prev => {
143
+ const nextVersion = prev.length === 0 ? 1 : prev[prev.length - 1].version + 1;
144
+ const entry = {
145
+ id: generateVersionId(),
146
+ version: nextVersion,
147
+ timestamp: new Date().toISOString(),
148
+ userId,
149
+ userName,
150
+ changes: revertChanges,
151
+ message: `Reverted to version ${versions[targetIdx].version}`,
152
+ };
153
+ return [...prev, entry];
154
+ });
155
+ }
156
+ return state;
157
+ }, [versions, userId, userName]);
158
+ const compareVersions = useCallback((versionA, versionB) => {
159
+ const idxA = versions.findIndex(v => v.id === versionA);
160
+ const idxB = versions.findIndex(v => v.id === versionB);
161
+ if (idxA === -1 || idxB === -1)
162
+ return null;
163
+ const [startIdx, endIdx] = idxA < idxB ? [idxA, idxB] : [idxB, idxA];
164
+ // Build state at each version
165
+ const buildState = (upTo) => {
166
+ const state = {};
167
+ for (let i = 0; i <= upTo; i++) {
168
+ for (const [field, change] of Object.entries(versions[i].changes)) {
169
+ state[field] = change.after;
170
+ }
171
+ }
172
+ return state;
173
+ };
174
+ const stateA = buildState(startIdx);
175
+ const stateB = buildState(endIdx);
176
+ const allFields = new Set([...Object.keys(stateA), ...Object.keys(stateB)]);
177
+ const diff = {};
178
+ for (const field of allFields) {
179
+ const valA = stateA[field];
180
+ const valB = stateB[field];
181
+ if (valA !== valB) {
182
+ diff[field] = { before: valA, after: valB };
183
+ }
184
+ }
185
+ return diff;
186
+ }, [versions]);
187
+ const addConflict = useCallback((conflict) => {
188
+ const newConflict = {
189
+ ...conflict,
190
+ id: generateConflictId(),
191
+ };
192
+ setConflicts(prev => [...prev, newConflict]);
193
+ }, []);
194
+ const clearHistory = useCallback(() => {
195
+ setVersions([]);
196
+ setConflicts([]);
197
+ }, []);
198
+ return {
199
+ versions,
200
+ currentVersion,
201
+ conflicts,
202
+ hasConflicts,
203
+ recordVersion,
204
+ resolveConflict,
205
+ resolveAllConflicts,
206
+ revertToVersion,
207
+ compareVersions,
208
+ addConflict,
209
+ clearHistory,
210
+ };
211
+ }
@@ -0,0 +1,82 @@
1
+ /**
2
+ * ObjectUI
3
+ * Copyright (c) 2024-present ObjectStack Inc.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+ export interface PresenceUser {
9
+ userId: string;
10
+ userName: string;
11
+ avatar?: string;
12
+ color: string;
13
+ cursor?: {
14
+ x: number;
15
+ y: number;
16
+ elementId?: string;
17
+ };
18
+ selection?: {
19
+ start: number;
20
+ end: number;
21
+ elementId?: string;
22
+ };
23
+ status: 'active' | 'idle' | 'away';
24
+ lastActivity: string;
25
+ }
26
+ export interface PresenceConfig {
27
+ /** Current user info */
28
+ user: {
29
+ id: string;
30
+ name: string;
31
+ avatar?: string;
32
+ };
33
+ /** Update throttle in ms (default: 50) */
34
+ throttleMs?: number;
35
+ /** Idle timeout in ms (default: 60000) */
36
+ idleTimeout?: number;
37
+ /** Away timeout in ms (default: 300000) */
38
+ awayTimeout?: number;
39
+ }
40
+ export interface PresenceResult {
41
+ /** All users present (excluding current user) */
42
+ users: PresenceUser[];
43
+ /** Total user count (including current) */
44
+ userCount: number;
45
+ /** Update current user's cursor position */
46
+ updateCursor: (position: {
47
+ x: number;
48
+ y: number;
49
+ elementId?: string;
50
+ }) => void;
51
+ /** Update current user's selection */
52
+ updateSelection: (selection: {
53
+ start: number;
54
+ end: number;
55
+ elementId?: string;
56
+ }) => void;
57
+ /** Current user's presence data */
58
+ currentUser: PresenceUser;
59
+ }
60
+ /**
61
+ * Hook for live cursors and presence tracking.
62
+ *
63
+ * Tracks cursor position with throttled updates, detects idle/away status
64
+ * through activity monitoring, and manages a set of remote users.
65
+ *
66
+ * @param sendPresence - Callback to broadcast presence updates to other users
67
+ * @param config - Presence configuration
68
+ */
69
+ export declare function usePresence(sendPresence: (user: PresenceUser) => void, config: PresenceConfig): PresenceResult;
70
+ /**
71
+ * Update remote user presence. Call this when receiving presence data
72
+ * from other users via the realtime channel.
73
+ *
74
+ * @returns A function to update or remove a remote user from the presence set.
75
+ */
76
+ export declare function createPresenceUpdater(): {
77
+ updateUser: (user: PresenceUser) => void;
78
+ removeUser: (userId: string) => void;
79
+ getUsers: () => PresenceUser[];
80
+ onUsersChange: (callback: (users: Map<string, PresenceUser>) => void) => () => void;
81
+ };
82
+ //# sourceMappingURL=usePresence.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"usePresence.d.ts","sourceRoot":"","sources":["../src/usePresence.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACtD,SAAS,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC/D,MAAM,EAAE,QAAQ,GAAG,MAAM,GAAG,MAAM,CAAC;IACnC,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,cAAc;IAC7B,wBAAwB;IACxB,IAAI,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACpD,0CAA0C;IAC1C,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,0CAA0C;IAC1C,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,2CAA2C;IAC3C,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,cAAc;IAC7B,iDAAiD;IACjD,KAAK,EAAE,YAAY,EAAE,CAAC;IACtB,2CAA2C;IAC3C,SAAS,EAAE,MAAM,CAAC;IAClB,4CAA4C;IAC5C,YAAY,EAAE,CAAC,QAAQ,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;IAC/E,sCAAsC;IACtC,eAAe,EAAE,CAAC,SAAS,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;IACzF,mCAAmC;IACnC,WAAW,EAAE,YAAY,CAAC;CAC3B;AAgBD;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CACzB,YAAY,EAAE,CAAC,IAAI,EAAE,YAAY,KAAK,IAAI,EAC1C,MAAM,EAAE,cAAc,GACrB,cAAc,CAmIhB;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,IAAI;IACvC,UAAU,EAAE,CAAC,IAAI,EAAE,YAAY,KAAK,IAAI,CAAC;IACzC,UAAU,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IACrC,QAAQ,EAAE,MAAM,YAAY,EAAE,CAAC;IAC/B,aAAa,EAAE,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,KAAK,IAAI,KAAK,MAAM,IAAI,CAAC;CACrF,CAyBA"}