refactor: consolidate space settings — both gear icons open same tabbed modal
Remove rstack-space-settings slide-out panel. Enhance the existing tabbed modal in rstack-space-switcher with add-member (username search + email invite) and pending email invites (with revoke). Header gear now calls openSettingsModal() on the space switcher instead of toggling the old panel. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
2cbff8925d
commit
690e4dedb4
|
|
@ -85,7 +85,6 @@ function extractCanvasContent(html: string): { body: string; styles: string; scr
|
|||
// space-settings, history-panel, and welcome overlay.
|
||||
bodyContent = bodyContent
|
||||
.replace(/<header[\s\S]*?<\/header>/i, "")
|
||||
.replace(/<rstack-space-settings[^>]*><\/rstack-space-settings>/gi, "")
|
||||
.replace(/<rstack-history-panel[^>]*><\/rstack-history-panel>/gi, "")
|
||||
.replace(/<!--\s*Welcome overlay[\s\S]*?<\/div>\s*<\/div>\s*<\/div>/i, "");
|
||||
// Strip the rstack-tab-row div (nested divs make simple regex unreliable)
|
||||
|
|
|
|||
|
|
@ -132,7 +132,7 @@ export class RStackAppSwitcher extends HTMLElement {
|
|||
#isOpen = false;
|
||||
#catalogOpen = false;
|
||||
#catalogBusy = false;
|
||||
#outsideClickHandler: ((e: MouseEvent) => void) | null = null;
|
||||
#outsideClickHandler: ((e: PointerEvent) => void) | null = null;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
|
@ -173,7 +173,7 @@ export class RStackAppSwitcher extends HTMLElement {
|
|||
// Clean up body class and outside-click listener on removal
|
||||
document.body.classList.remove("rstack-sidebar-open");
|
||||
if (this.#outsideClickHandler) {
|
||||
document.removeEventListener("click", this.#outsideClickHandler);
|
||||
document.removeEventListener("pointerdown", this.#outsideClickHandler);
|
||||
this.#outsideClickHandler = null;
|
||||
}
|
||||
}
|
||||
|
|
@ -404,16 +404,16 @@ export class RStackAppSwitcher extends HTMLElement {
|
|||
|
||||
// Close sidebar when clicking outside (on main content)
|
||||
if (this.#outsideClickHandler) {
|
||||
document.removeEventListener("click", this.#outsideClickHandler);
|
||||
document.removeEventListener("pointerdown", this.#outsideClickHandler);
|
||||
}
|
||||
this.#outsideClickHandler = (e: MouseEvent) => {
|
||||
this.#outsideClickHandler = (e: PointerEvent) => {
|
||||
if (!this.#isOpen) return;
|
||||
const path = e.composedPath();
|
||||
if (!path.includes(sidebar) && !path.includes(trigger)) {
|
||||
close();
|
||||
}
|
||||
};
|
||||
document.addEventListener("click", this.#outsideClickHandler);
|
||||
document.addEventListener("pointerdown", this.#outsideClickHandler);
|
||||
|
||||
// Prefetch module fragments on hover for faster tab switching.
|
||||
// Uses low-priority fetch so it doesn't compete with user-initiated requests.
|
||||
|
|
@ -589,7 +589,7 @@ const STYLES = `
|
|||
white-space: nowrap; min-width: 0; flex-shrink: 1;
|
||||
overflow: hidden; text-overflow: ellipsis;
|
||||
}
|
||||
.trigger:hover { background: var(--rs-bg-hover); }
|
||||
.trigger:hover, .trigger:active { background: var(--rs-bg-hover); }
|
||||
|
||||
.trigger-badge {
|
||||
display: inline-flex; align-items: center; justify-content: center;
|
||||
|
|
@ -609,6 +609,7 @@ const STYLES = `
|
|||
top: 56px; left: 0; bottom: 0;
|
||||
width: 280px;
|
||||
overflow-y: auto;
|
||||
touch-action: pan-y;
|
||||
z-index: 10001;
|
||||
background: var(--rs-bg-surface);
|
||||
border-right: 1px solid var(--rs-border);
|
||||
|
|
@ -675,7 +676,7 @@ a.rstack-header:hover { background: var(--rs-bg-hover); }
|
|||
transition: background 0.12s;
|
||||
color: var(--rs-text-primary);
|
||||
}
|
||||
.item-row:hover { background: var(--rs-bg-hover); }
|
||||
.item-row:hover, .item-row:active { background: var(--rs-bg-hover); }
|
||||
.item-row.active { background: var(--rs-bg-active); }
|
||||
|
||||
.item {
|
||||
|
|
|
|||
|
|
@ -862,7 +862,7 @@ const OVERLAY_CSS = `
|
|||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.collab-badge:hover {
|
||||
.collab-badge:hover, .collab-badge:active {
|
||||
border-color: var(--rs-border-strong, rgba(255,255,255,0.2));
|
||||
}
|
||||
|
||||
|
|
@ -938,6 +938,7 @@ const OVERLAY_CSS = `
|
|||
|
||||
.people-list {
|
||||
overflow-y: auto;
|
||||
touch-action: pan-y;
|
||||
flex: 1;
|
||||
padding: 8px;
|
||||
}
|
||||
|
|
@ -959,7 +960,7 @@ const OVERLAY_CSS = `
|
|||
transition: background 0.15s;
|
||||
}
|
||||
|
||||
.people-row:hover {
|
||||
.people-row:hover, .people-row:active {
|
||||
background: var(--rs-bg-hover, rgba(0,0,0,0.04));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -213,10 +213,10 @@ export class RStackCommentBell extends HTMLElement {
|
|||
this.#render();
|
||||
}
|
||||
};
|
||||
document.addEventListener("click", closeHandler, { once: true });
|
||||
document.addEventListener("pointerdown", closeHandler, { once: true });
|
||||
|
||||
// Stop propagation from panel clicks
|
||||
this.#shadow.querySelector(".panel")?.addEventListener("click", (e) => e.stopPropagation());
|
||||
this.#shadow.querySelector(".panel")?.addEventListener("pointerdown", (e) => e.stopPropagation());
|
||||
}
|
||||
|
||||
// New Comment button
|
||||
|
|
@ -291,7 +291,7 @@ const STYLES = `
|
|||
justify-content: center;
|
||||
transition: color 0.15s, background 0.15s;
|
||||
}
|
||||
.comment-btn:hover {
|
||||
.comment-btn:hover, .comment-btn:active {
|
||||
color: var(--rs-text-primary, #e2e8f0);
|
||||
background: var(--rs-bg-hover, rgba(255,255,255,0.05));
|
||||
}
|
||||
|
|
@ -364,13 +364,14 @@ const STYLES = `
|
|||
cursor: pointer;
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
.new-comment-btn:hover {
|
||||
.new-comment-btn:hover, .new-comment-btn:active {
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.panel-body {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
touch-action: pan-y;
|
||||
}
|
||||
|
||||
.panel-empty {
|
||||
|
|
@ -389,7 +390,7 @@ const STYLES = `
|
|||
transition: background 0.15s;
|
||||
border-bottom: 1px solid var(--rs-border, rgba(255,255,255,0.06));
|
||||
}
|
||||
.comment-item:hover {
|
||||
.comment-item:hover, .comment-item:active {
|
||||
background: var(--rs-bg-hover, rgba(255,255,255,0.05));
|
||||
}
|
||||
.comment-item:last-child {
|
||||
|
|
|
|||
|
|
@ -684,7 +684,7 @@ export class RStackIdentity extends HTMLElement {
|
|||
dropdown.classList.toggle("open");
|
||||
});
|
||||
|
||||
document.addEventListener("click", () => dropdown.classList.remove("open"));
|
||||
document.addEventListener("pointerdown", () => dropdown.classList.remove("open"));
|
||||
|
||||
this.#shadow.querySelectorAll("[data-action]").forEach((el) => {
|
||||
el.addEventListener("click", (e) => {
|
||||
|
|
|
|||
|
|
@ -249,7 +249,7 @@ export class RStackMi extends HTMLElement {
|
|||
});
|
||||
|
||||
// Close panel on outside click — use composedPath to pierce Shadow DOM
|
||||
document.addEventListener("click", (e) => {
|
||||
document.addEventListener("pointerdown", (e) => {
|
||||
const path = e.composedPath();
|
||||
if (!path.includes(this)) {
|
||||
panel.classList.remove("open");
|
||||
|
|
@ -258,8 +258,8 @@ export class RStackMi extends HTMLElement {
|
|||
});
|
||||
|
||||
// Prevent internal clicks from closing
|
||||
panel.addEventListener("mousedown", (e) => e.stopPropagation());
|
||||
bar.addEventListener("mousedown", (e) => e.stopPropagation());
|
||||
panel.addEventListener("pointerdown", (e) => e.stopPropagation());
|
||||
bar.addEventListener("pointerdown", (e) => e.stopPropagation());
|
||||
|
||||
// Minimize button
|
||||
this.#shadow.getElementById("mi-minimize")!.addEventListener("click", () => this.#minimize());
|
||||
|
|
@ -855,7 +855,7 @@ const STYLES = `
|
|||
font-size: 0.85rem; border-radius: 6px; transition: all 0.2s;
|
||||
flex-shrink: 0; line-height: 1;
|
||||
}
|
||||
.mi-mic-btn:hover { background: var(--rs-bg-hover); }
|
||||
.mi-mic-btn:hover, .mi-mic-btn:active { background: var(--rs-bg-hover); }
|
||||
.mi-mic-btn.recording {
|
||||
animation: micPulse 1.5s infinite;
|
||||
filter: saturate(2) brightness(1.1);
|
||||
|
|
@ -907,7 +907,7 @@ const STYLES = `
|
|||
font-size: 1.1rem; line-height: 1; padding: 2px 6px; border-radius: 4px;
|
||||
color: var(--rs-text-muted); transition: all 0.15s;
|
||||
}
|
||||
.mi-panel-btn:hover { background: var(--rs-bg-hover); color: var(--rs-text-primary); }
|
||||
.mi-panel-btn:hover, .mi-panel-btn:active { background: var(--rs-bg-hover); color: var(--rs-text-primary); }
|
||||
|
||||
/* ── Messages ── */
|
||||
.mi-messages {
|
||||
|
|
@ -935,7 +935,7 @@ const STYLES = `
|
|||
font-family: inherit; white-space: nowrap;
|
||||
background: var(--rs-btn-secondary-bg); color: var(--rs-text-primary);
|
||||
}
|
||||
.mi-suggest-chip:hover {
|
||||
.mi-suggest-chip:hover, .mi-suggest-chip:active {
|
||||
background: var(--rs-bg-hover);
|
||||
border-color: rgba(6,182,212,0.4);
|
||||
box-shadow: 0 0 0 1px rgba(6,182,212,0.15);
|
||||
|
|
@ -943,7 +943,7 @@ const STYLES = `
|
|||
.mi-suggest-chip.dynamic {
|
||||
border-color: rgba(124,58,237,0.3);
|
||||
}
|
||||
.mi-suggest-chip.dynamic:hover {
|
||||
.mi-suggest-chip.dynamic:hover, .mi-suggest-chip.dynamic:active {
|
||||
border-color: rgba(124,58,237,0.5);
|
||||
box-shadow: 0 0 0 1px rgba(124,58,237,0.15);
|
||||
}
|
||||
|
|
@ -1022,7 +1022,7 @@ const STYLES = `
|
|||
font-family: inherit;
|
||||
background: var(--rs-btn-secondary-bg); color: var(--rs-text-primary);
|
||||
}
|
||||
.mi-tool-chip:hover { background: var(--rs-bg-hover); }
|
||||
.mi-tool-chip:hover, .mi-tool-chip:active { background: var(--rs-bg-hover); }
|
||||
|
||||
/* ── Confirmation bar ── */
|
||||
.mi-confirm {
|
||||
|
|
@ -1080,7 +1080,7 @@ const STYLES = `
|
|||
font-size: 0.9rem; padding: 4px 8px; border-radius: 6px;
|
||||
color: #06b6d4; transition: all 0.15s; flex-shrink: 0;
|
||||
}
|
||||
.mi-send-btn:hover { background: rgba(6,182,212,0.12); }
|
||||
.mi-send-btn:hover, .mi-send-btn:active { background: rgba(6,182,212,0.12); }
|
||||
|
||||
/* ── Minimized pill ── */
|
||||
.mi-pill {
|
||||
|
|
@ -1092,7 +1092,7 @@ const STYLES = `
|
|||
display: none; align-items: center; gap: 6px;
|
||||
color: var(--rs-text-primary); transition: all 0.2s;
|
||||
}
|
||||
.mi-pill:hover { box-shadow: 0 4px 20px rgba(6,182,212,0.3); }
|
||||
.mi-pill:hover, .mi-pill:active { box-shadow: 0 4px 20px rgba(6,182,212,0.3); }
|
||||
.mi-pill.visible { display: flex; }
|
||||
.mi-pill-icon {
|
||||
background: linear-gradient(135deg, #06b6d4, #7c3aed);
|
||||
|
|
|
|||
|
|
@ -428,10 +428,10 @@ export class RStackNotificationBell extends HTMLElement {
|
|||
this.#render();
|
||||
}
|
||||
};
|
||||
document.addEventListener("click", closeHandler, { once: true });
|
||||
document.addEventListener("pointerdown", closeHandler, { once: true });
|
||||
|
||||
// Stop propagation from panel clicks
|
||||
this.#shadow.querySelector(".panel")?.addEventListener("click", (e) => e.stopPropagation());
|
||||
this.#shadow.querySelector(".panel")?.addEventListener("pointerdown", (e) => e.stopPropagation());
|
||||
|
||||
// Mark all read
|
||||
this.#shadow.querySelector('[data-action="mark-all-read"]')?.addEventListener("click", (e) => {
|
||||
|
|
@ -520,7 +520,7 @@ const STYLES = `
|
|||
justify-content: center;
|
||||
transition: color 0.15s, background 0.15s;
|
||||
}
|
||||
.bell-btn:hover {
|
||||
.bell-btn:hover, .bell-btn:active {
|
||||
color: var(--rs-text-primary, #e2e8f0);
|
||||
background: var(--rs-bg-hover, rgba(255,255,255,0.05));
|
||||
}
|
||||
|
|
@ -552,6 +552,7 @@ const STYLES = `
|
|||
width: 360px;
|
||||
max-height: 480px;
|
||||
overflow-y: auto;
|
||||
touch-action: pan-y;
|
||||
border-radius: 10px;
|
||||
background: var(--rs-bg-surface, #1e293b);
|
||||
border: 1px solid var(--rs-border, rgba(255,255,255,0.1));
|
||||
|
|
@ -589,7 +590,7 @@ const STYLES = `
|
|||
border-radius: 4px;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
.mark-all-btn:hover, .push-btn:hover {
|
||||
.mark-all-btn:hover, .push-btn:hover, .mark-all-btn:active, .push-btn:active {
|
||||
background: var(--rs-bg-hover, rgba(255,255,255,0.05));
|
||||
}
|
||||
|
||||
|
|
@ -614,7 +615,7 @@ const STYLES = `
|
|||
transition: background 0.15s;
|
||||
border-bottom: 1px solid var(--rs-border, rgba(255,255,255,0.1));
|
||||
}
|
||||
.notif-item:hover {
|
||||
.notif-item:hover, .notif-item:active {
|
||||
background: var(--rs-bg-hover, rgba(255,255,255,0.05));
|
||||
}
|
||||
.notif-item.unread {
|
||||
|
|
@ -687,7 +688,7 @@ const STYLES = `
|
|||
border: 1px solid var(--rs-border, rgba(255,255,255,0.15));
|
||||
color: var(--rs-text-muted, #94a3b8);
|
||||
}
|
||||
.notif-accept:hover, .notif-decline:hover {
|
||||
.notif-accept:hover, .notif-decline:hover, .notif-accept:active, .notif-decline:active {
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
|
|
@ -706,7 +707,7 @@ const STYLES = `
|
|||
.notif-item:hover .notif-dismiss {
|
||||
opacity: 1;
|
||||
}
|
||||
.notif-dismiss:hover {
|
||||
.notif-dismiss:hover, .notif-dismiss:active {
|
||||
color: var(--rs-text-primary, #e2e8f0);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -167,11 +167,11 @@ export class RStackSharePanel extends HTMLElement {
|
|||
});
|
||||
|
||||
// Stop propagation from panel clicks
|
||||
this.#shadow.querySelector(".panel")?.addEventListener("click", (e) => e.stopPropagation());
|
||||
this.#shadow.querySelector(".panel")?.addEventListener("pointerdown", (e) => e.stopPropagation());
|
||||
|
||||
// Close on outside click
|
||||
if (this.#open) {
|
||||
document.addEventListener("click", () => {
|
||||
document.addEventListener("pointerdown", () => {
|
||||
if (this.#open) {
|
||||
this.#open = false;
|
||||
this.#render();
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -123,7 +123,7 @@ export class RStackSpaceSwitcher extends HTMLElement {
|
|||
});
|
||||
|
||||
// Close menu only when clicking outside both trigger and menu
|
||||
document.addEventListener("click", (e) => {
|
||||
document.addEventListener("pointerdown", (e) => {
|
||||
if (!menu.classList.contains("open")) return;
|
||||
const path = e.composedPath();
|
||||
if (path.includes(menu) || path.includes(trigger)) return;
|
||||
|
|
@ -132,7 +132,7 @@ export class RStackSpaceSwitcher extends HTMLElement {
|
|||
});
|
||||
|
||||
// Prevent clicks inside menu from closing it
|
||||
menu.addEventListener("click", (e) => e.stopPropagation());
|
||||
menu.addEventListener("pointerdown", (e) => e.stopPropagation());
|
||||
}
|
||||
|
||||
#visibilityInfo(s: SpaceInfo): { cls: string; label: string } {
|
||||
|
|
@ -517,7 +517,12 @@ export class RStackSpaceSwitcher extends HTMLElement {
|
|||
document.body.appendChild(overlay);
|
||||
}
|
||||
|
||||
#showEditSpaceModal(slug: string, spaceName: string) {
|
||||
/** Public entry point — both gear icons call this */
|
||||
openSettingsModal(slug: string, name: string, tab: "settings" | "modules" | "members" | "invitations" = "settings") {
|
||||
this.#showEditSpaceModal(slug, name, tab);
|
||||
}
|
||||
|
||||
#showEditSpaceModal(slug: string, spaceName: string, initialTab: "settings" | "modules" | "members" | "invitations" = "settings") {
|
||||
if (document.querySelector(".rstack-auth-overlay")) return;
|
||||
|
||||
const overlay = document.createElement("div");
|
||||
|
|
@ -528,13 +533,13 @@ export class RStackSpaceSwitcher extends HTMLElement {
|
|||
<button class="close-btn" data-action="close">×</button>
|
||||
<h2>Edit Space</h2>
|
||||
<div class="tabs">
|
||||
<button class="tab active" data-tab="settings">Settings</button>
|
||||
<button class="tab" data-tab="modules">Modules</button>
|
||||
<button class="tab" data-tab="members">Members</button>
|
||||
<button class="tab" data-tab="invitations">Invitations</button>
|
||||
<button class="tab ${initialTab === "settings" ? "active" : ""}" data-tab="settings">Settings</button>
|
||||
<button class="tab ${initialTab === "modules" ? "active" : ""}" data-tab="modules">Modules</button>
|
||||
<button class="tab ${initialTab === "members" ? "active" : ""}" data-tab="members">Members</button>
|
||||
<button class="tab ${initialTab === "invitations" ? "active" : ""}" data-tab="invitations">Invitations</button>
|
||||
</div>
|
||||
|
||||
<div class="tab-panel" id="panel-settings">
|
||||
<div class="tab-panel ${initialTab !== "settings" ? "hidden" : ""}" id="panel-settings">
|
||||
<label class="field-label">Name</label>
|
||||
<input class="input" id="es-name" value="${spaceName.replace(/"/g, """)}" />
|
||||
<label class="field-label">Visibility</label>
|
||||
|
|
@ -555,16 +560,16 @@ export class RStackSpaceSwitcher extends HTMLElement {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tab-panel hidden" id="panel-modules">
|
||||
<div class="tab-panel ${initialTab !== "modules" ? "hidden" : ""}" id="panel-modules">
|
||||
<div id="es-modules-list" class="modules-list"><div class="loading">Loading modules...</div></div>
|
||||
<div class="status" id="es-modules-status"></div>
|
||||
</div>
|
||||
|
||||
<div class="tab-panel hidden" id="panel-members">
|
||||
<div class="tab-panel ${initialTab !== "members" ? "hidden" : ""}" id="panel-members">
|
||||
<div id="es-members-list" class="member-list"><div class="loading">Loading members...</div></div>
|
||||
</div>
|
||||
|
||||
<div class="tab-panel hidden" id="panel-invitations">
|
||||
<div class="tab-panel ${initialTab !== "invitations" ? "hidden" : ""}" id="panel-invitations">
|
||||
<div id="es-invitations-list" class="invitation-list"><div class="loading">Loading invitations...</div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -601,6 +606,11 @@ export class RStackSpaceSwitcher extends HTMLElement {
|
|||
// Delete handler
|
||||
overlay.querySelector("#es-delete")?.addEventListener("click", () => this.#deleteSpace(overlay, slug, spaceName));
|
||||
|
||||
// Eager-load the initially-active tab (if not settings, which loads above)
|
||||
if (initialTab === "modules") this.#loadModulesConfig(overlay, slug);
|
||||
if (initialTab === "members") this.#loadMembers(overlay, slug);
|
||||
if (initialTab === "invitations") this.#loadInvitations(overlay, slug);
|
||||
|
||||
document.body.appendChild(overlay);
|
||||
}
|
||||
|
||||
|
|
@ -691,29 +701,73 @@ export class RStackSpaceSwitcher extends HTMLElement {
|
|||
const data = await res.json();
|
||||
const members: Array<{ did: string; role: string; displayName?: string; isOwner?: boolean }> = data.members || [];
|
||||
|
||||
let membersHtml = "";
|
||||
if (members.length === 0) {
|
||||
container.innerHTML = `<div class="loading">No members</div>`;
|
||||
return;
|
||||
membersHtml = `<div class="loading">No members</div>`;
|
||||
} else {
|
||||
membersHtml = members.map((m) => {
|
||||
const displayName = m.displayName || m.did.slice(0, 20) + "...";
|
||||
const roleOptions = ["viewer", "member", "moderator", "admin"]
|
||||
.map((r) => `<option value="${r}" ${r === m.role ? "selected" : ""}>${r}</option>`)
|
||||
.join("");
|
||||
return `
|
||||
<div class="member-item${m.isOwner ? " member-owner" : ""}">
|
||||
<span class="member-name">${displayName}</span>
|
||||
${m.isOwner
|
||||
? `<span class="role-badge role-owner">owner</span>`
|
||||
: `<select class="role-select" data-did="${m.did}">${roleOptions}</select>
|
||||
<button class="member-remove" data-did="${m.did}" title="Remove member">×</button>`
|
||||
}
|
||||
</div>`;
|
||||
}).join("");
|
||||
}
|
||||
|
||||
container.innerHTML = members.map((m) => {
|
||||
const displayName = m.displayName || m.did.slice(0, 20) + "...";
|
||||
const roleOptions = ["viewer", "member", "moderator", "admin"]
|
||||
.map((r) => `<option value="${r}" ${r === m.role ? "selected" : ""}>${r}</option>`)
|
||||
.join("");
|
||||
return `
|
||||
<div class="member-item${m.isOwner ? " member-owner" : ""}">
|
||||
<span class="member-name">${displayName}</span>
|
||||
${m.isOwner
|
||||
? `<span class="role-badge role-owner">owner</span>`
|
||||
: `<select class="role-select" data-did="${m.did}">${roleOptions}</select>
|
||||
<button class="member-remove" data-did="${m.did}" title="Remove member">×</button>`
|
||||
}
|
||||
</div>`;
|
||||
}).join("");
|
||||
// Add Member section (admin-only)
|
||||
const addMemberHtml = `
|
||||
<div class="add-member-section">
|
||||
<div class="section-label">Add Member</div>
|
||||
<div class="add-member-toggle">
|
||||
<button class="add-toggle-btn active" data-add-mode="username">By Username</button>
|
||||
<button class="add-toggle-btn" data-add-mode="email">By Email</button>
|
||||
</div>
|
||||
<div class="add-member-form" id="add-member-form">
|
||||
<div class="add-member-username-mode">
|
||||
<div class="add-search-wrapper">
|
||||
<input class="input" id="am-username" placeholder="Search username..." autocomplete="off" style="margin-bottom:0" />
|
||||
<div class="add-search-dropdown" id="am-search-dropdown"></div>
|
||||
</div>
|
||||
<div class="add-selected-user" id="am-selected-user"></div>
|
||||
<div class="add-member-row">
|
||||
<select class="role-select" id="am-role">
|
||||
<option value="member">member</option>
|
||||
<option value="viewer">viewer</option>
|
||||
<option value="moderator">moderator</option>
|
||||
<option value="admin">admin</option>
|
||||
</select>
|
||||
<button class="btn-sm btn-sm--approve" id="am-add-btn" disabled>Add</button>
|
||||
</div>
|
||||
<div class="add-feedback" id="am-feedback"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="add-member-form hidden" id="add-email-form">
|
||||
<input type="email" class="input" id="am-email" placeholder="Email address..." style="margin-bottom:0" />
|
||||
<div class="add-member-row">
|
||||
<select class="role-select" id="am-email-role">
|
||||
<option value="member">member</option>
|
||||
<option value="viewer">viewer</option>
|
||||
<option value="moderator">moderator</option>
|
||||
<option value="admin">admin</option>
|
||||
</select>
|
||||
<button class="btn-sm btn-sm--approve" id="am-email-btn">Send Invite</button>
|
||||
</div>
|
||||
<div class="add-feedback" id="am-email-feedback"></div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
container.innerHTML = membersHtml + addMemberHtml;
|
||||
|
||||
// Role change handlers
|
||||
container.querySelectorAll(".role-select").forEach((sel) => {
|
||||
container.querySelectorAll(".role-select:not(#am-role):not(#am-email-role)").forEach((sel) => {
|
||||
sel.addEventListener("change", async () => {
|
||||
const el = sel as HTMLSelectElement;
|
||||
const did = el.dataset.did!;
|
||||
|
|
@ -746,51 +800,238 @@ export class RStackSpaceSwitcher extends HTMLElement {
|
|||
} catch { alert("Failed to remove member"); }
|
||||
});
|
||||
});
|
||||
|
||||
// Wire up add-member section
|
||||
this.#attachAddMemberHandlers(container, overlay, slug);
|
||||
} catch {
|
||||
container.innerHTML = `<div class="loading">Failed to load members</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
#attachAddMemberHandlers(container: HTMLElement, overlay: HTMLElement, slug: string) {
|
||||
// Toggle between username and email modes
|
||||
const usernameForm = container.querySelector("#add-member-form") as HTMLElement;
|
||||
const emailForm = container.querySelector("#add-email-form") as HTMLElement;
|
||||
|
||||
container.querySelectorAll(".add-toggle-btn").forEach((btn) => {
|
||||
btn.addEventListener("click", () => {
|
||||
container.querySelectorAll(".add-toggle-btn").forEach((b) => b.classList.remove("active"));
|
||||
btn.classList.add("active");
|
||||
const mode = (btn as HTMLElement).dataset.addMode;
|
||||
if (mode === "username") {
|
||||
usernameForm?.classList.remove("hidden");
|
||||
emailForm?.classList.add("hidden");
|
||||
} else {
|
||||
usernameForm?.classList.add("hidden");
|
||||
emailForm?.classList.remove("hidden");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Username search with debounce
|
||||
const usernameInput = container.querySelector("#am-username") as HTMLInputElement;
|
||||
const dropdown = container.querySelector("#am-search-dropdown") as HTMLElement;
|
||||
const selectedDiv = container.querySelector("#am-selected-user") as HTMLElement;
|
||||
const addBtn = container.querySelector("#am-add-btn") as HTMLButtonElement;
|
||||
const feedback = container.querySelector("#am-feedback") as HTMLElement;
|
||||
let selectedUser: { did: string; username: string; displayName: string } | null = null;
|
||||
let searchTimer: ReturnType<typeof setTimeout>;
|
||||
|
||||
if (usernameInput) {
|
||||
usernameInput.addEventListener("input", () => {
|
||||
clearTimeout(searchTimer);
|
||||
selectedUser = null;
|
||||
addBtn.disabled = true;
|
||||
selectedDiv.innerHTML = "";
|
||||
const q = usernameInput.value.trim();
|
||||
if (q.length < 2) { dropdown.innerHTML = ""; dropdown.classList.remove("open"); return; }
|
||||
|
||||
searchTimer = setTimeout(async () => {
|
||||
try {
|
||||
const token = getAccessToken();
|
||||
const res = await fetch(`/api/users/search?q=${encodeURIComponent(q)}&limit=5`, {
|
||||
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
||||
});
|
||||
if (!res.ok) { dropdown.classList.remove("open"); return; }
|
||||
const users: Array<{ id: string; did: string; username: string; displayName: string }> = await res.json();
|
||||
if (users.length === 0) {
|
||||
dropdown.innerHTML = `<div class="add-dd-empty">No users found</div>`;
|
||||
} else {
|
||||
dropdown.innerHTML = users.map((u, i) => `
|
||||
<div class="add-dd-item" data-idx="${i}">
|
||||
<span class="add-dd-name">${u.displayName || u.username}</span>
|
||||
<span class="add-dd-user">@${u.username}</span>
|
||||
</div>
|
||||
`).join("");
|
||||
// Attach click handlers
|
||||
dropdown.querySelectorAll(".add-dd-item").forEach((item) => {
|
||||
item.addEventListener("click", () => {
|
||||
const idx = parseInt((item as HTMLElement).dataset.idx || "0");
|
||||
const u = users[idx];
|
||||
selectedUser = { did: u.id, username: u.username, displayName: u.displayName || u.username };
|
||||
usernameInput.value = u.username;
|
||||
dropdown.innerHTML = "";
|
||||
dropdown.classList.remove("open");
|
||||
selectedDiv.innerHTML = `<span class="add-selected-chip">Selected: <strong>${selectedUser.displayName}</strong> (@${selectedUser.username})</span>`;
|
||||
addBtn.disabled = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
dropdown.classList.add("open");
|
||||
} catch { dropdown.classList.remove("open"); }
|
||||
}, 300);
|
||||
});
|
||||
|
||||
// Close dropdown on outside click
|
||||
overlay.addEventListener("click", (e) => {
|
||||
if (!(e.target as HTMLElement).closest(".add-search-wrapper")) {
|
||||
dropdown.classList.remove("open");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Add by username
|
||||
addBtn?.addEventListener("click", async () => {
|
||||
if (!selectedUser) return;
|
||||
const role = (container.querySelector("#am-role") as HTMLSelectElement).value;
|
||||
const token = getAccessToken();
|
||||
if (!token) return;
|
||||
try {
|
||||
const res = await fetch(`/api/spaces/${slug}/members/add`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
|
||||
body: JSON.stringify({ username: selectedUser.username, role }),
|
||||
});
|
||||
if (res.ok) {
|
||||
feedback.textContent = `Invite sent to ${selectedUser.username}`;
|
||||
feedback.style.color = "#34d399";
|
||||
selectedUser = null;
|
||||
usernameInput.value = "";
|
||||
selectedDiv.innerHTML = "";
|
||||
addBtn.disabled = true;
|
||||
setTimeout(() => { feedback.textContent = ""; }, 3000);
|
||||
this.#loadMembers(overlay, slug);
|
||||
} else {
|
||||
const err = await res.json();
|
||||
feedback.textContent = err.error || "Failed to invite";
|
||||
feedback.style.color = "#ef4444";
|
||||
setTimeout(() => { feedback.textContent = ""; }, 3000);
|
||||
}
|
||||
} catch {
|
||||
feedback.textContent = "Network error";
|
||||
feedback.style.color = "#ef4444";
|
||||
setTimeout(() => { feedback.textContent = ""; }, 3000);
|
||||
}
|
||||
});
|
||||
|
||||
// Add by email
|
||||
const emailInput = container.querySelector("#am-email") as HTMLInputElement;
|
||||
const emailBtn = container.querySelector("#am-email-btn") as HTMLButtonElement;
|
||||
const emailFeedback = container.querySelector("#am-email-feedback") as HTMLElement;
|
||||
|
||||
emailBtn?.addEventListener("click", async () => {
|
||||
if (!emailInput?.value) return;
|
||||
const role = (container.querySelector("#am-email-role") as HTMLSelectElement).value;
|
||||
const token = getAccessToken();
|
||||
if (!token) return;
|
||||
try {
|
||||
const res = await fetch(`/api/spaces/${slug}/invite`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
|
||||
body: JSON.stringify({ email: emailInput.value, role }),
|
||||
});
|
||||
if (res.ok) {
|
||||
emailFeedback.textContent = "Invite sent";
|
||||
emailFeedback.style.color = "#34d399";
|
||||
emailInput.value = "";
|
||||
setTimeout(() => { emailFeedback.textContent = ""; }, 3000);
|
||||
} else {
|
||||
const err = await res.json().catch(() => ({ error: "Failed to invite" })) as { error?: string };
|
||||
emailFeedback.textContent = err.error || "Failed to invite";
|
||||
emailFeedback.style.color = "#ef4444";
|
||||
setTimeout(() => { emailFeedback.textContent = ""; }, 3000);
|
||||
}
|
||||
} catch {
|
||||
emailFeedback.textContent = "Network error";
|
||||
emailFeedback.style.color = "#ef4444";
|
||||
setTimeout(() => { emailFeedback.textContent = ""; }, 3000);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async #loadInvitations(overlay: HTMLElement, slug: string) {
|
||||
const container = overlay.querySelector("#es-invitations-list") as HTMLElement;
|
||||
try {
|
||||
const token = getAccessToken();
|
||||
const res = await fetch(`/api/spaces/${slug}/access-requests`, {
|
||||
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
||||
});
|
||||
if (!res.ok) { container.innerHTML = `<div class="loading">Failed to load</div>`; return; }
|
||||
const data = await res.json();
|
||||
const requests: Array<{ id: string; requesterUsername: string; message?: string; status: string; createdAt: number }> = data.requests || [];
|
||||
const headers: Record<string, string> = {};
|
||||
if (token) headers["Authorization"] = `Bearer ${token}`;
|
||||
|
||||
if (requests.length === 0) {
|
||||
container.innerHTML = `<div class="loading">No access requests</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
const pending = requests.filter((r) => r.status === "pending");
|
||||
const resolved = requests.filter((r) => r.status !== "pending");
|
||||
// Fetch access requests and email invites in parallel
|
||||
const [reqRes, invRes] = await Promise.all([
|
||||
fetch(`/api/spaces/${slug}/access-requests`, { headers }),
|
||||
fetch(`/api/spaces/${slug}/invites`, { headers }).catch(() => null),
|
||||
]);
|
||||
|
||||
// Access requests
|
||||
let html = "";
|
||||
if (pending.length > 0) {
|
||||
html += pending.map((r) => `
|
||||
<div class="invite-item">
|
||||
<div class="invite-info">
|
||||
if (reqRes.ok) {
|
||||
const data = await reqRes.json();
|
||||
const requests: Array<{ id: string; requesterUsername: string; message?: string; status: string; createdAt: number }> = data.requests || [];
|
||||
const pending = requests.filter((r) => r.status === "pending");
|
||||
const resolved = requests.filter((r) => r.status !== "pending");
|
||||
|
||||
html += `<div class="section-label">Access Requests</div>`;
|
||||
if (pending.length > 0) {
|
||||
html += pending.map((r) => `
|
||||
<div class="invite-item">
|
||||
<div class="invite-info">
|
||||
<span class="invite-name">${r.requesterUsername}</span>
|
||||
${r.message ? `<span class="invite-msg">${r.message.replace(/</g, "<")}</span>` : ""}
|
||||
</div>
|
||||
<div class="invite-actions">
|
||||
<button class="btn-sm btn-sm--approve" data-req-id="${r.id}" data-action="approve">Approve</button>
|
||||
<button class="btn-sm btn-sm--deny" data-req-id="${r.id}" data-action="deny">Deny</button>
|
||||
</div>
|
||||
</div>`).join("");
|
||||
} else {
|
||||
html += `<div class="loading" style="padding:8px 0">No pending requests</div>`;
|
||||
}
|
||||
if (resolved.length > 0) {
|
||||
html += `<div class="section-label" style="opacity:0.4;padding:8px 0 4px">Resolved</div>`;
|
||||
html += resolved.map((r) => `
|
||||
<div class="invite-item invite-resolved">
|
||||
<span class="invite-name">${r.requesterUsername}</span>
|
||||
${r.message ? `<span class="invite-msg">${r.message.replace(/</g, "<")}</span>` : ""}
|
||||
</div>
|
||||
<div class="invite-actions">
|
||||
<button class="btn-sm btn-sm--approve" data-req-id="${r.id}" data-action="approve">Approve</button>
|
||||
<button class="btn-sm btn-sm--deny" data-req-id="${r.id}" data-action="deny">Deny</button>
|
||||
</div>
|
||||
</div>`).join("");
|
||||
<span class="invite-status invite-status--${r.status}">${r.status}</span>
|
||||
</div>`).join("");
|
||||
}
|
||||
} else {
|
||||
html += `<div class="loading">Failed to load access requests</div>`;
|
||||
}
|
||||
if (resolved.length > 0) {
|
||||
html += `<div class="section-label" style="opacity:0.4;padding:8px 0 4px">Resolved</div>`;
|
||||
html += resolved.map((r) => `
|
||||
<div class="invite-item invite-resolved">
|
||||
<span class="invite-name">${r.requesterUsername}</span>
|
||||
<span class="invite-status invite-status--${r.status}">${r.status}</span>
|
||||
</div>`).join("");
|
||||
|
||||
// Email invites
|
||||
html += `<div class="section-label" style="margin-top:16px">Pending Email Invites</div>`;
|
||||
if (invRes && invRes.ok) {
|
||||
const invData = await invRes.json();
|
||||
const invites: Array<{ id: string; email?: string; role: string; status: string; expiresAt: string }> = invData.invites || [];
|
||||
const pendingInvites = invites.filter((inv) => inv.status === "pending");
|
||||
|
||||
if (pendingInvites.length > 0) {
|
||||
html += pendingInvites.map((inv) => {
|
||||
const expiry = new Date(inv.expiresAt).toLocaleDateString();
|
||||
return `
|
||||
<div class="invite-item">
|
||||
<div class="invite-info">
|
||||
<span class="invite-name">${(inv.email || "Link invite").replace(/</g, "<")}</span>
|
||||
<span class="invite-msg">${inv.role} · expires ${expiry}</span>
|
||||
</div>
|
||||
<button class="btn-sm btn-sm--deny" data-revoke-id="${inv.id}">Revoke</button>
|
||||
</div>`;
|
||||
}).join("");
|
||||
} else {
|
||||
html += `<div class="loading" style="padding:8px 0">No pending invites</div>`;
|
||||
}
|
||||
} else {
|
||||
html += `<div class="loading" style="padding:8px 0">No pending invites</div>`;
|
||||
}
|
||||
|
||||
container.innerHTML = html;
|
||||
|
|
@ -813,6 +1054,23 @@ export class RStackSpaceSwitcher extends HTMLElement {
|
|||
} catch { alert("Failed to process request"); }
|
||||
});
|
||||
});
|
||||
|
||||
// Revoke invite handlers
|
||||
container.querySelectorAll("[data-revoke-id]").forEach((btn) => {
|
||||
btn.addEventListener("click", async () => {
|
||||
const id = (btn as HTMLElement).dataset.revokeId!;
|
||||
const token = getAccessToken();
|
||||
if (!token) return;
|
||||
try {
|
||||
const res = await fetch(`/api/spaces/${slug}/invites/${id}/revoke`, {
|
||||
method: "POST",
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
if (!res.ok) { const d = await res.json().catch(() => ({})); alert((d as any).error || "Failed"); return; }
|
||||
this.#loadInvitations(overlay, slug);
|
||||
} catch { alert("Failed to revoke invite"); }
|
||||
});
|
||||
});
|
||||
} catch {
|
||||
container.innerHTML = `<div class="loading">Failed to load invitations</div>`;
|
||||
}
|
||||
|
|
@ -1919,6 +2177,32 @@ select.input { appearance: auto; }
|
|||
input:checked + .toggle-slider { background: #06b6d4; }
|
||||
input:checked + .toggle-slider:before { transform: translateX(16px); }
|
||||
|
||||
/* Section labels */
|
||||
.section-label { font-size: 0.72rem; font-weight: 700; color: var(--rs-text-secondary); text-transform: uppercase; letter-spacing: 0.06em; padding: 8px 0 4px; }
|
||||
|
||||
/* Add member section */
|
||||
.add-member-section { margin-top: 16px; padding-top: 12px; border-top: 1px solid var(--rs-border-subtle); }
|
||||
.add-member-toggle { display: flex; gap: 4px; margin-bottom: 10px; }
|
||||
.add-toggle-btn {
|
||||
flex: 1; padding: 6px 10px; background: var(--rs-bg-hover); border: 1px solid var(--rs-input-border, #404040);
|
||||
color: var(--rs-text-secondary); border-radius: 6px; font-size: 0.78rem; cursor: pointer; transition: all 0.15s;
|
||||
}
|
||||
.add-toggle-btn.active { background: rgba(6,182,212,0.1); border-color: rgba(6,182,212,0.3); color: #06b6d4; }
|
||||
.add-member-form { display: flex; flex-direction: column; gap: 8px; }
|
||||
.add-member-form.hidden { display: none; }
|
||||
.add-member-row { display: flex; gap: 8px; align-items: center; }
|
||||
.add-search-wrapper { position: relative; }
|
||||
.add-search-dropdown { position: absolute; top: 100%; left: 0; right: 0; background: var(--rs-bg-surface); border: 1px solid var(--rs-border); border-radius: 8px; margin-top: 4px; overflow: hidden; z-index: 10; box-shadow: 0 4px 16px rgba(0,0,0,0.25); display: none; }
|
||||
.add-search-dropdown.open { display: block; }
|
||||
.add-dd-item { display: flex; align-items: center; gap: 8px; padding: 8px 12px; cursor: pointer; transition: background 0.1s; }
|
||||
.add-dd-item:hover { background: var(--rs-bg-hover); }
|
||||
.add-dd-name { font-size: 0.82rem; font-weight: 500; color: var(--rs-text-primary); }
|
||||
.add-dd-user { font-size: 0.72rem; color: var(--rs-text-muted); }
|
||||
.add-dd-empty { padding: 8px 12px; font-size: 0.8rem; color: var(--rs-text-muted); }
|
||||
.add-selected-chip { font-size: 0.78rem; color: #06b6d4; }
|
||||
.add-selected-user { min-height: 0; }
|
||||
.add-feedback { font-size: 0.78rem; min-height: 16px; }
|
||||
|
||||
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
|
||||
@keyframes slideUp { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } }
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -1182,9 +1182,9 @@ export class RStackTabBar extends HTMLElement {
|
|||
}
|
||||
});
|
||||
|
||||
// Drag-to-connect: mousedown starts a flow drag (skip if wiring)
|
||||
plane.addEventListener("mousedown", (e) => {
|
||||
if ((e as MouseEvent).button !== 0) return;
|
||||
// Drag-to-connect: pointerdown starts a flow drag (skip if wiring)
|
||||
plane.addEventListener("pointerdown", (e) => {
|
||||
if ((e as PointerEvent).button !== 0) return;
|
||||
if (this.#wiringActive) return;
|
||||
e.stopPropagation(); // prevent orbit
|
||||
this.#flowDragSource = layerId;
|
||||
|
|
@ -1195,20 +1195,25 @@ export class RStackTabBar extends HTMLElement {
|
|||
// 3D scene: orbit controls (drag on empty space to rotate)
|
||||
const sceneContainer = this.#shadow.getElementById("stack-3d");
|
||||
if (sceneContainer) {
|
||||
sceneContainer.addEventListener("mousedown", (e) => {
|
||||
let orbitPointerId: number | null = null;
|
||||
|
||||
sceneContainer.addEventListener("pointerdown", (e) => {
|
||||
const pe = e as PointerEvent;
|
||||
// Only orbit on left-click on empty space (not on layer planes)
|
||||
if ((e as MouseEvent).button !== 0) return;
|
||||
if (pe.button !== 0) return;
|
||||
if ((e.target as HTMLElement).closest(".layer-plane")) return;
|
||||
this.#orbitDragging = true;
|
||||
this.#orbitLastX = (e as MouseEvent).clientX;
|
||||
this.#orbitLastY = (e as MouseEvent).clientY;
|
||||
this.#orbitLastX = pe.clientX;
|
||||
this.#orbitLastY = pe.clientY;
|
||||
orbitPointerId = pe.pointerId;
|
||||
sceneContainer.setPointerCapture(pe.pointerId);
|
||||
sceneContainer.style.cursor = "grabbing";
|
||||
});
|
||||
|
||||
// Collect all layer planes + their rects for drag target detection
|
||||
const layerPlanes = [...this.#shadow.querySelectorAll<HTMLElement>(".layer-plane")];
|
||||
|
||||
const onMouseMove = (e: MouseEvent) => {
|
||||
const onPointerMove = (e: PointerEvent) => {
|
||||
if (this.#orbitDragging) {
|
||||
const dx = e.clientX - this.#orbitLastX;
|
||||
const dy = e.clientY - this.#orbitLastY;
|
||||
|
|
@ -1250,9 +1255,13 @@ export class RStackTabBar extends HTMLElement {
|
|||
}
|
||||
};
|
||||
|
||||
const onMouseUp = () => {
|
||||
const onPointerUp = (e: PointerEvent) => {
|
||||
if (this.#orbitDragging) {
|
||||
this.#orbitDragging = false;
|
||||
if (orbitPointerId !== null) {
|
||||
try { sceneContainer.releasePointerCapture(orbitPointerId); } catch {}
|
||||
orbitPointerId = null;
|
||||
}
|
||||
sceneContainer.style.cursor = "";
|
||||
}
|
||||
// Complete flow drag
|
||||
|
|
@ -1267,11 +1276,11 @@ export class RStackTabBar extends HTMLElement {
|
|||
};
|
||||
|
||||
// Attach to document for drag continuity (cleaned up on re-render)
|
||||
document.addEventListener("mousemove", onMouseMove);
|
||||
document.addEventListener("mouseup", onMouseUp);
|
||||
document.addEventListener("pointermove", onPointerMove);
|
||||
document.addEventListener("pointerup", onPointerUp);
|
||||
this.#docCleanup = () => {
|
||||
document.removeEventListener("mousemove", onMouseMove);
|
||||
document.removeEventListener("mouseup", onMouseUp);
|
||||
document.removeEventListener("pointermove", onPointerMove);
|
||||
document.removeEventListener("pointerup", onPointerUp);
|
||||
};
|
||||
|
||||
// Scroll to zoom
|
||||
|
|
@ -1894,6 +1903,7 @@ const STYLES = `
|
|||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: grab;
|
||||
touch-action: none;
|
||||
user-select: none;
|
||||
overflow: visible;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -307,7 +307,7 @@ export class TabCache {
|
|||
if (appEl) {
|
||||
// Remove duplicate shell chrome that shouldn't be in tab panes
|
||||
appEl.querySelectorAll(
|
||||
".rstack-tab-row, rstack-tab-bar, rstack-space-settings, rstack-history-panel",
|
||||
".rstack-tab-row, rstack-tab-bar, rstack-history-panel",
|
||||
).forEach((el) => el.remove());
|
||||
body = appEl.innerHTML;
|
||||
} else if (iframeWrap) {
|
||||
|
|
|
|||
|
|
@ -1916,10 +1916,7 @@
|
|||
<button class="rstack-header__history-btn" id="history-btn" title="History"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg></button>
|
||||
<rstack-history-panel type="canvas"></rstack-history-panel>
|
||||
</div>
|
||||
<div class="rstack-header__dropdown-wrap">
|
||||
<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-space-settings space="" module-id="rspace"></rstack-space-settings>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
<div class="rstack-header__center">
|
||||
<rstack-mi></rstack-mi>
|
||||
|
|
@ -2492,7 +2489,7 @@
|
|||
import { RStackSpaceSwitcher } from "@shared/components/rstack-space-switcher";
|
||||
import { RStackTabBar } from "@shared/components/rstack-tab-bar";
|
||||
import { RStackMi } from "@shared/components/rstack-mi";
|
||||
import { RStackSpaceSettings } from "@shared/components/rstack-space-settings";
|
||||
|
||||
import { RStackHistoryPanel } from "@shared/components/rstack-history-panel";
|
||||
import { RStackCommentBell } from "@shared/components/rstack-comment-bell";
|
||||
import { rspaceNavUrl } from "@shared/url-helpers";
|
||||
|
|
@ -2508,17 +2505,19 @@
|
|||
RStackSpaceSwitcher.define();
|
||||
RStackTabBar.define();
|
||||
RStackMi.define();
|
||||
RStackSpaceSettings.define();
|
||||
|
||||
RStackHistoryPanel.define();
|
||||
RStackCommentBell.define();
|
||||
|
||||
// ── Settings & history panel toggle (same as shell.ts) ──
|
||||
// ── Settings & history panel toggle ──
|
||||
document.getElementById("settings-btn")?.addEventListener("click", () => {
|
||||
document.querySelector("rstack-history-panel")?.close();
|
||||
document.querySelector("rstack-space-settings")?.toggle();
|
||||
const sw = document.querySelector("rstack-space-switcher");
|
||||
const slug = sw?.getAttribute("current") || "";
|
||||
const name = sw?.getAttribute("name") || slug;
|
||||
if (sw && slug) sw.openSettingsModal(slug, name);
|
||||
});
|
||||
document.getElementById("history-btn")?.addEventListener("click", () => {
|
||||
document.querySelector("rstack-space-settings")?.close();
|
||||
document.querySelector("rstack-history-panel")?.toggle();
|
||||
});
|
||||
|
||||
|
|
@ -2820,7 +2819,6 @@
|
|||
if (tabBar) {
|
||||
tabBar.setAttribute("space", communitySlug);
|
||||
document.querySelector("rstack-collab-overlay")?.setAttribute("space", communitySlug);
|
||||
document.querySelector("rstack-space-settings")?.setAttribute("space", communitySlug);
|
||||
|
||||
// Helper: look up a module's display name
|
||||
function getModuleLabel(id) {
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import { RStackAppSwitcher } from "../shared/components/rstack-app-switcher";
|
|||
import { RStackSpaceSwitcher } from "../shared/components/rstack-space-switcher";
|
||||
import { RStackTabBar } from "../shared/components/rstack-tab-bar";
|
||||
import { RStackMi } from "../shared/components/rstack-mi";
|
||||
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";
|
||||
|
|
@ -40,7 +40,7 @@ RStackAppSwitcher.define();
|
|||
RStackSpaceSwitcher.define();
|
||||
RStackTabBar.define();
|
||||
RStackMi.define();
|
||||
RStackSpaceSettings.define();
|
||||
|
||||
RStackModuleSetup.define();
|
||||
RStackHistoryPanel.define();
|
||||
RStackOfflineIndicator.define();
|
||||
|
|
|
|||
Loading…
Reference in New Issue