/** * — 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 = `
Share
QR Code
`; } 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; } `;