live-cursors 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,107 @@
1
+ //#region src/protocol.d.ts
2
+ declare const PROTOCOL_VERSION: 1;
3
+ interface CursorMove {
4
+ v: typeof PROTOCOL_VERSION;
5
+ t: "m";
6
+ id?: string;
7
+ x: number;
8
+ y: number;
9
+ m?: Record<string, unknown>;
10
+ }
11
+ interface CursorLeave {
12
+ v: typeof PROTOCOL_VERSION;
13
+ t: "l";
14
+ id?: string;
15
+ }
16
+ type CursorMessage = CursorMove | CursorLeave;
17
+ /**
18
+ * Turns wire data into a message, or null when it is unusable.
19
+ * Coordinates arrive from other clients, so they are clamped rather than trusted.
20
+ */
21
+ declare function parseMessage(raw: unknown): CursorMessage | null;
22
+ declare function moveMessage(id: string, x: number, y: number, meta?: Record<string, unknown>): CursorMove;
23
+ declare function leaveMessage(id: string): CursorLeave;
24
+ //#endregion
25
+ //#region src/types.d.ts
26
+ interface Point {
27
+ x: number;
28
+ y: number;
29
+ }
30
+ interface Size {
31
+ width: number;
32
+ height: number;
33
+ }
34
+ interface RemoteCursor {
35
+ id: string;
36
+ /** normalized 0..1 within the coordinate space */
37
+ x: number;
38
+ y: number;
39
+ meta?: Record<string, unknown>;
40
+ updatedAt: number;
41
+ }
42
+ /**
43
+ * Anything that can carry cursor messages between clients.
44
+ *
45
+ * `senderId` is for transports that already know the author of a message
46
+ * (Laravel Echo presence channels, Supabase Realtime). When present it wins
47
+ * over the id inside the payload, so a client cannot claim someone else's id.
48
+ */
49
+ interface CursorTransport {
50
+ send: (message: CursorMessage) => void;
51
+ onMessage: (handler: (raw: unknown, senderId?: string) => void) => (() => void) | void;
52
+ close?: () => void;
53
+ }
54
+ interface PointerLike {
55
+ clientX: number;
56
+ clientY: number;
57
+ }
58
+ /**
59
+ * Maps pointer positions to normalized coordinates and back.
60
+ *
61
+ * `viewport` keeps a cursor at the same screen position for everyone regardless of
62
+ * window size and scroll offset; `element` pins it to content that scrolls with the page.
63
+ */
64
+ interface CoordinateSpace {
65
+ toNormalized: (pointer: PointerLike) => Point | null;
66
+ fromNormalized: (point: Point) => Point;
67
+ size: () => Size;
68
+ target: () => HTMLElement | null;
69
+ }
70
+ //#endregion
71
+ //#region src/cursors.d.ts
72
+ interface CursorsOptions {
73
+ transport: CursorTransport;
74
+ /** id of the local participant; messages carrying it are ignored on receive */
75
+ selfId: string;
76
+ space?: CoordinateSpace;
77
+ /** how often a move goes out; 40ms is ~25fps and enough for smooth rendering */
78
+ throttleMs?: number;
79
+ /** a cursor with no update within this window disappears */
80
+ ttlMs?: number;
81
+ sweepMs?: number;
82
+ /** upper bound on tracked participants — a flooding client cannot grow the map past it */
83
+ maxRemote?: number;
84
+ enabled?: boolean;
85
+ /** extra payload sent with every move; keep it tiny and free of personal data */
86
+ meta?: () => Record<string, unknown> | undefined;
87
+ onError?: (error: unknown) => void;
88
+ }
89
+ interface CursorsInstance {
90
+ attach: (element?: HTMLElement) => () => void;
91
+ detach: () => void;
92
+ subscribe: (listener: () => void) => () => void;
93
+ getSnapshot: () => readonly RemoteCursor[];
94
+ get: (id: string) => RemoteCursor | undefined;
95
+ /** drop a cursor now, without waiting for its ttl — e.g. when presence reports someone left */
96
+ remove: (id: string) => void;
97
+ /** feed a raw message in by hand — for transports wired outside this instance */
98
+ ingest: (raw: unknown, senderId?: string) => void;
99
+ setEnabled: (enabled: boolean) => void;
100
+ isEnabled: () => boolean;
101
+ leave: () => void;
102
+ readonly space: CoordinateSpace;
103
+ destroy: () => void;
104
+ }
105
+ declare function createCursors(options: CursorsOptions): CursorsInstance;
106
+ //#endregion
107
+ export { CursorTransport as a, RemoteCursor as c, CursorMessage as d, CursorMove as f, parseMessage as g, moveMessage as h, CoordinateSpace as i, Size as l, leaveMessage as m, CursorsOptions as n, Point as o, PROTOCOL_VERSION as p, createCursors as r, PointerLike as s, CursorsInstance as t, CursorLeave as u };
package/dist/dom.d.mts ADDED
@@ -0,0 +1,20 @@
1
+ import { c as RemoteCursor, t as CursorsInstance } from "./cursors-CyiNtLCv.mjs";
2
+ //#region src/dom/index.d.ts
3
+ interface RenderCursorsOptions {
4
+ cursors: CursorsInstance;
5
+ /** element the overlay is placed in; it needs a non-static position. Defaults to a fixed, viewport-sized overlay */
6
+ container?: HTMLElement;
7
+ color?: (cursor: RemoteCursor) => string;
8
+ label?: (cursor: RemoteCursor) => string | null | undefined;
9
+ zIndex?: number;
10
+ }
11
+ interface CursorsRenderer {
12
+ destroy: () => void;
13
+ }
14
+ declare const CURSOR_CSS = "\n.lc-overlay{position:absolute;top:0;left:0;pointer-events:none;overflow:hidden}\n.lc-overlay[data-fixed=true]{position:fixed;inset:0;width:100%;height:100%}\n.lc-cursor{position:absolute;top:0;left:0;color:var(--lc-color,#888);display:flex;align-items:flex-start;gap:2px;will-change:transform;transition:transform var(--lc-transition,80ms) linear}\n.lc-arrow{flex:none;filter:drop-shadow(0 1px 3px rgba(0,0,0,.3))}\n.lc-label{display:flex;align-items:center;gap:4px;margin-top:12px;padding:2px 7px;border-radius:100px;max-width:var(--lc-label-max-width,160px);overflow:hidden;white-space:nowrap;text-overflow:ellipsis;font:600 11px/1.3 var(--lc-font,system-ui,sans-serif);color:var(--lc-label-color,#fff);background:var(--lc-color,#888);box-shadow:0 2px 8px rgba(0,0,0,.2)}\n@media (prefers-reduced-motion:reduce){.lc-cursor{transition:none}}\n";
15
+ declare function ensureCursorStyles(): void;
16
+ /** Arrow markup; its color comes from the `--lc-color` custom property of the cursor element. */
17
+ declare const ARROW_SVG = "<svg class=\"lc-arrow\" width=\"18\" height=\"18\" viewBox=\"0 0 18 18\" aria-hidden=\"true\"><path d=\"M2 2L14.5 8L9 9.5L7 16L2 2Z\" fill=\"currentColor\" stroke=\"#fff\" stroke-width=\"1.5\" stroke-linejoin=\"round\"/></svg>";
18
+ declare function renderCursors(options: RenderCursorsOptions): CursorsRenderer;
19
+ //#endregion
20
+ export { ARROW_SVG, CURSOR_CSS, CursorsRenderer, RenderCursorsOptions, ensureCursorStyles, renderCursors };
package/dist/dom.mjs ADDED
@@ -0,0 +1,97 @@
1
+ import { t as cursorColor } from "./color-CN1S_Vq9.mjs";
2
+ //#region src/dom/index.ts
3
+ const STYLE_ID = "live-cursors-style";
4
+ const CURSOR_CSS = `
5
+ .lc-overlay{position:absolute;top:0;left:0;pointer-events:none;overflow:hidden}
6
+ .lc-overlay[data-fixed=true]{position:fixed;inset:0;width:100%;height:100%}
7
+ .lc-cursor{position:absolute;top:0;left:0;color:var(--lc-color,#888);display:flex;align-items:flex-start;gap:2px;will-change:transform;transition:transform var(--lc-transition,80ms) linear}
8
+ .lc-arrow{flex:none;filter:drop-shadow(0 1px 3px rgba(0,0,0,.3))}
9
+ .lc-label{display:flex;align-items:center;gap:4px;margin-top:12px;padding:2px 7px;border-radius:100px;max-width:var(--lc-label-max-width,160px);overflow:hidden;white-space:nowrap;text-overflow:ellipsis;font:600 11px/1.3 var(--lc-font,system-ui,sans-serif);color:var(--lc-label-color,#fff);background:var(--lc-color,#888);box-shadow:0 2px 8px rgba(0,0,0,.2)}
10
+ @media (prefers-reduced-motion:reduce){.lc-cursor{transition:none}}
11
+ `;
12
+ function ensureCursorStyles() {
13
+ if (typeof document === "undefined" || document.getElementById(STYLE_ID)) return;
14
+ const style = document.createElement("style");
15
+ style.id = STYLE_ID;
16
+ style.textContent = CURSOR_CSS;
17
+ document.head.append(style);
18
+ }
19
+ /** Arrow markup; its color comes from the `--lc-color` custom property of the cursor element. */
20
+ const ARROW_SVG = `<svg class="lc-arrow" width="18" height="18" viewBox="0 0 18 18" aria-hidden="true"><path d="M2 2L14.5 8L9 9.5L7 16L2 2Z" fill="currentColor" stroke="#fff" stroke-width="1.5" stroke-linejoin="round"/></svg>`;
21
+ function renderCursors(options) {
22
+ const { cursors, container, color = (cursor) => cursorColor(cursor.id), label, zIndex = 10 } = options;
23
+ ensureCursorStyles();
24
+ const overlay = document.createElement("div");
25
+ overlay.className = "lc-overlay";
26
+ overlay.setAttribute("aria-hidden", "true");
27
+ overlay.style.zIndex = String(zIndex);
28
+ if (!container) overlay.dataset.fixed = "true";
29
+ (container ?? document.body).append(overlay);
30
+ const nodes = /* @__PURE__ */ new Map();
31
+ let frame = null;
32
+ const syncSize = () => {
33
+ if (!container) return;
34
+ const { width, height } = cursors.space.size();
35
+ overlay.style.width = `${width}px`;
36
+ overlay.style.height = `${height}px`;
37
+ };
38
+ const paint = () => {
39
+ frame = null;
40
+ syncSize();
41
+ const seen = /* @__PURE__ */ new Set();
42
+ for (const cursor of cursors.getSnapshot()) {
43
+ seen.add(cursor.id);
44
+ let node = nodes.get(cursor.id);
45
+ if (!node) {
46
+ node = document.createElement("div");
47
+ node.className = "lc-cursor";
48
+ node.innerHTML = ARROW_SVG;
49
+ nodes.set(cursor.id, node);
50
+ overlay.append(node);
51
+ }
52
+ node.style.setProperty("--lc-color", color(cursor));
53
+ const text = label?.(cursor);
54
+ if (text) {
55
+ let pill = node.querySelector(".lc-label");
56
+ if (!pill) {
57
+ pill = document.createElement("span");
58
+ pill.className = "lc-label";
59
+ node.append(pill);
60
+ }
61
+ if (pill.textContent !== text) pill.textContent = text;
62
+ } else node.querySelector(".lc-label")?.remove();
63
+ const point = cursors.space.fromNormalized(cursor);
64
+ node.style.transform = `translate(${point.x}px, ${point.y}px)`;
65
+ }
66
+ for (const [id, node] of nodes) if (!seen.has(id)) {
67
+ node.remove();
68
+ nodes.delete(id);
69
+ }
70
+ };
71
+ const schedule = () => {
72
+ if (frame !== null) return;
73
+ frame = requestAnimationFrame(paint);
74
+ };
75
+ const unsubscribe = cursors.subscribe(schedule);
76
+ const resizeObserver = container && typeof ResizeObserver !== "undefined" ? new ResizeObserver(schedule) : null;
77
+ const mutationObserver = container && typeof MutationObserver !== "undefined" ? new MutationObserver(schedule) : null;
78
+ resizeObserver?.observe(container);
79
+ mutationObserver?.observe(container, {
80
+ childList: true,
81
+ subtree: true,
82
+ characterData: true
83
+ });
84
+ window.addEventListener("resize", schedule, { passive: true });
85
+ schedule();
86
+ return { destroy() {
87
+ unsubscribe();
88
+ if (frame !== null) cancelAnimationFrame(frame);
89
+ resizeObserver?.disconnect();
90
+ mutationObserver?.disconnect();
91
+ window.removeEventListener("resize", schedule);
92
+ overlay.remove();
93
+ nodes.clear();
94
+ } };
95
+ }
96
+ //#endregion
97
+ export { ARROW_SVG, CURSOR_CSS, ensureCursorStyles, renderCursors };
@@ -0,0 +1,26 @@
1
+ import { a as CursorTransport } from "./cursors-CyiNtLCv.mjs";
2
+ //#region src/transports/echo.d.ts
3
+ /**
4
+ * The slice of a Laravel Echo private/presence channel this transport needs.
5
+ * Typed structurally so the package never has to depend on laravel-echo itself.
6
+ */
7
+ interface EchoWhisperChannel {
8
+ whisper: (event: string, data: unknown) => unknown;
9
+ listenForWhisper: (event: string, callback: (payload: unknown) => void) => unknown;
10
+ stopListeningForWhisper?: (event: string) => unknown;
11
+ }
12
+ interface EchoTransportOptions {
13
+ channel: EchoWhisperChannel | (() => EchoWhisperChannel);
14
+ /** whisper event name; must match on every client */
15
+ event?: string;
16
+ }
17
+ /**
18
+ * Laravel Echo transport over client events (whisper). Requires a private or presence
19
+ * channel — Reverb and Pusher reject client events on public channels.
20
+ *
21
+ * Echo's `stopListeningForWhisper(event)` drops every listener of an event on the channel,
22
+ * so give this transport an `event` name of its own if the app whispers the same event elsewhere.
23
+ */
24
+ declare function echoTransport(options: EchoTransportOptions): CursorTransport;
25
+ //#endregion
26
+ export { EchoTransportOptions, EchoWhisperChannel, echoTransport };
package/dist/echo.mjs ADDED
@@ -0,0 +1,24 @@
1
+ //#region src/transports/echo.ts
2
+ /**
3
+ * Laravel Echo transport over client events (whisper). Requires a private or presence
4
+ * channel — Reverb and Pusher reject client events on public channels.
5
+ *
6
+ * Echo's `stopListeningForWhisper(event)` drops every listener of an event on the channel,
7
+ * so give this transport an `event` name of its own if the app whispers the same event elsewhere.
8
+ */
9
+ function echoTransport(options) {
10
+ const { channel, event = "cursor" } = options;
11
+ const resolve = () => typeof channel === "function" ? channel() : channel;
12
+ return {
13
+ send(message) {
14
+ resolve().whisper(event, message);
15
+ },
16
+ onMessage(handler) {
17
+ const target = resolve();
18
+ target.listenForWhisper(event, (payload) => handler(payload));
19
+ return () => target.stopListeningForWhisper?.(event);
20
+ }
21
+ };
22
+ }
23
+ //#endregion
24
+ export { echoTransport };
@@ -0,0 +1,40 @@
1
+ import { i as CoordinateSpace } from "./cursors-CyiNtLCv.mjs";
2
+ //#region src/color.d.ts
3
+ /**
4
+ * Deterministic hue per id: the same participant keeps the same color across
5
+ * reloads and across clients without any color negotiation.
6
+ */
7
+ declare function hashHue(id: string): number;
8
+ interface ColorOptions {
9
+ saturation?: number;
10
+ lightness?: number;
11
+ }
12
+ declare function cursorColor(id: string, options?: ColorOptions): string;
13
+ /**
14
+ * Picks from a fixed palette instead of the full hue circle — for brand palettes
15
+ * or when hues have to stay distinguishable for color-blind viewers.
16
+ */
17
+ declare function paletteColor(id: string, palette: readonly string[]): string;
18
+ //#endregion
19
+ //#region src/space.d.ts
20
+ declare function viewportSpace(): CoordinateSpace;
21
+ /**
22
+ * Scroll-aware space: coordinates are relative to the scrollable content of an element,
23
+ * so a cursor stays on the content it points at even when participants scroll differently.
24
+ */
25
+ declare function elementSpace(resolve: HTMLElement | (() => HTMLElement | null)): CoordinateSpace;
26
+ declare function customSpace(space: CoordinateSpace): CoordinateSpace;
27
+ //#endregion
28
+ //#region src/throttle.d.ts
29
+ interface Throttled<A extends unknown[]> {
30
+ (...args: A): void;
31
+ flush: () => void;
32
+ cancel: () => void;
33
+ }
34
+ /**
35
+ * Leading + trailing throttle: the first move goes out immediately and the last
36
+ * one is never dropped, which is what keeps a cursor from freezing mid-screen.
37
+ */
38
+ declare function throttle<A extends unknown[]>(fn: (...args: A) => void, ms: number): Throttled<A>;
39
+ //#endregion
40
+ export { viewportSpace as a, hashHue as c, elementSpace as i, paletteColor as l, throttle as n, ColorOptions as o, customSpace as r, cursorColor as s, Throttled as t };
@@ -0,0 +1,3 @@
1
+ import { a as CursorTransport, c as RemoteCursor, d as CursorMessage, f as CursorMove, g as parseMessage, h as moveMessage, i as CoordinateSpace, l as Size, m as leaveMessage, n as CursorsOptions, o as Point, p as PROTOCOL_VERSION, r as createCursors, s as PointerLike, t as CursorsInstance, u as CursorLeave } from "./cursors-CyiNtLCv.mjs";
2
+ import { a as viewportSpace, c as hashHue, i as elementSpace, l as paletteColor, n as throttle, o as ColorOptions, r as customSpace, s as cursorColor, t as Throttled } from "./index-4O7Ji1vk.mjs";
3
+ export { type ColorOptions, type CoordinateSpace, type CursorLeave, type CursorMessage, type CursorMove, type CursorTransport, type CursorsInstance, type CursorsOptions, PROTOCOL_VERSION, type Point, type PointerLike, type RemoteCursor, type Size, type Throttled, createCursors, cursorColor, customSpace, elementSpace, hashHue, leaveMessage, moveMessage, paletteColor, parseMessage, throttle, viewportSpace };
package/dist/index.mjs ADDED
@@ -0,0 +1,3 @@
1
+ import { n as hashHue, r as paletteColor, t as cursorColor } from "./color-CN1S_Vq9.mjs";
2
+ import { a as viewportSpace, c as moveMessage, i as elementSpace, l as parseMessage, n as throttle, o as PROTOCOL_VERSION, r as customSpace, s as leaveMessage, t as createCursors } from "./cursors-BhKZnRWg.mjs";
3
+ export { PROTOCOL_VERSION, createCursors, cursorColor, customSpace, elementSpace, hashHue, leaveMessage, moveMessage, paletteColor, parseMessage, throttle, viewportSpace };
@@ -0,0 +1,26 @@
1
+ import { a as CursorTransport, c as RemoteCursor, n as CursorsOptions, t as CursorsInstance } from "./cursors-CyiNtLCv.mjs";
2
+ import { ReactElement, ReactNode } from "react";
3
+ //#region src/react/index.d.ts
4
+ interface UseCursorsOptions extends Omit<CursorsOptions, "transport" | "space"> {
5
+ /** a factory keeps BroadcastChannel and WebSocket out of the server render */
6
+ transport: CursorTransport | (() => CursorTransport);
7
+ space?: CursorsOptions["space"];
8
+ container?: HTMLElement | null;
9
+ }
10
+ interface UseCursorsReturn {
11
+ cursors: CursorsInstance | null;
12
+ list: readonly RemoteCursor[];
13
+ }
14
+ declare function useCursors(options: UseCursorsOptions): UseCursorsReturn;
15
+ interface LiveCursorsProps {
16
+ cursors: CursorsInstance | null;
17
+ color?: (cursor: RemoteCursor) => string;
18
+ label?: (cursor: RemoteCursor) => string | null | undefined;
19
+ /** custom label content, keeping the built-in arrow; `children` replaces the whole cursor */
20
+ renderLabel?: (cursor: RemoteCursor, color: string) => ReactNode;
21
+ zIndex?: number;
22
+ children?: (cursor: RemoteCursor, color: string) => ReactNode;
23
+ }
24
+ declare function LiveCursors(props: LiveCursorsProps): ReactElement | null;
25
+ //#endregion
26
+ export { LiveCursors, LiveCursorsProps, UseCursorsOptions, UseCursorsReturn, useCursors };
package/dist/react.mjs ADDED
@@ -0,0 +1,91 @@
1
+ import { t as cursorColor } from "./color-CN1S_Vq9.mjs";
2
+ import { i as elementSpace, t as createCursors } from "./cursors-BhKZnRWg.mjs";
3
+ import { ARROW_SVG, ensureCursorStyles } from "./dom.mjs";
4
+ import { createElement, useEffect, useRef, useState, useSyncExternalStore } from "react";
5
+ //#region src/react/index.ts
6
+ const EMPTY = Object.freeze([]);
7
+ function useCursors(options) {
8
+ const { transport, container, space, ...rest } = options;
9
+ const [instance, setInstance] = useState(null);
10
+ const settingsRef = useRef(rest);
11
+ settingsRef.current = rest;
12
+ const containerRef = useRef(container);
13
+ containerRef.current = container;
14
+ const setupRef = useRef({
15
+ transport,
16
+ space
17
+ });
18
+ useEffect(() => {
19
+ const setup = setupRef.current;
20
+ const created = createCursors({
21
+ ...settingsRef.current,
22
+ transport: typeof setup.transport === "function" ? setup.transport() : setup.transport,
23
+ space: setup.space ?? (containerRef.current !== void 0 ? elementSpace(() => containerRef.current ?? null) : void 0)
24
+ });
25
+ setInstance(created);
26
+ return () => {
27
+ created.destroy();
28
+ setInstance(null);
29
+ };
30
+ }, []);
31
+ useEffect(() => {
32
+ if (!instance) return;
33
+ if (container === void 0) return instance.attach();
34
+ if (container === null) return;
35
+ return instance.attach(container);
36
+ }, [instance, container]);
37
+ return {
38
+ cursors: instance,
39
+ list: useSyncExternalStore((onChange) => instance?.subscribe(onChange) ?? (() => {}), () => instance?.getSnapshot() ?? EMPTY, () => EMPTY)
40
+ };
41
+ }
42
+ function LiveCursors(props) {
43
+ const { cursors, color, label, renderLabel, zIndex = 10, children } = props;
44
+ const [resizeTick, setResizeTick] = useState(0);
45
+ ensureCursorStyles();
46
+ const list = useSyncExternalStore((onChange) => cursors?.subscribe(onChange) ?? (() => {}), () => cursors?.getSnapshot() ?? EMPTY, () => EMPTY);
47
+ useEffect(() => {
48
+ const onResize = () => setResizeTick((value) => value + 1);
49
+ window.addEventListener("resize", onResize, { passive: true });
50
+ return () => window.removeEventListener("resize", onResize);
51
+ }, []);
52
+ const isFixed = cursors?.space.target() === null;
53
+ const size = cursors?.space.size() ?? {
54
+ width: 0,
55
+ height: 0
56
+ };
57
+ if (!cursors) return null;
58
+ return createElement("div", {
59
+ "className": "lc-overlay",
60
+ "aria-hidden": true,
61
+ "data-fixed": isFixed ? "true" : void 0,
62
+ "style": isFixed ? { zIndex } : {
63
+ zIndex,
64
+ width: size.width,
65
+ height: size.height
66
+ }
67
+ }, list.map((cursor) => {
68
+ const tint = color?.(cursor) ?? cursorColor(cursor.id);
69
+ const point = cursors.space.fromNormalized(cursor);
70
+ const text = label?.(cursor);
71
+ return createElement("div", {
72
+ key: cursor.id,
73
+ className: "lc-cursor",
74
+ style: {
75
+ "transform": `translate(${point.x}px, ${point.y}px)`,
76
+ "--lc-color": tint
77
+ }
78
+ }, children ? children(cursor, tint) : [createElement("span", {
79
+ key: "arrow",
80
+ dangerouslySetInnerHTML: { __html: ARROW_SVG }
81
+ }), renderLabel ? createElement("span", {
82
+ key: "label",
83
+ className: "lc-label"
84
+ }, renderLabel(cursor, tint)) : text ? createElement("span", {
85
+ key: "label",
86
+ className: "lc-label"
87
+ }, text) : null]);
88
+ }));
89
+ }
90
+ //#endregion
91
+ export { LiveCursors, useCursors };
package/dist/vue.d.mts ADDED
@@ -0,0 +1,60 @@
1
+ import { a as CursorTransport, c as RemoteCursor, n as CursorsOptions, t as CursorsInstance } from "./cursors-CyiNtLCv.mjs";
2
+ import { PropType, ShallowRef } from "vue";
3
+ //#region src/vue/index.d.ts
4
+ type MaybeGetter<T> = T | (() => T);
5
+ interface UseCursorsOptions extends Omit<CursorsOptions, "transport" | "space"> {
6
+ /** a factory keeps BroadcastChannel and WebSocket out of the server render */
7
+ transport: MaybeGetter<CursorTransport>;
8
+ space?: CursorsOptions["space"];
9
+ /** shorthand for `space: elementSpace(...)`; also the element pointer events bind to */
10
+ container?: MaybeGetter<HTMLElement | null | undefined>;
11
+ }
12
+ interface UseCursorsReturn {
13
+ cursors: ShallowRef<CursorsInstance | null>;
14
+ list: ShallowRef<readonly RemoteCursor[]>;
15
+ }
16
+ declare function useCursors(options: UseCursorsOptions): UseCursorsReturn;
17
+ declare const LiveCursors: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
18
+ cursors: {
19
+ type: PropType<CursorsInstance | null>;
20
+ default: null;
21
+ };
22
+ color: {
23
+ type: PropType<(cursor: RemoteCursor) => string>;
24
+ default: undefined;
25
+ };
26
+ label: {
27
+ type: PropType<(cursor: RemoteCursor) => string | null | undefined>;
28
+ default: undefined;
29
+ };
30
+ zIndex: {
31
+ type: NumberConstructor;
32
+ default: number;
33
+ };
34
+ }>, () => import("vue").VNode<import("vue").RendererNode, import("vue").RendererElement, {
35
+ [key: string]: any;
36
+ }> | null, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
37
+ cursors: {
38
+ type: PropType<CursorsInstance | null>;
39
+ default: null;
40
+ };
41
+ color: {
42
+ type: PropType<(cursor: RemoteCursor) => string>;
43
+ default: undefined;
44
+ };
45
+ label: {
46
+ type: PropType<(cursor: RemoteCursor) => string | null | undefined>;
47
+ default: undefined;
48
+ };
49
+ zIndex: {
50
+ type: NumberConstructor;
51
+ default: number;
52
+ };
53
+ }>> & Readonly<{}>, {
54
+ cursors: CursorsInstance | null;
55
+ color: (cursor: RemoteCursor) => string;
56
+ label: (cursor: RemoteCursor) => string | null | undefined;
57
+ zIndex: number;
58
+ }, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
59
+ //#endregion
60
+ export { LiveCursors, UseCursorsOptions, UseCursorsReturn, useCursors };
package/dist/vue.mjs ADDED
@@ -0,0 +1,144 @@
1
+ import { t as cursorColor } from "./color-CN1S_Vq9.mjs";
2
+ import { i as elementSpace, t as createCursors } from "./cursors-BhKZnRWg.mjs";
3
+ import { ARROW_SVG, ensureCursorStyles } from "./dom.mjs";
4
+ import { computed, defineComponent, h, onBeforeUnmount, onMounted, shallowRef, toValue, watch } from "vue";
5
+ //#region src/vue/index.ts
6
+ function useCursors(options) {
7
+ const cursors = shallowRef(null);
8
+ const list = shallowRef([]);
9
+ let unsubscribe = null;
10
+ onMounted(() => {
11
+ const { transport, container, space, ...rest } = options;
12
+ const resolveContainer = () => toValue(container) ?? null;
13
+ const instance = createCursors({
14
+ ...rest,
15
+ transport: typeof transport === "function" ? transport() : transport,
16
+ space: space ?? (container ? elementSpace(resolveContainer) : void 0)
17
+ });
18
+ cursors.value = instance;
19
+ unsubscribe = instance.subscribe(() => {
20
+ list.value = instance.getSnapshot();
21
+ });
22
+ if (container) watch(() => resolveContainer(), (el) => {
23
+ if (el) instance.attach(el);
24
+ }, {
25
+ immediate: true,
26
+ flush: "post"
27
+ });
28
+ else instance.attach();
29
+ });
30
+ onBeforeUnmount(() => {
31
+ unsubscribe?.();
32
+ cursors.value?.destroy();
33
+ cursors.value = null;
34
+ list.value = [];
35
+ });
36
+ return {
37
+ cursors,
38
+ list
39
+ };
40
+ }
41
+ const LiveCursors = defineComponent({
42
+ name: "LiveCursors",
43
+ props: {
44
+ cursors: {
45
+ type: Object,
46
+ default: null
47
+ },
48
+ color: {
49
+ type: Function,
50
+ default: void 0
51
+ },
52
+ label: {
53
+ type: Function,
54
+ default: void 0
55
+ },
56
+ zIndex: {
57
+ type: Number,
58
+ default: 10
59
+ }
60
+ },
61
+ setup(props, { slots }) {
62
+ const list = shallowRef([]);
63
+ const version = shallowRef(0);
64
+ let unsubscribe = null;
65
+ ensureCursorStyles();
66
+ watch(() => props.cursors, (instance) => {
67
+ unsubscribe?.();
68
+ unsubscribe = null;
69
+ list.value = [];
70
+ if (!instance) return;
71
+ unsubscribe = instance.subscribe(() => {
72
+ list.value = instance.getSnapshot();
73
+ version.value++;
74
+ });
75
+ }, { immediate: true });
76
+ onMounted(() => {
77
+ const bump = () => {
78
+ version.value++;
79
+ };
80
+ window.addEventListener("resize", bump, { passive: true });
81
+ const target = props.cursors?.space.target() ?? null;
82
+ const resizeObserver = target && typeof ResizeObserver !== "undefined" ? new ResizeObserver(bump) : null;
83
+ const mutationObserver = target && typeof MutationObserver !== "undefined" ? new MutationObserver(bump) : null;
84
+ if (target) {
85
+ resizeObserver?.observe(target);
86
+ mutationObserver?.observe(target, {
87
+ childList: true,
88
+ subtree: true,
89
+ characterData: true
90
+ });
91
+ }
92
+ onBeforeUnmount(() => {
93
+ window.removeEventListener("resize", bump);
94
+ resizeObserver?.disconnect();
95
+ mutationObserver?.disconnect();
96
+ });
97
+ });
98
+ onBeforeUnmount(() => unsubscribe?.());
99
+ const tint = (cursor) => props.color?.(cursor) ?? cursorColor(cursor.id);
100
+ const overlaySize = computed(() => {
101
+ version.value;
102
+ return props.cursors?.space.size() ?? {
103
+ width: 0,
104
+ height: 0
105
+ };
106
+ });
107
+ const isFixed = computed(() => props.cursors?.space.target() === null);
108
+ return () => {
109
+ const instance = props.cursors;
110
+ if (!instance) return null;
111
+ return h("div", {
112
+ "class": "lc-overlay",
113
+ "aria-hidden": "true",
114
+ "data-fixed": isFixed.value ? "true" : void 0,
115
+ "style": isFixed.value ? { zIndex: props.zIndex } : {
116
+ zIndex: props.zIndex,
117
+ width: `${overlaySize.value.width}px`,
118
+ height: `${overlaySize.value.height}px`
119
+ }
120
+ }, list.value.map((cursor) => {
121
+ version.value;
122
+ const point = instance.space.fromNormalized(cursor);
123
+ const color = tint(cursor);
124
+ const text = props.label?.(cursor);
125
+ return h("div", {
126
+ key: cursor.id,
127
+ class: "lc-cursor",
128
+ style: {
129
+ "transform": `translate(${point.x}px, ${point.y}px)`,
130
+ "--lc-color": color
131
+ }
132
+ }, slots.default ? slots.default({
133
+ cursor,
134
+ color
135
+ }) : [h("span", { innerHTML: ARROW_SVG }), slots.label ? h("span", { class: "lc-label" }, slots.label({
136
+ cursor,
137
+ color
138
+ })) : text ? h("span", { class: "lc-label" }, text) : null]);
139
+ }));
140
+ };
141
+ }
142
+ });
143
+ //#endregion
144
+ export { LiveCursors, useCursors };
@@ -0,0 +1,20 @@
1
+ import { a as CursorTransport } from "./cursors-CyiNtLCv.mjs";
2
+ //#region src/transports/websocket.d.ts
3
+ interface WebSocketTransportOptions {
4
+ url: string | (() => string);
5
+ /** sent as `{ room, ...message }` so a single endpoint can host many rooms */
6
+ room?: string;
7
+ protocols?: string | string[];
8
+ reconnect?: boolean;
9
+ /** first retry delay; doubles up to maxDelayMs */
10
+ reconnectDelayMs?: number;
11
+ maxDelayMs?: number;
12
+ onError?: (error: unknown) => void;
13
+ }
14
+ /**
15
+ * Raw WebSocket transport. Cursor updates are disposable, so nothing is buffered
16
+ * while the socket is down — the next pointer move takes over.
17
+ */
18
+ declare function websocketTransport(options: WebSocketTransportOptions): CursorTransport;
19
+ //#endregion
20
+ export { WebSocketTransportOptions, websocketTransport };