— Share button with dropdown panel.
+ *
+ * Shows a share icon in the header. Click opens a dropdown with:
+ * - QR code for the current page URL
+ * - Copyable link input
+ * - Email invite form (POST /api/spaces/:slug/invite)
+ *
+ * Attributes:
+ * share-url — Override the URL to share (defaults to window.location.href)
+ */
+
+export class RStackSharePanel extends HTMLElement {
+ #shadow: ShadowRoot;
+ #open = false;
+
+ constructor() {
+ super();
+ this.#shadow = this.attachShadow({ mode: "open" });
+ }
+
+ connectedCallback() {
+ this.#render();
+ }
+
+ get #shareUrl(): string {
+ return this.getAttribute("share-url") || window.location.href;
+ }
+
+ get #spaceSlug(): string {
+ return document.body?.dataset?.spaceSlug || "";
+ }
+
+ #togglePanel() {
+ this.#open = !this.#open;
+ this.#render();
+ }
+
+ async #copyUrl() {
+ try {
+ await navigator.clipboard.writeText(this.#shareUrl);
+ const btn = this.#shadow.getElementById("copy-btn");
+ if (btn) {
+ btn.textContent = "Copied!";
+ setTimeout(() => { btn.textContent = "Copy"; }, 2000);
+ }
+ } catch { /* clipboard unavailable */ }
+ }
+
+ async #sendInvite() {
+ const input = this.#shadow.getElementById("email-input") as HTMLInputElement;
+ const status = this.#shadow.getElementById("email-status");
+ if (!input || !status) return;
+
+ const email = input.value.trim();
+ if (!email) return;
+
+ const slug = this.#spaceSlug;
+ if (!slug) {
+ status.textContent = "No space context";
+ status.style.color = "#ef4444";
+ setTimeout(() => { status.textContent = ""; }, 3000);
+ return;
+ }
+
+ status.textContent = "Sending...";
+ status.style.color = "";
+ try {
+ const res = await fetch(`/api/spaces/${slug}/invite`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ email, shareUrl: this.#shareUrl }),
+ });
+ if (res.ok) {
+ status.textContent = "Invite sent!";
+ status.style.color = "#10b981";
+ input.value = "";
+ } else {
+ status.textContent = "Failed to send";
+ status.style.color = "#ef4444";
+ }
+ } catch {
+ status.textContent = "Failed to send";
+ status.style.color = "#ef4444";
+ }
+ setTimeout(() => { status.textContent = ""; }, 4000);
+ }
+
+ #render() {
+ const url = this.#shareUrl;
+ const qrSrc = `https://api.qrserver.com/v1/create-qr-code/?size=180x180&data=${encodeURIComponent(url)}`;
+
+ let panelHTML = "";
+ if (this.#open) {
+ panelHTML = `
+
+
+
+
+

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `;
+ }
+
+ this.#shadow.innerHTML = `
+
+
+
+ ${panelHTML}
+
+ `;
+
+ // ── Event listeners ──
+ this.#shadow.getElementById("share-toggle")?.addEventListener("click", (e) => {
+ e.stopPropagation();
+ this.#togglePanel();
+ });
+
+ this.#shadow.getElementById("close-btn")?.addEventListener("click", (e) => {
+ e.stopPropagation();
+ this.#open = false;
+ this.#render();
+ });
+
+ this.#shadow.getElementById("copy-btn")?.addEventListener("click", (e) => {
+ e.stopPropagation();
+ this.#copyUrl();
+ });
+
+ this.#shadow.getElementById("send-btn")?.addEventListener("click", (e) => {
+ e.stopPropagation();
+ this.#sendInvite();
+ });
+
+ // Enter key on email input triggers send
+ this.#shadow.getElementById("email-input")?.addEventListener("keydown", (e) => {
+ if ((e as KeyboardEvent).key === "Enter") {
+ e.stopPropagation();
+ this.#sendInvite();
+ }
+ });
+
+ // Stop propagation from panel clicks
+ this.#shadow.querySelector(".panel")?.addEventListener("click", (e) => e.stopPropagation());
+
+ // Close on outside click
+ if (this.#open) {
+ document.addEventListener("click", () => {
+ if (this.#open) {
+ this.#open = false;
+ this.#render();
+ }
+ }, { once: true });
+ }
+ }
+
+ static define(tag = "rstack-share-panel") {
+ if (!customElements.get(tag)) customElements.define(tag, RStackSharePanel);
+ }
+}
+
+// ============================================================================
+// STYLES
+// ============================================================================
+
+const STYLES = `
+:host {
+ display: inline-flex;
+ align-items: center;
+}
+
+.share-wrapper {
+ position: relative;
+}
+
+.share-btn {
+ background: none;
+ border: none;
+ color: var(--rs-text-muted, #94a3b8);
+ cursor: pointer;
+ padding: 6px;
+ border-radius: 8px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: color 0.15s, background 0.15s;
+}
+.share-btn:hover {
+ color: var(--rs-text-primary, #e2e8f0);
+ background: var(--rs-bg-hover, rgba(255,255,255,0.05));
+}
+
+.panel {
+ position: absolute;
+ top: 100%;
+ right: 0;
+ margin-top: 8px;
+ width: 320px;
+ border-radius: 10px;
+ background: var(--rs-bg-surface, #1e293b);
+ border: 1px solid var(--rs-border, rgba(255,255,255,0.1));
+ box-shadow: var(--rs-shadow-lg, 0 8px 30px rgba(0,0,0,0.3));
+ z-index: 200;
+ overflow: hidden;
+}
+
+.panel-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 12px 16px;
+ border-bottom: 1px solid var(--rs-border, rgba(255,255,255,0.1));
+}
+
+.panel-title {
+ font-size: 0.875rem;
+ font-weight: 600;
+ color: var(--rs-text-primary, #e2e8f0);
+}
+
+.close-btn {
+ background: none;
+ border: none;
+ font-size: 18px;
+ color: var(--rs-text-muted, #94a3b8);
+ cursor: pointer;
+ padding: 0 4px;
+ line-height: 1;
+}
+.close-btn:hover {
+ color: var(--rs-text-primary, #e2e8f0);
+}
+
+.panel-body {
+ padding: 0;
+}
+
+.section {
+ padding: 12px 16px;
+ border-bottom: 1px solid var(--rs-border, rgba(255,255,255,0.1));
+}
+.section:last-child {
+ border-bottom: none;
+}
+
+.qr-section {
+ text-align: center;
+}
+.qr-section img {
+ border-radius: 8px;
+}
+
+.link-row {
+ display: flex;
+ gap: 8px;
+ align-items: center;
+}
+
+.link-row input {
+ flex: 1;
+ padding: 6px 10px;
+ border: 1px solid var(--rs-border, rgba(255,255,255,0.1));
+ border-radius: 6px;
+ font-size: 12px;
+ color: var(--rs-text-primary, #e2e8f0);
+ background: var(--rs-bg-hover, rgba(255,255,255,0.05));
+ outline: none;
+ min-width: 0;
+}
+.link-row input:focus {
+ border-color: #14b8a6;
+}
+
+label {
+ display: block;
+ font-size: 12px;
+ color: var(--rs-text-muted, #94a3b8);
+ margin-bottom: 8px;
+}
+
+.email-row {
+ display: flex;
+ gap: 8px;
+ align-items: center;
+}
+
+.email-row input {
+ flex: 1;
+ padding: 6px 10px;
+ border: 1px solid var(--rs-border, rgba(255,255,255,0.1));
+ border-radius: 6px;
+ font-size: 12px;
+ color: var(--rs-text-primary, #e2e8f0);
+ background: var(--rs-bg-hover, rgba(255,255,255,0.05));
+ outline: none;
+ min-width: 0;
+}
+.email-row input:focus {
+ border-color: #14b8a6;
+}
+
+.email-status {
+ font-size: 11px;
+ margin-top: 6px;
+ min-height: 16px;
+}
+
+.btn-teal {
+ padding: 6px 14px;
+ border: none;
+ border-radius: 6px;
+ background: #14b8a6;
+ color: white;
+ font-size: 12px;
+ font-weight: 600;
+ cursor: pointer;
+ white-space: nowrap;
+ transition: background 0.15s;
+}
+.btn-teal:hover {
+ background: #0d9488;
+}
+
+.btn-blue {
+ padding: 6px 14px;
+ border: none;
+ border-radius: 6px;
+ background: #3b82f6;
+ color: white;
+ font-size: 12px;
+ font-weight: 600;
+ cursor: pointer;
+ white-space: nowrap;
+ transition: background 0.15s;
+}
+.btn-blue:hover {
+ background: #2563eb;
+}
+`;
diff --git a/website/canvas.html b/website/canvas.html
index d0e5e69..a10904c 100644
--- a/website/canvas.html
+++ b/website/canvas.html
@@ -940,161 +940,6 @@
touch-action: none; /* Prevent browser gestures, handle manually */
}
- /* ── Share badge & panel ── */
- /* share-badge is now a header icon button — see .rstack-header__share-btn in shell.css overrides below */
-
- #share-panel {
- position: fixed;
- top: 56px;
- right: 60px;
- width: 320px;
- background: var(--rs-bg-surface);
- border-radius: 12px;
- box-shadow: var(--rs-shadow-lg);
- z-index: 10001;
- display: none;
- overflow: hidden;
- }
-
- #share-panel.open {
- display: block;
- }
-
- #share-panel-header {
- padding: 12px 16px;
- border-bottom: 1px solid var(--rs-toolbar-panel-border);
- display: flex;
- align-items: center;
- justify-content: space-between;
- }
-
- #share-panel-header h3 {
- font-size: 14px;
- color: var(--rs-text-primary);
- margin: 0;
- }
-
- #share-panel-close {
- background: none;
- border: none;
- font-size: 18px;
- color: var(--rs-text-secondary);
- cursor: pointer;
- padding: 0 4px;
- line-height: 1;
- }
-
- #share-panel-close:hover {
- color: var(--rs-text-primary);
- }
-
- #share-panel-body {
- padding: 0;
- }
-
- .share-section {
- padding: 12px 16px;
- border-bottom: 1px solid var(--rs-border-subtle);
- }
-
- .share-section:last-child {
- border-bottom: none;
- }
-
- #share-qr {
- display: block;
- margin: 0 auto;
- border-radius: 8px;
- }
-
- .share-link-row {
- display: flex;
- gap: 8px;
- align-items: center;
- }
-
- .share-link-row input {
- flex: 1;
- padding: 6px 10px;
- border: 1px solid var(--rs-input-border);
- border-radius: 6px;
- font-size: 12px;
- color: var(--rs-input-text);
- background: var(--rs-input-bg);
- outline: none;
- }
-
- .share-link-row input:focus {
- border-color: #14b8a6;
- }
-
- #share-copy-btn {
- padding: 6px 14px;
- border: none;
- border-radius: 6px;
- background: #14b8a6;
- color: white;
- font-size: 12px;
- font-weight: 600;
- cursor: pointer;
- white-space: nowrap;
- transition: background 0.15s;
- }
-
- #share-copy-btn:hover {
- background: #0d9488;
- }
-
- .share-section label {
- display: block;
- font-size: 12px;
- color: var(--rs-text-muted);
- margin-bottom: 8px;
- }
-
- .share-email-row {
- display: flex;
- gap: 8px;
- align-items: center;
- }
-
- .share-email-row input {
- flex: 1;
- padding: 6px 10px;
- border: 1px solid var(--rs-input-border);
- border-radius: 6px;
- font-size: 12px;
- color: var(--rs-input-text);
- background: var(--rs-bg-surface);
- outline: none;
- }
-
- .share-email-row input:focus {
- border-color: #14b8a6;
- }
-
- #share-send-btn {
- padding: 6px 14px;
- border: none;
- border-radius: 6px;
- background: #3b82f6;
- color: white;
- font-size: 12px;
- font-weight: 600;
- cursor: pointer;
- white-space: nowrap;
- transition: background 0.15s;
- }
-
- #share-send-btn:hover {
- background: #2563eb;
- }
-
- #share-email-status {
- font-size: 11px;
- margin-top: 6px;
- min-height: 16px;
- }
/* ── People Online badge ── */
#people-online-badge {
@@ -1377,12 +1222,8 @@
/* Dark/light mode handled by CSS custom properties in theme.css */
- /* ── Share & People panel mobile ── */
+ /* ── People panel mobile ── */
@media (max-width: 640px) {
- #share-panel {
- width: calc(100vw - 32px);
- right: 16px;
- }
#people-online-badge {
right: 16px;
bottom: 12px;
@@ -2110,7 +1951,8 @@
@@ -2416,29 +2258,6 @@
-
-
-
-
-
![QR Code]()
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
@@ -2659,6 +2478,8 @@
onArrowRemoved,
} from "@lib";
import { RStackIdentity } from "@shared/components/rstack-identity";
+ import { RStackNotificationBell } from "@shared/components/rstack-notification-bell";
+ import { RStackSharePanel } from "@shared/components/rstack-share-panel";
import { RStackAppSwitcher } from "@shared/components/rstack-app-switcher";
import { RStackSpaceSwitcher } from "@shared/components/rstack-space-switcher";
import { RStackTabBar } from "@shared/components/rstack-tab-bar";
@@ -2672,6 +2493,8 @@
// Register shell header components
RStackIdentity.define();
+ RStackNotificationBell.define();
+ RStackSharePanel.define();
RStackAppSwitcher.define();
RStackSpaceSwitcher.define();
RStackTabBar.define();
@@ -3538,80 +3361,6 @@
}
});
- // ── Share panel ──
- const sharePanel = document.getElementById("share-panel");
- const shareBadge = document.getElementById("share-badge");
-
- function getShareUrl() {
- const proto = window.location.protocol;
- const host = window.location.host.split(":")[0];
- if (host.endsWith("rspace.online") && host.split(".").length >= 3) {
- return `${proto}//${host}/rspace`;
- }
- return `${proto}//${window.location.host}/${communitySlug}/rspace`;
- }
-
- if (shareBadge) {
- shareBadge.addEventListener("click", () => {
- const isOpen = sharePanel.classList.toggle("open");
- if (isOpen) {
- // Close people panel if open
- peoplePanel.classList.remove("open");
- const url = getShareUrl();
- document.getElementById("share-url").value = url;
- document.getElementById("share-qr").src =
- `https://api.qrserver.com/v1/create-qr-code/?size=180x180&data=${encodeURIComponent(url)}`;
- }
- });
- }
-
- document.getElementById("share-panel-close")?.addEventListener("click", () => {
- sharePanel.classList.remove("open");
- });
-
- document.getElementById("share-copy-btn")?.addEventListener("click", async () => {
- const url = document.getElementById("share-url").value;
- await navigator.clipboard.writeText(url);
- const btn = document.getElementById("share-copy-btn");
- btn.textContent = "Copied!";
- setTimeout(() => btn.textContent = "Copy", 2000);
- });
-
- document.getElementById("share-send-btn")?.addEventListener("click", async () => {
- const email = document.getElementById("share-email").value.trim();
- const status = document.getElementById("share-email-status");
- if (!email) return;
- status.textContent = "Sending...";
- status.style.color = "";
- try {
- const res = await fetch(`/api/spaces/${communitySlug}/invite`, {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ email, shareUrl: getShareUrl() }),
- });
- if (res.ok) {
- status.textContent = "Invite sent!";
- status.style.color = "#10b981";
- document.getElementById("share-email").value = "";
- } else {
- status.textContent = "Failed to send";
- status.style.color = "#ef4444";
- }
- } catch {
- status.textContent = "Failed to send";
- status.style.color = "#ef4444";
- }
- setTimeout(() => status.textContent = "", 4000);
- });
-
- // Click-outside closes share panel
- document.addEventListener("click", (e) => {
- if (sharePanel?.classList.contains("open") &&
- !sharePanel.contains(e.target) &&
- (!shareBadge || !shareBadge.contains(e.target))) {
- sharePanel.classList.remove("open");
- }
- });
function navigateToPeer(cursor) {
const rect = canvas.getBoundingClientRect();
diff --git a/website/shell.ts b/website/shell.ts
index 3fe60e8..25da0dd 100644
--- a/website/shell.ts
+++ b/website/shell.ts
@@ -17,6 +17,7 @@ import { RStackSpaceSettings } from "../shared/components/rstack-space-settings"
import { RStackModuleSetup } from "../shared/components/rstack-module-setup";
import { RStackHistoryPanel } from "../shared/components/rstack-history-panel";
import { RStackOfflineIndicator } from "../shared/components/rstack-offline-indicator";
+import { RStackSharePanel } from "../shared/components/rstack-share-panel";
import { RStackCollabOverlay } from "../shared/components/rstack-collab-overlay";
import { RStackUserDashboard } from "../shared/components/rstack-user-dashboard";
import { rspaceNavUrl } from "../shared/url-helpers";
@@ -40,6 +41,7 @@ RStackSpaceSettings.define();
RStackModuleSetup.define();
RStackHistoryPanel.define();
RStackOfflineIndicator.define();
+RStackSharePanel.define();
RStackCollabOverlay.define();
RStackUserDashboard.define();