Merge branch 'dev'
CI/CD / deploy (push) Successful in 3m25s
Details
CI/CD / deploy (push) Successful in 3m25s
Details
This commit is contained in:
commit
fa08c00d38
|
|
@ -11,9 +11,9 @@ const routes = new Hono();
|
||||||
routes.get('/', (c) => c.text('rAuctions — coming soon'));
|
routes.get('/', (c) => c.text('rAuctions — coming soon'));
|
||||||
|
|
||||||
export const auctionsModule: RSpaceModule = {
|
export const auctionsModule: RSpaceModule = {
|
||||||
id: 'auctions',
|
id: 'rauctions',
|
||||||
name: 'rAuctions',
|
name: 'rAuctions',
|
||||||
icon: '🏛',
|
icon: '🎭',
|
||||||
description: 'Community auctions with USDC',
|
description: 'Community auctions with USDC',
|
||||||
routes,
|
routes,
|
||||||
scoping: { defaultScope: 'space', userConfigurable: true },
|
scoping: { defaultScope: 'space', userConfigurable: true },
|
||||||
|
|
|
||||||
|
|
@ -3787,7 +3787,7 @@ async function serveStatic(path: string, url?: URL): Promise<Response | null> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Module ID aliases (plural/misspelling → canonical) ──
|
// ── Module ID aliases (plural/misspelling → canonical) ──
|
||||||
const MODULE_ALIASES: Record<string, string> = { rsheet: "rsheets" };
|
const MODULE_ALIASES: Record<string, string> = { rsheet: "rsheets", auctions: "rauctions" };
|
||||||
function resolveModuleAlias(id: string): string { return MODULE_ALIASES[id] ?? id; }
|
function resolveModuleAlias(id: string): string { return MODULE_ALIASES[id] ?? id; }
|
||||||
|
|
||||||
// ── Standalone domain → module lookup ──
|
// ── Standalone domain → module lookup ──
|
||||||
|
|
|
||||||
|
|
@ -16,40 +16,45 @@ const COMPAT_POLYFILLS = `<script>(function(){if(typeof structuredClone!=="funct
|
||||||
// ── Dynamic per-module favicon (inline, runs after body parse) ──
|
// ── Dynamic per-module favicon (inline, runs after body parse) ──
|
||||||
// Badge map mirrors MODULE_BADGES from rstack-app-switcher.ts — kept in sync manually.
|
// Badge map mirrors MODULE_BADGES from rstack-app-switcher.ts — kept in sync manually.
|
||||||
const FAVICON_BADGE_MAP: Record<string, { badge: string; color: string }> = {
|
const FAVICON_BADGE_MAP: Record<string, { badge: string; color: string }> = {
|
||||||
rspace: { badge: "r🎨", color: "#5eead4" },
|
rspace: { badge: "r🎨", color: "#5eead4" },
|
||||||
rnotes: { badge: "r📝", color: "#fcd34d" },
|
rdocs: { badge: "r📄", color: "#a5b4fc" },
|
||||||
rpubs: { badge: "r📖", color: "#fda4af" },
|
rdesign: { badge: "r🎨", color: "#c4b5fd" },
|
||||||
rswag: { badge: "r👕", color: "#fda4af" },
|
rnotes: { badge: "r📝", color: "#fcd34d" },
|
||||||
rsplat: { badge: "r🔮", color: "#d8b4fe" },
|
rpubs: { badge: "r📖", color: "#fda4af" },
|
||||||
rcal: { badge: "r📅", color: "#7dd3fc" },
|
rsheets: { badge: "r📑", color: "#86efac" },
|
||||||
rtrips: { badge: "r✈️", color: "#6ee7b7" },
|
rsplat: { badge: "r🔮", color: "#d8b4fe" },
|
||||||
rmaps: { badge: "r🗺", color: "#86efac" },
|
rswag: { badge: "r👕", color: "#fda4af" },
|
||||||
rchats: { badge: "r🗨", color: "#6ee7b7" },
|
rchats: { badge: "r🗨", color: "#6ee7b7" },
|
||||||
rinbox: { badge: "r📨", color: "#a5b4fc" },
|
rforum: { badge: "r💬", color: "#fcd34d" },
|
||||||
rmail: { badge: "r✉️", color: "#93c5fd" },
|
rinbox: { badge: "r📨", color: "#a5b4fc" },
|
||||||
rforum: { badge: "r💬", color: "#fcd34d" },
|
rmail: { badge: "r✉️", color: "#93c5fd" },
|
||||||
rmeets: { badge: "r📹", color: "#67e8f9" },
|
rmeets: { badge: "r📹", color: "#67e8f9" },
|
||||||
|
rcal: { badge: "r📅", color: "#7dd3fc" },
|
||||||
rchoices: { badge: "r☑️", color: "#f0abfc" },
|
rchoices: { badge: "r☑️", color: "#f0abfc" },
|
||||||
|
rschedule: { badge: "r⏱", color: "#a5b4fc" },
|
||||||
|
rtasks: { badge: "r📋", color: "#cbd5e1" },
|
||||||
|
rtime: { badge: "r⏳", color: "#a78bfa" },
|
||||||
rvote: { badge: "r🗳", color: "#c4b5fd" },
|
rvote: { badge: "r🗳", color: "#c4b5fd" },
|
||||||
|
crowdsurf: { badge: "r🏄", color: "#fde68a" },
|
||||||
|
rnetwork: { badge: "r🌐", color: "#93c5fd" },
|
||||||
|
rsocials: { badge: "r📢", color: "#7dd3fc" },
|
||||||
|
rexchange: { badge: "r💱", color: "#fde047" },
|
||||||
rflows: { badge: "r🌊", color: "#bef264" },
|
rflows: { badge: "r🌊", color: "#bef264" },
|
||||||
rwallet: { badge: "r💰", color: "#fde047" },
|
rwallet: { badge: "r💰", color: "#fde047" },
|
||||||
rcart: { badge: "r🛒", color: "#fdba74" },
|
rcart: { badge: "r🛒", color: "#fdba74" },
|
||||||
rauctions: { badge: "r🏛", color: "#fca5a5" },
|
rauctions: { badge: "r🎭", color: "#fca5a5" },
|
||||||
rtube: { badge: "r🎬", color: "#f9a8d4" },
|
rgov: { badge: "r⚖️", color: "#94a3b8" },
|
||||||
rphotos: { badge: "r📸", color: "#f9a8d4" },
|
rphotos: { badge: "r📸", color: "#f9a8d4" },
|
||||||
rnetwork: { badge: "r🌐", color: "#93c5fd" },
|
|
||||||
rsocials: { badge: "r📢", color: "#7dd3fc" },
|
|
||||||
rfiles: { badge: "r📁", color: "#67e8f9" },
|
rfiles: { badge: "r📁", color: "#67e8f9" },
|
||||||
|
rtube: { badge: "r🎬", color: "#f9a8d4" },
|
||||||
rbooks: { badge: "r📚", color: "#fda4af" },
|
rbooks: { badge: "r📚", color: "#fda4af" },
|
||||||
rdata: { badge: "r📊", color: "#d8b4fe" },
|
rmaps: { badge: "r🗺", color: "#86efac" },
|
||||||
|
rtrips: { badge: "r✈️", color: "#6ee7b7" },
|
||||||
rbnb: { badge: "r🏠", color: "#fbbf24" },
|
rbnb: { badge: "r🏠", color: "#fbbf24" },
|
||||||
rvnb: { badge: "r🚐", color: "#a5f3fc" },
|
rvnb: { badge: "r🚐", color: "#a5f3fc" },
|
||||||
rtasks: { badge: "r📋", color: "#cbd5e1" },
|
rdata: { badge: "r📊", color: "#d8b4fe" },
|
||||||
rschedule: { badge: "r⏱", color: "#a5b4fc" },
|
ragents: { badge: "r🤖", color: "#6ee7b7" },
|
||||||
crowdsurf: { badge: "r🏄", color: "#fde68a" },
|
|
||||||
rids: { badge: "r🪪", color: "#6ee7b7" },
|
rids: { badge: "r🪪", color: "#6ee7b7" },
|
||||||
rdesign: { badge: "r🎨", color: "#7c3aed" },
|
|
||||||
rtime: { badge: "r⏳", color: "#a78bfa" },
|
|
||||||
rstack: { badge: "r✨", color: "#c4b5fd" },
|
rstack: { badge: "r✨", color: "#c4b5fd" },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,47 +20,54 @@ export interface AppSwitcherModule {
|
||||||
|
|
||||||
// Pastel badge abbreviations & colors for each module
|
// Pastel badge abbreviations & colors for each module
|
||||||
const MODULE_BADGES: Record<string, { badge: string; color: string }> = {
|
const MODULE_BADGES: Record<string, { badge: string; color: string }> = {
|
||||||
// Creating
|
// Create
|
||||||
rspace: { badge: "r🎨", color: "#5eead4" }, // teal-300
|
rspace: { badge: "r🎨", color: "#5eead4" }, // teal-300
|
||||||
|
rdocs: { badge: "r📄", color: "#a5b4fc" }, // indigo-300
|
||||||
rnotes: { badge: "r📝", color: "#fcd34d" }, // amber-300
|
rnotes: { badge: "r📝", color: "#fcd34d" }, // amber-300
|
||||||
rpubs: { badge: "r📖", color: "#fda4af" }, // rose-300
|
rpubs: { badge: "r📖", color: "#fda4af" }, // rose-300
|
||||||
|
rdesign: { badge: "r🎨", color: "#c4b5fd" }, // violet-300
|
||||||
|
rsheets: { badge: "r📑", color: "#86efac" }, // green-300
|
||||||
rswag: { badge: "r👕", color: "#fda4af" }, // rose-300
|
rswag: { badge: "r👕", color: "#fda4af" }, // rose-300
|
||||||
rsplat: { badge: "r🔮", color: "#d8b4fe" }, // purple-300
|
rsplat: { badge: "r🔮", color: "#d8b4fe" }, // purple-300
|
||||||
// Planning
|
// Communicate
|
||||||
rcal: { badge: "r📅", color: "#7dd3fc" }, // sky-300
|
|
||||||
rtrips: { badge: "r✈️", color: "#6ee7b7" }, // emerald-300
|
|
||||||
rmaps: { badge: "r🗺", color: "#86efac" }, // green-300
|
|
||||||
// Communicating
|
|
||||||
rchats: { badge: "r🗨", color: "#6ee7b7" }, // emerald-200
|
rchats: { badge: "r🗨", color: "#6ee7b7" }, // emerald-200
|
||||||
rinbox: { badge: "r📨", color: "#a5b4fc" }, // indigo-300
|
rinbox: { badge: "r📨", color: "#a5b4fc" }, // indigo-300
|
||||||
rmail: { badge: "r✉️", color: "#93c5fd" }, // blue-200
|
rmail: { badge: "r✉️", color: "#93c5fd" }, // blue-200
|
||||||
rforum: { badge: "r💬", color: "#fcd34d" }, // amber-200
|
rforum: { badge: "r💬", color: "#fcd34d" }, // amber-200
|
||||||
rmeets: { badge: "r📹", color: "#67e8f9" }, // cyan-300
|
rmeets: { badge: "r📹", color: "#67e8f9" }, // cyan-300
|
||||||
// Deciding
|
// Coordinate
|
||||||
|
rcal: { badge: "r📅", color: "#7dd3fc" }, // sky-300
|
||||||
rchoices: { badge: "r☑️", color: "#f0abfc" }, // fuchsia-300
|
rchoices: { badge: "r☑️", color: "#f0abfc" }, // fuchsia-300
|
||||||
|
rschedule: { badge: "r⏱", color: "#a5b4fc" }, // indigo-200
|
||||||
|
rtasks: { badge: "r📋", color: "#cbd5e1" }, // slate-300
|
||||||
|
rtime: { badge: "r⏳", color: "#a78bfa" }, // violet-400
|
||||||
rvote: { badge: "r🗳", color: "#c4b5fd" }, // violet-300
|
rvote: { badge: "r🗳", color: "#c4b5fd" }, // violet-300
|
||||||
// Funding & Commerce
|
crowdsurf: { badge: "r🏄", color: "#fde68a" }, // amber-200
|
||||||
|
// Connect
|
||||||
|
rnetwork: { badge: "r🌐", color: "#93c5fd" }, // blue-300
|
||||||
|
rsocials: { badge: "r📢", color: "#7dd3fc" }, // sky-200
|
||||||
|
// Commerce
|
||||||
|
rexchange: { badge: "r💱", color: "#fde047" }, // yellow-300
|
||||||
rflows: { badge: "r🌊", color: "#bef264" }, // lime-300
|
rflows: { badge: "r🌊", color: "#bef264" }, // lime-300
|
||||||
rwallet: { badge: "r💰", color: "#fde047" }, // yellow-300
|
rwallet: { badge: "r💰", color: "#fde047" }, // yellow-300
|
||||||
rcart: { badge: "r🛒", color: "#fdba74" }, // orange-300
|
rcart: { badge: "r🛒", color: "#fdba74" }, // orange-300
|
||||||
rauctions: { badge: "r🏛", color: "#fca5a5" }, // red-300
|
rauctions: { badge: "r🎭", color: "#fca5a5" }, // red-300
|
||||||
rtube: { badge: "r🎬", color: "#f9a8d4" }, // pink-300
|
// Govern
|
||||||
// Sharing
|
rgov: { badge: "r⚖️", color: "#94a3b8" }, // slate-400
|
||||||
|
// Media
|
||||||
rphotos: { badge: "r📸", color: "#f9a8d4" }, // pink-200
|
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
|
rfiles: { badge: "r📁", color: "#67e8f9" }, // cyan-300
|
||||||
|
rtube: { badge: "r🎬", color: "#f9a8d4" }, // pink-300
|
||||||
rbooks: { badge: "r📚", color: "#fda4af" }, // rose-300
|
rbooks: { badge: "r📚", color: "#fda4af" }, // rose-300
|
||||||
// Observing
|
|
||||||
rdata: { badge: "r📊", color: "#d8b4fe" }, // purple-300
|
|
||||||
// Travel & Stay
|
// Travel & Stay
|
||||||
|
rmaps: { badge: "r🗺", color: "#86efac" }, // green-300
|
||||||
|
rtrips: { badge: "r✈️", color: "#6ee7b7" }, // emerald-300
|
||||||
rbnb: { badge: "r🏠", color: "#fbbf24" }, // amber-300
|
rbnb: { badge: "r🏠", color: "#fbbf24" }, // amber-300
|
||||||
rvnb: { badge: "r🚐", color: "#a5f3fc" }, // cyan-200
|
rvnb: { badge: "r🚐", color: "#a5f3fc" }, // cyan-200
|
||||||
// Coordinate
|
// Observe
|
||||||
rtasks: { badge: "r📋", color: "#cbd5e1" }, // slate-300
|
rdata: { badge: "r📊", color: "#d8b4fe" }, // purple-300
|
||||||
rschedule: { badge: "r⏱", color: "#a5b4fc" }, // indigo-200
|
// Platform
|
||||||
crowdsurf: { badge: "r🏄", color: "#fde68a" }, // amber-200
|
ragents: { badge: "r🤖", color: "#6ee7b7" }, // emerald-300
|
||||||
// Identity & Infrastructure
|
|
||||||
rids: { badge: "r🪪", color: "#6ee7b7" }, // emerald-300
|
rids: { badge: "r🪪", color: "#6ee7b7" }, // emerald-300
|
||||||
rstack: { badge: "r✨", color: "" }, // gradient (handled separately)
|
rstack: { badge: "r✨", color: "" }, // gradient (handled separately)
|
||||||
};
|
};
|
||||||
|
|
@ -68,45 +75,53 @@ const MODULE_BADGES: Record<string, { badge: string; color: string }> = {
|
||||||
// Category definitions for the rApp dropdown (display-only grouping)
|
// Category definitions for the rApp dropdown (display-only grouping)
|
||||||
const MODULE_CATEGORIES: Record<string, string> = {
|
const MODULE_CATEGORIES: Record<string, string> = {
|
||||||
// Create
|
// Create
|
||||||
rspace: "Create",
|
rdocs: "Create",
|
||||||
|
rdesign: "Create",
|
||||||
rnotes: "Create",
|
rnotes: "Create",
|
||||||
rpubs: "Create",
|
rpubs: "Create",
|
||||||
|
rsheets: "Create",
|
||||||
rsplat: "Create",
|
rsplat: "Create",
|
||||||
|
rspace: "Create",
|
||||||
rswag: "Create",
|
rswag: "Create",
|
||||||
// Communicate
|
// Communicate
|
||||||
rchats: "Communicate",
|
rchats: "Communicate",
|
||||||
|
rforum: "Communicate",
|
||||||
rinbox: "Communicate",
|
rinbox: "Communicate",
|
||||||
rmail: "Communicate",
|
rmail: "Communicate",
|
||||||
rforum: "Communicate",
|
|
||||||
rmeets: "Communicate",
|
rmeets: "Communicate",
|
||||||
// Coordinate
|
// Coordinate
|
||||||
|
crowdsurf: "Coordinate",
|
||||||
rcal: "Coordinate",
|
rcal: "Coordinate",
|
||||||
|
rchoices: "Coordinate",
|
||||||
rschedule: "Coordinate",
|
rschedule: "Coordinate",
|
||||||
rtasks: "Coordinate",
|
rtasks: "Coordinate",
|
||||||
rchoices: "Coordinate",
|
rtime: "Coordinate",
|
||||||
rvote: "Coordinate",
|
rvote: "Coordinate",
|
||||||
crowdsurf: "Coordinate",
|
|
||||||
// Connect
|
// Connect
|
||||||
rnetwork: "Connect",
|
rnetwork: "Connect",
|
||||||
rsocials: "Connect",
|
rsocials: "Connect",
|
||||||
// Commerce
|
// Commerce
|
||||||
|
rauctions: "Commerce",
|
||||||
|
rcart: "Commerce",
|
||||||
|
rexchange: "Commerce",
|
||||||
rflows: "Commerce",
|
rflows: "Commerce",
|
||||||
rwallet: "Commerce",
|
rwallet: "Commerce",
|
||||||
rcart: "Commerce",
|
// Govern
|
||||||
rauctions: "Commerce",
|
rgov: "Govern",
|
||||||
// Media
|
// Media
|
||||||
rphotos: "Media",
|
|
||||||
rfiles: "Media",
|
|
||||||
rtube: "Media",
|
|
||||||
rbooks: "Media",
|
rbooks: "Media",
|
||||||
|
rfiles: "Media",
|
||||||
|
rphotos: "Media",
|
||||||
|
rtube: "Media",
|
||||||
// Travel & Stay
|
// Travel & Stay
|
||||||
|
rbnb: "Travel & Stay",
|
||||||
rmaps: "Travel & Stay",
|
rmaps: "Travel & Stay",
|
||||||
rtrips: "Travel & Stay",
|
rtrips: "Travel & Stay",
|
||||||
rbnb: "Travel & Stay",
|
|
||||||
rvnb: "Travel & Stay",
|
rvnb: "Travel & Stay",
|
||||||
// Observe
|
// Observe
|
||||||
rdata: "Observe",
|
rdata: "Observe",
|
||||||
// Platform
|
// Platform
|
||||||
|
ragents: "Platform",
|
||||||
rids: "Platform",
|
rids: "Platform",
|
||||||
rstack: "Platform",
|
rstack: "Platform",
|
||||||
};
|
};
|
||||||
|
|
@ -117,6 +132,7 @@ const CATEGORY_ORDER = [
|
||||||
"Coordinate",
|
"Coordinate",
|
||||||
"Connect",
|
"Connect",
|
||||||
"Commerce",
|
"Commerce",
|
||||||
|
"Govern",
|
||||||
"Media",
|
"Media",
|
||||||
"Travel & Stay",
|
"Travel & Stay",
|
||||||
"Observe",
|
"Observe",
|
||||||
|
|
@ -132,11 +148,22 @@ export class RStackAppSwitcher extends HTMLElement {
|
||||||
#isOpen = false;
|
#isOpen = false;
|
||||||
#catalogOpen = false;
|
#catalogOpen = false;
|
||||||
#catalogBusy = false;
|
#catalogBusy = false;
|
||||||
|
#sortMode: 'function' | 'alpha' = 'function';
|
||||||
|
#pinnedIds: Set<string> = new Set();
|
||||||
#outsideClickHandler: ((e: PointerEvent) => void) | null = null;
|
#outsideClickHandler: ((e: PointerEvent) => void) | null = null;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.#shadow = this.attachShadow({ mode: "open" });
|
this.#shadow = this.attachShadow({ mode: "open" });
|
||||||
|
// Restore persisted preferences
|
||||||
|
try {
|
||||||
|
const sort = localStorage.getItem("rspace-sort-mode");
|
||||||
|
if (sort === "alpha" || sort === "function") this.#sortMode = sort;
|
||||||
|
} catch {}
|
||||||
|
try {
|
||||||
|
const pinned = JSON.parse(localStorage.getItem("rspace-pinned-modules") || "[]");
|
||||||
|
if (Array.isArray(pinned)) this.#pinnedIds = new Set(pinned);
|
||||||
|
} catch {}
|
||||||
}
|
}
|
||||||
|
|
||||||
static get observedAttributes() {
|
static get observedAttributes() {
|
||||||
|
|
@ -193,19 +220,8 @@ export class RStackAppSwitcher extends HTMLElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
#renderGroupedModules(current: string): string {
|
#renderGroupedModules(current: string): string {
|
||||||
// Group modules by category
|
const alpha = (a: AppSwitcherModule, b: AppSwitcherModule) =>
|
||||||
const groups = new Map<string, AppSwitcherModule[]>();
|
a.name.localeCompare(b.name);
|
||||||
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)
|
// rStack header (clickable)
|
||||||
let html = `
|
let html = `
|
||||||
|
|
@ -218,6 +234,15 @@ export class RStackAppSwitcher extends HTMLElement {
|
||||||
</a>
|
</a>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
// Pinned section
|
||||||
|
const pinnedModules = this.#modules
|
||||||
|
.filter((m) => this.#pinnedIds.has(m.id))
|
||||||
|
.sort(alpha);
|
||||||
|
if (pinnedModules.length > 0) {
|
||||||
|
html += `<div class="category-header recent-header">Pinned</div>`;
|
||||||
|
html += pinnedModules.map((m) => this.#renderItem(m, current)).join("");
|
||||||
|
}
|
||||||
|
|
||||||
// Recently Used section
|
// Recently Used section
|
||||||
const recentIds = RStackAppSwitcher.#getRecentModules().filter((id) => id !== current);
|
const recentIds = RStackAppSwitcher.#getRecentModules().filter((id) => id !== current);
|
||||||
const recentModules = recentIds
|
const recentModules = recentIds
|
||||||
|
|
@ -228,15 +253,38 @@ export class RStackAppSwitcher extends HTMLElement {
|
||||||
html += recentModules.map((m) => this.#renderItem(m, current)).join("");
|
html += recentModules.map((m) => this.#renderItem(m, current)).join("");
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const cat of CATEGORY_ORDER) {
|
if (this.#sortMode === 'alpha') {
|
||||||
const items = groups.get(cat);
|
// Flat alphabetical list
|
||||||
if (!items || items.length === 0) continue;
|
const sorted = [...this.#modules].sort(alpha);
|
||||||
html += `<div class="category-header">${cat}</div>`;
|
html += `<div class="category-header">All rApps</div>`;
|
||||||
html += items.map((m) => this.#renderItem(m, current)).join("");
|
html += sorted.map((m) => this.#renderItem(m, current)).join("");
|
||||||
}
|
} else {
|
||||||
if (uncategorized.length > 0) {
|
// Group by category, alphabetical within each group
|
||||||
html += `<div class="category-header">Other</div>`;
|
const groups = new Map<string, AppSwitcherModule[]>();
|
||||||
html += uncategorized.map((m) => this.#renderItem(m, current)).join("");
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const cat of CATEGORY_ORDER) {
|
||||||
|
const items = groups.get(cat);
|
||||||
|
if (!items || items.length === 0) continue;
|
||||||
|
items.sort(alpha);
|
||||||
|
html += `<div class="category-header">${cat}</div>`;
|
||||||
|
html += items.map((m) => this.#renderItem(m, current)).join("");
|
||||||
|
}
|
||||||
|
if (uncategorized.length > 0) {
|
||||||
|
uncategorized.sort(alpha);
|
||||||
|
html += `<div class="category-header">Other</div>`;
|
||||||
|
html += uncategorized.map((m) => this.#renderItem(m, current)).join("");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// "Manage rApps" catalog section
|
// "Manage rApps" catalog section
|
||||||
|
|
@ -278,8 +326,12 @@ export class RStackAppSwitcher extends HTMLElement {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Footer
|
// Sort toggle + Footer
|
||||||
html += `
|
html += `
|
||||||
|
<div class="sort-toggle">
|
||||||
|
<button class="sort-btn ${this.#sortMode === 'function' ? 'sort-active' : ''}" id="sort-function" title="Sort by function">By Function</button>
|
||||||
|
<button class="sort-btn ${this.#sortMode === 'alpha' ? 'sort-active' : ''}" id="sort-alpha" title="Sort alphabetically">A–Z</button>
|
||||||
|
</div>
|
||||||
<div class="rstack-footer">
|
<div class="rstack-footer">
|
||||||
<a href="https://rstack.online" target="_blank" rel="noopener">rstack.online — self-hosted, community-run</a>
|
<a href="https://rstack.online" target="_blank" rel="noopener">rstack.online — self-hosted, community-run</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -319,8 +371,6 @@ export class RStackAppSwitcher extends HTMLElement {
|
||||||
: `<span class="item-icon">${m.icon}</span>`;
|
: `<span class="item-icon">${m.icon}</span>`;
|
||||||
|
|
||||||
const space = this.#getSpaceSlug();
|
const space = this.#getSpaceSlug();
|
||||||
// On bare domain or standalone r*.online: link to landing pages.
|
|
||||||
// On demo.rspace.online or user subdomains: use rspaceNavUrl for in-app navigation.
|
|
||||||
const host = window.location.host.split(":")[0];
|
const host = window.location.host.split(":")[0];
|
||||||
const isBareDomain = host === "rspace.online" || host === "www.rspace.online";
|
const isBareDomain = host === "rspace.online" || host === "www.rspace.online";
|
||||||
const href =
|
const href =
|
||||||
|
|
@ -332,6 +382,8 @@ export class RStackAppSwitcher extends HTMLElement {
|
||||||
? `<span class="scope-badge scope-global" title="Global data (shared across spaces)">G</span>`
|
? `<span class="scope-badge scope-global" title="Global data (shared across spaces)">G</span>`
|
||||||
: "";
|
: "";
|
||||||
|
|
||||||
|
const isPinned = this.#pinnedIds.has(m.id);
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="item-row ${m.id === current ? "active" : ""}">
|
<div class="item-row ${m.id === current ? "active" : ""}">
|
||||||
<a class="item"
|
<a class="item"
|
||||||
|
|
@ -346,6 +398,9 @@ export class RStackAppSwitcher extends HTMLElement {
|
||||||
<span class="item-desc">${m.description}</span>
|
<span class="item-desc">${m.description}</span>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
|
<button class="pin-btn ${isPinned ? 'pin-active' : ''}"
|
||||||
|
data-pin-id="${m.id}"
|
||||||
|
title="${isPinned ? 'Unpin' : 'Pin to top'}">★</button>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
@ -465,6 +520,34 @@ export class RStackAppSwitcher extends HTMLElement {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Sort toggle
|
||||||
|
this.#shadow.getElementById("sort-function")?.addEventListener("click", () => {
|
||||||
|
this.#sortMode = 'function';
|
||||||
|
try { localStorage.setItem("rspace-sort-mode", "function"); } catch {}
|
||||||
|
this.#render();
|
||||||
|
});
|
||||||
|
this.#shadow.getElementById("sort-alpha")?.addEventListener("click", () => {
|
||||||
|
this.#sortMode = 'alpha';
|
||||||
|
try { localStorage.setItem("rspace-sort-mode", "alpha"); } catch {}
|
||||||
|
this.#render();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Pin/unpin buttons
|
||||||
|
this.#shadow.querySelectorAll<HTMLElement>("[data-pin-id]").forEach((btn) => {
|
||||||
|
btn.addEventListener("click", (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
const id = btn.dataset.pinId!;
|
||||||
|
if (this.#pinnedIds.has(id)) {
|
||||||
|
this.#pinnedIds.delete(id);
|
||||||
|
} else {
|
||||||
|
this.#pinnedIds.add(id);
|
||||||
|
}
|
||||||
|
try { localStorage.setItem("rspace-pinned-modules", JSON.stringify([...this.#pinnedIds])); } catch {}
|
||||||
|
this.#render();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// Catalog toggle
|
// Catalog toggle
|
||||||
this.#shadow.getElementById("catalog-toggle")?.addEventListener("click", () => {
|
this.#shadow.getElementById("catalog-toggle")?.addEventListener("click", () => {
|
||||||
this.#catalogOpen = !this.#catalogOpen;
|
this.#catalogOpen = !this.#catalogOpen;
|
||||||
|
|
@ -789,6 +872,40 @@ a.rstack-header:hover, a.rstack-header:active { background: var(--rs-bg-hover);
|
||||||
}
|
}
|
||||||
.catalog-btn--remove:hover { border-color: #ef4444; background: rgba(239,68,68,0.1); }
|
.catalog-btn--remove:hover { border-color: #ef4444; background: rgba(239,68,68,0.1); }
|
||||||
|
|
||||||
|
/* ── Pin star button ── */
|
||||||
|
.pin-btn {
|
||||||
|
width: 28px; height: 28px; flex-shrink: 0;
|
||||||
|
border: none; background: none; cursor: pointer;
|
||||||
|
font-size: 0.85rem; line-height: 1;
|
||||||
|
color: var(--rs-text-secondary); opacity: 0;
|
||||||
|
transition: opacity 0.15s, color 0.15s;
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.item-row:hover .pin-btn,
|
||||||
|
.pin-btn.pin-active { opacity: 1; }
|
||||||
|
.pin-btn.pin-active { color: #fbbf24; }
|
||||||
|
.pin-btn:hover { color: #fbbf24; background: rgba(251,191,36,0.1); }
|
||||||
|
|
||||||
|
/* ── Sort toggle ── */
|
||||||
|
.sort-toggle {
|
||||||
|
display: flex; gap: 4px; padding: 6px 14px;
|
||||||
|
border-top: 1px solid var(--rs-border-subtle);
|
||||||
|
}
|
||||||
|
.sort-btn {
|
||||||
|
flex: 1; padding: 4px 8px; border-radius: 5px;
|
||||||
|
border: 1px solid var(--rs-border); background: none;
|
||||||
|
font-size: 0.65rem; font-weight: 600;
|
||||||
|
text-transform: uppercase; letter-spacing: 0.04em;
|
||||||
|
color: var(--rs-text-secondary); cursor: pointer;
|
||||||
|
transition: background 0.12s, color 0.12s, border-color 0.12s;
|
||||||
|
}
|
||||||
|
.sort-btn:hover { background: var(--rs-bg-hover); }
|
||||||
|
.sort-btn.sort-active {
|
||||||
|
background: var(--rs-bg-active); color: var(--rs-text-primary);
|
||||||
|
border-color: var(--rs-accent, #6366f1);
|
||||||
|
}
|
||||||
|
|
||||||
/* Mobile: sidebar overlays instead of pushing */
|
/* Mobile: sidebar overlays instead of pushing */
|
||||||
@media (max-width: 640px) {
|
@media (max-width: 640px) {
|
||||||
.sidebar { box-shadow: 4px 0 20px rgba(0,0,0,0.3); }
|
.sidebar { box-shadow: 4px 0 20px rgba(0,0,0,0.3); }
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue