Merge branch 'dev'

This commit is contained in:
Jeff Emmett 2026-03-02 23:41:57 -08:00
commit 94eb6ed5bc
6 changed files with 52 additions and 22 deletions

View File

@ -383,11 +383,19 @@ export class FolkShape extends FolkElement {
this.#updateCursors();
this.x = Number(this.getAttribute("x")) || 0;
this.y = Number(this.getAttribute("y")) || 0;
this.width = Number(this.getAttribute("width")) || "auto";
this.height = Number(this.getAttribute("height")) || "auto";
this.rotation = (Number(this.getAttribute("rotation")) || 0) * (Math.PI / 180);
// Only initialize from HTML attributes if they exist — when properties
// are set via JS before DOM insertion, attributes are absent and reading
// them would overwrite the already-set values with 0/"auto".
const attrX = this.getAttribute("x");
const attrY = this.getAttribute("y");
const attrW = this.getAttribute("width");
const attrH = this.getAttribute("height");
const attrR = this.getAttribute("rotation");
if (attrX !== null) this.x = Number(attrX) || 0;
if (attrY !== null) this.y = Number(attrY) || 0;
if (attrW !== null) this.width = Number(attrW) || "auto";
if (attrH !== null) this.height = Number(attrH) || "auto";
if (attrR !== null) this.rotation = (Number(attrR) || 0) * (Math.PI / 180);
this.#rect.transformOrigin = { x: 0, y: 0 };
this.#rect.rotateOrigin = { x: 0.5, y: 0.5 };

View File

@ -292,11 +292,12 @@ export class FolkWrapper extends FolkShape {
<div class="tags"></div>
`;
// Replace existing content structure
// Replace existing content structure — save parent ref before clearing
const existingSlot = root.querySelector("slot");
if (existingSlot?.parentElement) {
existingSlot.parentElement.innerHTML = "";
existingSlot.parentElement.appendChild(wrapper);
const slotParent = existingSlot?.parentElement;
if (slotParent) {
slotParent.innerHTML = "";
slotParent.appendChild(wrapper);
}
// Get references

View File

@ -533,7 +533,7 @@ app.post("/api/image-gen", async (c) => {
};
const styledPrompt = (stylePrompts[style] || "") + prompt;
const res = await fetch("https://queue.fal.run/fal-ai/flux-pro/v1.1", {
const res = await fetch("https://fal.run/fal-ai/flux-pro/v1.1", {
method: "POST",
headers: {
Authorization: `Key ${FAL_KEY}`,
@ -567,7 +567,7 @@ app.post("/api/video-gen/t2v", async (c) => {
const { prompt, duration } = await c.req.json();
if (!prompt) return c.json({ error: "prompt required" }, 400);
const res = await fetch("https://queue.fal.run/fal-ai/wan/v2.1", {
const res = await fetch("https://fal.run/fal-ai/wan/v2.1", {
method: "POST",
headers: {
Authorization: `Key ${FAL_KEY}`,
@ -600,7 +600,7 @@ app.post("/api/video-gen/i2v", async (c) => {
const { image, prompt, duration } = await c.req.json();
if (!image) return c.json({ error: "image required" }, 400);
const res = await fetch("https://queue.fal.run/fal-ai/kling-video/v1/standard/image-to-video", {
const res = await fetch("https://fal.run/fal-ai/kling-video/v1/standard/image-to-video", {
method: "POST",
headers: {
Authorization: `Key ${FAL_KEY}`,

View File

@ -135,7 +135,7 @@ export class TabCache {
this.panes.set(moduleId, pane);
// Load module-specific assets
this.loadAssets(content.scripts, content.styles);
this.loadAssets(content.scripts, content.styles, content.inlineStyles, moduleId);
// Update shell state
this.currentModuleId = moduleId;
@ -159,6 +159,7 @@ export class TabCache {
title: string;
scripts: string[];
styles: string[];
inlineStyles: string[];
} | null {
const parser = new DOMParser();
const doc = parser.parseFromString(html, "text/html");
@ -197,11 +198,20 @@ export class TabCache {
styles.push(href);
});
return { body, title, scripts, styles };
// Extract inline <style> blocks (exclude shell-embedded styles)
const inlineStyles: string[] = [];
doc.querySelectorAll("head style").forEach((s) => {
const css = s.textContent || "";
// Skip the shell's own embedded styles (tab-pane, spinner, etc.)
if (css.includes(".rspace-tab-pane")) return;
if (css.trim()) inlineStyles.push(css);
});
return { body, title, scripts, styles, inlineStyles };
}
/** Load script and style assets (idempotent — browsers deduplicate module scripts) */
private loadAssets(scripts: string[], styles: string[]): void {
private loadAssets(scripts: string[], styles: string[], inlineStyles: string[] = [], moduleId?: string): void {
for (const src of scripts) {
const el = document.createElement("script");
el.type = "module";
@ -217,6 +227,16 @@ export class TabCache {
el.href = href;
document.head.appendChild(el);
}
// Inject inline <style> blocks (tagged for deduplication)
for (let i = 0; i < inlineStyles.length; i++) {
const tag = `tab-style-${moduleId || "unknown"}-${i}`;
if (document.querySelector(`style[data-tab-style="${tag}"]`)) continue;
const el = document.createElement("style");
el.dataset.tabStyle = tag;
el.textContent = inlineStyles[i];
document.head.appendChild(el);
}
}
/** Show a specific pane and update shell state */

View File

@ -3655,6 +3655,8 @@
// Find and remove the nearest SVG element under cursor
const hit = document.elementFromPoint(e.clientX, e.clientY);
if (hit && hit !== wbOverlay && wbOverlay.contains(hit)) {
const wbId = hit.getAttribute("data-wb-id");
if (wbId) sync.hardDeleteShape(wbId);
hit.remove();
}
}
@ -3766,15 +3768,14 @@
}
});
// Eraser: click on existing SVG strokes to delete them + remove from Automerge
// Eraser click fallback — deletion is handled in pointerdown above,
// but catch any clicks that slip through (e.g. keyboard-triggered)
wbOverlay.addEventListener("click", (e) => {
if (wbTool !== "eraser") return;
const hit = e.target;
if (hit && hit !== wbOverlay && wbOverlay.contains(hit)) {
const wbId = hit.getAttribute("data-wb-id");
if (wbId) {
sync.hardDeleteShape(wbId);
}
if (wbId) sync.hardDeleteShape(wbId);
hit.remove();
}
});

View File

@ -40,12 +40,12 @@ self.addEventListener("fetch", (event) => {
// Skip non-http(s) schemes (chrome-extension://, etc.) — they can't be cached
if (!url.protocol.startsWith("http")) return;
// Skip WebSocket and API requests entirely
// Skip WebSocket and API requests entirely (including module APIs like /space/module/api/...)
if (
event.request.url.startsWith("ws://") ||
event.request.url.startsWith("wss://") ||
url.pathname.startsWith("/ws/") ||
url.pathname.startsWith("/api/")
url.pathname.includes("/api/")
) {
return;
}
@ -101,7 +101,7 @@ self.addEventListener("fetch", (event) => {
}
return response;
})
.catch(() => cached as Response);
.catch(() => cached || new Response("Offline", { status: 503 }));
return cached || fetchPromise;
})
);