Merge branch 'dev'
CI/CD / deploy (push) Successful in 2m28s
Details
CI/CD / deploy (push) Successful in 2m28s
Details
This commit is contained in:
commit
c2fb1b2858
|
|
@ -362,6 +362,29 @@ class FolkTimebankApp extends HTMLElement {
|
|||
if (name === 'view' && (val === 'canvas' || val === 'collaborate' || val === 'dashboard')) this.currentView = val;
|
||||
}
|
||||
|
||||
private _onShellTabChange = (e: Event) => {
|
||||
const tab = (e as CustomEvent).detail?.tab;
|
||||
if (!tab) return;
|
||||
if (tab === 'pool') this._switchView('canvas');
|
||||
else if (tab === 'fulfillment') this._switchView('dashboard');
|
||||
// 'cyclos' tab is a redirect, no client-side handling needed
|
||||
};
|
||||
|
||||
private _switchView(view: 'canvas' | 'collaborate' | 'dashboard') {
|
||||
if (view === this.currentView) return;
|
||||
this._history.push(view);
|
||||
this.currentView = view;
|
||||
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 = view === 'canvas' ? 'flex' : 'none';
|
||||
if (collabView) collabView.style.display = view === 'collaborate' ? 'flex' : 'none';
|
||||
if (dashView) dashView.style.display = view === 'dashboard' ? 'flex' : 'none';
|
||||
if (view === 'canvas') { this.resizePoolCanvas(); this.rebuildSidebar(); }
|
||||
if (view === 'collaborate') this.refreshCollaborate();
|
||||
if (view === 'dashboard') this.refreshDashboard();
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.space = this.getAttribute('space') || 'demo';
|
||||
const rawView = this.getAttribute('view');
|
||||
|
|
@ -383,6 +406,7 @@ class FolkTimebankApp extends HTMLElement {
|
|||
this.fetchData();
|
||||
this._stopPresence = startPresenceHeartbeat(() => ({ module: 'rtime', context: 'Timebank' }));
|
||||
if (this.space !== 'demo') this.subscribeOffline();
|
||||
document.addEventListener('rapp-tab-change', this._onShellTabChange);
|
||||
window.addEventListener('rspace-view-restored', this._onViewRestored as EventListener);
|
||||
}
|
||||
|
||||
|
|
@ -421,6 +445,7 @@ class FolkTimebankApp extends HTMLElement {
|
|||
|
||||
disconnectedCallback() {
|
||||
this._history.destroy();
|
||||
document.removeEventListener('rapp-tab-change', this._onShellTabChange);
|
||||
window.removeEventListener('rspace-view-restored', this._onViewRestored as EventListener);
|
||||
if (this.animFrame) cancelAnimationFrame(this.animFrame);
|
||||
this._stopPresence?.(); this._offlineUnsub?.(); this._offlineUnsub = null;
|
||||
|
|
@ -435,18 +460,7 @@ class FolkTimebankApp extends HTMLElement {
|
|||
|
||||
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();
|
||||
this._switchView(e.detail.view);
|
||||
};
|
||||
|
||||
private async fetchData() {
|
||||
|
|
@ -528,18 +542,6 @@ class FolkTimebankApp extends HTMLElement {
|
|||
private render() {
|
||||
this.shadow.innerHTML = `
|
||||
<style>${CSS_TEXT}</style>
|
||||
<div class="tab-bar">
|
||||
<div class="tab active" data-view="canvas">Canvas</div>
|
||||
<div class="tab" data-view="collaborate">Collaborate</div>
|
||||
<div class="tab" data-view="dashboard">Fulfillment</div>
|
||||
<button class="theme-toggle" id="themeToggle" title="Toggle light/dark theme">☀</button>
|
||||
</div>
|
||||
<div class="stats-bar">
|
||||
<div class="stat"><span class="stat-value" id="statHours">0</span> hours available</div>
|
||||
<div class="stat"><span class="stat-value" id="statContributors">0</span> contributors</div>
|
||||
<div class="skill-bar" id="skillBar"></div>
|
||||
<div class="skill-legend" id="skillLegend"></div>
|
||||
</div>
|
||||
<div class="main">
|
||||
<div id="canvas-view">
|
||||
<!-- Left pool panel -->
|
||||
|
|
@ -547,9 +549,18 @@ class FolkTimebankApp extends HTMLElement {
|
|||
<div class="pool-panel-header">
|
||||
<span>Commitment Pool</span>
|
||||
<span class="pool-hint">drag orb to weave \u2192</span>
|
||||
<button class="theme-toggle" id="themeToggle" title="Toggle light/dark theme">☀</button>
|
||||
<button id="poolPanelToggle" title="Collapse panel">\u27E8</button>
|
||||
</div>
|
||||
<canvas id="pool-canvas"></canvas>
|
||||
<div class="pool-legend" id="poolLegend">
|
||||
<div class="pool-legend-stats">
|
||||
<span><strong id="statHours">0</strong> hours</span>
|
||||
<span class="pool-legend-sep">\u00b7</span>
|
||||
<span><strong id="statContributors">0</strong> contributors</span>
|
||||
</div>
|
||||
<div class="pool-legend-skills" id="skillLegend"></div>
|
||||
</div>
|
||||
<div class="pool-detail" id="poolDetail">
|
||||
<div class="pool-detail-header">
|
||||
<div class="pool-detail-dot" id="detailDot"></div>
|
||||
|
|
@ -781,26 +792,6 @@ class FolkTimebankApp extends HTMLElement {
|
|||
this.canvas = this.shadow.getElementById('pool-canvas') as HTMLCanvasElement;
|
||||
this.ctx = this.canvas.getContext('2d')!;
|
||||
|
||||
// Tab switching (3 tabs: canvas, collaborate, dashboard)
|
||||
this.shadow.querySelectorAll('.tab').forEach(tab => {
|
||||
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')!;
|
||||
const collabView = this.shadow.getElementById('collaborate-view')!;
|
||||
const dashView = this.shadow.getElementById('dashboard-view')!;
|
||||
canvasView.style.display = view === 'canvas' ? 'flex' : 'none';
|
||||
collabView.style.display = view === 'collaborate' ? 'flex' : 'none';
|
||||
dashView.style.display = view === 'dashboard' ? 'flex' : 'none';
|
||||
if (view === 'canvas') { this.resizePoolCanvas(); this.rebuildSidebar(); }
|
||||
if (view === 'collaborate') this.refreshCollaborate();
|
||||
if (view === 'dashboard') this.refreshDashboard();
|
||||
});
|
||||
});
|
||||
|
||||
// Theme toggle
|
||||
this.shadow.getElementById('themeToggle')!.addEventListener('click', () => {
|
||||
this._theme = this._theme === 'dark' ? 'light' : 'dark';
|
||||
|
|
@ -1222,20 +1213,17 @@ class FolkTimebankApp extends HTMLElement {
|
|||
|
||||
private updateStats() {
|
||||
const total = this.commitments.reduce((s, c) => s + c.hours, 0);
|
||||
this.shadow.getElementById('statHours')!.textContent = String(total);
|
||||
this.shadow.getElementById('statContributors')!.textContent = String(this.commitments.length);
|
||||
const hoursEl = this.shadow.getElementById('statHours');
|
||||
const contribEl = this.shadow.getElementById('statContributors');
|
||||
if (hoursEl) hoursEl.textContent = String(total);
|
||||
if (contribEl) contribEl.textContent = String(this.commitments.length);
|
||||
const bySkill: Record<string, number> = {};
|
||||
this.commitments.forEach(c => { bySkill[c.skill] = (bySkill[c.skill] || 0) + c.hours; });
|
||||
const bar = this.shadow.getElementById('skillBar')!;
|
||||
const legend = this.shadow.getElementById('skillLegend')!;
|
||||
bar.innerHTML = ''; legend.innerHTML = '';
|
||||
const legend = this.shadow.getElementById('skillLegend');
|
||||
if (!legend) return;
|
||||
legend.innerHTML = '';
|
||||
if (total === 0) return;
|
||||
for (const [skill, hrs] of Object.entries(bySkill)) {
|
||||
const seg = document.createElement('div');
|
||||
seg.className = 'skill-bar-segment';
|
||||
seg.style.width = ((hrs as number) / total * 100) + '%';
|
||||
seg.style.background = SKILL_COLORS[skill] || '#888';
|
||||
bar.appendChild(seg);
|
||||
const item = document.createElement('div');
|
||||
item.className = 'skill-legend-item';
|
||||
item.innerHTML = '<div class="skill-legend-dot" style="background:' + (SKILL_COLORS[skill] || '#888') + '"></div>' + (SKILL_LABELS[skill] || skill) + ' ' + hrs + 'h';
|
||||
|
|
@ -3040,10 +3028,10 @@ class FolkTimebankApp extends HTMLElement {
|
|||
if (this._tour) return;
|
||||
const { TourEngine } = await import('../../../shared/tour-engine');
|
||||
this._tour = new TourEngine(this.shadow as unknown as ShadowRoot, [
|
||||
{ target: '.tab-bar', title: 'Canvas & Collaborate', message: 'The Canvas shows commitment orbs alongside the weaving SVG. Collaborate matches intents via the solver.' },
|
||||
{ target: '.pool-panel-header', title: 'Canvas & Collaborate', message: 'The Canvas shows commitment orbs alongside the weaving SVG. Collaborate matches intents via the solver.' },
|
||||
{ target: '#pool-canvas', title: 'Commitment Pool', message: 'Each floating orb represents a time commitment — sized by hours, colored by skill. Long-press to drag onto the canvas.' },
|
||||
{ target: '#addBtn', title: 'Add a Commitment', message: 'Pledge your hours with a skill category. Your commitment joins the pool for others to see.', advanceOnClick: true },
|
||||
{ target: '.stats-bar', title: 'Community Stats', message: 'See total hours available and how many contributors are in the pool at a glance.' },
|
||||
{ target: '.pool-legend', title: 'Community Stats', message: 'See total hours available and how many contributors are in the pool at a glance.' },
|
||||
], 'rtime_tour_done', () => this.shadow.querySelector('.main') as HTMLElement);
|
||||
}
|
||||
|
||||
|
|
@ -3400,12 +3388,7 @@ class FolkTimebankApp extends HTMLElement {
|
|||
this.rebuildSidebar();
|
||||
|
||||
// Switch to canvas view
|
||||
this.currentView = 'canvas';
|
||||
this.shadow.querySelectorAll('.tab').forEach(t => t.classList.toggle('active', (t as HTMLElement).dataset.view === 'canvas'));
|
||||
this.shadow.getElementById('canvas-view')!.style.display = 'flex';
|
||||
this.shadow.getElementById('collaborate-view')!.style.display = 'none';
|
||||
const dashView = this.shadow.getElementById('dashboard-view');
|
||||
if (dashView) dashView.style.display = 'none';
|
||||
this._switchView('canvas');
|
||||
}
|
||||
|
||||
private renderSkillPrices() {
|
||||
|
|
@ -3588,42 +3571,22 @@ const CSS_TEXT = `
|
|||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tab-bar {
|
||||
display: flex;
|
||||
background: #1e293b;
|
||||
border-bottom: 1px solid #334155;
|
||||
.pool-legend {
|
||||
padding: 0.5rem 0.75rem;
|
||||
background: rgba(30, 27, 75, 0.6);
|
||||
border-top: 1px solid #334155;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.tab {
|
||||
padding: 0.65rem 1.5rem;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
color: #64748b;
|
||||
cursor: pointer;
|
||||
border-bottom: 2px solid transparent;
|
||||
transition: all 0.2s;
|
||||
user-select: none;
|
||||
.pool-legend-stats {
|
||||
display: flex; align-items: center; gap: 0.4rem;
|
||||
font-size: 0.78rem; color: #94a3b8; margin-bottom: 0.3rem;
|
||||
}
|
||||
.tab:hover { color: #e2e8f0; }
|
||||
.tab.active { color: #8b5cf6; border-bottom-color: #8b5cf6; }
|
||||
|
||||
.stats-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1.5rem;
|
||||
padding: 0.6rem 1.5rem;
|
||||
background: linear-gradient(135deg, #1e1b4b 0%, #2d1b3d 100%);
|
||||
border-bottom: 1px solid #334155;
|
||||
flex-shrink: 0;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.stat { display: flex; align-items: center; gap: 0.4rem; font-size: 0.82rem; color: #94a3b8; }
|
||||
.stat-value { font-weight: 700; font-size: 1rem; color: #f1f5f9; }
|
||||
.skill-bar { display: flex; height: 6px; border-radius: 3px; overflow: hidden; flex: 1; min-width: 150px; max-width: 300px; }
|
||||
.skill-bar-segment { transition: width 0.5s ease; }
|
||||
.skill-legend { display: flex; gap: 0.75rem; flex-wrap: wrap; }
|
||||
.skill-legend-item { display: flex; align-items: center; gap: 0.3rem; font-size: 0.75rem; color: #94a3b8; }
|
||||
.skill-legend-dot { width: 8px; height: 8px; border-radius: 50%; }
|
||||
.pool-legend-stats strong { font-weight: 700; color: #f1f5f9; }
|
||||
.pool-legend-sep { color: #475569; }
|
||||
.pool-legend-skills { display: flex; gap: 0.6rem; flex-wrap: wrap; }
|
||||
.skill-legend { display: flex; gap: 0.6rem; flex-wrap: wrap; }
|
||||
.skill-legend-item { display: flex; align-items: center; gap: 0.25rem; font-size: 0.7rem; color: #94a3b8; }
|
||||
.skill-legend-dot { width: 7px; height: 7px; border-radius: 50%; }
|
||||
|
||||
.main { flex: 1; position: relative; overflow: hidden; }
|
||||
|
||||
|
|
@ -4320,11 +4283,10 @@ const CSS_TEXT = `
|
|||
|
||||
/* Light theme overrides */
|
||||
:host([data-theme="light"]) { background: #f8fafc; color: #1e293b; }
|
||||
:host([data-theme="light"]) .tab-bar { background: #fff; border-bottom-color: #e2e8f0; }
|
||||
:host([data-theme="light"]) .tab { color: #94a3b8; }
|
||||
:host([data-theme="light"]) .tab:hover { color: #1e293b; }
|
||||
:host([data-theme="light"]) .tab.active { color: #8b5cf6; }
|
||||
:host([data-theme="light"]) .stats-bar { background: linear-gradient(135deg, #f0ecff 0%, #fdf2f8 100%); border-bottom-color: #e2e8f0; }
|
||||
:host([data-theme="light"]) .pool-legend { background: rgba(240, 236, 255, 0.6); border-top-color: #e2e8f0; }
|
||||
:host([data-theme="light"]) .pool-legend-stats { color: #64748b; }
|
||||
:host([data-theme="light"]) .skill-legend-item { color: #64748b; }
|
||||
:host([data-theme="light"]) .pool-legend-stats strong { color: #1e293b; }
|
||||
:host([data-theme="light"]) .stat { color: #64748b; }
|
||||
:host([data-theme="light"]) .stat-value { color: #1e293b; }
|
||||
:host([data-theme="light"]) .pool-panel { background: #fff; border-right-color: #e2e8f0; }
|
||||
|
|
@ -4407,9 +4369,7 @@ const CSS_TEXT = `
|
|||
.exec-step-checklist input[type="checkbox"] { accent-color: #8b5cf6; }
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.tab-bar { gap: 0; }
|
||||
.tab { padding: 0.45rem 1rem; font-size: 0.82rem; }
|
||||
.stats-bar { padding: 0.35rem 0.75rem; gap: 0.75rem; }
|
||||
.pool-legend { padding: 0.35rem 0.5rem; }
|
||||
.stat { font-size: 0.75rem; }
|
||||
.stat-value { font-size: 0.85rem; }
|
||||
.skill-bar { min-width: 80px; max-width: 160px; }
|
||||
|
|
@ -4434,9 +4394,7 @@ const CSS_TEXT = `
|
|||
.task-edit-panel { width: 95vw; }
|
||||
}
|
||||
@media (max-width: 640px) {
|
||||
.tab { padding: 0.4rem 0.75rem; font-size: 0.78rem; }
|
||||
.stats-bar { padding: 0.3rem 0.5rem; gap: 0.5rem; }
|
||||
.skill-legend { display: none; }
|
||||
.pool-legend-skills { display: none; }
|
||||
.pool-panel.collapsed { width: 100% !important; }
|
||||
}
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -1108,19 +1108,49 @@ routes.post("/api/cyclos/transfers", async (c) => {
|
|||
|
||||
// ── Page routes ──
|
||||
|
||||
routes.get("/", (c) => {
|
||||
const space = c.req.param("space") || "demo";
|
||||
// ── Shell tabs for rTime ──
|
||||
const RTIME_TABS = [
|
||||
{ id: "pool", label: "Commitment Pool" },
|
||||
{ id: "fulfillment", label: "Fulfillment" },
|
||||
{ id: "cyclos", label: "Open in Cyclos" },
|
||||
] as const;
|
||||
|
||||
return c.html(renderShell({
|
||||
const RTIME_TAB_IDS = new Set(RTIME_TABS.map(t => t.id));
|
||||
|
||||
function renderTimePage(space: string, view: string, activeTab: string, isSubdomain: boolean) {
|
||||
return renderShell({
|
||||
title: `${space} — rTime | rSpace`,
|
||||
moduleId: "rtime",
|
||||
spaceSlug: space,
|
||||
modules: getModuleInfoList(),
|
||||
theme: "dark",
|
||||
body: `<folk-timebank-app space="${space}" view="pool"></folk-timebank-app>`,
|
||||
scripts: `<script type="module" src="/modules/rtime/folk-timebank-app.js?v=1"></script>`,
|
||||
body: `<folk-timebank-app space="${space}" view="${view}"></folk-timebank-app>`,
|
||||
scripts: `<script type="module" src="/modules/rtime/folk-timebank-app.js?v=2"></script>`,
|
||||
styles: `<link rel="stylesheet" href="/modules/rtime/rtime.css">`,
|
||||
}));
|
||||
tabs: [...RTIME_TABS],
|
||||
activeTab,
|
||||
tabBasePath: isSubdomain ? `/rtime` : `/${space}/rtime`,
|
||||
});
|
||||
}
|
||||
|
||||
routes.get("/pool", (c) => {
|
||||
const space = c.req.param("space") || "demo";
|
||||
return c.html(renderTimePage(space, "canvas", "pool", c.get("isSubdomain")));
|
||||
});
|
||||
|
||||
routes.get("/fulfillment", (c) => {
|
||||
const space = c.req.param("space") || "demo";
|
||||
return c.html(renderTimePage(space, "dashboard", "fulfillment", c.get("isSubdomain")));
|
||||
});
|
||||
|
||||
routes.get("/cyclos", (c) => {
|
||||
const cyclosUrl = process.env.CYCLOS_URL || "https://www.cyclos.org";
|
||||
return c.redirect(cyclosUrl, 302);
|
||||
});
|
||||
|
||||
routes.get("/", (c) => {
|
||||
const space = c.req.param("space") || "demo";
|
||||
return c.html(renderTimePage(space, "canvas", "pool", c.get("isSubdomain")));
|
||||
});
|
||||
|
||||
routes.post("/api/tasks/:id/export-to-backlog", async (c) => {
|
||||
|
|
@ -1175,49 +1205,18 @@ routes.post("/api/tasks/:id/export-to-backlog", async (c) => {
|
|||
});
|
||||
});
|
||||
|
||||
// Legacy routes — redirect to new tab paths
|
||||
routes.get("/canvas", (c) => {
|
||||
const space = c.req.param("space") || "demo";
|
||||
|
||||
return c.html(renderShell({
|
||||
title: `${space} — Canvas | rTime | rSpace`,
|
||||
moduleId: "rtime",
|
||||
spaceSlug: space,
|
||||
modules: getModuleInfoList(),
|
||||
theme: "dark",
|
||||
body: `<folk-timebank-app space="${space}" view="canvas"></folk-timebank-app>`,
|
||||
scripts: `<script type="module" src="/modules/rtime/folk-timebank-app.js?v=1"></script>`,
|
||||
styles: `<link rel="stylesheet" href="/modules/rtime/rtime.css">`,
|
||||
}));
|
||||
return c.redirect(c.get("isSubdomain") ? `/rtime/pool` : `/${space}/rtime/pool`, 301);
|
||||
});
|
||||
|
||||
routes.get("/collaborate", (c) => {
|
||||
const space = c.req.param("space") || "demo";
|
||||
|
||||
return c.html(renderShell({
|
||||
title: `${space} — Collaborate | rTime | rSpace`,
|
||||
moduleId: "rtime",
|
||||
spaceSlug: space,
|
||||
modules: getModuleInfoList(),
|
||||
theme: "dark",
|
||||
body: `<folk-timebank-app space="${space}" view="collaborate"></folk-timebank-app>`,
|
||||
scripts: `<script type="module" src="/modules/rtime/folk-timebank-app.js?v=1"></script>`,
|
||||
styles: `<link rel="stylesheet" href="/modules/rtime/rtime.css">`,
|
||||
}));
|
||||
return c.redirect(c.get("isSubdomain") ? `/rtime/pool` : `/${space}/rtime/pool`, 301);
|
||||
});
|
||||
|
||||
routes.get("/dashboard", (c) => {
|
||||
const space = c.req.param("space") || "demo";
|
||||
|
||||
return c.html(renderShell({
|
||||
title: `${space} — Fulfillment | rTime | rSpace`,
|
||||
moduleId: "rtime",
|
||||
spaceSlug: space,
|
||||
modules: getModuleInfoList(),
|
||||
theme: "dark",
|
||||
body: `<folk-timebank-app space="${space}" view="dashboard"></folk-timebank-app>`,
|
||||
scripts: `<script type="module" src="/modules/rtime/folk-timebank-app.js?v=1"></script>`,
|
||||
styles: `<link rel="stylesheet" href="/modules/rtime/rtime.css">`,
|
||||
}));
|
||||
return c.redirect(c.get("isSubdomain") ? `/rtime/fulfillment` : `/${space}/rtime/fulfillment`, 301);
|
||||
});
|
||||
|
||||
// ── Module export ──
|
||||
|
|
|
|||
|
|
@ -871,6 +871,9 @@ export function renderShell(opts: ShellOptions): string {
|
|||
};
|
||||
}
|
||||
|
||||
// Valid module IDs for this space (used to filter out stale/phantom tabs)
|
||||
const validModuleIds = new Set(moduleList.map(m => m.id));
|
||||
|
||||
// ── Restore tabs from localStorage ──
|
||||
let layers;
|
||||
try {
|
||||
|
|
@ -879,12 +882,15 @@ export function renderShell(opts: ShellOptions): string {
|
|||
if (!Array.isArray(layers)) layers = [];
|
||||
} catch(e) { layers = []; }
|
||||
|
||||
// Filter out stale tabs whose moduleId no longer exists
|
||||
layers = deduplicateLayers(layers.filter(l => validModuleIds.has(l.moduleId)));
|
||||
|
||||
// Ensure the current module is in the tab list
|
||||
if (!layers.find(l => l.moduleId === currentModuleId)) {
|
||||
layers.push(makeLayer(currentModuleId, layers.length));
|
||||
}
|
||||
|
||||
// Persist immediately (includes the newly-added tab)
|
||||
// Persist immediately (cleaned list)
|
||||
localStorage.setItem(TABS_KEY, JSON.stringify(layers));
|
||||
|
||||
// Render all tabs with the current one active
|
||||
|
|
@ -954,7 +960,7 @@ export function renderShell(opts: ShellOptions): string {
|
|||
if (!next.has(mid) && tabCache) tabCache.removePane(mid);
|
||||
}
|
||||
|
||||
layers = deduplicateLayers(remoteLayers);
|
||||
layers = deduplicateLayers(remoteLayers.filter(l => validModuleIds.has(l.moduleId)));
|
||||
|
||||
// Always ensure the current module stays in the tab list
|
||||
if (currentModuleId && !layers.find(l => l.moduleId === currentModuleId)) {
|
||||
|
|
@ -1001,7 +1007,8 @@ export function renderShell(opts: ShellOptions): string {
|
|||
// Server-authoritative: adopt server tabs exactly.
|
||||
// Only skip tabs the user closed in THIS session (prevents
|
||||
// close-then-immediate-resurrect before save propagates).
|
||||
var serverTabs = data.tabs.filter(t => !_closedModuleIds.has(t.moduleId));
|
||||
var serverTabs = data.tabs.filter(t => validModuleIds.has(t.moduleId) && !_closedModuleIds.has(t.moduleId));
|
||||
serverTabs = deduplicateLayers(serverTabs);
|
||||
serverTabs.forEach((l, i) => { l.order = i; });
|
||||
layers = serverTabs;
|
||||
// Ensure the currently navigated module is present
|
||||
|
|
@ -1711,6 +1718,7 @@ export function renderExternalAppShell(opts: ExternalAppShellOptions): string {
|
|||
|
||||
if (tabBar) {
|
||||
tabBar.setModules(moduleList);
|
||||
const validModuleIds = new Set(moduleList.map(m => m.id));
|
||||
function getModuleLabel(id) {
|
||||
const m = moduleList.find(mod => mod.id === id);
|
||||
return m ? m.name : id;
|
||||
|
|
@ -1718,13 +1726,20 @@ export function renderExternalAppShell(opts: ExternalAppShellOptions): string {
|
|||
function makeLayer(id, order) {
|
||||
return { id: 'layer-' + id, moduleId: id, label: getModuleLabel(id), order, color: '', visible: true, createdAt: Date.now() };
|
||||
}
|
||||
function deduplicateLayers(list) {
|
||||
const seen = new Set();
|
||||
return list.filter(l => { if (seen.has(l.moduleId)) return false; seen.add(l.moduleId); return true; });
|
||||
}
|
||||
let layers;
|
||||
try { const saved = localStorage.getItem(TABS_KEY); layers = saved ? JSON.parse(saved) : []; if (!Array.isArray(layers)) layers = []; } catch(e) { layers = []; }
|
||||
// Filter out stale/phantom tabs and deduplicate
|
||||
layers = deduplicateLayers(layers.filter(l => validModuleIds.has(l.moduleId)));
|
||||
if (!layers.find(l => l.moduleId === currentModuleId)) layers.push(makeLayer(currentModuleId, layers.length));
|
||||
localStorage.setItem(TABS_KEY, JSON.stringify(layers));
|
||||
tabBar.setLayers(layers);
|
||||
tabBar.setAttribute('active', 'layer-' + currentModuleId);
|
||||
if (tabBar.trackRecent) tabBar.trackRecent(currentModuleId);
|
||||
const _closedModuleIds = new Set();
|
||||
function saveTabs() {
|
||||
localStorage.setItem(TABS_KEY, JSON.stringify(layers));
|
||||
try {
|
||||
|
|
@ -1754,7 +1769,8 @@ export function renderExternalAppShell(opts: ExternalAppShellOptions): string {
|
|||
.then(function(r) { return r.ok ? r.json() : null; })
|
||||
.then(function(data) {
|
||||
if (!data?.tabs || !Array.isArray(data.tabs) || data.tabs.length === 0) { saveTabs(); return; }
|
||||
layers = data.tabs;
|
||||
// Filter stale moduleIds and tabs closed this session
|
||||
layers = deduplicateLayers(data.tabs.filter(function(t) { return validModuleIds.has(t.moduleId) && !_closedModuleIds.has(t.moduleId); }));
|
||||
layers.forEach(function(l, i) { l.order = i; });
|
||||
if (!layers.find(function(l) { return l.moduleId === currentModuleId; })) layers.push(makeLayer(currentModuleId, layers.length));
|
||||
tabBar.setLayers(layers);
|
||||
|
|
@ -1765,8 +1781,8 @@ export function renderExternalAppShell(opts: ExternalAppShellOptions): string {
|
|||
})();
|
||||
tabBar.addEventListener('layer-switch', (e) => { saveTabs(); window.location.href = window.__rspaceNavUrl(spaceSlug, e.detail.moduleId); });
|
||||
tabBar.addEventListener('layer-add', (e) => { const { moduleId } = e.detail; if (!layers.find(l => l.moduleId === moduleId)) layers.push(makeLayer(moduleId, layers.length)); saveTabs(); window.location.href = window.__rspaceNavUrl(spaceSlug, moduleId); });
|
||||
tabBar.addEventListener('layer-close', (e) => { const { layerId } = e.detail; tabBar.removeLayer(layerId); layers = layers.filter(l => l.id !== layerId); saveTabs(); if (layerId === 'layer-' + currentModuleId && layers.length > 0) window.location.href = window.__rspaceNavUrl(spaceSlug, layers[0].moduleId); });
|
||||
tabBar.addEventListener('layers-close-all', () => { layers = []; tabBar.setLayers([]); tabBar.setAttribute('active', ''); saveTabs(); var dashUrl = window.location.hostname.endsWith('.rspace.online') ? '/' : '/' + spaceSlug; window.location.href = dashUrl; });
|
||||
tabBar.addEventListener('layer-close', (e) => { const { layerId } = e.detail; const closedMid = layerId.replace('layer-', ''); _closedModuleIds.add(closedMid); tabBar.removeLayer(layerId); layers = layers.filter(l => l.id !== layerId); saveTabs(); if (layerId === 'layer-' + currentModuleId && layers.length > 0) window.location.href = window.__rspaceNavUrl(spaceSlug, layers[0].moduleId); });
|
||||
tabBar.addEventListener('layers-close-all', () => { layers.forEach(l => _closedModuleIds.add(l.moduleId)); layers = []; tabBar.setLayers([]); tabBar.setAttribute('active', ''); saveTabs(); var dashUrl = window.location.hostname.endsWith('.rspace.online') ? '/' : '/' + spaceSlug; window.location.href = dashUrl; });
|
||||
tabBar.addEventListener('layer-reorder', (e) => { const { layerId, newIndex } = e.detail; const oldIdx = layers.findIndex(l => l.id === layerId); if (oldIdx === -1 || oldIdx === newIndex) return; const [moved] = layers.splice(oldIdx, 1); layers.splice(newIndex, 0, moved); layers.forEach((l, i) => l.order = i); saveTabs(); tabBar.setLayers(layers); });
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
Loading…
Reference in New Issue