diff --git a/server/dashboard-routes.ts b/server/dashboard-routes.ts
new file mode 100644
index 00000000..9de747bc
--- /dev/null
+++ b/server/dashboard-routes.ts
@@ -0,0 +1,27 @@
+/**
+ * Dashboard summary API — single aggregation endpoint for dashboard widgets.
+ *
+ * GET /dashboard-summary/:space → { tasks, calendar, flows }
+ *
+ * Uses existing MI data functions (zero new DB queries).
+ */
+
+import { Hono } from "hono";
+import { getRecentTasksForMI } from "../modules/rtasks/mod";
+import { getUpcomingEventsForMI } from "../modules/rcal/mod";
+import { getRecentFlowsForMI } from "../modules/rflows/mod";
+
+const dashboardRoutes = new Hono();
+
+dashboardRoutes.get("/dashboard-summary/:space", (c) => {
+ const space = c.req.param("space");
+ if (!space) return c.json({ error: "space required" }, 400);
+
+ const tasks = getRecentTasksForMI(space, 5);
+ const calendar = getUpcomingEventsForMI(space, 14, 5);
+ const flows = getRecentFlowsForMI(space, 3);
+
+ return c.json({ tasks, calendar, flows });
+});
+
+export { dashboardRoutes };
diff --git a/server/index.ts b/server/index.ts
index cedef69f..793edee2 100644
--- a/server/index.ts
+++ b/server/index.ts
@@ -526,6 +526,10 @@ app.route("/api/mi", miRoutes);
app.route("/rtasks/check", checklistCheckRoutes);
app.route("/api/rtasks", checklistApiRoutes);
+// ── Dashboard summary API ──
+import { dashboardRoutes } from "./dashboard-routes";
+app.route("/api", dashboardRoutes);
+
// ── Bug Report API ──
app.route("/api/bug-report", bugReportRouter);
diff --git a/server/shell.ts b/server/shell.ts
index c321b1ef..0ced9fdb 100644
--- a/server/shell.ts
+++ b/server/shell.ts
@@ -1164,6 +1164,12 @@ export function renderShell(opts: ShellOptions): string {
tabBar.addEventListener('layer-switch', (e) => {
const { layerId, moduleId } = e.detail;
currentModuleId = moduleId;
+ // Hide dashboard overlay if it was showing
+ const dashOnSwitch = document.querySelector('rstack-user-dashboard');
+ if (dashOnSwitch && dashOnSwitch.style.display !== 'none') {
+ dashOnSwitch.style.display = 'none';
+ tabBar.setAttribute('home-active', 'false');
+ }
saveTabs();
if (tabCache) {
const switchId = moduleId; // capture for staleness check
@@ -1322,6 +1328,7 @@ export function renderShell(opts: ShellOptions): string {
const { moduleId: targetModule, spaceSlug: targetSpace } = e.detail;
const dashboard = document.querySelector('rstack-user-dashboard');
if (dashboard) dashboard.style.display = 'none';
+ tabBar.setAttribute('home-active', 'false');
// If navigating to a different space, do a full navigation
if (targetSpace && targetSpace !== spaceSlug) {
@@ -1348,6 +1355,34 @@ export function renderShell(opts: ShellOptions): string {
}
});
+ // ── Home button: toggle dashboard overlay when tabs are open ──
+ tabBar.addEventListener('home-click', () => {
+ const dashboard = document.querySelector('rstack-user-dashboard');
+ if (!dashboard) return;
+
+ // If no tabs, dashboard is already visible — no-op
+ if (layers.length === 0) return;
+
+ const isVisible = dashboard.style.display !== 'none';
+ if (isVisible) {
+ // Hide dashboard overlay, return to active tab
+ dashboard.style.display = 'none';
+ tabBar.setAttribute('home-active', 'false');
+ // Restore active tab pane
+ if (tabCache && currentModuleId) {
+ tabCache.switchTo(currentModuleId);
+ }
+ } else {
+ // Show dashboard as overlay, hide active pane
+ if (tabCache) tabCache.hideAllPanes();
+ dashboard.style.display = '';
+ if (dashboard.refresh) dashboard.refresh();
+ tabBar.setAttribute('home-active', 'true');
+ var dashUrl = window.location.hostname.endsWith('.rspace.online') ? '/' : '/' + spaceSlug;
+ history.pushState({ dashboard: true, spaceSlug: spaceSlug }, '', dashUrl);
+ }
+ });
+
tabBar.addEventListener('view-toggle', (e) => {
const { mode } = e.detail;
document.dispatchEvent(new CustomEvent('layer-view-mode', { detail: { mode } }));
diff --git a/shared/components/rstack-tab-bar.ts b/shared/components/rstack-tab-bar.ts
index 376a5487..c39d66e8 100644
--- a/shared/components/rstack-tab-bar.ts
+++ b/shared/components/rstack-tab-bar.ts
@@ -134,7 +134,7 @@ export class RStackTabBar extends HTMLElement {
}
static get observedAttributes() {
- return ["active", "space", "view-mode"];
+ return ["active", "space", "view-mode", "home-active"];
}
get active(): string {
@@ -415,15 +415,14 @@ export class RStackTabBar extends HTMLElement {
@@ -942,6 +941,11 @@ export class RStackTabBar extends HTMLElement {
// Clean up previous document-level listeners to prevent leak
if (this.#docCleanup) { this.#docCleanup(); this.#docCleanup = null; }
+ // Home button — always present, fires home-click event
+ this.#shadow.getElementById("home-btn")?.addEventListener("click", () => {
+ this.dispatchEvent(new CustomEvent("home-click", { bubbles: true }));
+ });
+
// Tab clicks — dispatch event but do NOT set active yet.
// The shell's event handler calls switchTo() and sets active only after success.
this.#shadow.querySelectorAll
(".tab").forEach(tab => {
@@ -1535,6 +1539,38 @@ const STYLES = `
display: block;
}
+/* ── Persistent home button ── */
+.tab-home {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 1px;
+ padding: 4px 10px;
+ border: none;
+ background: none;
+ color: var(--rs-text-muted);
+ cursor: pointer;
+ border-radius: 6px;
+ transition: color 0.15s, background 0.15s;
+ flex-shrink: 0;
+ font-size: inherit;
+ font-family: inherit;
+ line-height: 1;
+}
+.tab-home:hover {
+ color: var(--rs-text-primary);
+ background: rgba(255,255,255,0.06);
+}
+.tab-home--active {
+ color: #14b8a6;
+ background: rgba(20,184,166,0.1);
+}
+.tab-home__label {
+ font-size: 0.55rem;
+ font-weight: 600;
+ letter-spacing: 0.02em;
+}
+
/* Active indicator line at bottom */
.tab-indicator {
position: absolute;
diff --git a/shared/components/rstack-user-dashboard.ts b/shared/components/rstack-user-dashboard.ts
index 0e6caadf..e5f5c551 100644
--- a/shared/components/rstack-user-dashboard.ts
+++ b/shared/components/rstack-user-dashboard.ts
@@ -1,8 +1,8 @@
/**
- * — Space-centric dashboard shown when all tabs are closed.
+ * — Customizable space dashboard with widget cards.
*
- * Sections: space header + stats, members, tools open, recent activity,
- * and quick actions.
+ * Always accessible via the persistent home icon in the tab bar.
+ * Users can toggle and reorder summary widget cards from any rApp.
*
* Attributes:
* space — current space slug
@@ -33,16 +33,85 @@ interface TabLayer {
icon?: string;
}
+interface WidgetConfig {
+ id: string;
+ enabled: boolean;
+ order: number;
+}
+
+interface SummaryTask {
+ id: string;
+ title: string;
+ status: string;
+ priority: string;
+ createdAt: number;
+}
+
+interface SummaryEvent {
+ title: string;
+ start: string;
+ end: string;
+ allDay: boolean;
+ location: string | null;
+}
+
+interface SummaryFlow {
+ id: string;
+ name: string;
+ nodeCount: number;
+ createdAt: number;
+}
+
+interface WalletBalance {
+ tokenId: string;
+ symbol: string;
+ balance: number;
+}
+
+// ── Widget registry ──
+
+const WIDGET_REGISTRY: Record = {
+ tasks: { label: "Open Tasks", icon: "\u{1F4CB}", requiresAuth: false, defaultEnabled: true },
+ calendar: { label: "Upcoming Events", icon: "\u{1F4C5}", requiresAuth: false, defaultEnabled: true },
+ activity: { label: "Recent Activity", icon: "\u{1F514}", requiresAuth: true, defaultEnabled: true },
+ members: { label: "Members", icon: "\u{1F465}", requiresAuth: false, defaultEnabled: true },
+ tools: { label: "Recently Open", icon: "\u{1F527}", requiresAuth: false, defaultEnabled: true },
+ quickactions: { label: "Quick Actions", icon: "\u26A1", requiresAuth: false, defaultEnabled: true },
+ wallet: { label: "Wallet Balances", icon: "\u{1F4B0}", requiresAuth: true, defaultEnabled: false },
+ flows: { label: "Active Flows", icon: "\u{1F30A}", requiresAuth: false, defaultEnabled: false },
+};
+
+const WIDGET_ORDER = ["tasks", "calendar", "activity", "members", "tools", "quickactions", "wallet", "flows"];
+
const CACHE_TTL = 30_000; // 30s
+const PRIORITY_COLORS: Record = {
+ HIGH: "#ef4444",
+ MEDIUM: "#f59e0b",
+ LOW: "#22c55e",
+};
+
export class RStackUserDashboard extends HTMLElement {
#shadow: ShadowRoot;
#members: MemberInfo[] = [];
#notifications: NotificationItem[] = [];
#openTabs: TabLayer[] = [];
+ #tasks: SummaryTask[] = [];
+ #events: SummaryEvent[] = [];
+ #flows: SummaryFlow[] = [];
+ #walletBalances: WalletBalance[] = [];
#membersLoading = true;
#notifLoading = true;
+ #summaryLoading = true;
+ #walletLoading = true;
#lastFetch = 0;
+ #widgetConfigs: WidgetConfig[] = [];
+ #isCustomizing = false;
constructor() {
super();
@@ -58,26 +127,23 @@ export class RStackUserDashboard extends HTMLElement {
}
connectedCallback() {
+ this.#loadWidgetConfigs();
this.#render();
- // Defer data loading until the dashboard is actually visible.
- // It starts display:none and only shows when all tabs are closed.
if (this.offsetParent !== null) {
this.#loadData();
}
}
attributeChangedCallback() {
- // Space changed — reset cache
this.#lastFetch = 0;
+ this.#loadWidgetConfigs();
this.#render();
}
- /** Called from shell.ts before tabs are cleared */
setOpenTabs(tabs: TabLayer[]) {
this.#openTabs = tabs;
}
- /** Reload data when dashboard becomes visible again */
refresh() {
if (Date.now() - this.#lastFetch < CACHE_TTL) {
this.#render();
@@ -85,16 +151,74 @@ export class RStackUserDashboard extends HTMLElement {
}
this.#membersLoading = true;
this.#notifLoading = true;
+ this.#summaryLoading = true;
+ this.#walletLoading = true;
this.#render();
this.#loadData();
}
+ // ── Widget config persistence ──
+
+ #widgetConfigKey(): string {
+ return `rspace_dashboard_widgets_${this.space || "default"}`;
+ }
+
+ #loadWidgetConfigs() {
+ try {
+ const raw = localStorage.getItem(this.#widgetConfigKey());
+ if (raw) {
+ const parsed = JSON.parse(raw) as WidgetConfig[];
+ if (Array.isArray(parsed) && parsed.length > 0) {
+ // Merge with registry in case new widgets were added
+ const known = new Set(parsed.map(c => c.id));
+ let maxOrder = Math.max(...parsed.map(c => c.order));
+ for (const id of WIDGET_ORDER) {
+ if (!known.has(id)) {
+ const w = WIDGET_REGISTRY[id];
+ parsed.push({ id, enabled: w.defaultEnabled, order: ++maxOrder });
+ }
+ }
+ this.#widgetConfigs = parsed;
+ return;
+ }
+ }
+ } catch { /* ignore */ }
+
+ // Default config
+ this.#widgetConfigs = WIDGET_ORDER.map((id, i) => ({
+ id,
+ enabled: WIDGET_REGISTRY[id].defaultEnabled,
+ order: i,
+ }));
+ }
+
+ #saveWidgetConfigs() {
+ try {
+ localStorage.setItem(this.#widgetConfigKey(), JSON.stringify(this.#widgetConfigs));
+ } catch { /* ignore */ }
+ }
+
+ // ── Data loading ──
+
async #loadData() {
this.#lastFetch = Date.now();
- await Promise.all([
- this.#fetchMembers(),
- this.#fetchNotifications(),
- ]);
+ const enabled = new Set(this.#widgetConfigs.filter(c => c.enabled).map(c => c.id));
+ const fetches: Promise[] = [];
+
+ if (enabled.has("members")) fetches.push(this.#fetchMembers());
+ else { this.#membersLoading = false; }
+
+ if (enabled.has("activity")) fetches.push(this.#fetchNotifications());
+ else { this.#notifLoading = false; }
+
+ if (enabled.has("tasks") || enabled.has("calendar") || enabled.has("flows")) {
+ fetches.push(this.#fetchSummary());
+ } else { this.#summaryLoading = false; }
+
+ if (enabled.has("wallet")) fetches.push(this.#fetchWallet());
+ else { this.#walletLoading = false; }
+
+ await Promise.all(fetches);
}
async #fetchMembers() {
@@ -119,7 +243,7 @@ export class RStackUserDashboard extends HTMLElement {
return;
}
try {
- const res = await fetch("/api/notifications?limit=10", {
+ const res = await fetch("/api/notifications?limit=5", {
headers: { Authorization: `Bearer ${token}` },
});
if (res.ok) {
@@ -131,6 +255,44 @@ export class RStackUserDashboard extends HTMLElement {
this.#render();
}
+ async #fetchSummary() {
+ this.#summaryLoading = true;
+ try {
+ const res = await fetch(`/api/dashboard-summary/${encodeURIComponent(this.space)}`);
+ if (res.ok) {
+ const data = await res.json();
+ this.#tasks = data.tasks || [];
+ this.#events = data.calendar || [];
+ this.#flows = data.flows || [];
+ }
+ } catch { /* offline */ }
+ this.#summaryLoading = false;
+ this.#render();
+ }
+
+ async #fetchWallet() {
+ this.#walletLoading = true;
+ const token = getAccessToken();
+ if (!token) {
+ this.#walletLoading = false;
+ this.#render();
+ return;
+ }
+ try {
+ const res = await fetch("/api/crdt-tokens/my-balances", {
+ headers: { Authorization: `Bearer ${token}` },
+ });
+ if (res.ok) {
+ const data = await res.json();
+ this.#walletBalances = data.balances || [];
+ }
+ } catch { /* offline */ }
+ this.#walletLoading = false;
+ this.#render();
+ }
+
+ // ── Helpers ──
+
#timeAgo(iso: string): string {
const diff = Date.now() - new Date(iso).getTime();
const mins = Math.floor(diff / 60_000);
@@ -143,6 +305,11 @@ export class RStackUserDashboard extends HTMLElement {
return `${Math.floor(days / 30)}mo ago`;
}
+ #formatEventTime(iso: string, allDay: boolean): string {
+ if (allDay) return new Date(iso).toLocaleDateString(undefined, { month: "short", day: "numeric" });
+ return new Date(iso).toLocaleString(undefined, { month: "short", day: "numeric", hour: "numeric", minute: "2-digit" });
+ }
+
#dispatch(moduleId: string) {
this.dispatchEvent(new CustomEvent("dashboard-navigate", {
bubbles: true,
@@ -164,11 +331,10 @@ export class RStackUserDashboard extends HTMLElement {
return (m.displayName || m.username || "?").charAt(0).toUpperCase();
}
- // Module icons for "tools open" chips
#moduleIcon(moduleId: string): string {
const icons: Record = {
rspace: "\u{1F30C}", rnotes: "\u{1F4DD}", rmeets: "\u{1F4F9}", rvote: "\u{1F5F3}",
- rnetwork: "\u{1F465}", rcalendar: "\u{1F4C5}", rtasks: "\u{2705}",
+ rnetwork: "\u{1F465}", rcalendar: "\u{1F4C5}", rtasks: "\u2705",
rwallet: "\u{1F4B0}", rmail: "\u{1F4E7}", rmaps: "\u{1F5FA}",
rprompt: "\u{1F916}", rzine: "\u{1F4D6}", rfiles: "\u{1F4C1}",
};
@@ -177,17 +343,195 @@ export class RStackUserDashboard extends HTMLElement {
#moduleLabel(tab: TabLayer): string {
if (tab.label) return tab.label;
- // Capitalize moduleId
const id = tab.moduleId;
return id.charAt(0).toUpperCase() + id.slice(1);
}
- #render() {
+ // ── Per-widget renderers ──
+
+ #renderWidgetTasks(): string {
+ if (this.#summaryLoading) return `Loading...
`;
+ if (this.#tasks.length === 0) return `No open tasks
`;
+ return `${this.#tasks.map(t => `
+
+ `).join("")}
`;
+ }
+
+ #renderWidgetCalendar(): string {
+ if (this.#summaryLoading) return `Loading...
`;
+ if (this.#events.length === 0) return `No upcoming events
`;
+ return `${this.#events.map(e => `
+
+ `).join("")}
`;
+ }
+
+ #renderWidgetActivity(): string {
const session = getSession();
+ if (!session) return `Sign in to see recent activity
`;
+ if (this.#notifLoading) return `Loading...
`;
+ if (this.#notifications.length === 0) return `No recent activity
`;
+ return `${this.#notifications.map(n => `
+
+ `).join("")}
`;
+ }
+
+ #renderWidgetMembers(): string {
+ if (this.#membersLoading) return `Loading members...
`;
+ if (this.#members.length === 0) return `No members found
`;
+ return `${this.#members.map(m => `
+
+ `).join("")}
+ ${this.#members.length} member${this.#members.length !== 1 ? "s" : ""}
+
`;
+ }
+
+ #renderWidgetTools(): string {
+ if (this.#openTabs.length === 0) return `No recent tabs
`;
+ return `${this.#openTabs.map(t => `
+
+ `).join("")}
`;
+ }
+
+ #renderWidgetQuickActions(): string {
+ return `
+
+
+
+
`;
+ }
+
+ #renderWidgetWallet(): string {
+ const session = getSession();
+ if (!session) return `Sign in to see wallet
`;
+ if (this.#walletLoading) return `Loading...
`;
+ if (this.#walletBalances.length === 0) return `No token balances
`;
+ return `${this.#walletBalances.map(b => `
+
+ `).join("")}
`;
+ }
+
+ #renderWidgetFlows(): string {
+ if (this.#summaryLoading) return `Loading...
`;
+ if (this.#flows.length === 0) return `No active flows
`;
+ return `${this.#flows.map(f => `
+
+ `).join("")}
`;
+ }
+
+ #escHtml(s: string): string {
+ return s.replace(/&/g, "&").replace(//g, ">").replace(/"/g, """);
+ }
+
+ // ── Widget card wrapper ──
+
+ #renderWidget(config: WidgetConfig): string {
+ const w = WIDGET_REGISTRY[config.id];
+ if (!w) return "";
+
+ const renderers: Record string> = {
+ tasks: () => this.#renderWidgetTasks(),
+ calendar: () => this.#renderWidgetCalendar(),
+ activity: () => this.#renderWidgetActivity(),
+ members: () => this.#renderWidgetMembers(),
+ tools: () => this.#renderWidgetTools(),
+ quickactions: () => this.#renderWidgetQuickActions(),
+ wallet: () => this.#renderWidgetWallet(),
+ flows: () => this.#renderWidgetFlows(),
+ };
+
+ const count = this.#getWidgetCount(config.id);
+ const countBadge = count !== null ? `${count}` : "";
+ const content = renderers[config.id]?.() || "";
+
+ return `
+
+
+ ${content}
+
+ `;
+ }
+
+ #getWidgetCount(id: string): number | null {
+ switch (id) {
+ case "tasks": return this.#summaryLoading ? null : this.#tasks.length || null;
+ case "calendar": return this.#summaryLoading ? null : this.#events.length || null;
+ case "activity": return this.#notifLoading ? null : this.#notifications.filter(n => !n.read).length || null;
+ case "members": return this.#membersLoading ? null : this.#members.length || null;
+ case "flows": return this.#summaryLoading ? null : this.#flows.length || null;
+ default: return null;
+ }
+ }
+
+ // ── Customize panel ──
+
+ #renderCustomizePanel(): string {
+ const sorted = [...this.#widgetConfigs].sort((a, b) => a.order - b.order);
+ return `
+
+
+ ${sorted.map((c, i) => {
+ const w = WIDGET_REGISTRY[c.id];
+ if (!w) return "";
+ return `
+
+
+
+ ${i > 0 ? `` : ``}
+ ${i < sorted.length - 1 ? `` : ``}
+
+
+ `;
+ }).join("")}
+
+ `;
+ }
+
+ // ── Main render ──
+
+ #render() {
const space = this.space;
const spaceName = space.charAt(0).toUpperCase() + space.slice(1);
- // ── Stats pills ──
+ // Stats pills
const statPills: string[] = [];
if (!this.#membersLoading) {
statPills.push(`${this.#members.length} member${this.#members.length !== 1 ? "s" : ""}`);
@@ -196,58 +540,10 @@ export class RStackUserDashboard extends HTMLElement {
statPills.push(`${this.#openTabs.length} tab${this.#openTabs.length !== 1 ? "s" : ""} open`);
}
- // ── Members ──
- let membersHTML: string;
- if (this.#membersLoading) {
- membersHTML = `Loading members...
`;
- } else if (this.#members.length === 0) {
- membersHTML = `No members found
`;
- } else {
- membersHTML = this.#members.map(m => `
-
- `).join("");
- }
-
- // ── Tools open ──
- let toolsHTML: string;
- if (this.#openTabs.length === 0) {
- toolsHTML = `No tools were open
`;
- } else {
- toolsHTML = this.#openTabs.map(t => `
-
- `).join("");
- }
-
- // ── Notifications / Recent Activity ──
- let activityHTML: string;
- if (!session) {
- activityHTML = `Sign in to see recent activity
`;
- } else if (this.#notifLoading) {
- activityHTML = `Loading...
`;
- } else if (this.#notifications.length === 0) {
- activityHTML = `No recent activity
`;
- } else {
- activityHTML = this.#notifications.map(n => `
-
- `).join("");
- }
-
- // ── Quick Actions ──
+ // Enabled widgets sorted by order
+ const enabled = this.#widgetConfigs
+ .filter(c => c.enabled)
+ .sort((a, b) => a.order - b.order);
this.#shadow.innerHTML = `
@@ -255,42 +551,18 @@ export class RStackUserDashboard extends HTMLElement {
`;
@@ -299,14 +571,50 @@ export class RStackUserDashboard extends HTMLElement {
}
#attachEvents() {
- // Member cards → open rNetwork
- this.#shadow.querySelectorAll(".member-card").forEach(el => {
- el.addEventListener("click", () => {
- this.#dispatch((el as HTMLElement).dataset.navigate || "rnetwork");
+ // Customize button
+ this.#shadow.getElementById("customize-btn")?.addEventListener("click", () => {
+ this.#isCustomizing = !this.#isCustomizing;
+ this.#render();
+ });
+
+ // Customize done button
+ this.#shadow.getElementById("customize-done")?.addEventListener("click", () => {
+ this.#isCustomizing = false;
+ this.#render();
+ // Reload data for newly enabled widgets
+ this.#lastFetch = 0;
+ this.#loadData();
+ });
+
+ // Widget toggles
+ this.#shadow.querySelectorAll("[data-widget-toggle]").forEach(input => {
+ input.addEventListener("change", () => {
+ const id = input.dataset.widgetToggle!;
+ const config = this.#widgetConfigs.find(c => c.id === id);
+ if (config) {
+ config.enabled = input.checked;
+ this.#saveWidgetConfigs();
+ this.#render();
+ }
});
});
- // Tool chips → re-open tab
+ // Move up/down arrows
+ this.#shadow.querySelectorAll("[data-move-up]").forEach(btn => {
+ btn.addEventListener("click", () => this.#moveWidget(btn.dataset.moveUp!, -1));
+ });
+ this.#shadow.querySelectorAll("[data-move-down]").forEach(btn => {
+ btn.addEventListener("click", () => this.#moveWidget(btn.dataset.moveDown!, 1));
+ });
+
+ // Navigation from widget items
+ this.#shadow.querySelectorAll("[data-navigate]").forEach(el => {
+ el.addEventListener("click", () => {
+ this.#dispatch(el.dataset.navigate!);
+ });
+ });
+
+ // Tool chips
this.#shadow.querySelectorAll(".tool-chip").forEach(el => {
el.addEventListener("click", () => {
this.#dispatch((el as HTMLElement).dataset.module!);
@@ -318,11 +626,7 @@ export class RStackUserDashboard extends HTMLElement {
el.addEventListener("click", () => {
const url = (el as HTMLElement).dataset.actionUrl;
const parsed = this.#parseActionUrl(url || null);
- if (parsed) {
- this.#dispatch(parsed.moduleId);
- } else {
- this.#dispatch("rspace");
- }
+ this.#dispatch(parsed?.moduleId || "rspace");
});
});
@@ -334,6 +638,22 @@ export class RStackUserDashboard extends HTMLElement {
});
}
+ #moveWidget(id: string, direction: number) {
+ const sorted = [...this.#widgetConfigs].sort((a, b) => a.order - b.order);
+ const idx = sorted.findIndex(c => c.id === id);
+ if (idx < 0) return;
+ const targetIdx = idx + direction;
+ if (targetIdx < 0 || targetIdx >= sorted.length) return;
+
+ // Swap order values
+ const tmp = sorted[idx].order;
+ sorted[idx].order = sorted[targetIdx].order;
+ sorted[targetIdx].order = tmp;
+
+ this.#saveWidgetConfigs();
+ this.#render();
+ }
+
static define(tag = "rstack-user-dashboard") {
if (!customElements.get(tag)) customElements.define(tag, RStackUserDashboard);
}
@@ -363,7 +683,7 @@ const STYLES = `
margin: 0 auto;
display: flex;
flex-direction: column;
- gap: 28px;
+ gap: 16px;
}
/* ── Space header ── */
@@ -374,6 +694,8 @@ const STYLES = `
gap: 16px;
}
+.space-header__info { flex: 1; min-width: 0; }
+
.space-icon {
width: 48px;
height: 48px;
@@ -411,71 +733,112 @@ const STYLES = `
white-space: nowrap;
}
-/* ── Two-column grid ── */
-
-.two-col {
- display: grid;
- grid-template-columns: 1fr 1fr;
- gap: 20px;
+.customize-btn {
+ flex-shrink: 0;
+ background: none;
+ border: 1px solid var(--rs-border, rgba(255,255,255,0.08));
+ color: var(--rs-text-muted, #94a3b8);
+ border-radius: 8px;
+ padding: 8px;
+ cursor: pointer;
+ transition: color 0.15s, border-color 0.15s;
+}
+.customize-btn:hover {
+ color: var(--rs-text-primary, #e2e8f0);
+ border-color: var(--rs-accent, #06b6d4);
}
-/* ── Sections ── */
+/* ── Widget cards ���─ */
-.section-header {
+.widget-card {
+ background: var(--rs-bg-surface, #1e293b);
+ border: 1px solid var(--rs-border, rgba(255,255,255,0.08));
+ border-radius: 10px;
+ padding: 16px;
+}
+
+.widget-card__header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;
}
-.section-header h2 {
- margin: 0;
+.widget-card__title {
font-size: 0.85rem;
font-weight: 600;
color: var(--rs-text-secondary, #cbd5e1);
- text-transform: uppercase;
- letter-spacing: 0.04em;
}
-.section-empty {
- padding: 20px 16px;
- text-align: center;
- color: var(--rs-text-muted, #94a3b8);
- font-size: 0.78rem;
+.widget-card__count {
+ font-size: 0.7rem;
+ padding: 2px 8px;
+ border-radius: 10px;
+ background: rgba(20,184,166,0.15);
+ color: #14b8a6;
+ font-weight: 600;
}
-/* ── Members ── */
+/* ── Widget list (tasks, calendar, flows, wallet) ── */
-.members-list {
+.widget-list {
display: flex;
flex-direction: column;
- border-radius: 10px;
- background: var(--rs-bg-surface, #1e293b);
- border: 1px solid var(--rs-border, rgba(255,255,255,0.08));
- overflow: hidden;
}
-.member-card {
+.widget-list-item {
display: flex;
align-items: center;
- gap: 10px;
- padding: 10px 14px;
+ gap: 8px;
+ padding: 8px 4px;
cursor: pointer;
transition: background 0.15s;
border: none;
- border-bottom: 1px solid var(--rs-border, rgba(255,255,255,0.06));
+ border-bottom: 1px solid var(--rs-border, rgba(255,255,255,0.04));
background: none;
text-align: left;
color: inherit;
font: inherit;
width: 100%;
}
-.member-card:last-child { border-bottom: none; }
-.member-card:hover {
- background: var(--rs-bg-hover, rgba(255,255,255,0.05));
+.widget-list-item:last-child { border-bottom: none; }
+.widget-list-item:hover {
+ background: var(--rs-bg-hover, rgba(255,255,255,0.04));
}
-.member-avatar {
+.widget-list-text {
+ flex: 1;
+ font-size: 0.8rem;
+ color: var(--rs-text-primary, #e2e8f0);
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.widget-list-meta {
+ font-size: 0.7rem;
+ color: var(--rs-text-muted, #64748b);
+ flex-shrink: 0;
+ white-space: nowrap;
+}
+
+.priority-dot {
+ width: 6px;
+ height: 6px;
+ border-radius: 50%;
+ flex-shrink: 0;
+}
+
+/* ── Members (compact row) ── */
+
+.members-row {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ flex-wrap: wrap;
+}
+
+.member-circle {
width: 32px;
height: 32px;
border-radius: 50%;
@@ -484,28 +847,22 @@ const STYLES = `
display: flex;
align-items: center;
justify-content: center;
- font-size: 0.8rem;
+ font-size: 0.75rem;
font-weight: 700;
flex-shrink: 0;
+ border: none;
+ cursor: pointer;
+ transition: transform 0.15s;
}
+.member-circle:hover { transform: scale(1.1); }
-.member-info { min-width: 0; }
-
-.member-name {
- font-size: 0.8rem;
- font-weight: 600;
- color: var(--rs-text-primary, #e2e8f0);
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-
-.member-handle {
+.member-count {
font-size: 0.7rem;
- color: var(--rs-text-muted, #64748b);
+ color: var(--rs-text-muted, #94a3b8);
+ margin-left: 4px;
}
-/* ── Tools open ── */
+/* ── Tools grid ── */
.tools-grid {
display: flex;
@@ -519,7 +876,7 @@ const STYLES = `
gap: 6px;
padding: 8px 14px;
border-radius: 8px;
- background: var(--rs-bg-surface, #1e293b);
+ background: rgba(255,255,255,0.04);
border: 1px solid var(--rs-border, rgba(255,255,255,0.08));
color: var(--rs-text-primary, #e2e8f0);
font-size: 0.78rem;
@@ -535,15 +892,11 @@ const STYLES = `
.tool-icon { font-size: 0.95rem; }
-/* ── Recent Activity ── */
+/* ── Activity list ── */
.activity-list {
display: flex;
flex-direction: column;
- border-radius: 10px;
- background: var(--rs-bg-surface, #1e293b);
- border: 1px solid var(--rs-border, rgba(255,255,255,0.08));
- overflow: hidden;
}
.activity-item {
@@ -551,11 +904,11 @@ const STYLES = `
align-items: flex-start;
justify-content: space-between;
gap: 10px;
- padding: 10px 16px;
+ padding: 8px 4px;
cursor: pointer;
transition: background 0.15s;
border: none;
- border-bottom: 1px solid var(--rs-border, rgba(255,255,255,0.06));
+ border-bottom: 1px solid var(--rs-border, rgba(255,255,255,0.04));
background: none;
text-align: left;
color: inherit;
@@ -564,7 +917,7 @@ const STYLES = `
}
.activity-item:last-child { border-bottom: none; }
.activity-item:hover {
- background: var(--rs-bg-hover, rgba(255,255,255,0.05));
+ background: var(--rs-bg-hover, rgba(255,255,255,0.04));
}
.activity-item.unread {
background: rgba(6,182,212,0.04);
@@ -613,7 +966,7 @@ const STYLES = `
gap: 8px;
padding: 10px 18px;
border-radius: 8px;
- background: var(--rs-bg-surface, #1e293b);
+ background: rgba(255,255,255,0.04);
border: 1px solid var(--rs-border, rgba(255,255,255,0.08));
color: var(--rs-text-primary, #e2e8f0);
font-size: 0.8rem;
@@ -629,6 +982,97 @@ const STYLES = `
.action-icon { font-size: 1rem; }
+.section-empty {
+ padding: 16px 8px;
+ text-align: center;
+ color: var(--rs-text-muted, #94a3b8);
+ font-size: 0.78rem;
+}
+
+/* ─��� Customize panel ── */
+
+.customize-panel {
+ background: var(--rs-bg-surface, #1e293b);
+ border: 1px solid var(--rs-border, rgba(255,255,255,0.08));
+ border-radius: 10px;
+ padding: 16px;
+}
+
+.customize-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: 12px;
+ font-size: 0.85rem;
+ font-weight: 600;
+ color: var(--rs-text-primary, #e2e8f0);
+}
+
+.customize-done {
+ padding: 4px 12px;
+ border-radius: 6px;
+ background: rgba(20,184,166,0.15);
+ color: #14b8a6;
+ border: none;
+ font-size: 0.78rem;
+ font-weight: 600;
+ cursor: pointer;
+}
+.customize-done:hover { background: rgba(20,184,166,0.25); }
+
+.customize-row {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 6px 0;
+ border-bottom: 1px solid var(--rs-border, rgba(255,255,255,0.04));
+}
+.customize-row:last-child { border-bottom: none; }
+
+.customize-toggle {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ font-size: 0.8rem;
+ color: var(--rs-text-primary, #e2e8f0);
+ cursor: pointer;
+}
+
+.customize-toggle input[type="checkbox"] {
+ accent-color: #14b8a6;
+ width: 16px;
+ height: 16px;
+}
+
+.customize-arrows {
+ display: flex;
+ gap: 4px;
+}
+
+.customize-arrow {
+ background: none;
+ border: 1px solid var(--rs-border, rgba(255,255,255,0.08));
+ color: var(--rs-text-muted, #94a3b8);
+ border-radius: 4px;
+ width: 24px;
+ height: 24px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ font-size: 0.6rem;
+ transition: color 0.15s, border-color 0.15s;
+}
+.customize-arrow:hover {
+ color: var(--rs-text-primary, #e2e8f0);
+ border-color: var(--rs-accent, #06b6d4);
+}
+
+.customize-arrow-placeholder {
+ width: 24px;
+ height: 24px;
+}
+
/* ── Responsive ── */
@media (max-width: 640px) {
@@ -637,7 +1081,6 @@ const STYLES = `
overflow-y: visible;
}
.dashboard { padding: 20px 12px; }
- .two-col { grid-template-columns: 1fr; }
}
@media (max-width: 480px) {
diff --git a/shared/tab-cache.ts b/shared/tab-cache.ts
index e6c3b478..2ccbe1e7 100644
--- a/shared/tab-cache.ts
+++ b/shared/tab-cache.ts
@@ -94,9 +94,10 @@ export class TabCache {
return;
}
- // If returning from dashboard, hide it
+ // If returning from dashboard, hide it and clear home-active
const dashboard = document.querySelector("rstack-user-dashboard");
if (dashboard) (dashboard as HTMLElement).style.display = "none";
+ document.querySelector("rstack-tab-bar")?.setAttribute("home-active", "false");
const key = this.paneKey(stateSpace, state.moduleId);
if (this.panes.has(key)) {
if (stateSpace !== this.spaceSlug) {