feat: categorized rApp dropdown in tab bar + button
- Tab bar + button now shows full rApp dropdown with names, icons, descriptions - Grouped by category (Creating, Planning, Communicating, etc.) - Only shows modules not already open as tabs - Shell passes module list to tab bar via setModules() Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
15e6a9b9ba
commit
726ef43952
|
|
@ -158,6 +158,9 @@ export function renderShell(opts: ShellOptions): string {
|
|||
const moduleList = ${moduleListJSON};
|
||||
|
||||
if (tabBar) {
|
||||
// Provide module list for the + add menu dropdown
|
||||
tabBar.setModules(moduleList);
|
||||
|
||||
// Helper: look up a module's display name
|
||||
function getModuleLabel(id) {
|
||||
const m = moduleList.find(mod => mod.id === id);
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@
|
|||
import type { Layer, LayerFlow, FlowKind } from "../../lib/layer-types";
|
||||
import { FLOW_COLORS, FLOW_LABELS } from "../../lib/layer-types";
|
||||
|
||||
// Re-export badge info so the tab bar can show module colors
|
||||
// Badge info for tab display
|
||||
const MODULE_BADGES: Record<string, { badge: string; color: string }> = {
|
||||
rspace: { badge: "rS", color: "#5eead4" },
|
||||
rnotes: { badge: "rN", color: "#fcd34d" },
|
||||
|
|
@ -53,9 +53,37 @@ const MODULE_BADGES: Record<string, { badge: string; color: string }> = {
|
|||
rwork: { badge: "rWo", color: "#cbd5e1" },
|
||||
};
|
||||
|
||||
// Category definitions for the + menu dropdown grouping
|
||||
const MODULE_CATEGORIES: Record<string, string> = {
|
||||
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",
|
||||
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",
|
||||
];
|
||||
|
||||
export interface TabBarModule {
|
||||
id: string;
|
||||
name: string;
|
||||
icon: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export class RStackTabBar extends HTMLElement {
|
||||
#shadow: ShadowRoot;
|
||||
#layers: Layer[] = [];
|
||||
#modules: TabBarModule[] = [];
|
||||
#flows: LayerFlow[] = [];
|
||||
#viewMode: "flat" | "stack" = "flat";
|
||||
#draggedTabId: string | null = null;
|
||||
|
|
@ -110,6 +138,11 @@ export class RStackTabBar extends HTMLElement {
|
|||
this.#render();
|
||||
}
|
||||
|
||||
/** Provide the available module list (for the + add menu) */
|
||||
setModules(modules: TabBarModule[]) {
|
||||
this.#modules = modules;
|
||||
}
|
||||
|
||||
/** Set the inter-layer flows (for stack view) */
|
||||
setFlows(flows: LayerFlow[]) {
|
||||
this.#flows = flows;
|
||||
|
|
@ -184,25 +217,62 @@ export class RStackTabBar extends HTMLElement {
|
|||
}
|
||||
|
||||
#renderAddMenu(): string {
|
||||
// Group available modules (show ones not yet added as layers)
|
||||
const existingModuleIds = new Set(this.#layers.map(l => l.moduleId));
|
||||
const available = Object.entries(MODULE_BADGES)
|
||||
.filter(([id]) => !existingModuleIds.has(id))
|
||||
.map(([id, info]) => ({ id, ...info }));
|
||||
|
||||
if (available.length === 0) {
|
||||
return `<div class="add-menu" id="add-menu"><div class="add-menu-empty">All modules added</div></div>`;
|
||||
// Use server module list if available, fall back to MODULE_BADGES keys
|
||||
const availableModules: Array<{ id: string; name: string; icon: string; description: string }> =
|
||||
this.#modules.length > 0
|
||||
? this.#modules.filter(m => !existingModuleIds.has(m.id))
|
||||
: Object.keys(MODULE_BADGES)
|
||||
.filter(id => !existingModuleIds.has(id))
|
||||
.map(id => ({ id, name: id, icon: "", description: "" }));
|
||||
|
||||
if (availableModules.length === 0) {
|
||||
return `<div class="add-menu" id="add-menu"><div class="add-menu-empty">All rApps added</div></div>`;
|
||||
}
|
||||
|
||||
// Group by category
|
||||
const groups = new Map<string, typeof availableModules>();
|
||||
const uncategorized: typeof availableModules = [];
|
||||
for (const m of availableModules) {
|
||||
const cat = MODULE_CATEGORIES[m.id];
|
||||
if (cat) {
|
||||
if (!groups.has(cat)) groups.set(cat, []);
|
||||
groups.get(cat)!.push(m);
|
||||
} else {
|
||||
uncategorized.push(m);
|
||||
}
|
||||
}
|
||||
|
||||
let html = "";
|
||||
for (const cat of CATEGORY_ORDER) {
|
||||
const items = groups.get(cat);
|
||||
if (!items || items.length === 0) continue;
|
||||
html += `<div class="add-menu-category">${cat}</div>`;
|
||||
html += items.map(m => this.#renderAddMenuItem(m)).join("");
|
||||
}
|
||||
if (uncategorized.length > 0) {
|
||||
html += `<div class="add-menu-category">Other</div>`;
|
||||
html += uncategorized.map(m => this.#renderAddMenuItem(m)).join("");
|
||||
}
|
||||
|
||||
return `<div class="add-menu" id="add-menu">${html}</div>`;
|
||||
}
|
||||
|
||||
#renderAddMenuItem(m: { id: string; name: string; icon: string; description: string }): string {
|
||||
const badge = MODULE_BADGES[m.id];
|
||||
const badgeHtml = badge
|
||||
? `<span class="add-menu-badge" style="background:${badge.color}">${badge.badge}</span>`
|
||||
: `<span class="add-menu-icon">${m.icon}</span>`;
|
||||
|
||||
return `
|
||||
<div class="add-menu" id="add-menu">
|
||||
${available.map(m => `
|
||||
<button class="add-menu-item" data-add-module="${m.id}">
|
||||
<span class="add-menu-badge" style="background:${m.color}">${m.badge}</span>
|
||||
<span>${m.id}</span>
|
||||
</button>
|
||||
`).join("")}
|
||||
</div>
|
||||
<button class="add-menu-item" data-add-module="${m.id}">
|
||||
${badgeHtml}
|
||||
<div class="add-menu-text">
|
||||
<span class="add-menu-name">${m.name} <span class="add-menu-emoji">${m.icon}</span></span>
|
||||
${m.description ? `<span class="add-menu-desc">${m.description}</span>` : ""}
|
||||
</div>
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
|
||||
|
|
@ -800,13 +870,14 @@ const STYLES = `
|
|||
left: auto;
|
||||
right: 0;
|
||||
margin-top: 4px;
|
||||
min-width: 180px;
|
||||
max-height: 300px;
|
||||
min-width: 260px;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
border-radius: 8px;
|
||||
border-radius: 10px;
|
||||
padding: 4px;
|
||||
z-index: 100;
|
||||
box-shadow: 0 8px 30px rgba(0,0,0,0.25);
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
:host-context([data-theme="dark"]) .add-menu {
|
||||
background: #1e293b;
|
||||
|
|
@ -817,6 +888,21 @@ const STYLES = `
|
|||
border: 1px solid rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.add-menu-category {
|
||||
padding: 6px 10px 3px;
|
||||
font-size: 0.6rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
opacity: 0.45;
|
||||
user-select: none;
|
||||
}
|
||||
.add-menu-category:not(:first-child) {
|
||||
border-top: 1px solid rgba(128,128,128,0.12);
|
||||
margin-top: 2px;
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.add-menu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
@ -824,7 +910,7 @@ const STYLES = `
|
|||
width: 100%;
|
||||
padding: 6px 10px;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
font-size: 0.8rem;
|
||||
|
|
@ -839,8 +925,8 @@ const STYLES = `
|
|||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 5px;
|
||||
font-size: 0.55rem;
|
||||
font-weight: 900;
|
||||
|
|
@ -848,6 +934,41 @@ const STYLES = `
|
|||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.add-menu-icon {
|
||||
font-size: 1.1rem;
|
||||
width: 24px;
|
||||
text-align: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.add-menu-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.add-menu-name {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.add-menu-emoji {
|
||||
font-size: 0.8rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.add-menu-desc {
|
||||
font-size: 0.65rem;
|
||||
opacity: 0.5;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.add-menu-empty {
|
||||
padding: 12px;
|
||||
text-align: center;
|
||||
|
|
|
|||
Loading…
Reference in New Issue