diff --git a/modules/rdocs/components/folk-docs-app.ts b/modules/rdocs/components/folk-docs-app.ts index e2929980..6ddb0d89 100644 --- a/modules/rdocs/components/folk-docs-app.ts +++ b/modules/rdocs/components/folk-docs-app.ts @@ -690,6 +690,8 @@ Gear: EUR 400 (10%)

Maya is tracking expenses in rF this.doc = null; for (const unsub of this._offlineNotebookUnsubs) unsub(); this._offlineNotebookUnsubs = []; + // Clear viewId so overlay stops filtering cursors + window.dispatchEvent(new CustomEvent('rspace-view-change', { detail: { viewId: null } })); } /** Extract notebook + notes from Automerge doc into component state */ @@ -1477,6 +1479,10 @@ Gear: EUR 400 (10%)

Maya is tracking expenses in rF this.renderMeta(); this.mountEditor(this.selectedNote); this.broadcastPresence(); + // Notify overlay which sub-document we're viewing (scopes SVG cursors) + window.dispatchEvent(new CustomEvent('rspace-view-change', { + detail: { viewId: this.selectedNote.id, viewLabel: this.selectedNote.title }, + })); } } diff --git a/shared/components/rstack-collab-overlay.ts b/shared/components/rstack-collab-overlay.ts index 03115141..56344859 100644 --- a/shared/components/rstack-collab-overlay.ts +++ b/shared/components/rstack-collab-overlay.ts @@ -34,6 +34,7 @@ interface PeerState { lastSeen: number; module?: string; // which rApp they're in context?: string; // human-readable view label (e.g. "My Notebook > Note Title") + viewId?: string | null; // sub-document view (e.g. noteId) — cursors hidden when mismatched } export class RStackCollabOverlay extends HTMLElement { @@ -60,6 +61,7 @@ export class RStackCollabOverlay extends HTMLElement { #openActionsId: string | null = null; // which peer's actions dropdown is open #spaceMembers: { did: string; displayName: string; role: string }[] = []; #space: string | null = null; + #viewId: string | null = null; // sub-document view filter (e.g. noteId) constructor() { super(); @@ -102,12 +104,16 @@ export class RStackCollabOverlay extends HTMLElement { this.#tryConnect(); } + // Listen for sub-document view changes (e.g. rDocs note navigation) + window.addEventListener('rspace-view-change', this.#onViewChange); + // Click-outside closes panel (listen on document, check composedPath for shadow DOM) document.addEventListener('click', this.#onDocumentClick); } disconnectedCallback() { document.removeEventListener('click', this.#onDocumentClick); + window.removeEventListener('rspace-view-change', this.#onViewChange); if (!this.#externalPeers) { window.removeEventListener('rspace-doc-subscribe', this.#onDocSubscribe); this.#unsubAwareness?.(); @@ -171,6 +177,19 @@ export class RStackCollabOverlay extends HTMLElement { } }; + #onViewChange = (e: Event) => { + const { viewId } = (e as CustomEvent).detail ?? {}; + this.#viewId = viewId || null; + // Re-broadcast so peers learn our current viewId + this.#broadcastPresence(this.#lastCursor, undefined); + // Re-render cursors immediately (hides/shows based on new viewId) + if (!this.#badgeOnly) { + this.#renderCursors(); + this.#renderFocusRings(); + } + if (this.#panelOpen) this.#renderPanel(); + }; + // ── Runtime connection ── #runtimePollInterval: ReturnType | null = null; @@ -368,6 +387,7 @@ export class RStackCollabOverlay extends HTMLElement { cursor: msg.cursor ?? existing?.cursor ?? null, selection: msg.selection ?? existing?.selection ?? null, lastSeen: Date.now(), + viewId: msg.viewId ?? existing?.viewId ?? null, }; this.#peers.set(msg.peer, peer); this.#ensureGcTimer(); @@ -391,6 +411,7 @@ export class RStackCollabOverlay extends HTMLElement { selection, username: this.#localUsername, color: this.#localColor, + viewId: this.#viewId, }); } @@ -598,12 +619,13 @@ export class RStackCollabOverlay extends HTMLElement { if (peer.module) ctxParts.push(peer.module); if (peer.context) ctxParts.push(peer.context); const ctxStr = ctxParts.join(' · '); + const differentView = !!(this.#viewId && peer.viewId && this.#viewId !== peer.viewId); fragments.push(` -

+
${this.#escHtml(peer.username)} - ${ctxStr ? `${this.#escHtml(ctxStr)}` : ''} + ${ctxStr ? `${differentView ? '\u{1F4C4} ' : ''}${this.#escHtml(ctxStr)}` : ''}
${isCanvas ? `` : ''}
@@ -758,6 +780,8 @@ export class RStackCollabOverlay extends HTMLElement { for (const peer of this.#peers.values()) { if (!peer.cursor) continue; + // Skip peers viewing a different sub-document (e.g. different note in rDocs) + if (this.#viewId && peer.viewId && this.#viewId !== peer.viewId) continue; const age = now - peer.lastSeen; const opacity = age > 5000 ? 0.3 : 1; @@ -797,6 +821,8 @@ export class RStackCollabOverlay extends HTMLElement { for (const peer of this.#peers.values()) { if (!peer.selection) continue; + // Skip peers viewing a different sub-document + if (this.#viewId && peer.viewId && this.#viewId !== peer.viewId) continue; const target = this.#findCollabEl(peer.selection); if (!target) continue; @@ -1035,6 +1061,10 @@ const OVERLAY_CSS = ` color: var(--rs-text-muted, #64748b); } + .people-row.different-view { + opacity: 0.65; + } + /* ── Mode toggle (Online/Offline) ── */ .mode-toggle { diff --git a/shared/local-first/sync.ts b/shared/local-first/sync.ts index 7677613c..ab1670d0 100644 --- a/shared/local-first/sync.ts +++ b/shared/local-first/sync.ts @@ -49,6 +49,7 @@ export interface AwarenessMessage { selection?: string; username?: string; color?: string; + viewId?: string | null; } /** Client sends full encrypted Automerge binary for server-side opaque storage. */