diff --git a/modules/crowdsurf/components/folk-crowdsurf-dashboard.ts b/modules/crowdsurf/components/folk-crowdsurf-dashboard.ts index 4d599cfd..3992390a 100644 --- a/modules/crowdsurf/components/folk-crowdsurf-dashboard.ts +++ b/modules/crowdsurf/components/folk-crowdsurf-dashboard.ts @@ -13,6 +13,7 @@ import type { CrowdSurfDoc, CrowdSurfPrompt, Contribution } from '../schemas'; import { getDecayProgress, getTimeRemaining, getRightSwipeCount, isReadyToTrigger, getUrgency, parseContributions, crowdsurfSchema, crowdsurfDocId } from '../schemas'; import { getModuleApiBase } from "../../../shared/url-helpers"; import type { DocumentId } from "../../../shared/local-first/document"; +import { ViewHistory } from "../../../shared/view-history.js"; // ── Auth helpers ── function getSession(): { accessToken: string; claims: { sub: string; did?: string; username?: string } } | null { @@ -45,6 +46,7 @@ class FolkCrowdSurfDashboard extends HTMLElement { // State private activeTab: ViewTab = 'discover'; + private _history = new ViewHistory('discover', 'crowdsurf'); private loading = true; private prompts: CrowdSurfPrompt[] = []; private currentPromptIndex = 0; @@ -95,9 +97,12 @@ class FolkCrowdSurfDashboard extends HTMLElement { this.initMultiplayer(); } this._stopPresence = startPresenceHeartbeat(() => ({ module: 'crowdsurf', context: this.prompts[this.currentPromptIndex]?.text || 'CrowdSurf' })); + window.addEventListener('rspace-view-restored', this._onViewRestored as EventListener); } disconnectedCallback() { + this._history.destroy(); + window.removeEventListener('rspace-view-restored', this._onViewRestored as EventListener); this._stopPresence?.(); this._lfcUnsub?.(); this._lfcUnsub = null; @@ -105,6 +110,13 @@ class FolkCrowdSurfDashboard extends HTMLElement { if (this._expiryTimer !== null) clearInterval(this._expiryTimer); } + private _onViewRestored = (e: CustomEvent) => { + if (e.detail?.moduleId !== 'crowdsurf') return; + this.activeTab = e.detail.view; + this.render(); + this.bindEvents(); + }; + // ── Multiplayer init ── private async initMultiplayer() { @@ -337,6 +349,7 @@ class FolkCrowdSurfDashboard extends HTMLElement { this.lfClient.createPrompt(prompt); } + this._history.push('discover'); this.activeTab = 'discover'; this.render(); this.bindEvents(); @@ -738,6 +751,7 @@ class FolkCrowdSurfDashboard extends HTMLElement { btn.addEventListener('click', () => { const tab = btn.dataset.tab as ViewTab; if (tab && tab !== this.activeTab) { + this._history.push(tab); this.activeTab = tab; this.render(); this.bindEvents(); @@ -761,6 +775,7 @@ class FolkCrowdSurfDashboard extends HTMLElement { // Go to create tab this.shadow.querySelector('[data-action="go-create"]')?.addEventListener('click', () => { + this._history.push('create'); this.activeTab = 'create'; this.render(); this.bindEvents(); diff --git a/modules/rbnb/components/folk-bnb-view.ts b/modules/rbnb/components/folk-bnb-view.ts index e661cd81..acee44ea 100644 --- a/modules/rbnb/components/folk-bnb-view.ts +++ b/modules/rbnb/components/folk-bnb-view.ts @@ -13,6 +13,7 @@ import { startPresenceHeartbeat } from '../../../shared/collab-presence'; import { getModuleApiBase } from "../../../shared/url-helpers"; import type { DocumentId } from "../../../shared/local-first/document"; import { bnbSchema, bnbDocId } from "../schemas"; +import { ViewHistory } from "../../../shared/view-history.js"; const BNB_TOUR_STEPS: TourStep[] = [ { target: '.bnb-search', title: 'Search', message: 'Filter listings by location, type, or economy model.' }, @@ -63,6 +64,7 @@ class FolkBnbView extends HTMLElement { #stats: any = null; #selectedStay: any = null; #view: 'grid' | 'map' = 'grid'; + private _history = new ViewHistory<'grid' | 'map'>('grid', 'rbnb'); #search = ''; #typeFilter = ''; #economyFilter = ''; @@ -78,13 +80,23 @@ class FolkBnbView extends HTMLElement { this.#loadData(); this._stopPresence = startPresenceHeartbeat(() => ({ module: 'rbnb', context: 'Listings' })); if (this.#space !== 'demo') this.subscribeOffline(); + window.addEventListener('rspace-view-restored', this._onViewRestored as EventListener); } disconnectedCallback() { + this._history.destroy(); + window.removeEventListener('rspace-view-restored', this._onViewRestored as EventListener); this._stopPresence?.(); this._offlineUnsub?.(); this._offlineUnsub = null; } + private _onViewRestored = (e: CustomEvent) => { + if (e.detail?.moduleId !== 'rbnb') return; + this.#view = e.detail.view; + this.#render(); + this.#renderContent(); + }; + private async subscribeOffline() { const runtime = (window as any).__rspaceOfflineRuntime; if (!runtime?.isInitialized) return; @@ -230,7 +242,9 @@ class FolkBnbView extends HTMLElement { // View toggles for (const btn of this.querySelectorAll('.bnb-view__toggle')) { btn.addEventListener('click', () => { - this.#view = (btn as HTMLElement).dataset.view as 'grid' | 'map'; + const v = (btn as HTMLElement).dataset.view as 'grid' | 'map'; + this._history.push(v); + this.#view = v; this.#render(); this.#renderContent(); }); diff --git a/modules/rflows/components/folk-mortgage-simulator.ts b/modules/rflows/components/folk-mortgage-simulator.ts index 531209c1..e0c0ee8f 100644 --- a/modules/rflows/components/folk-mortgage-simulator.ts +++ b/modules/rflows/components/folk-mortgage-simulator.ts @@ -25,6 +25,7 @@ import { calculateAffordability, } from '../lib/mortgage-engine'; import type { MortgageSummary } from '../lib/mortgage-engine'; +import { ViewHistory } from "../../../shared/view-history.js"; type ViewMode = 'mycelial' | 'flow' | 'grid' | 'lender' | 'borrower'; @@ -90,6 +91,7 @@ class FolkMortgageSimulator extends HTMLElement { private summary!: MortgageSummary; private currentMonth = DEFAULT_CONFIG.startMonth; private viewMode: ViewMode = 'mycelial'; + private _history = new ViewHistory('mycelial', 'rflows'); private selectedTrancheId: string | null = null; private controlsOpen = true; private playing = false; @@ -121,12 +123,26 @@ class FolkMortgageSimulator extends HTMLElement { this._recompute(); this._attachListeners(); this.render(); + window.addEventListener('rspace-view-restored', this._onViewRestored as EventListener); } disconnectedCallback() { + this._history.destroy(); + window.removeEventListener('rspace-view-restored', this._onViewRestored as EventListener); this._stopPlayback(); } + private _onViewRestored = (e: CustomEvent) => { + if (e.detail?.moduleId !== 'rflows') return; + this.viewMode = e.detail.view; + const tabs = this.shadow.querySelectorAll('.view-tab'); + tabs.forEach((tab: Element) => { + const t = tab as HTMLElement; + t.className = `view-tab ${t.dataset.view === this.viewMode ? 'active' : ''}`; + }); + this._updateView(); + }; + // ─── Core simulation ────────────────────────────────── private _recompute() { @@ -1548,6 +1564,7 @@ class FolkMortgageSimulator extends HTMLElement { switch (action) { case 'view': { + this._history.push(target.dataset.view as ViewMode); this.viewMode = target.dataset.view as ViewMode; // Update tab highlights in-place const tabs = this.shadow.querySelectorAll('.view-tab'); diff --git a/modules/rnetwork/components/folk-crm-view.ts b/modules/rnetwork/components/folk-crm-view.ts index 49cad350..e8ebde55 100644 --- a/modules/rnetwork/components/folk-crm-view.ts +++ b/modules/rnetwork/components/folk-crm-view.ts @@ -70,11 +70,13 @@ import { TourEngine } from "../../../shared/tour-engine"; import { startPresenceHeartbeat } from '../../../shared/collab-presence'; import type { DocumentId } from "../../../shared/local-first/document"; import { networkSchema, networkDocId } from "../schemas"; +import { ViewHistory } from "../../../shared/view-history.js"; class FolkCrmView extends HTMLElement { private shadow: ShadowRoot; private space = ""; private activeTab: Tab = "pipeline"; + private _history = new ViewHistory("pipeline", "rnetwork"); private searchQuery = ""; private sortColumn = ""; private sortAsc = true; @@ -129,6 +131,7 @@ class FolkCrmView extends HTMLElement { private _onTabChange = (e: Event) => { const tab = (e as CustomEvent).detail?.tab; if (tab && tab !== this.activeTab) { + this._history.push(tab as Tab); this.activeTab = tab as Tab; this.searchQuery = ""; this.sortColumn = ""; @@ -164,13 +167,23 @@ class FolkCrmView extends HTMLElement { setTimeout(() => this._tour.start(), 1200); } this._stopPresence = startPresenceHeartbeat(() => ({ module: 'rnetwork', context: this.graphSelectedId ? 'CRM' : `CRM - ${this.activeTab}` })); + window.addEventListener('rspace-view-restored', this._onViewRestored as EventListener); } disconnectedCallback() { + this._history.destroy(); + window.removeEventListener('rspace-view-restored', this._onViewRestored as EventListener); document.removeEventListener("rapp-tab-change", this._onTabChange); this._stopPresence?.(); } + private _onViewRestored = (e: CustomEvent) => { + if (e.detail?.moduleId !== 'rnetwork') return; + this.activeTab = e.detail.view; + this.searchQuery = ""; + this.render(); + }; + private async subscribeCollabOverlay() { const runtime = (window as any).__rspaceOfflineRuntime; if (!runtime?.isInitialized) return; diff --git a/modules/rswag/components/folk-swag-designer.ts b/modules/rswag/components/folk-swag-designer.ts index 9f4dda97..8940c7d8 100644 --- a/modules/rswag/components/folk-swag-designer.ts +++ b/modules/rswag/components/folk-swag-designer.ts @@ -156,6 +156,7 @@ import type { SwagDoc, SwagDesign } from "../schemas"; import { swagSchema, swagDocId } from "../schemas"; import { startPresenceHeartbeat } from '../../../shared/collab-presence'; import type { DocumentId } from "../../../shared/local-first/document"; +import { ViewHistory } from "../../../shared/view-history.js"; // Auth helpers function getSession(): { accessToken: string; claims: { sub: string; did?: string; username?: string } } | null { @@ -195,6 +196,7 @@ class FolkSwagDesigner extends HTMLElement { // Tab state private activeTab: TabId = "browse"; + private _history = new ViewHistory("browse", "rswag"); // Demo state private selectedProduct = "tee"; @@ -284,15 +286,24 @@ class FolkSwagDesigner extends HTMLElement { setTimeout(() => this._tour.start(), 1200); } this._stopPresence = startPresenceHeartbeat(() => ({ module: 'rswag', context: 'Swag Designer' })); + window.addEventListener('rspace-view-restored', this._onViewRestored as EventListener); } disconnectedCallback() { + this._history.destroy(); + window.removeEventListener('rspace-view-restored', this._onViewRestored as EventListener); this._stopPresence?.(); this._lfcUnsub?.(); this._lfcUnsub = null; this.lfClient?.disconnect(); } + private _onViewRestored = (e: CustomEvent) => { + if (e.detail?.moduleId !== 'rswag') return; + this.activeTab = e.detail.view; + this.render(); + }; + private async initMultiplayer() { try { this.lfClient = new SwagLocalFirstClient(this.space); @@ -929,7 +940,9 @@ class FolkSwagDesigner extends HTMLElement { // Tab switching this.shadow.querySelectorAll(".tab-btn").forEach(btn => { btn.addEventListener("click", () => { - this.activeTab = btn.dataset.tab as TabId; + const tab = btn.dataset.tab as TabId; + this._history.push(tab); + this.activeTab = tab; this.render(); }); }); @@ -938,7 +951,9 @@ class FolkSwagDesigner extends HTMLElement { this.shadow.querySelectorAll("[data-tab]").forEach(btn => { if (!btn.classList.contains("tab-btn")) { btn.addEventListener("click", () => { - this.activeTab = btn.dataset.tab as TabId; + const tab = btn.dataset.tab as TabId; + this._history.push(tab); + this.activeTab = tab; this.render(); }); } @@ -1014,6 +1029,7 @@ class FolkSwagDesigner extends HTMLElement { this.shadow.querySelectorAll(".dither-btn").forEach(btn => { btn.addEventListener("click", () => { this.ditherDesignSlug = btn.dataset.slug || ""; + this._history.push("dither"); this.activeTab = "dither"; this.render(); }); diff --git a/modules/rtime/components/folk-timebank-app.ts b/modules/rtime/components/folk-timebank-app.ts index ea3205c9..8e4e0581 100644 --- a/modules/rtime/components/folk-timebank-app.ts +++ b/modules/rtime/components/folk-timebank-app.ts @@ -8,6 +8,7 @@ import type { DocumentId } from "../../../shared/local-first/document"; import { startPresenceHeartbeat } from '../../../shared/collab-presence'; import { commitmentsSchema, commitmentsDocId } from "../schemas"; +import { ViewHistory } from "../../../shared/view-history.js"; // ── Constants ── @@ -254,6 +255,7 @@ class FolkTimebankApp extends HTMLElement { private shadow: ShadowRoot; private space = 'demo'; private currentView: 'canvas' | 'collaborate' | 'dashboard' = 'canvas'; + private _history = new ViewHistory<'canvas' | 'collaborate' | 'dashboard'>('canvas', 'rtime'); // Pool panel state private canvas!: HTMLCanvasElement; @@ -357,6 +359,7 @@ class FolkTimebankApp extends HTMLElement { this.fetchData(); this._stopPresence = startPresenceHeartbeat(() => ({ module: 'rtime', context: 'Timebank' })); if (this.space !== 'demo') this.subscribeOffline(); + window.addEventListener('rspace-view-restored', this._onViewRestored as EventListener); } private async subscribeOffline() { @@ -392,10 +395,28 @@ class FolkTimebankApp extends HTMLElement { } disconnectedCallback() { + this._history.destroy(); + window.removeEventListener('rspace-view-restored', this._onViewRestored as EventListener); if (this.animFrame) cancelAnimationFrame(this.animFrame); this._stopPresence?.(); this._offlineUnsub?.(); this._offlineUnsub = null; } + private _onViewRestored = (e: CustomEvent) => { + if (e.detail?.moduleId !== 'rtime') return; + this.currentView = e.detail.view; + // Toggle visibility of view panels + const canvasView = this.shadow.getElementById('canvas-view'); + const collabView = this.shadow.getElementById('collaborate-view'); + const dashView = this.shadow.getElementById('dashboard-view'); + if (canvasView) canvasView.style.display = this.currentView === 'canvas' ? 'flex' : 'none'; + if (collabView) collabView.style.display = this.currentView === 'collaborate' ? 'flex' : 'none'; + if (dashView) dashView.style.display = this.currentView === 'dashboard' ? 'flex' : 'none'; + this.shadow.querySelectorAll('.tab').forEach(t => t.classList.toggle('active', (t as HTMLElement).dataset.view === this.currentView)); + if (this.currentView === 'canvas') { this.resizePoolCanvas(); this.rebuildSidebar(); } + if (this.currentView === 'collaborate') this.refreshCollaborate(); + if (this.currentView === 'dashboard') this.refreshDashboard(); + }; + private async fetchData() { const base = this.getApiBase(); try { @@ -707,6 +728,7 @@ class FolkTimebankApp extends HTMLElement { tab.addEventListener('click', () => { const view = (tab as HTMLElement).dataset.view as 'canvas' | 'collaborate' | 'dashboard'; if (view === this.currentView) return; + this._history.push(view); this.currentView = view; this.shadow.querySelectorAll('.tab').forEach(t => t.classList.toggle('active', (t as HTMLElement).dataset.view === view)); const canvasView = this.shadow.getElementById('canvas-view')!; diff --git a/modules/rtube/components/folk-video-player.ts b/modules/rtube/components/folk-video-player.ts index c912e878..d9ffeb26 100644 --- a/modules/rtube/components/folk-video-player.ts +++ b/modules/rtube/components/folk-video-player.ts @@ -10,6 +10,7 @@ import { authFetch, requireAuth } from "../../../shared/auth-fetch"; import { startPresenceHeartbeat } from '../../../shared/collab-presence'; import type { DocumentId } from "../../../shared/local-first/document"; import { tubeSchema, tubeDocId } from "../schemas"; +import { ViewHistory } from "../../../shared/view-history.js"; class FolkVideoPlayer extends HTMLElement { private shadow: ShadowRoot; @@ -17,6 +18,7 @@ class FolkVideoPlayer extends HTMLElement { private videos: Array<{ name: string; size: number; duration?: string; date?: string }> = []; private currentVideo: string | null = null; private mode: "library" | "live" | "360live" = "library"; + private _history = new ViewHistory<"library" | "live" | "360live">("library", "rtube"); private streamKey = ""; private searchTerm = ""; private isDemo = false; @@ -63,13 +65,25 @@ class FolkVideoPlayer extends HTMLElement { setTimeout(() => this._tour.start(), 1200); } this._stopPresence = startPresenceHeartbeat(() => ({ module: 'rtube', context: this.currentVideo || 'Video' })); + window.addEventListener('rspace-view-restored', this._onViewRestored as EventListener); } disconnectedCallback() { + this._history.destroy(); + window.removeEventListener('rspace-view-restored', this._onViewRestored as EventListener); this._stopPresence?.(); this._offlineUnsub?.(); this._offlineUnsub = null; } + private _onViewRestored = (e: CustomEvent) => { + if (e.detail?.moduleId !== 'rtube') return; + if (this.mode === "360live" && e.detail.view !== "360live") { + this.destroyHlsPlayers(); + } + this.mode = e.detail.view; + this.render(); + }; + private async subscribeOffline() { const runtime = (window as any).__rspaceOfflineRuntime; if (!runtime?.isInitialized) return; @@ -587,6 +601,7 @@ class FolkVideoPlayer extends HTMLElement { if (this.mode === "360live" && newMode !== "360live") { this.destroyHlsPlayers(); } + this._history.push(newMode); this.mode = newMode; this.render(); }); diff --git a/modules/rvnb/components/folk-vnb-view.ts b/modules/rvnb/components/folk-vnb-view.ts index 7e0e5f9b..1f760f5c 100644 --- a/modules/rvnb/components/folk-vnb-view.ts +++ b/modules/rvnb/components/folk-vnb-view.ts @@ -13,6 +13,7 @@ import { startPresenceHeartbeat } from '../../../shared/collab-presence'; import { getModuleApiBase } from "../../../shared/url-helpers"; import type { DocumentId } from "../../../shared/local-first/document"; import { vnbSchema, vnbDocId } from "../schemas"; +import { ViewHistory } from "../../../shared/view-history.js"; const VNB_TOUR_STEPS: TourStep[] = [ { target: '.vnb-search', title: 'Search', message: 'Filter by vehicle type, dates, or economy model.' }, @@ -68,6 +69,7 @@ class FolkVnbView extends HTMLElement { #stats: any = null; #selectedRental: any = null; #view: 'grid' | 'map' = 'grid'; + private _history = new ViewHistory<'grid' | 'map'>('grid', 'rvnb'); #search = ''; #typeFilter = ''; #economyFilter = ''; @@ -83,13 +85,23 @@ class FolkVnbView extends HTMLElement { this.#loadData(); this._stopPresence = startPresenceHeartbeat(() => ({ module: 'rvnb', context: 'Venues' })); if (this.#space !== 'demo') this.subscribeOffline(); + window.addEventListener('rspace-view-restored', this._onViewRestored as EventListener); } disconnectedCallback() { + this._history.destroy(); + window.removeEventListener('rspace-view-restored', this._onViewRestored as EventListener); this._stopPresence?.(); this._offlineUnsub?.(); this._offlineUnsub = null; } + private _onViewRestored = (e: CustomEvent) => { + if (e.detail?.moduleId !== 'rvnb') return; + this.#view = e.detail.view; + this.#render(); + this.#renderContent(); + }; + private async subscribeOffline() { const runtime = (window as any).__rspaceOfflineRuntime; if (!runtime?.isInitialized) return; @@ -234,7 +246,9 @@ class FolkVnbView extends HTMLElement { // View toggles for (const btn of this.querySelectorAll('.vnb-view__toggle')) { btn.addEventListener('click', () => { - this.#view = (btn as HTMLElement).dataset.view as 'grid' | 'map'; + const v = (btn as HTMLElement).dataset.view as 'grid' | 'map'; + this._history.push(v); + this.#view = v; this.#render(); this.#renderContent(); }); diff --git a/modules/rwallet/components/folk-wallet-viewer.ts b/modules/rwallet/components/folk-wallet-viewer.ts index ba9062b6..450aec87 100644 --- a/modules/rwallet/components/folk-wallet-viewer.ts +++ b/modules/rwallet/components/folk-wallet-viewer.ts @@ -18,6 +18,7 @@ import type { WalletDoc, WatchedAddress } from "../schemas"; import { walletSchema, walletDocId } from "../schemas"; import { startPresenceHeartbeat } from '../../../shared/collab-presence'; import type { DocumentId } from "../../../shared/local-first/document"; +import { ViewHistory } from "../../../shared/view-history.js"; interface ChainInfo { chainId: string; @@ -197,6 +198,7 @@ class FolkWalletViewer extends HTMLElement { // Visualization state private activeView: ViewTab = "budget"; + private _history = new ViewHistory("budget", "rwallet"); private transfers: Map | null = null; private transfersLoading = false; private d3Ready = false; @@ -265,9 +267,12 @@ class FolkWalletViewer extends HTMLElement { setTimeout(() => this._tour.start(), 1200); } this._stopPresence = startPresenceHeartbeat(() => ({ module: 'rwallet', context: 'Wallet' })); + window.addEventListener('rspace-view-restored', this._onViewRestored as EventListener); } disconnectedCallback() { + this._history.destroy(); + window.removeEventListener('rspace-view-restored', this._onViewRestored as EventListener); this._stopPresence?.(); if (this.flowsPlayInterval) { clearInterval(this.flowsPlayInterval); @@ -275,6 +280,15 @@ class FolkWalletViewer extends HTMLElement { } } + private _onViewRestored = (e: CustomEvent) => { + if (e.detail?.moduleId !== 'rwallet') return; + this.activeView = e.detail.view; + this.render(); + if (this.activeView !== "balances") { + requestAnimationFrame(() => this.drawActiveVisualization()); + } + }; + private checkAuthState() { try { const session = localStorage.getItem("encryptid_session"); @@ -1265,6 +1279,7 @@ class FolkWalletViewer extends HTMLElement { private handleViewTabClick(view: ViewTab) { if (this.activeView === view) return; + this._history.push(view); this.activeView = view; if (view !== "balances" && !this.transfers && !this.isDemo) {