Merge branch 'dev'

This commit is contained in:
Jeff Emmett 2026-03-04 11:12:29 -08:00
commit 8fcbeab93c
4 changed files with 64 additions and 16 deletions

View File

@ -11,11 +11,25 @@ import { resolve } from "node:path";
import { renderShell } from "../../server/shell";
import { getModuleInfoList } from "../../shared/module";
import type { RSpaceModule } from "../../shared/module";
import { loadCommunity, getDocumentData } from "../../server/community-store";
const DIST_DIR = resolve(import.meta.dir, "../../dist");
const routes = new Hono();
// GET /api/meta — space metadata (owner, members) for space-settings fallback
routes.get("/api/meta", async (c) => {
const space = c.req.param("space") || "demo";
try {
await loadCommunity(space);
const doc = getDocumentData(space);
if (!doc) return c.json({ meta: {} });
return c.json({ meta: { ownerDID: doc.meta?.ownerDID, members: doc.members } });
} catch {
return c.json({ meta: {} });
}
});
/**
* Extract body content and scripts from the full canvas.html page.
* Strips the shell chrome (header, tab-bar, welcome overlay) that renderShell provides,

View File

@ -60,6 +60,10 @@ export class RStackNotificationBell extends HTMLElement {
this.#open = false;
this.#render();
this.#fetchCount();
// Restart poll timer if it was stopped by a 401
if (!this.#pollTimer) {
this.#pollTimer = setInterval(() => this.#fetchCount(), POLL_INTERVAL);
}
};
#onWsNotification = (e: CustomEvent) => {
@ -96,6 +100,11 @@ export class RStackNotificationBell extends HTMLElement {
const data = await res.json();
this.#unreadCount = data.unreadCount || 0;
this.#render();
} else if (res.status === 401) {
// Token rejected — stop polling until auth changes
this.#unreadCount = 0;
if (this.#pollTimer) { clearInterval(this.#pollTimer); this.#pollTimer = null; }
this.#render();
}
} catch {
// Silently fail

View File

@ -91,8 +91,12 @@ export class RStackSpaceSettings extends HTMLElement {
}
} else {
// Fallback: fetch from API
// On subdomain routing (jeff.rspace.online), omit space prefix to avoid double-prefix
const host = window.location.hostname;
const isSubdomain = host.split(".").length >= 3 && !host.startsWith("www.");
const metaUrl = isSubdomain ? "/rspace/api/meta" : `/${this._space}/rspace/api/meta`;
try {
const res = await fetch(`/${this._space}/rspace/api/meta`, {
const res = await fetch(metaUrl, {
headers: token ? { "Authorization": `Bearer ${token}` } : {},
});
if (res.ok) {

View File

@ -110,6 +110,8 @@ export class RStackTabBar extends HTMLElement {
#wiringSourceFeedId = "";
#wiringSourceKind: FlowKind | null = null;
#escHandler: ((e: KeyboardEvent) => void) | null = null;
// Cleanup for document-level listeners (prevent leak on re-render)
#docCleanup: (() => void) | null = null;
// Recent apps: moduleId → last-used timestamp
#recentApps: Map<string, number> = new Map();
@ -789,6 +791,9 @@ export class RStackTabBar extends HTMLElement {
// ── Events ──
#attachEvents() {
// Clean up previous document-level listeners to prevent leak
if (this.#docCleanup) { this.#docCleanup(); this.#docCleanup = null; }
// Tab clicks
this.#shadow.querySelectorAll<HTMLElement>(".tab").forEach(tab => {
tab.addEventListener("click", (e) => {
@ -945,20 +950,6 @@ export class RStackTabBar extends HTMLElement {
this.#flowDragSource = layerId;
plane.classList.add("flow-drag-source");
});
plane.addEventListener("mouseenter", () => {
if (this.#flowDragSource && this.#flowDragSource !== layerId) {
this.#flowDragTarget = layerId;
plane.classList.add("flow-drag-target");
}
});
plane.addEventListener("mouseleave", () => {
if (this.#flowDragTarget === layerId) {
this.#flowDragTarget = null;
plane.classList.remove("flow-drag-target");
}
});
});
// 3D scene: orbit controls (drag on empty space to rotate)
@ -974,6 +965,9 @@ export class RStackTabBar extends HTMLElement {
sceneContainer.style.cursor = "grabbing";
});
// Collect all layer planes + their rects for drag target detection
const layerPlanes = [...this.#shadow.querySelectorAll<HTMLElement>(".layer-plane")];
const onMouseMove = (e: MouseEvent) => {
if (this.#orbitDragging) {
const dx = e.clientX - this.#orbitLastX;
@ -985,6 +979,29 @@ export class RStackTabBar extends HTMLElement {
const scene = this.#shadow.getElementById("stack-scene");
if (scene) scene.style.transform = `rotateX(${this.#sceneRotX}deg) rotateZ(${this.#sceneRotZ}deg)`;
}
// Drag-to-connect: track target via bounding rects (robust for 3D)
if (this.#flowDragSource) {
let newTarget: string | null = null;
for (const p of layerPlanes) {
const r = p.getBoundingClientRect();
if (e.clientX >= r.left && e.clientX <= r.right &&
e.clientY >= r.top && e.clientY <= r.bottom &&
p.dataset.layerId !== this.#flowDragSource) {
newTarget = p.dataset.layerId!;
break;
}
}
if (newTarget !== this.#flowDragTarget) {
this.#shadow.querySelectorAll(".flow-drag-target")
.forEach(el => el.classList.remove("flow-drag-target"));
this.#flowDragTarget = newTarget;
if (newTarget) {
this.#shadow.querySelector(`.layer-plane[data-layer-id="${newTarget}"]`)
?.classList.add("flow-drag-target");
}
}
}
};
const onMouseUp = () => {
@ -1003,9 +1020,13 @@ export class RStackTabBar extends HTMLElement {
});
};
// Attach to document for drag continuity
// Attach to document for drag continuity (cleaned up on re-render)
document.addEventListener("mousemove", onMouseMove);
document.addEventListener("mouseup", onMouseUp);
this.#docCleanup = () => {
document.removeEventListener("mousemove", onMouseMove);
document.removeEventListener("mouseup", onMouseUp);
};
// Scroll to zoom
sceneContainer.addEventListener("wheel", (e) => {