feat(shell): add share button to global header with QR, copy link, email invite
Extract canvas inline share panel into reusable <rstack-share-panel> web component and add it to the shell header between notification bell and settings gear. Canvas now uses the component too, removing ~230 lines of inline HTML/CSS/JS. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
e5814b12c6
commit
9b81ba70b6
|
|
@ -186,6 +186,7 @@ export function renderShell(opts: ShellOptions): string {
|
|||
<a class="rstack-header__demo-btn" ${spaceSlug === "demo" ? 'data-hide' : ''} href="${shellDemoUrl}">Try Demo</a>
|
||||
<rstack-offline-indicator></rstack-offline-indicator>
|
||||
<rstack-notification-bell></rstack-notification-bell>
|
||||
<rstack-share-panel></rstack-share-panel>
|
||||
<button class="rstack-header__settings-btn" id="settings-btn" title="Space Settings"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg></button>
|
||||
<rstack-identity></rstack-identity>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,362 @@
|
|||
/**
|
||||
* <rstack-share-panel> — 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 = `
|
||||
<div class="panel">
|
||||
<div class="panel-header">
|
||||
<span class="panel-title">Share</span>
|
||||
<button class="close-btn" id="close-btn">×</button>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="section qr-section">
|
||||
<img src="${qrSrc}" width="180" height="180" alt="QR Code">
|
||||
</div>
|
||||
<div class="section link-row">
|
||||
<input id="url-input" type="text" readonly value="${url.replace(/"/g, """)}">
|
||||
<button id="copy-btn" class="btn-teal">Copy</button>
|
||||
</div>
|
||||
<div class="section">
|
||||
<label>Invite by email</label>
|
||||
<div class="email-row">
|
||||
<input id="email-input" type="email" placeholder="friend@example.com">
|
||||
<button id="send-btn" class="btn-blue">Send</button>
|
||||
</div>
|
||||
<div id="email-status" class="email-status"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
this.#shadow.innerHTML = `
|
||||
<style>${STYLES}</style>
|
||||
<div class="share-wrapper">
|
||||
<button class="share-btn" id="share-toggle" aria-label="Share this page">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8"/>
|
||||
<polyline points="16 6 12 2 8 6"/>
|
||||
<line x1="12" y1="2" x2="12" y2="15"/>
|
||||
</svg>
|
||||
</button>
|
||||
${panelHTML}
|
||||
</div>
|
||||
`;
|
||||
|
||||
// ── 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;
|
||||
}
|
||||
`;
|
||||
|
|
@ -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 @@
|
|||
</div>
|
||||
<div class="rstack-header__right">
|
||||
<a class="rstack-header__demo-btn" href="https://demo.rspace.online/rspace">Try Demo</a>
|
||||
<button class="rstack-header__settings-btn" id="share-badge" title="Share this space"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8"/><polyline points="16 6 12 2 8 6"/><line x1="12" y1="2" x2="12" y2="15"/></svg></button>
|
||||
<rstack-notification-bell></rstack-notification-bell>
|
||||
<rstack-share-panel></rstack-share-panel>
|
||||
<button class="rstack-header__settings-btn" id="settings-btn" title="Space Settings"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg></button>
|
||||
<rstack-identity></rstack-identity>
|
||||
</div>
|
||||
|
|
@ -2416,29 +2258,6 @@
|
|||
<button id="mp-notify-switch">Switch to Multiplayer</button>
|
||||
</div>
|
||||
|
||||
<div id="share-panel">
|
||||
<div id="share-panel-header">
|
||||
<h3>Share Space</h3>
|
||||
<button id="share-panel-close">×</button>
|
||||
</div>
|
||||
<div id="share-panel-body">
|
||||
<div class="share-section">
|
||||
<img id="share-qr" width="180" height="180" alt="QR Code">
|
||||
</div>
|
||||
<div class="share-section share-link-row">
|
||||
<input id="share-url" type="text" readonly>
|
||||
<button id="share-copy-btn">Copy</button>
|
||||
</div>
|
||||
<div class="share-section">
|
||||
<label>Invite by email</label>
|
||||
<div class="share-email-row">
|
||||
<input id="share-email" type="email" placeholder="friend@example.com">
|
||||
<button id="share-send-btn">Send</button>
|
||||
</div>
|
||||
<div id="share-email-status"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="ping-toast">
|
||||
<span id="ping-toast-text"></span>
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue