/** * — Dropdown to switch between rSpace modules. * * Attributes: * current — the active module ID (highlighted) * * Methods: * setModules(list) — provide the list of available modules */ export interface AppSwitcherModule { id: string; name: string; icon: string; description: string; standaloneDomain?: string; } // Pastel badge abbreviations & colors for each module const MODULE_BADGES: Record = { // Creating rspace: { badge: "rS", color: "#5eead4" }, // teal-300 rnotes: { badge: "rN", color: "#fcd34d" }, // amber-300 rpubs: { badge: "rP", color: "#fda4af" }, // rose-300 rswag: { badge: "rSw", color: "#fda4af" }, // rose-300 rsplat: { badge: "r3", color: "#d8b4fe" }, // purple-300 // Planning rcal: { badge: "rC", color: "#7dd3fc" }, // sky-300 rtrips: { badge: "rT", color: "#6ee7b7" }, // emerald-300 rmaps: { badge: "rM", color: "#86efac" }, // green-300 // Communicating rchats: { badge: "rCh", color: "#6ee7b7" }, // emerald-200 rinbox: { badge: "rI", color: "#a5b4fc" }, // indigo-300 rmail: { badge: "rMa", color: "#93c5fd" }, // blue-200 rforum: { badge: "rFo", color: "#fcd34d" }, // amber-200 // Deciding rchoices: { badge: "rCo", color: "#f0abfc" }, // fuchsia-300 rvote: { badge: "rV", color: "#c4b5fd" }, // violet-300 // Funding & Commerce rfunds: { badge: "rF", color: "#bef264" }, // lime-300 rwallet: { badge: "rW", color: "#fde047" }, // yellow-300 rcart: { badge: "rCt", color: "#fdba74" }, // orange-300 rauctions: { badge: "rA", color: "#fca5a5" }, // red-300 rproviders: { badge: "rPr", color: "#fdba74" }, // orange-300 rtube: { badge: "rTu", color: "#f9a8d4" }, // pink-300 // Sharing rphotos: { badge: "rPh", color: "#f9a8d4" }, // pink-200 rnetwork: { badge: "rNe", color: "#93c5fd" }, // blue-300 rsocials: { badge: "rSo", color: "#7dd3fc" }, // sky-200 rfiles: { badge: "rFi", color: "#67e8f9" }, // cyan-300 rbooks: { badge: "rB", color: "#fda4af" }, // rose-300 // Observing rdata: { badge: "rD", color: "#d8b4fe" }, // purple-300 // Work & Productivity rwork: { badge: "rWo", color: "#cbd5e1" }, // slate-300 // Identity & Infrastructure rids: { badge: "rId", color: "#6ee7b7" }, // emerald-300 rstack: { badge: "r*", color: "" }, // gradient (handled separately) }; // Category definitions for the rApp dropdown (display-only grouping) const MODULE_CATEGORIES: Record = { rspace: "Creating", rnotes: "Creating", rpubs: "Creating", rtube: "Creating", rswag: "Creating", rsplat: "Creating", rcal: "Planning", rtrips: "Planning", rmaps: "Planning", rchats: "Communicating", rinbox: "Communicating", rmail: "Communicating", rforum: "Communicating", rchoices: "Deciding", rvote: "Deciding", rfunds: "Funding & Commerce", rwallet: "Funding & Commerce", rcart: "Funding & Commerce", rauctions: "Funding & Commerce", rproviders: "Funding & Commerce", rphotos: "Sharing", rnetwork: "Sharing", rsocials: "Sharing", rfiles: "Sharing", rbooks: "Sharing", rdata: "Observing", rwork: "Work & Productivity", rids: "Identity & Infrastructure", rstack: "Identity & Infrastructure", }; const CATEGORY_ORDER = [ "Creating", "Planning", "Communicating", "Deciding", "Funding & Commerce", "Sharing", "Observing", "Work & Productivity", "Identity & Infrastructure", ]; import { rspaceNavUrl, getCurrentSpace } from "../url-helpers"; export class RStackAppSwitcher extends HTMLElement { #shadow: ShadowRoot; #modules: AppSwitcherModule[] = []; constructor() { super(); this.#shadow = this.attachShadow({ mode: "open" }); } static get observedAttributes() { return ["current"]; } get current(): string { return this.getAttribute("current") || ""; } connectedCallback() { this.#render(); } attributeChangedCallback() { this.#render(); } setModules(modules: AppSwitcherModule[]) { this.#modules = modules; this.#render(); } #renderGroupedModules(current: string): string { // Group modules by category const groups = new Map(); const uncategorized: AppSwitcherModule[] = []; for (const m of this.#modules) { const cat = MODULE_CATEGORIES[m.id]; if (cat) { if (!groups.has(cat)) groups.set(cat, []); groups.get(cat)!.push(m); } else { uncategorized.push(m); } } // rStack header (clickable) let html = ` r*
rStack Self-hosted community app suite
`; for (const cat of CATEGORY_ORDER) { const items = groups.get(cat); if (!items || items.length === 0) continue; html += `
${cat}
`; html += items.map((m) => this.#renderItem(m, current)).join(""); } if (uncategorized.length > 0) { html += `
Other
`; html += uncategorized.map((m) => this.#renderItem(m, current)).join(""); } // Footer html += ` `; return html; } #renderItem(m: AppSwitcherModule, current: string): string { const badgeInfo = MODULE_BADGES[m.id]; const badgeHtml = badgeInfo ? `${badgeInfo.badge}` : `${m.icon}`; const space = this.#getSpaceSlug(); // On demo (bare domain or demo subdomain): link to landing pages const host = window.location.host.split(":")[0]; const onRspace = host.includes("rspace.online"); const href = onRspace && space === "demo" ? `${window.location.protocol}//rspace.online/${m.id}` : rspaceNavUrl(space, m.id); return ` `; } #render() { const current = this.current; const currentMod = this.#modules.find((m) => m.id === current); const badgeInfo = currentMod ? MODULE_BADGES[currentMod.id] : null; const triggerContent = badgeInfo ? `${badgeInfo.badge} ${currentMod!.name}` : currentMod ? `${currentMod.icon} ${currentMod.name}` : `r* rStack`; this.#shadow.innerHTML = `
`; const trigger = this.#shadow.getElementById("trigger")!; const menu = this.#shadow.getElementById("menu")!; trigger.addEventListener("click", (e) => { e.stopPropagation(); menu.classList.toggle("open"); }); // Prevent external links from closing the menu prematurely this.#shadow.querySelectorAll(".item-ext").forEach((el) => { el.addEventListener("click", (e) => e.stopPropagation()); }); document.addEventListener("click", () => menu.classList.remove("open")); } #getSpaceSlug(): string { // Read from the space switcher or URL const spaceSwitcher = document.querySelector("rstack-space-switcher"); if (spaceSwitcher) return spaceSwitcher.getAttribute("current") || "demo"; return getCurrentSpace(); } static define(tag = "rstack-app-switcher") { if (!customElements.get(tag)) customElements.define(tag, RStackAppSwitcher); } } const STYLES = ` :host { display: contents; } .switcher { position: relative; } .trigger { display: flex; align-items: center; gap: 6px; padding: 6px 14px; border-radius: 8px; border: none; font-size: 0.9rem; font-weight: 600; cursor: pointer; transition: background 0.15s; background: rgba(255,255,255,0.08); color: inherit; } :host-context([data-theme="light"]) .trigger { background: rgba(0,0,0,0.05); color: #0f172a; } :host-context([data-theme="dark"]) .trigger { background: rgba(255,255,255,0.08); color: #e2e8f0; } .trigger:hover { background: rgba(255,255,255,0.12); } :host-context([data-theme="light"]) .trigger:hover { background: rgba(0,0,0,0.08); } .trigger-badge { display: inline-flex; align-items: center; justify-content: center; width: 22px; height: 22px; border-radius: 5px; font-size: 0.6rem; font-weight: 900; color: #0f172a; line-height: 1; flex-shrink: 0; } .trigger-badge.rstack-gradient { background: linear-gradient(135deg, #67e8f9, #c4b5fd, #fda4af); } .caret { font-size: 0.7em; opacity: 0.6; } .menu { position: absolute; top: 100%; left: 0; margin-top: 6px; min-width: 300px; border-radius: 12px; overflow: hidden; overflow-y: auto; max-height: 70vh; box-shadow: 0 8px 30px rgba(0,0,0,0.25); display: none; z-index: 200; } .menu.open { display: block; } :host-context([data-theme="light"]) .menu { background: white; border: 1px solid rgba(0,0,0,0.1); } :host-context([data-theme="dark"]) .menu { background: #1e293b; border: 1px solid rgba(255,255,255,0.1); } /* rStack header */ a.rstack-header { display: flex; align-items: center; gap: 10px; padding: 12px 14px; border-bottom: 1px solid rgba(128,128,128,0.15); text-decoration: none; color: inherit; cursor: pointer; transition: background 0.12s; } a.rstack-header:hover { background: rgba(255,255,255,0.05); } .rstack-badge { display: flex; align-items: center; justify-content: center; width: 28px; height: 28px; border-radius: 8px; background: linear-gradient(135deg, #67e8f9, #c4b5fd, #fda4af); font-size: 0.7rem; font-weight: 900; color: #0f172a; line-height: 1; flex-shrink: 0; } .rstack-info { display: flex; flex-direction: column; } .rstack-title { font-size: 0.875rem; font-weight: 700; } .rstack-subtitle { font-size: 0.65rem; opacity: 0.5; } :host-context([data-theme="light"]) .rstack-title { color: #0f172a; } :host-context([data-theme="dark"]) .rstack-title { color: white; } /* Footer */ .rstack-footer { padding: 10px 14px; text-align: center; border-top: 1px solid rgba(128,128,128,0.15); } .rstack-footer a { font-size: 0.7rem; opacity: 0.4; text-decoration: none; color: inherit; transition: opacity 0.15s; } .rstack-footer a:hover { opacity: 0.8; } :host-context([data-theme="light"]) .rstack-footer a:hover { color: #06b6d4; } :host-context([data-theme="dark"]) .rstack-footer a:hover { color: #22d3ee; } .item-row { display: flex; align-items: center; transition: background 0.12s; } :host-context([data-theme="light"]) .item-row { color: #374151; } :host-context([data-theme="light"]) .item-row:hover { background: #f1f5f9; } :host-context([data-theme="light"]) .item-row.active { background: #e0f2fe; } :host-context([data-theme="dark"]) .item-row { color: #e2e8f0; } :host-context([data-theme="dark"]) .item-row:hover { background: rgba(255,255,255,0.05); } :host-context([data-theme="dark"]) .item-row.active { background: rgba(6,182,212,0.1); } .item { display: flex; align-items: center; gap: 10px; padding: 8px 14px; text-decoration: none; cursor: pointer; flex: 1; min-width: 0; color: inherit; } .item-ext { display: flex; align-items: center; justify-content: center; width: 32px; height: 100%; flex-shrink: 0; font-size: 0.8rem; text-decoration: none; opacity: 0; transition: opacity 0.15s; } .item-row:hover .item-ext { opacity: 0.5; } .item-ext:hover { opacity: 1 !important; } :host-context([data-theme="light"]) .item-ext { color: #06b6d4; } :host-context([data-theme="dark"]) .item-ext { color: #22d3ee; } .item-badge { display: flex; align-items: center; justify-content: center; width: 28px; height: 28px; border-radius: 6px; font-size: 0.6rem; font-weight: 900; color: #0f172a; line-height: 1; flex-shrink: 0; } .item-icon { font-size: 1.3rem; width: 28px; text-align: center; flex-shrink: 0; } .item-text { display: flex; flex-direction: column; min-width: 0; flex: 1; } .item-name-row { display: flex; align-items: center; gap: 6px; } .item-name { font-size: 0.875rem; font-weight: 600; } .item-emoji { font-size: 0.875rem; flex-shrink: 0; } .item-desc { font-size: 0.7rem; opacity: 0.5; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .category-header { padding: 8px 14px 4px; font-size: 0.6rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.08em; opacity: 0.5; user-select: none; } .category-header:not(:first-child) { border-top: 1px solid rgba(128,128,128,0.15); margin-top: 4px; padding-top: 10px; } `;