feat(app-switcher): emoji badges + recently used section
Replace text abbreviation badges (rN, rPh, etc.) with r+emoji format (r📝, r📸, etc.), remove duplicate emoji from item rows, and add a "Recently Used" section at the top of the sidebar persisted via localStorage. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
c8e95ed506
commit
fc65bec9dc
|
|
@ -20,42 +20,44 @@ export interface AppSwitcherModule {
|
|||
// Pastel badge abbreviations & colors for each module
|
||||
const MODULE_BADGES: Record<string, { badge: string; color: string }> = {
|
||||
// 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<string, string> = {
|
|||
rinbox: "Communicating",
|
||||
rmail: "Communicating",
|
||||
rforum: "Communicating",
|
||||
rmeets: "Communicating",
|
||||
rchoices: "Deciding",
|
||||
rvote: "Deciding",
|
||||
rflows: "Funding & Commerce",
|
||||
|
|
@ -86,6 +89,7 @@ const MODULE_CATEGORIES: Record<string, string> = {
|
|||
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 = `
|
||||
<a class="rstack-header" href="https://rstack.online" target="_blank" rel="noopener">
|
||||
<span class="rstack-badge">r*</span>
|
||||
<span class="rstack-badge">r✨</span>
|
||||
<div class="rstack-info">
|
||||
<span class="rstack-title">rStack</span>
|
||||
<span class="rstack-subtitle">Self-hosted community app suite</span>
|
||||
|
|
@ -171,6 +193,16 @@ export class RStackAppSwitcher extends HTMLElement {
|
|||
</a>
|
||||
`;
|
||||
|
||||
// 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 += `<div class="category-header recent-header">Recent</div>`;
|
||||
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 {
|
|||
<span class="item-name-row">
|
||||
<span class="item-name">${m.name}</span>
|
||||
${scopeBadge}
|
||||
<span class="item-emoji">${m.icon}</span>
|
||||
</span>
|
||||
<span class="item-desc">${m.description}</span>
|
||||
</div>
|
||||
|
|
@ -241,7 +272,7 @@ export class RStackAppSwitcher extends HTMLElement {
|
|||
? `<span class="trigger-badge" style="background:${badgeInfo.color}">${badgeInfo.badge}</span> ${currentMod!.name}`
|
||||
: currentMod
|
||||
? `${currentMod.icon} ${currentMod.name}`
|
||||
: `<span class="trigger-badge rstack-gradient">r*</span> rSpace`;
|
||||
: `<span class="trigger-badge rstack-gradient">r✨</span> rSpace`;
|
||||
|
||||
this.#shadow.innerHTML = `
|
||||
<style>${STYLES}</style>
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue