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 {
- ${this.#layers.length === 0 - ? `
- - - - - Dashboard -
` - : this.#layers.map(l => this.#renderTab(l, active)).join("")} + + ${this.#layers.map(l => this.#renderTab(l, active)).join("")}
@@ -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 ` +
+
+ ${w.icon} ${w.label} + ${countBadge} +
+ ${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 ` +
+
+ Customize Dashboard + +
+ ${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 {
${spaceName.charAt(0)}
-
+

${spaceName}

${statPills.length > 0 ? `
${statPills.join("")}
` : ""}
+
-
-
-

Members

-
${membersHTML}
-
-
-

Tools Open

-
${toolsHTML}
-
-
+ ${this.#isCustomizing ? this.#renderCustomizePanel() : ""} -
-

Recent Activity

-
${activityHTML}
-
- -
-

Quick Actions

-
- - - -
-
+ ${enabled.map(c => this.#renderWidget(c)).join("")}
`; @@ -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) {