@cloudsignal/collaborate 0.1.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,409 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { ReactNode, RefObject, MutableRefObject } from 'react';
3
+
4
+ /** A user present in a collaborative space */
5
+ interface SpaceUser {
6
+ userId: string;
7
+ name: string;
8
+ color: string;
9
+ avatar?: string;
10
+ data?: Record<string, unknown>;
11
+ joinedAt: number;
12
+ lastSeen: number;
13
+ }
14
+ /** How to connect to CloudSignal — supports credentials, token, or external IdP */
15
+ interface SpaceConnectionConfig {
16
+ /** WebSocket URL (e.g. wss://connect.cloudsignal.app:18885/) */
17
+ host: string;
18
+ /** Direct credential auth */
19
+ username?: string;
20
+ password?: string;
21
+ /** Token-based auth */
22
+ organizationId?: string;
23
+ secretKey?: string;
24
+ /** External IdP token (Supabase, Clerk, etc.) */
25
+ externalToken?: string;
26
+ /** Token service URL for V2 auth */
27
+ tokenServiceUrl?: string;
28
+ }
29
+ interface SpaceProps {
30
+ /** Unique space identifier — used as MQTT topic namespace */
31
+ id: string;
32
+ /** Connection configuration */
33
+ connection: SpaceConnectionConfig;
34
+ /** Display name for the local user */
35
+ userName: string;
36
+ /** Hex color for the local user (auto-assigned if omitted) */
37
+ userColor?: string;
38
+ /** Optional avatar URL */
39
+ userAvatar?: string;
40
+ /** Arbitrary metadata attached to the user's presence */
41
+ userData?: Record<string, unknown>;
42
+ /** Enable debug logging */
43
+ debug?: boolean;
44
+ /** Presence heartbeat interval in ms (default: 10000) */
45
+ presenceHeartbeatMs?: number;
46
+ /** Time before a user is considered offline in ms (default: 30000) */
47
+ presenceTimeoutMs?: number;
48
+ /** Connection status change callback */
49
+ onConnectionChange?: (connected: boolean) => void;
50
+ children: ReactNode;
51
+ }
52
+ type TopicHandler = (subtopic: string, payload: unknown) => void;
53
+ interface SpaceContextValue {
54
+ spaceId: string;
55
+ self: SpaceUser;
56
+ isConnected: boolean;
57
+ isConnecting: boolean;
58
+ error: Error | null;
59
+ /** Time before a user is considered offline in ms */
60
+ presenceTimeoutMs: number;
61
+ /** Publish a message to a space topic (automatically prefixed) */
62
+ publish: (subtopic: string, payload: unknown, options?: PublishOptions) => void;
63
+ /** Register a handler for a topic segment — returns unsubscribe function */
64
+ addTopicHandler: (segment: string, handler: TopicHandler) => () => void;
65
+ }
66
+ interface PublishOptions {
67
+ qos?: 0 | 1 | 2;
68
+ retain?: boolean;
69
+ }
70
+ interface CursorData {
71
+ userId: string;
72
+ name: string;
73
+ x: number;
74
+ y: number;
75
+ color: string;
76
+ ts: number;
77
+ lastSeen: number;
78
+ }
79
+ interface UseCursorsOptions {
80
+ /** Throttle publish rate in ms (default: 30 → ~33Hz) */
81
+ throttleMs?: number;
82
+ /** Time before a cursor fades out in ms (default: 3000) */
83
+ staleMs?: number;
84
+ }
85
+ interface UseCursorsReturn {
86
+ /** Ref-based cursor map for imperative rendering (no re-renders) */
87
+ cursorsRef: RefObject<Map<string, CursorData>>;
88
+ /** Callback ref — set by CursorOverlay to sync DOM on each message */
89
+ onUpdateRef: MutableRefObject<(() => void) | null>;
90
+ /** State snapshot of cursors, updated at 1Hz for display purposes */
91
+ cursors: CursorData[];
92
+ /** Publish the local user's cursor position (normalized 0–1) */
93
+ publishCursor: (x: number, y: number) => void;
94
+ }
95
+ interface UsePresenceReturn {
96
+ /** All users currently in the space (including self) */
97
+ members: SpaceUser[];
98
+ /** Number of members */
99
+ count: number;
100
+ /** The local user */
101
+ self: SpaceUser;
102
+ /** Register a callback for when a user joins */
103
+ onJoin: (callback: (user: SpaceUser) => void) => () => void;
104
+ /** Register a callback for when a user leaves */
105
+ onLeave: (callback: (user: SpaceUser) => void) => () => void;
106
+ }
107
+ interface UseLockReturn {
108
+ /** Whether this component is currently locked */
109
+ isLocked: boolean;
110
+ /** The user who holds the lock (null if unlocked) */
111
+ lockedBy: SpaceUser | null;
112
+ /** Whether the local user holds the lock */
113
+ isLockedByMe: boolean;
114
+ /** Acquire the lock */
115
+ lock: () => void;
116
+ /** Release the lock */
117
+ unlock: () => void;
118
+ }
119
+ interface UseTypingIndicatorReturn {
120
+ /** Users currently typing */
121
+ typingUsers: SpaceUser[];
122
+ /** Signal that the local user started typing */
123
+ startTyping: () => void;
124
+ /** Signal that the local user stopped typing */
125
+ stopTyping: () => void;
126
+ /** Whether the local user is currently marked as typing */
127
+ isTyping: boolean;
128
+ }
129
+ interface Reaction {
130
+ id: string;
131
+ userId: string;
132
+ name: string;
133
+ emoji: string;
134
+ color: string;
135
+ ts: number;
136
+ /** Horizontal position for float animation (0–100), assigned on creation */
137
+ x?: number;
138
+ }
139
+ interface UseReactionsOptions {
140
+ /** Max visible reactions at once (default: 20) */
141
+ maxVisible?: number;
142
+ /** How long a reaction stays visible in ms (default: 3000) */
143
+ durationMs?: number;
144
+ }
145
+ interface UseReactionsReturn {
146
+ /** Currently visible reactions */
147
+ reactions: Reaction[];
148
+ /** Send an emoji reaction */
149
+ sendReaction: (emoji: string) => void;
150
+ }
151
+ interface UseBroadcastReturn<T = unknown> {
152
+ /** Send a broadcast message */
153
+ broadcast: (data: T) => void;
154
+ /** Last received message */
155
+ lastMessage: T | null;
156
+ /** Register a callback for incoming broadcasts */
157
+ onMessage: (callback: (data: T) => void) => () => void;
158
+ }
159
+ type UseSharedStateReturn<T> = [
160
+ /** Current value */
161
+ T,
162
+ /** Update the value (last-write-wins across clients) */
163
+ (value: T) => void
164
+ ];
165
+ interface AvatarStackProps {
166
+ /** Max avatars before showing "+N" (default: 5) */
167
+ max?: number;
168
+ /** Avatar size in pixels (default: 32) */
169
+ size?: number;
170
+ className?: string;
171
+ }
172
+ interface CursorOverlayProps {
173
+ /** Throttle publish rate in ms (default: 30) */
174
+ throttleMs?: number;
175
+ /** Cursor stale timeout in ms (default: 3000) */
176
+ staleMs?: number;
177
+ className?: string;
178
+ children?: ReactNode;
179
+ }
180
+ interface TypingIndicatorProps {
181
+ /** Optional input ID to scope typing to */
182
+ inputId?: string;
183
+ className?: string;
184
+ }
185
+ interface LockIndicatorProps {
186
+ /** ID of the component being locked */
187
+ componentId: string;
188
+ className?: string;
189
+ children?: ReactNode;
190
+ }
191
+ interface ReactionBarProps {
192
+ /** Emoji options to show (default: common set) */
193
+ emojis?: string[];
194
+ className?: string;
195
+ }
196
+ interface PresenceBorderProps {
197
+ /** Component ID for lock tracking */
198
+ componentId: string;
199
+ /** Border width in pixels (default: 2) */
200
+ borderWidth?: number;
201
+ className?: string;
202
+ children?: ReactNode;
203
+ }
204
+
205
+ declare function Space({ id, connection, userName, userColor, userAvatar, userData, debug, presenceHeartbeatMs, presenceTimeoutMs, onConnectionChange, children, }: SpaceProps): react_jsx_runtime.JSX.Element;
206
+
207
+ /**
208
+ * Access the current Space context.
209
+ * Must be used within a `<Space>` provider.
210
+ */
211
+ declare function useSpace(): SpaceContextValue;
212
+
213
+ /**
214
+ * Track who's in the space.
215
+ * Presence heartbeats are managed by the Space provider —
216
+ * this hook only reads and exposes the member list.
217
+ */
218
+ declare function usePresence(): UsePresenceReturn;
219
+
220
+ /**
221
+ * Live cursor tracking.
222
+ *
223
+ * Performance: Cursor data is stored in refs (not state) to avoid
224
+ * re-renders on every MQTT message (~33 messages/sec per user).
225
+ * The `onUpdateRef` callback lets CursorOverlay sync DOM imperatively.
226
+ * A 1Hz state snapshot is provided for non-critical display (e.g. online count).
227
+ */
228
+ declare function useCursors(options?: UseCursorsOptions): UseCursorsReturn;
229
+
230
+ /**
231
+ * Component locking — "I'm editing this."
232
+ *
233
+ * Only one user can hold a lock at a time.
234
+ * Automatically unlocks when the component unmounts.
235
+ */
236
+ declare function useLock(componentId: string): UseLockReturn;
237
+
238
+ /**
239
+ * Typing indicator — "Alice is typing..."
240
+ *
241
+ * Auto-resets after 3s of inactivity. Throttled to max 1 publish per 2s.
242
+ * Users not seen typing in 4s are cleaned from the list.
243
+ */
244
+ declare function useTypingIndicator(inputId?: string): UseTypingIndicatorReturn;
245
+
246
+ /**
247
+ * Emoji reactions — floating emojis that auto-expire.
248
+ */
249
+ declare function useReactions(options?: UseReactionsOptions): UseReactionsReturn;
250
+
251
+ /**
252
+ * Generic pub/sub for custom events.
253
+ *
254
+ * If `event` is provided, scopes to that event name.
255
+ * Otherwise, receives all broadcast messages.
256
+ *
257
+ * @example
258
+ * // Scoped to "chat" events
259
+ * const { broadcast, onMessage } = useBroadcast<ChatMessage>('chat')
260
+ * broadcast({ text: 'Hello!' })
261
+ *
262
+ * @example
263
+ * // All broadcast events
264
+ * const { onMessage } = useBroadcast()
265
+ */
266
+ declare function useBroadcast<T = unknown>(event?: string): UseBroadcastReturn<T>;
267
+
268
+ /**
269
+ * Synced key-value state across all users in the space.
270
+ *
271
+ * Uses Last-Write-Wins (LWW) merge — the update with the newest
272
+ * timestamp wins. Retained messages ensure new joiners get current state.
273
+ *
274
+ * @example
275
+ * const [count, setCount] = useSharedState('counter', 0)
276
+ * <button onClick={() => setCount(count + 1)}>Count: {count}</button>
277
+ */
278
+ declare function useSharedState<T>(key: string, initialValue: T): UseSharedStateReturn<T>;
279
+
280
+ /**
281
+ * Shows who's online — overlapping avatars with a "+N" overflow badge.
282
+ *
283
+ * @example
284
+ * <AvatarStack max={4} size={36} className="absolute top-4 right-4" />
285
+ */
286
+ declare function AvatarStack({ max, size, className }: AvatarStackProps): react_jsx_runtime.JSX.Element;
287
+
288
+ /**
289
+ * Drop-in cursor overlay — wraps children and renders live cursors.
290
+ *
291
+ * Mouse tracking and cursor rendering are handled automatically.
292
+ * Uses imperative DOM manipulation for sub-frame performance.
293
+ *
294
+ * @example
295
+ * <CursorOverlay>
296
+ * <div style={{ width: '100%', height: '400px' }}>
297
+ * Your collaborative content here
298
+ * </div>
299
+ * </CursorOverlay>
300
+ */
301
+ declare function CursorOverlay({ throttleMs, staleMs, className, children, }: CursorOverlayProps): react_jsx_runtime.JSX.Element;
302
+
303
+ /**
304
+ * Displays who's currently typing.
305
+ *
306
+ * Renders nothing if nobody is typing.
307
+ * Automatically formats: "Alice is typing...", "Alice and Bob are typing...",
308
+ * "3 people are typing..."
309
+ *
310
+ * @example
311
+ * <TypingIndicator className="text-sm text-gray-500 h-5" />
312
+ */
313
+ declare function TypingIndicator({ inputId, className }: TypingIndicatorProps): react_jsx_runtime.JSX.Element;
314
+
315
+ /**
316
+ * Shows lock status for a component and renders a colored border
317
+ * when locked by another user.
318
+ *
319
+ * @example
320
+ * <LockIndicator componentId="title-field">
321
+ * <input
322
+ * onFocus={() => lock()}
323
+ * onBlur={() => unlock()}
324
+ * disabled={isLocked && !isLockedByMe}
325
+ * />
326
+ * </LockIndicator>
327
+ *
328
+ * Or use the hook directly for more control:
329
+ * const { isLocked, lockedBy, lock, unlock } = useLock('title-field')
330
+ */
331
+ declare function LockIndicator({ componentId, className, children }: LockIndicatorProps): react_jsx_runtime.JSX.Element;
332
+
333
+ /**
334
+ * Emoji reaction bar with floating animations.
335
+ *
336
+ * Renders a row of emoji buttons + floating reactions from all users.
337
+ *
338
+ * @example
339
+ * <ReactionBar emojis={['👍', '❤️', '🎉']} className="fixed bottom-4 right-4" />
340
+ */
341
+ declare function ReactionBar({ emojis, className }: ReactionBarProps): react_jsx_runtime.JSX.Element;
342
+
343
+ /**
344
+ * Wraps a component with a colored border showing who's focused on it.
345
+ * Automatically locks/unlocks on focus/blur of child interactive elements.
346
+ *
347
+ * @example
348
+ * <PresenceBorder componentId="description-field">
349
+ * <textarea placeholder="Description..." />
350
+ * </PresenceBorder>
351
+ */
352
+ declare function PresenceBorder({ componentId, borderWidth, className, children, }: PresenceBorderProps): react_jsx_runtime.JSX.Element;
353
+
354
+ /**
355
+ * Deterministic color assignment based on userId.
356
+ * Same user always gets the same color across sessions.
357
+ */
358
+ declare function getColorForUser(userId: string): string;
359
+ /**
360
+ * Get user initials from display name (for avatars).
361
+ * "Alice Smith" → "AS", "bob" → "B"
362
+ */
363
+ declare function getInitials(name: string): string;
364
+
365
+ /** MQTT topic prefix for all space collaboration messages */
366
+ declare const TOPIC_PREFIX = "$spaces";
367
+ /** Topic segments for each collaboration primitive */
368
+ declare const TOPICS: {
369
+ readonly PRESENCE: "presence";
370
+ readonly CURSORS: "cursors";
371
+ readonly LOCKS: "locks";
372
+ readonly TYPING: "typing";
373
+ readonly REACTIONS: "reactions";
374
+ readonly BROADCAST: "broadcast";
375
+ readonly STATE: "state";
376
+ };
377
+ /** Default timing constants */
378
+ declare const DEFAULTS: {
379
+ /** Presence heartbeat interval (ms) */
380
+ readonly PRESENCE_HEARTBEAT_MS: 10000;
381
+ /** Time before a user is considered offline (ms) */
382
+ readonly PRESENCE_TIMEOUT_MS: 30000;
383
+ /** Cursor publish throttle (ms) — ~33Hz */
384
+ readonly CURSOR_THROTTLE_MS: 30;
385
+ /** Time before a cursor fades out (ms) */
386
+ readonly CURSOR_STALE_MS: 3000;
387
+ /** Typing indicator auto-reset (ms) */
388
+ readonly TYPING_TIMEOUT_MS: 3000;
389
+ /** Minimum interval between typing publishes (ms) */
390
+ readonly TYPING_THROTTLE_MS: 2000;
391
+ /** Time before a typing user is cleaned from the list (ms) */
392
+ readonly TYPING_STALE_MS: 4000;
393
+ /** How long a reaction stays visible (ms) */
394
+ readonly REACTION_DURATION_MS: 3000;
395
+ /** Max visible reactions at once */
396
+ readonly REACTION_MAX_VISIBLE: 20;
397
+ /** Stats sync interval (ms) — for cursor count etc. */
398
+ readonly STATS_TICK_MS: 1000;
399
+ /** Opacity fade-out sync interval (ms) */
400
+ readonly FADE_TICK_MS: 200;
401
+ };
402
+ /** Build a full MQTT topic for a space */
403
+ declare function spaceTopic(spaceId: string, segment: string): string;
404
+ /** Build the wildcard subscription for a space */
405
+ declare function spaceWildcard(spaceId: string): string;
406
+
407
+ declare const VERSION = "0.1.0";
408
+
409
+ export { AvatarStack, type AvatarStackProps, Space as CloudSignalSpace, type CursorData, CursorOverlay, type CursorOverlayProps, DEFAULTS, LockIndicator, type LockIndicatorProps, PresenceBorder, type PresenceBorderProps, type PublishOptions, type Reaction, ReactionBar, type ReactionBarProps, Space, type SpaceConnectionConfig, type SpaceContextValue, type SpaceProps, type SpaceUser, TOPICS, TOPIC_PREFIX, TypingIndicator, type TypingIndicatorProps, type UseBroadcastReturn, type UseCursorsOptions, type UseCursorsReturn, type UseLockReturn, type UsePresenceReturn, type UseReactionsOptions, type UseReactionsReturn, type UseSharedStateReturn, type UseTypingIndicatorReturn, VERSION, getColorForUser, getInitials, spaceTopic, spaceWildcard, useBroadcast, useCursors, useLock, usePresence, useReactions, useSharedState, useSpace, useTypingIndicator };