Merge branch 'dev'
CI/CD / deploy (push) Successful in 2m55s Details

This commit is contained in:
Jeff Emmett 2026-04-10 17:48:41 -04:00
commit a89a6fbebb
9 changed files with 145 additions and 4 deletions

View File

@ -13,6 +13,7 @@ import type { CrowdSurfDoc, CrowdSurfPrompt, Contribution } from '../schemas';
import { getDecayProgress, getTimeRemaining, getRightSwipeCount, isReadyToTrigger, getUrgency, parseContributions, crowdsurfSchema, crowdsurfDocId } from '../schemas'; import { getDecayProgress, getTimeRemaining, getRightSwipeCount, isReadyToTrigger, getUrgency, parseContributions, crowdsurfSchema, crowdsurfDocId } from '../schemas';
import { getModuleApiBase } from "../../../shared/url-helpers"; import { getModuleApiBase } from "../../../shared/url-helpers";
import type { DocumentId } from "../../../shared/local-first/document"; import type { DocumentId } from "../../../shared/local-first/document";
import { ViewHistory } from "../../../shared/view-history.js";
// ── Auth helpers ── // ── Auth helpers ──
function getSession(): { accessToken: string; claims: { sub: string; did?: string; username?: string } } | null { function getSession(): { accessToken: string; claims: { sub: string; did?: string; username?: string } } | null {
@ -45,6 +46,7 @@ class FolkCrowdSurfDashboard extends HTMLElement {
// State // State
private activeTab: ViewTab = 'discover'; private activeTab: ViewTab = 'discover';
private _history = new ViewHistory<ViewTab>('discover', 'crowdsurf');
private loading = true; private loading = true;
private prompts: CrowdSurfPrompt[] = []; private prompts: CrowdSurfPrompt[] = [];
private currentPromptIndex = 0; private currentPromptIndex = 0;
@ -95,9 +97,12 @@ class FolkCrowdSurfDashboard extends HTMLElement {
this.initMultiplayer(); this.initMultiplayer();
} }
this._stopPresence = startPresenceHeartbeat(() => ({ module: 'crowdsurf', context: this.prompts[this.currentPromptIndex]?.text || 'CrowdSurf' })); this._stopPresence = startPresenceHeartbeat(() => ({ module: 'crowdsurf', context: this.prompts[this.currentPromptIndex]?.text || 'CrowdSurf' }));
window.addEventListener('rspace-view-restored', this._onViewRestored as EventListener);
} }
disconnectedCallback() { disconnectedCallback() {
this._history.destroy();
window.removeEventListener('rspace-view-restored', this._onViewRestored as EventListener);
this._stopPresence?.(); this._stopPresence?.();
this._lfcUnsub?.(); this._lfcUnsub?.();
this._lfcUnsub = null; this._lfcUnsub = null;
@ -105,6 +110,13 @@ class FolkCrowdSurfDashboard extends HTMLElement {
if (this._expiryTimer !== null) clearInterval(this._expiryTimer); 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 ── // ── Multiplayer init ──
private async initMultiplayer() { private async initMultiplayer() {
@ -337,6 +349,7 @@ class FolkCrowdSurfDashboard extends HTMLElement {
this.lfClient.createPrompt(prompt); this.lfClient.createPrompt(prompt);
} }
this._history.push('discover');
this.activeTab = 'discover'; this.activeTab = 'discover';
this.render(); this.render();
this.bindEvents(); this.bindEvents();
@ -738,6 +751,7 @@ class FolkCrowdSurfDashboard extends HTMLElement {
btn.addEventListener('click', () => { btn.addEventListener('click', () => {
const tab = btn.dataset.tab as ViewTab; const tab = btn.dataset.tab as ViewTab;
if (tab && tab !== this.activeTab) { if (tab && tab !== this.activeTab) {
this._history.push(tab);
this.activeTab = tab; this.activeTab = tab;
this.render(); this.render();
this.bindEvents(); this.bindEvents();
@ -761,6 +775,7 @@ class FolkCrowdSurfDashboard extends HTMLElement {
// Go to create tab // Go to create tab
this.shadow.querySelector('[data-action="go-create"]')?.addEventListener('click', () => { this.shadow.querySelector('[data-action="go-create"]')?.addEventListener('click', () => {
this._history.push('create');
this.activeTab = 'create'; this.activeTab = 'create';
this.render(); this.render();
this.bindEvents(); this.bindEvents();

View File

@ -13,6 +13,7 @@ import { startPresenceHeartbeat } from '../../../shared/collab-presence';
import { getModuleApiBase } from "../../../shared/url-helpers"; import { getModuleApiBase } from "../../../shared/url-helpers";
import type { DocumentId } from "../../../shared/local-first/document"; import type { DocumentId } from "../../../shared/local-first/document";
import { bnbSchema, bnbDocId } from "../schemas"; import { bnbSchema, bnbDocId } from "../schemas";
import { ViewHistory } from "../../../shared/view-history.js";
const BNB_TOUR_STEPS: TourStep[] = [ const BNB_TOUR_STEPS: TourStep[] = [
{ target: '.bnb-search', title: 'Search', message: 'Filter listings by location, type, or economy model.' }, { 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; #stats: any = null;
#selectedStay: any = null; #selectedStay: any = null;
#view: 'grid' | 'map' = 'grid'; #view: 'grid' | 'map' = 'grid';
private _history = new ViewHistory<'grid' | 'map'>('grid', 'rbnb');
#search = ''; #search = '';
#typeFilter = ''; #typeFilter = '';
#economyFilter = ''; #economyFilter = '';
@ -78,13 +80,23 @@ class FolkBnbView extends HTMLElement {
this.#loadData(); this.#loadData();
this._stopPresence = startPresenceHeartbeat(() => ({ module: 'rbnb', context: 'Listings' })); this._stopPresence = startPresenceHeartbeat(() => ({ module: 'rbnb', context: 'Listings' }));
if (this.#space !== 'demo') this.subscribeOffline(); if (this.#space !== 'demo') this.subscribeOffline();
window.addEventListener('rspace-view-restored', this._onViewRestored as EventListener);
} }
disconnectedCallback() { disconnectedCallback() {
this._history.destroy();
window.removeEventListener('rspace-view-restored', this._onViewRestored as EventListener);
this._stopPresence?.(); this._stopPresence?.();
this._offlineUnsub?.(); this._offlineUnsub = null; 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() { private async subscribeOffline() {
const runtime = (window as any).__rspaceOfflineRuntime; const runtime = (window as any).__rspaceOfflineRuntime;
if (!runtime?.isInitialized) return; if (!runtime?.isInitialized) return;
@ -230,7 +242,9 @@ class FolkBnbView extends HTMLElement {
// View toggles // View toggles
for (const btn of this.querySelectorAll('.bnb-view__toggle')) { for (const btn of this.querySelectorAll('.bnb-view__toggle')) {
btn.addEventListener('click', () => { 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.#render();
this.#renderContent(); this.#renderContent();
}); });

View File

@ -25,6 +25,7 @@ import {
calculateAffordability, calculateAffordability,
} from '../lib/mortgage-engine'; } from '../lib/mortgage-engine';
import type { MortgageSummary } 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'; type ViewMode = 'mycelial' | 'flow' | 'grid' | 'lender' | 'borrower';
@ -90,6 +91,7 @@ class FolkMortgageSimulator extends HTMLElement {
private summary!: MortgageSummary; private summary!: MortgageSummary;
private currentMonth = DEFAULT_CONFIG.startMonth; private currentMonth = DEFAULT_CONFIG.startMonth;
private viewMode: ViewMode = 'mycelial'; private viewMode: ViewMode = 'mycelial';
private _history = new ViewHistory<ViewMode>('mycelial', 'rflows');
private selectedTrancheId: string | null = null; private selectedTrancheId: string | null = null;
private controlsOpen = true; private controlsOpen = true;
private playing = false; private playing = false;
@ -121,12 +123,26 @@ class FolkMortgageSimulator extends HTMLElement {
this._recompute(); this._recompute();
this._attachListeners(); this._attachListeners();
this.render(); this.render();
window.addEventListener('rspace-view-restored', this._onViewRestored as EventListener);
} }
disconnectedCallback() { disconnectedCallback() {
this._history.destroy();
window.removeEventListener('rspace-view-restored', this._onViewRestored as EventListener);
this._stopPlayback(); 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 ────────────────────────────────── // ─── Core simulation ──────────────────────────────────
private _recompute() { private _recompute() {
@ -1548,6 +1564,7 @@ class FolkMortgageSimulator extends HTMLElement {
switch (action) { switch (action) {
case 'view': { case 'view': {
this._history.push(target.dataset.view as ViewMode);
this.viewMode = target.dataset.view as ViewMode; this.viewMode = target.dataset.view as ViewMode;
// Update tab highlights in-place // Update tab highlights in-place
const tabs = this.shadow.querySelectorAll('.view-tab'); const tabs = this.shadow.querySelectorAll('.view-tab');

View File

@ -70,11 +70,13 @@ import { TourEngine } from "../../../shared/tour-engine";
import { startPresenceHeartbeat } from '../../../shared/collab-presence'; import { startPresenceHeartbeat } from '../../../shared/collab-presence';
import type { DocumentId } from "../../../shared/local-first/document"; import type { DocumentId } from "../../../shared/local-first/document";
import { networkSchema, networkDocId } from "../schemas"; import { networkSchema, networkDocId } from "../schemas";
import { ViewHistory } from "../../../shared/view-history.js";
class FolkCrmView extends HTMLElement { class FolkCrmView extends HTMLElement {
private shadow: ShadowRoot; private shadow: ShadowRoot;
private space = ""; private space = "";
private activeTab: Tab = "pipeline"; private activeTab: Tab = "pipeline";
private _history = new ViewHistory<Tab>("pipeline", "rnetwork");
private searchQuery = ""; private searchQuery = "";
private sortColumn = ""; private sortColumn = "";
private sortAsc = true; private sortAsc = true;
@ -129,6 +131,7 @@ class FolkCrmView extends HTMLElement {
private _onTabChange = (e: Event) => { private _onTabChange = (e: Event) => {
const tab = (e as CustomEvent).detail?.tab; const tab = (e as CustomEvent).detail?.tab;
if (tab && tab !== this.activeTab) { if (tab && tab !== this.activeTab) {
this._history.push(tab as Tab);
this.activeTab = tab as Tab; this.activeTab = tab as Tab;
this.searchQuery = ""; this.searchQuery = "";
this.sortColumn = ""; this.sortColumn = "";
@ -164,13 +167,23 @@ class FolkCrmView extends HTMLElement {
setTimeout(() => this._tour.start(), 1200); setTimeout(() => this._tour.start(), 1200);
} }
this._stopPresence = startPresenceHeartbeat(() => ({ module: 'rnetwork', context: this.graphSelectedId ? 'CRM' : `CRM - ${this.activeTab}` })); this._stopPresence = startPresenceHeartbeat(() => ({ module: 'rnetwork', context: this.graphSelectedId ? 'CRM' : `CRM - ${this.activeTab}` }));
window.addEventListener('rspace-view-restored', this._onViewRestored as EventListener);
} }
disconnectedCallback() { disconnectedCallback() {
this._history.destroy();
window.removeEventListener('rspace-view-restored', this._onViewRestored as EventListener);
document.removeEventListener("rapp-tab-change", this._onTabChange); document.removeEventListener("rapp-tab-change", this._onTabChange);
this._stopPresence?.(); this._stopPresence?.();
} }
private _onViewRestored = (e: CustomEvent) => {
if (e.detail?.moduleId !== 'rnetwork') return;
this.activeTab = e.detail.view;
this.searchQuery = "";
this.render();
};
private async subscribeCollabOverlay() { private async subscribeCollabOverlay() {
const runtime = (window as any).__rspaceOfflineRuntime; const runtime = (window as any).__rspaceOfflineRuntime;
if (!runtime?.isInitialized) return; if (!runtime?.isInitialized) return;

View File

@ -156,6 +156,7 @@ import type { SwagDoc, SwagDesign } from "../schemas";
import { swagSchema, swagDocId } from "../schemas"; import { swagSchema, swagDocId } from "../schemas";
import { startPresenceHeartbeat } from '../../../shared/collab-presence'; import { startPresenceHeartbeat } from '../../../shared/collab-presence';
import type { DocumentId } from "../../../shared/local-first/document"; import type { DocumentId } from "../../../shared/local-first/document";
import { ViewHistory } from "../../../shared/view-history.js";
// Auth helpers // Auth helpers
function getSession(): { accessToken: string; claims: { sub: string; did?: string; username?: string } } | null { function getSession(): { accessToken: string; claims: { sub: string; did?: string; username?: string } } | null {
@ -195,6 +196,7 @@ class FolkSwagDesigner extends HTMLElement {
// Tab state // Tab state
private activeTab: TabId = "browse"; private activeTab: TabId = "browse";
private _history = new ViewHistory<TabId>("browse", "rswag");
// Demo state // Demo state
private selectedProduct = "tee"; private selectedProduct = "tee";
@ -284,15 +286,24 @@ class FolkSwagDesigner extends HTMLElement {
setTimeout(() => this._tour.start(), 1200); setTimeout(() => this._tour.start(), 1200);
} }
this._stopPresence = startPresenceHeartbeat(() => ({ module: 'rswag', context: 'Swag Designer' })); this._stopPresence = startPresenceHeartbeat(() => ({ module: 'rswag', context: 'Swag Designer' }));
window.addEventListener('rspace-view-restored', this._onViewRestored as EventListener);
} }
disconnectedCallback() { disconnectedCallback() {
this._history.destroy();
window.removeEventListener('rspace-view-restored', this._onViewRestored as EventListener);
this._stopPresence?.(); this._stopPresence?.();
this._lfcUnsub?.(); this._lfcUnsub?.();
this._lfcUnsub = null; this._lfcUnsub = null;
this.lfClient?.disconnect(); this.lfClient?.disconnect();
} }
private _onViewRestored = (e: CustomEvent) => {
if (e.detail?.moduleId !== 'rswag') return;
this.activeTab = e.detail.view;
this.render();
};
private async initMultiplayer() { private async initMultiplayer() {
try { try {
this.lfClient = new SwagLocalFirstClient(this.space); this.lfClient = new SwagLocalFirstClient(this.space);
@ -929,7 +940,9 @@ class FolkSwagDesigner extends HTMLElement {
// Tab switching // Tab switching
this.shadow.querySelectorAll<HTMLElement>(".tab-btn").forEach(btn => { this.shadow.querySelectorAll<HTMLElement>(".tab-btn").forEach(btn => {
btn.addEventListener("click", () => { 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(); this.render();
}); });
}); });
@ -938,7 +951,9 @@ class FolkSwagDesigner extends HTMLElement {
this.shadow.querySelectorAll<HTMLElement>("[data-tab]").forEach(btn => { this.shadow.querySelectorAll<HTMLElement>("[data-tab]").forEach(btn => {
if (!btn.classList.contains("tab-btn")) { if (!btn.classList.contains("tab-btn")) {
btn.addEventListener("click", () => { 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(); this.render();
}); });
} }
@ -1014,6 +1029,7 @@ class FolkSwagDesigner extends HTMLElement {
this.shadow.querySelectorAll<HTMLElement>(".dither-btn").forEach(btn => { this.shadow.querySelectorAll<HTMLElement>(".dither-btn").forEach(btn => {
btn.addEventListener("click", () => { btn.addEventListener("click", () => {
this.ditherDesignSlug = btn.dataset.slug || ""; this.ditherDesignSlug = btn.dataset.slug || "";
this._history.push("dither");
this.activeTab = "dither"; this.activeTab = "dither";
this.render(); this.render();
}); });

View File

@ -8,6 +8,7 @@
import type { DocumentId } from "../../../shared/local-first/document"; import type { DocumentId } from "../../../shared/local-first/document";
import { startPresenceHeartbeat } from '../../../shared/collab-presence'; import { startPresenceHeartbeat } from '../../../shared/collab-presence';
import { commitmentsSchema, commitmentsDocId } from "../schemas"; import { commitmentsSchema, commitmentsDocId } from "../schemas";
import { ViewHistory } from "../../../shared/view-history.js";
// ── Constants ── // ── Constants ──
@ -254,6 +255,7 @@ class FolkTimebankApp extends HTMLElement {
private shadow: ShadowRoot; private shadow: ShadowRoot;
private space = 'demo'; private space = 'demo';
private currentView: 'canvas' | 'collaborate' | 'dashboard' = 'canvas'; private currentView: 'canvas' | 'collaborate' | 'dashboard' = 'canvas';
private _history = new ViewHistory<'canvas' | 'collaborate' | 'dashboard'>('canvas', 'rtime');
// Pool panel state // Pool panel state
private canvas!: HTMLCanvasElement; private canvas!: HTMLCanvasElement;
@ -357,6 +359,7 @@ class FolkTimebankApp extends HTMLElement {
this.fetchData(); this.fetchData();
this._stopPresence = startPresenceHeartbeat(() => ({ module: 'rtime', context: 'Timebank' })); this._stopPresence = startPresenceHeartbeat(() => ({ module: 'rtime', context: 'Timebank' }));
if (this.space !== 'demo') this.subscribeOffline(); if (this.space !== 'demo') this.subscribeOffline();
window.addEventListener('rspace-view-restored', this._onViewRestored as EventListener);
} }
private async subscribeOffline() { private async subscribeOffline() {
@ -392,10 +395,28 @@ class FolkTimebankApp extends HTMLElement {
} }
disconnectedCallback() { disconnectedCallback() {
this._history.destroy();
window.removeEventListener('rspace-view-restored', this._onViewRestored as EventListener);
if (this.animFrame) cancelAnimationFrame(this.animFrame); if (this.animFrame) cancelAnimationFrame(this.animFrame);
this._stopPresence?.(); this._offlineUnsub?.(); this._offlineUnsub = null; 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() { private async fetchData() {
const base = this.getApiBase(); const base = this.getApiBase();
try { try {
@ -707,6 +728,7 @@ class FolkTimebankApp extends HTMLElement {
tab.addEventListener('click', () => { tab.addEventListener('click', () => {
const view = (tab as HTMLElement).dataset.view as 'canvas' | 'collaborate' | 'dashboard'; const view = (tab as HTMLElement).dataset.view as 'canvas' | 'collaborate' | 'dashboard';
if (view === this.currentView) return; if (view === this.currentView) return;
this._history.push(view);
this.currentView = view; this.currentView = view;
this.shadow.querySelectorAll('.tab').forEach(t => t.classList.toggle('active', (t as HTMLElement).dataset.view === view)); this.shadow.querySelectorAll('.tab').forEach(t => t.classList.toggle('active', (t as HTMLElement).dataset.view === view));
const canvasView = this.shadow.getElementById('canvas-view')!; const canvasView = this.shadow.getElementById('canvas-view')!;

View File

@ -10,6 +10,7 @@ import { authFetch, requireAuth } from "../../../shared/auth-fetch";
import { startPresenceHeartbeat } from '../../../shared/collab-presence'; import { startPresenceHeartbeat } from '../../../shared/collab-presence';
import type { DocumentId } from "../../../shared/local-first/document"; import type { DocumentId } from "../../../shared/local-first/document";
import { tubeSchema, tubeDocId } from "../schemas"; import { tubeSchema, tubeDocId } from "../schemas";
import { ViewHistory } from "../../../shared/view-history.js";
class FolkVideoPlayer extends HTMLElement { class FolkVideoPlayer extends HTMLElement {
private shadow: ShadowRoot; private shadow: ShadowRoot;
@ -17,6 +18,7 @@ class FolkVideoPlayer extends HTMLElement {
private videos: Array<{ name: string; size: number; duration?: string; date?: string }> = []; private videos: Array<{ name: string; size: number; duration?: string; date?: string }> = [];
private currentVideo: string | null = null; private currentVideo: string | null = null;
private mode: "library" | "live" | "360live" = "library"; private mode: "library" | "live" | "360live" = "library";
private _history = new ViewHistory<"library" | "live" | "360live">("library", "rtube");
private streamKey = ""; private streamKey = "";
private searchTerm = ""; private searchTerm = "";
private isDemo = false; private isDemo = false;
@ -63,13 +65,25 @@ class FolkVideoPlayer extends HTMLElement {
setTimeout(() => this._tour.start(), 1200); setTimeout(() => this._tour.start(), 1200);
} }
this._stopPresence = startPresenceHeartbeat(() => ({ module: 'rtube', context: this.currentVideo || 'Video' })); this._stopPresence = startPresenceHeartbeat(() => ({ module: 'rtube', context: this.currentVideo || 'Video' }));
window.addEventListener('rspace-view-restored', this._onViewRestored as EventListener);
} }
disconnectedCallback() { disconnectedCallback() {
this._history.destroy();
window.removeEventListener('rspace-view-restored', this._onViewRestored as EventListener);
this._stopPresence?.(); this._stopPresence?.();
this._offlineUnsub?.(); this._offlineUnsub = null; 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() { private async subscribeOffline() {
const runtime = (window as any).__rspaceOfflineRuntime; const runtime = (window as any).__rspaceOfflineRuntime;
if (!runtime?.isInitialized) return; if (!runtime?.isInitialized) return;
@ -587,6 +601,7 @@ class FolkVideoPlayer extends HTMLElement {
if (this.mode === "360live" && newMode !== "360live") { if (this.mode === "360live" && newMode !== "360live") {
this.destroyHlsPlayers(); this.destroyHlsPlayers();
} }
this._history.push(newMode);
this.mode = newMode; this.mode = newMode;
this.render(); this.render();
}); });

View File

@ -13,6 +13,7 @@ import { startPresenceHeartbeat } from '../../../shared/collab-presence';
import { getModuleApiBase } from "../../../shared/url-helpers"; import { getModuleApiBase } from "../../../shared/url-helpers";
import type { DocumentId } from "../../../shared/local-first/document"; import type { DocumentId } from "../../../shared/local-first/document";
import { vnbSchema, vnbDocId } from "../schemas"; import { vnbSchema, vnbDocId } from "../schemas";
import { ViewHistory } from "../../../shared/view-history.js";
const VNB_TOUR_STEPS: TourStep[] = [ const VNB_TOUR_STEPS: TourStep[] = [
{ target: '.vnb-search', title: 'Search', message: 'Filter by vehicle type, dates, or economy model.' }, { 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; #stats: any = null;
#selectedRental: any = null; #selectedRental: any = null;
#view: 'grid' | 'map' = 'grid'; #view: 'grid' | 'map' = 'grid';
private _history = new ViewHistory<'grid' | 'map'>('grid', 'rvnb');
#search = ''; #search = '';
#typeFilter = ''; #typeFilter = '';
#economyFilter = ''; #economyFilter = '';
@ -83,13 +85,23 @@ class FolkVnbView extends HTMLElement {
this.#loadData(); this.#loadData();
this._stopPresence = startPresenceHeartbeat(() => ({ module: 'rvnb', context: 'Venues' })); this._stopPresence = startPresenceHeartbeat(() => ({ module: 'rvnb', context: 'Venues' }));
if (this.#space !== 'demo') this.subscribeOffline(); if (this.#space !== 'demo') this.subscribeOffline();
window.addEventListener('rspace-view-restored', this._onViewRestored as EventListener);
} }
disconnectedCallback() { disconnectedCallback() {
this._history.destroy();
window.removeEventListener('rspace-view-restored', this._onViewRestored as EventListener);
this._stopPresence?.(); this._stopPresence?.();
this._offlineUnsub?.(); this._offlineUnsub = null; 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() { private async subscribeOffline() {
const runtime = (window as any).__rspaceOfflineRuntime; const runtime = (window as any).__rspaceOfflineRuntime;
if (!runtime?.isInitialized) return; if (!runtime?.isInitialized) return;
@ -234,7 +246,9 @@ class FolkVnbView extends HTMLElement {
// View toggles // View toggles
for (const btn of this.querySelectorAll('.vnb-view__toggle')) { for (const btn of this.querySelectorAll('.vnb-view__toggle')) {
btn.addEventListener('click', () => { 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.#render();
this.#renderContent(); this.#renderContent();
}); });

View File

@ -18,6 +18,7 @@ import type { WalletDoc, WatchedAddress } from "../schemas";
import { walletSchema, walletDocId } from "../schemas"; import { walletSchema, walletDocId } from "../schemas";
import { startPresenceHeartbeat } from '../../../shared/collab-presence'; import { startPresenceHeartbeat } from '../../../shared/collab-presence';
import type { DocumentId } from "../../../shared/local-first/document"; import type { DocumentId } from "../../../shared/local-first/document";
import { ViewHistory } from "../../../shared/view-history.js";
interface ChainInfo { interface ChainInfo {
chainId: string; chainId: string;
@ -197,6 +198,7 @@ class FolkWalletViewer extends HTMLElement {
// Visualization state // Visualization state
private activeView: ViewTab = "budget"; private activeView: ViewTab = "budget";
private _history = new ViewHistory<ViewTab>("budget", "rwallet");
private transfers: Map<string, any> | null = null; private transfers: Map<string, any> | null = null;
private transfersLoading = false; private transfersLoading = false;
private d3Ready = false; private d3Ready = false;
@ -265,9 +267,12 @@ class FolkWalletViewer extends HTMLElement {
setTimeout(() => this._tour.start(), 1200); setTimeout(() => this._tour.start(), 1200);
} }
this._stopPresence = startPresenceHeartbeat(() => ({ module: 'rwallet', context: 'Wallet' })); this._stopPresence = startPresenceHeartbeat(() => ({ module: 'rwallet', context: 'Wallet' }));
window.addEventListener('rspace-view-restored', this._onViewRestored as EventListener);
} }
disconnectedCallback() { disconnectedCallback() {
this._history.destroy();
window.removeEventListener('rspace-view-restored', this._onViewRestored as EventListener);
this._stopPresence?.(); this._stopPresence?.();
if (this.flowsPlayInterval) { if (this.flowsPlayInterval) {
clearInterval(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() { private checkAuthState() {
try { try {
const session = localStorage.getItem("encryptid_session"); const session = localStorage.getItem("encryptid_session");
@ -1265,6 +1279,7 @@ class FolkWalletViewer extends HTMLElement {
private handleViewTabClick(view: ViewTab) { private handleViewTabClick(view: ViewTab) {
if (this.activeView === view) return; if (this.activeView === view) return;
this._history.push(view);
this.activeView = view; this.activeView = view;
if (view !== "balances" && !this.transfers && !this.isDemo) { if (view !== "balances" && !this.transfers && !this.isDemo) {