diff --git a/shared/components/rstack-app-switcher.ts b/shared/components/rstack-app-switcher.ts index 270b71f..2ab35ea 100644 --- a/shared/components/rstack-app-switcher.ts +++ b/shared/components/rstack-app-switcher.ts @@ -20,42 +20,44 @@ export interface AppSwitcherModule { // 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 + rspace: { badge: "r🎨", color: "#5eead4" }, // teal-300 + rnotes: { badge: "r📝", color: "#fcd34d" }, // amber-300 + rpubs: { badge: "r📖", color: "#fda4af" }, // rose-300 + rswag: { badge: "r👕", color: "#fda4af" }, // rose-300 + rsplat: { badge: "r🔮", 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 + rcal: { badge: "r📅", color: "#7dd3fc" }, // sky-300 + rtrips: { badge: "r✈️", color: "#6ee7b7" }, // emerald-300 + rmaps: { badge: "r🗺", 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 + rchats: { badge: "r🗨", color: "#6ee7b7" }, // emerald-200 + rinbox: { badge: "r📨", color: "#a5b4fc" }, // indigo-300 + rmail: { badge: "r✉️", color: "#93c5fd" }, // blue-200 + rforum: { badge: "r💬", color: "#fcd34d" }, // amber-200 + rmeets: { badge: "r📹", color: "#67e8f9" }, // cyan-300 // Deciding - rchoices: { badge: "rCo", color: "#f0abfc" }, // fuchsia-300 - rvote: { badge: "rV", color: "#c4b5fd" }, // violet-300 + rchoices: { badge: "r☑️", color: "#f0abfc" }, // fuchsia-300 + rvote: { badge: "r🗳", color: "#c4b5fd" }, // violet-300 // Funding & Commerce - rflows: { badge: "rFl", 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 - rtube: { badge: "rTu", color: "#f9a8d4" }, // pink-300 + rflows: { badge: "r🌊", color: "#bef264" }, // lime-300 + rwallet: { badge: "r💰", color: "#fde047" }, // yellow-300 + rcart: { badge: "r🛒", color: "#fdba74" }, // orange-300 + rauctions: { badge: "r🏛", color: "#fca5a5" }, // red-300 + rtube: { badge: "r🎬", 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 + rphotos: { badge: "r📸", color: "#f9a8d4" }, // pink-200 + rnetwork: { badge: "r🌐", color: "#93c5fd" }, // blue-300 + rsocials: { badge: "r📢", color: "#7dd3fc" }, // sky-200 + rfiles: { badge: "r📁", color: "#67e8f9" }, // cyan-300 + rbooks: { badge: "r📚", color: "#fda4af" }, // rose-300 // Observing - rdata: { badge: "rD", color: "#d8b4fe" }, // purple-300 + rdata: { badge: "r📊", color: "#d8b4fe" }, // purple-300 // Work & Productivity - rwork: { badge: "rWo", color: "#cbd5e1" }, // slate-300 + rwork: { badge: "r📋", color: "#cbd5e1" }, // slate-300 + rschedule: { badge: "r⏱", color: "#a5b4fc" }, // indigo-200 // Identity & Infrastructure - rids: { badge: "rId", color: "#6ee7b7" }, // emerald-300 - rstack: { badge: "r*", color: "" }, // gradient (handled separately) + rids: { badge: "r🪪", color: "#6ee7b7" }, // emerald-300 + rstack: { badge: "r✨", color: "" }, // gradient (handled separately) }; // Category definitions for the rApp dropdown (display-only grouping) @@ -73,6 +75,7 @@ const MODULE_CATEGORIES: Record = { rinbox: "Communicating", rmail: "Communicating", rforum: "Communicating", + rmeets: "Communicating", rchoices: "Deciding", rvote: "Deciding", rflows: "Funding & Commerce", @@ -86,6 +89,7 @@ const MODULE_CATEGORIES: Record = { rbooks: "Sharing", rdata: "Observing", rwork: "Work & Productivity", + rschedule: "Work & Productivity", rids: "Identity & Infrastructure", rstack: "Identity & Infrastructure", }; @@ -123,7 +127,25 @@ export class RStackAppSwitcher extends HTMLElement { return this.getAttribute("current") || ""; } + static #getRecentModules(): string[] { + try { + const raw = localStorage.getItem("rspace-recent-modules"); + if (!raw) return []; + const arr = JSON.parse(raw); + return Array.isArray(arr) ? arr : []; + } catch { return []; } + } + + static #recordRecentModule(id: string) { + try { + const recent = RStackAppSwitcher.#getRecentModules().filter((x) => x !== id); + recent.unshift(id); + localStorage.setItem("rspace-recent-modules", JSON.stringify(recent.slice(0, 3))); + } catch {} + } + connectedCallback() { + if (this.current) RStackAppSwitcher.#recordRecentModule(this.current); this.#render(); } @@ -163,7 +185,7 @@ export class RStackAppSwitcher extends HTMLElement { // rStack header (clickable) let html = ` - r* + r✨
rStack Self-hosted community app suite @@ -171,6 +193,16 @@ export class RStackAppSwitcher extends HTMLElement { `; + // Recently Used section + const recentIds = RStackAppSwitcher.#getRecentModules().filter((id) => id !== current); + const recentModules = recentIds + .map((id) => this.#modules.find((m) => m.id === id)) + .filter((m): m is AppSwitcherModule => !!m); + if (recentModules.length > 0) { + html += `
Recent
`; + html += recentModules.map((m) => this.#renderItem(m, current)).join(""); + } + for (const cat of CATEGORY_ORDER) { const items = groups.get(cat); if (!items || items.length === 0) continue; @@ -222,7 +254,6 @@ export class RStackAppSwitcher extends HTMLElement { ${m.name} ${scopeBadge} - ${m.icon} ${m.description}
@@ -241,7 +272,7 @@ export class RStackAppSwitcher extends HTMLElement { ? `${badgeInfo.badge} ${currentMod!.name}` : currentMod ? `${currentMod.icon} ${currentMod.name}` - : `r* rSpace`; + : `r✨ rSpace`; this.#shadow.innerHTML = ` @@ -308,6 +339,7 @@ export class RStackAppSwitcher extends HTMLElement { el.addEventListener("click", (e) => { const moduleId = (el as HTMLElement).dataset.id; if (!moduleId) return; + RStackAppSwitcher.#recordRecentModule(moduleId); // Skip interception if tab system isn't active (landing pages) if (!(window as any).__rspaceTabBar) return; // Only intercept same-origin links @@ -358,8 +390,8 @@ const STYLES = ` .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: var(--rs-text-inverse); - line-height: 1; flex-shrink: 0; + font-size: 0.65rem; font-weight: 900; color: var(--rs-text-inverse); + line-height: 1; flex-shrink: 0; white-space: nowrap; } .trigger-badge.rstack-gradient { background: linear-gradient(135deg, #67e8f9, #c4b5fd, #fda4af); @@ -461,14 +493,13 @@ a.rstack-header:hover { background: var(--rs-bg-hover); } .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: var(--rs-text-inverse); - line-height: 1; flex-shrink: 0; + font-size: 0.7rem; font-weight: 900; color: var(--rs-text-inverse); + line-height: 1; flex-shrink: 0; white-space: nowrap; } .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; } .scope-badge { @@ -486,6 +517,9 @@ a.rstack-header:hover { background: var(--rs-bg-hover); } border-top: 1px solid var(--rs-border-subtle); margin-top: 4px; padding-top: 10px; } +.recent-header { + border-top: none; margin-top: 0; padding-top: 8px; +} /* Mobile: sidebar overlays instead of pushing */ @media (max-width: 640px) {