feat: customizable dashboard with persistent home icon and widget system
Adds always-visible home button in tab bar, toggleable dashboard overlay, widget card system with 8 widgets (tasks, calendar, activity, members, tools, quick actions, wallet, flows), customize mode with toggle/reorder, and dashboard summary API endpoint. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
2f4258aa32
commit
e6328581a7
|
|
@ -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 };
|
||||||
|
|
@ -526,6 +526,10 @@ app.route("/api/mi", miRoutes);
|
||||||
app.route("/rtasks/check", checklistCheckRoutes);
|
app.route("/rtasks/check", checklistCheckRoutes);
|
||||||
app.route("/api/rtasks", checklistApiRoutes);
|
app.route("/api/rtasks", checklistApiRoutes);
|
||||||
|
|
||||||
|
// ── Dashboard summary API ──
|
||||||
|
import { dashboardRoutes } from "./dashboard-routes";
|
||||||
|
app.route("/api", dashboardRoutes);
|
||||||
|
|
||||||
// ── Bug Report API ──
|
// ── Bug Report API ──
|
||||||
app.route("/api/bug-report", bugReportRouter);
|
app.route("/api/bug-report", bugReportRouter);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1164,6 +1164,12 @@ export function renderShell(opts: ShellOptions): string {
|
||||||
tabBar.addEventListener('layer-switch', (e) => {
|
tabBar.addEventListener('layer-switch', (e) => {
|
||||||
const { layerId, moduleId } = e.detail;
|
const { layerId, moduleId } = e.detail;
|
||||||
currentModuleId = moduleId;
|
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();
|
saveTabs();
|
||||||
if (tabCache) {
|
if (tabCache) {
|
||||||
const switchId = moduleId; // capture for staleness check
|
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 { moduleId: targetModule, spaceSlug: targetSpace } = e.detail;
|
||||||
const dashboard = document.querySelector('rstack-user-dashboard');
|
const dashboard = document.querySelector('rstack-user-dashboard');
|
||||||
if (dashboard) dashboard.style.display = 'none';
|
if (dashboard) dashboard.style.display = 'none';
|
||||||
|
tabBar.setAttribute('home-active', 'false');
|
||||||
|
|
||||||
// If navigating to a different space, do a full navigation
|
// If navigating to a different space, do a full navigation
|
||||||
if (targetSpace && targetSpace !== spaceSlug) {
|
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) => {
|
tabBar.addEventListener('view-toggle', (e) => {
|
||||||
const { mode } = e.detail;
|
const { mode } = e.detail;
|
||||||
document.dispatchEvent(new CustomEvent('layer-view-mode', { detail: { mode } }));
|
document.dispatchEvent(new CustomEvent('layer-view-mode', { detail: { mode } }));
|
||||||
|
|
|
||||||
|
|
@ -134,7 +134,7 @@ export class RStackTabBar extends HTMLElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
static get observedAttributes() {
|
static get observedAttributes() {
|
||||||
return ["active", "space", "view-mode"];
|
return ["active", "space", "view-mode", "home-active"];
|
||||||
}
|
}
|
||||||
|
|
||||||
get active(): string {
|
get active(): string {
|
||||||
|
|
@ -415,15 +415,14 @@ export class RStackTabBar extends HTMLElement {
|
||||||
<style>${STYLES}</style>
|
<style>${STYLES}</style>
|
||||||
<div class="tab-bar" data-view="${this.#viewMode}">
|
<div class="tab-bar" data-view="${this.#viewMode}">
|
||||||
<div class="tabs-scroll">
|
<div class="tabs-scroll">
|
||||||
${this.#layers.length === 0
|
<button class="tab-home ${!active || this.getAttribute('home-active') === 'true' ? 'tab-home--active' : ''}"
|
||||||
? `<div class="tab active tab--dashboard">
|
id="home-btn" title="Dashboard">
|
||||||
<span class="tab-indicator" style="background:#5eead4"></span>
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor">
|
||||||
<span class="tab-badge" style="background:#5eead4">
|
<path d="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z"/>
|
||||||
<svg width="10" height="10" viewBox="0 0 24 24" fill="currentColor"><path d="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z"/></svg>
|
</svg>
|
||||||
</span>
|
<span class="tab-home__label">Home</span>
|
||||||
<span class="tab-label">Dashboard</span>
|
</button>
|
||||||
</div>`
|
${this.#layers.map(l => this.#renderTab(l, active)).join("")}
|
||||||
: this.#layers.map(l => this.#renderTab(l, active)).join("")}
|
|
||||||
</div>
|
</div>
|
||||||
<div class="tab-add-wrap">
|
<div class="tab-add-wrap">
|
||||||
<button class="tab-add" id="add-btn" title="Add layer">+</button>
|
<button class="tab-add" id="add-btn" title="Add layer">+</button>
|
||||||
|
|
@ -942,6 +941,11 @@ export class RStackTabBar extends HTMLElement {
|
||||||
// Clean up previous document-level listeners to prevent leak
|
// Clean up previous document-level listeners to prevent leak
|
||||||
if (this.#docCleanup) { this.#docCleanup(); this.#docCleanup = null; }
|
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.
|
// Tab clicks — dispatch event but do NOT set active yet.
|
||||||
// The shell's event handler calls switchTo() and sets active only after success.
|
// The shell's event handler calls switchTo() and sets active only after success.
|
||||||
this.#shadow.querySelectorAll<HTMLElement>(".tab").forEach(tab => {
|
this.#shadow.querySelectorAll<HTMLElement>(".tab").forEach(tab => {
|
||||||
|
|
@ -1535,6 +1539,38 @@ const STYLES = `
|
||||||
display: block;
|
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 */
|
/* Active indicator line at bottom */
|
||||||
.tab-indicator {
|
.tab-indicator {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -94,9 +94,10 @@ export class TabCache {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If returning from dashboard, hide it
|
// If returning from dashboard, hide it and clear home-active
|
||||||
const dashboard = document.querySelector("rstack-user-dashboard");
|
const dashboard = document.querySelector("rstack-user-dashboard");
|
||||||
if (dashboard) (dashboard as HTMLElement).style.display = "none";
|
if (dashboard) (dashboard as HTMLElement).style.display = "none";
|
||||||
|
document.querySelector("rstack-tab-bar")?.setAttribute("home-active", "false");
|
||||||
const key = this.paneKey(stateSpace, state.moduleId);
|
const key = this.paneKey(stateSpace, state.moduleId);
|
||||||
if (this.panes.has(key)) {
|
if (this.panes.has(key)) {
|
||||||
if (stateSpace !== this.spaceSlug) {
|
if (stateSpace !== this.spaceSlug) {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue