Merge branch 'dev'
CI/CD / deploy (push) Successful in 2m25s Details

This commit is contained in:
Jeff Emmett 2026-04-13 11:17:39 -04:00
commit 513096a32e
3 changed files with 39 additions and 2 deletions

View File

@ -690,6 +690,8 @@ Gear: EUR 400 (10%)</code></pre><p><em>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%)</code></pre><p><em>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 },
}));
}
}

View File

@ -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<typeof setInterval> | 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(`
<div class="people-row">
<div class="people-row${differentView ? ' different-view' : ''}">
<span class="dot" style="background:${peer.color}"></span>
<div class="name-block">
<span class="name">${this.#escHtml(peer.username)}</span>
${ctxStr ? `<span class="peer-context">${this.#escHtml(ctxStr)}</span>` : ''}
${ctxStr ? `<span class="peer-context">${differentView ? '\u{1F4C4} ' : ''}${this.#escHtml(ctxStr)}</span>` : ''}
</div>
${isCanvas ? `<button class="actions-btn" data-pid="${this.#escHtml(peer.peerId)}">&gt;</button>` : ''}
</div>
@ -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 {

View File

@ -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. */