diff --git a/shared/components/rstack-comment-bell.ts b/shared/components/rstack-comment-bell.ts index e222a4f..b700929 100644 --- a/shared/components/rstack-comment-bell.ts +++ b/shared/components/rstack-comment-bell.ts @@ -17,6 +17,7 @@ export class RStackCommentBell extends HTMLElement { #shadow: ShadowRoot; #count = 0; #pollTimer: ReturnType | null = null; + #syncRef: any = null; constructor() { super(); @@ -25,27 +26,42 @@ export class RStackCommentBell extends HTMLElement { connectedCallback() { this.#render(); + // Try to pick up existing CommunitySync + this.#syncRef = (window as any).__communitySync || null; this.#refreshCount(); this.#pollTimer = setInterval(() => this.#refreshCount(), POLL_INTERVAL); window.addEventListener("comment-pins-changed", this.#onPinsChanged); + // Listen for CommunitySync becoming available (may connect after mount) + document.addEventListener("community-sync-ready", this.#onSyncReady); } disconnectedCallback() { if (this.#pollTimer) clearInterval(this.#pollTimer); window.removeEventListener("comment-pins-changed", this.#onPinsChanged); + document.removeEventListener("community-sync-ready", this.#onSyncReady); } + #onSyncReady = (e: Event) => { + const sync = (e as CustomEvent).detail?.sync; + if (sync) { + this.#syncRef = sync; + this.#refreshCount(); + } + }; + #onPinsChanged = () => { this.#refreshCount(); }; #refreshCount() { - const sync = (window as any).__communitySync; + // Prefer cached ref, fall back to global + const sync = this.#syncRef || (window as any).__communitySync; + if (sync && !this.#syncRef) this.#syncRef = sync; const pins = sync?.doc?.commentPins; if (!pins) { if (this.#count !== 0) { this.#count = 0; - this.#render(); + this.#updateBadge(); } return; } @@ -54,23 +70,19 @@ export class RStackCommentBell extends HTMLElement { ).length; if (newCount !== this.#count) { this.#count = newCount; - this.#render(); + this.#updateBadge(); } } + /** Full render — only called once in connectedCallback. */ #render() { - const badge = - this.#count > 0 - ? `${this.#count > 99 ? "99+" : this.#count}` - : ""; - this.#shadow.innerHTML = ` `; @@ -82,6 +94,18 @@ export class RStackCommentBell extends HTMLElement { }); } + /** Lightweight badge update — no innerHTML churn. */ + #updateBadge() { + const badge = this.#shadow.querySelector(".badge") as HTMLElement | null; + if (!badge) return; + if (this.#count > 0) { + badge.textContent = this.#count > 99 ? "99+" : String(this.#count); + badge.style.display = ""; + } else { + badge.style.display = "none"; + } + } + static define(tag = "rstack-comment-bell") { if (!customElements.get(tag)) customElements.define(tag, RStackCommentBell); } diff --git a/shared/components/rstack-history-panel.ts b/shared/components/rstack-history-panel.ts index bf07cbe..9fd70a8 100644 --- a/shared/components/rstack-history-panel.ts +++ b/shared/components/rstack-history-panel.ts @@ -58,20 +58,49 @@ export class RStackHistoryPanel extends HTMLElement { } }; + private _syncReadyHandler = (e: Event) => { + const sync = (e as CustomEvent).detail?.sync; + if (sync?.doc) { + this._doc = sync.doc; + if (this._open) { + this._refreshHistory(); + this._render(); + } + } + }; + connectedCallback() { if (!this.shadowRoot) this.attachShadow({ mode: "open" }); this._render(); + // Pick up CommunitySync when it connects (may be after mount) + document.addEventListener("community-sync-ready", this._syncReadyHandler); } disconnectedCallback() { document.removeEventListener("click", this._clickOutsideHandler, true); + document.removeEventListener("community-sync-ready", this._syncReadyHandler); } open() { this._open = true; + // Lazy doc acquisition: try CommunitySync global first, then offline runtime + if (!this._doc) { + const sync = (window as any).__communitySync; + if (sync?.doc) { + this._doc = sync.doc; + } else { + const rt = (window as any).__rspaceOfflineRuntime; + if (rt?.documentManager) { + const docs = rt.documentManager.listAll?.() || []; + if (docs.length > 0) { + const doc = rt.documentManager.get(docs[0]); + if (doc) this._doc = doc; + } + } + } + } this._refreshHistory(); this._render(); - this._positionPanel(); document.getElementById("history-btn")?.classList.add("active"); document.addEventListener("click", this._clickOutsideHandler, true); } @@ -88,13 +117,16 @@ export class RStackHistoryPanel extends HTMLElement { if (this._open) this.close(); else this.open(); } - /** Position the panel below the history button, right-aligned to button. */ + /** Position the panel below the history button, right-aligned to button, clamped to viewport. */ private _positionPanel() { const panel = this.shadowRoot?.querySelector(".panel") as HTMLElement | null; const btn = document.getElementById("history-btn"); if (!panel || !btn) return; const rect = btn.getBoundingClientRect(); - panel.style.top = `${rect.bottom + 6}px`; + const topPos = rect.bottom + 6; + panel.style.top = `${topPos}px`; + // Clamp max-height so panel never overflows bottom of viewport + panel.style.maxHeight = `${window.innerHeight - topPos - 8}px`; // Right-align to button's right edge, but clamp so left edge stays on screen const rightOffset = window.innerWidth - rect.right; panel.style.right = `${rightOffset}px`; @@ -369,6 +401,8 @@ export class RStackHistoryPanel extends HTMLElement { `; this._bindEvents(); + // Re-position after every render since innerHTML replacement destroys inline styles + if (this._open) this._positionPanel(); } private _renderTimeMachineDetails() { diff --git a/shared/components/rstack-space-settings.ts b/shared/components/rstack-space-settings.ts index 2d96456..e4561b4 100644 --- a/shared/components/rstack-space-settings.ts +++ b/shared/components/rstack-space-settings.ts @@ -115,22 +115,25 @@ export class RStackSpaceSettings extends HTMLElement { if (this._open) this.close(); else this.open(); } - /** Position the panel below the settings button, left-aligned with screen clamping. */ + /** Position the panel below the settings button, clamped to viewport. */ private _positionPanel() { const panel = this.shadowRoot?.querySelector(".panel") as HTMLElement | null; const btn = document.getElementById("settings-btn"); if (!panel || !btn) return; const rect = btn.getBoundingClientRect(); const panelWidth = Math.min(380, window.innerWidth * 0.9); + const topPos = rect.bottom + 6; // Align panel's left edge with the button, but clamp so it stays on screen let left = rect.left; if (left + panelWidth > window.innerWidth - 8) { left = window.innerWidth - panelWidth - 8; } if (left < 8) left = 8; - panel.style.top = `${rect.bottom + 6}px`; + panel.style.top = `${topPos}px`; panel.style.left = `${left}px`; panel.style.right = "auto"; + // Clamp max-height so panel never overflows bottom of viewport + panel.style.maxHeight = `${window.innerHeight - topPos - 8}px`; } private async _loadData() { @@ -140,7 +143,7 @@ export class RStackSpaceSettings extends HTMLElement { const token = getToken(); // Load community data from WS-synced doc (via global) - const sync = (window as any).__rspaceCommunitySync; + const sync = (window as any).__communitySync; if (sync?.doc) { const data = sync.doc; this._ownerDID = data.meta?.ownerDID || ""; @@ -297,6 +300,9 @@ export class RStackSpaceSettings extends HTMLElement { return; } + // Track whether this is a re-render (panel already exists) + const isRerender = !!this.shadowRoot.querySelector(".panel"); + const roleOptions = (currentRole: string) => { const roles = ["viewer", "member", "moderator", "admin"]; return roles.map(r => ``).join(""); @@ -469,6 +475,8 @@ export class RStackSpaceSettings extends HTMLElement { `; this._bindEvents(); + // Re-position after every render since innerHTML replacement destroys inline styles + this._positionPanel(); } private _bindEvents() {