truecanvas 0.6.2 → 0.6.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -28,6 +28,16 @@ html, body { margin: 0; height: 100%; overflow: hidden; background: var(--bg); c
|
|
|
28
28
|
#stage.panning { cursor: grabbing; }
|
|
29
29
|
#world { position: absolute; left: 0; top: 0; transform-origin: 0 0; will-change: transform; }
|
|
30
30
|
.frame { position: absolute; }
|
|
31
|
+
/* real-site frames: one click to interact with them, like an embedded prototype */
|
|
32
|
+
.frame.live-frame .frame-box { cursor: pointer; }
|
|
33
|
+
.frame.live-frame:not(.interacting) .frame-box::after { content: "Click to interact"; position: absolute; left: 50%; top: 16px; transform: translateX(-50%); padding: 6px 12px; border-radius: 999px; background: rgba(28,27,25,.86); color: #fff; font-size: 12px; font-weight: 550; opacity: 0; transition: opacity .15s var(--ease-out); pointer-events: none; }
|
|
34
|
+
.frame.live-frame:not(.interacting) .frame-box:hover::after { opacity: 1; }
|
|
35
|
+
#stage.commenting .frame.live-frame .frame-box::after, #stage.panning .frame.live-frame .frame-box::after { opacity: 0 !important; }
|
|
36
|
+
.frame.interacting .frame-box { outline: 2px solid var(--accent); outline-offset: 2px; cursor: auto; }
|
|
37
|
+
.frame.interacting .frame-box iframe { pointer-events: auto; }
|
|
38
|
+
.interact-tag { display: none; margin-left: 8px; align-items: center; gap: 6px; color: var(--accent); font-weight: 600; }
|
|
39
|
+
.frame.interacting .interact-tag { display: inline-flex; }
|
|
40
|
+
.interact-tag button { height: 20px; padding: 0 8px; border-radius: 6px; border: 0; background: var(--accent); color: #fff; font: inherit; font-size: 11px; font-weight: 600; cursor: pointer; }
|
|
31
41
|
.frame-label { position: absolute; left: 0; bottom: 100%; padding-bottom: 6px; white-space: nowrap; color: var(--muted);
|
|
32
42
|
font-size: 12px; transform-origin: 0 100%; }
|
|
33
43
|
.frame-label b { color: var(--text); font-weight: 550; margin-right: 6px; }
|
|
@@ -294,6 +304,33 @@ button:focus-visible { outline: 2px solid #3b82f6; outline-offset: 1px; }
|
|
|
294
304
|
: '<a class="who-btn" href="/signin?next=' + encodeURIComponent(here) + '">Sign in</a>'));
|
|
295
305
|
}
|
|
296
306
|
|
|
307
|
+
// a page frame of a version with a live site shows the real site, on its own origin
|
|
308
|
+
function liveSrc(path) {
|
|
309
|
+
const home = new URL(manifest.live.url);
|
|
310
|
+
const url = new URL(path || "/", home);
|
|
311
|
+
const token = home.searchParams.get("tc_access");
|
|
312
|
+
if (token) url.searchParams.set("tc_access", token);
|
|
313
|
+
return url.href;
|
|
314
|
+
}
|
|
315
|
+
const showsLive = (f) => !!(manifest.live && f.route);
|
|
316
|
+
/**
|
|
317
|
+
* A frame outside a live session: the real site for page frames of a version
|
|
318
|
+
* that has one (animations and all), the frozen copy otherwise. The frozen
|
|
319
|
+
* image stays underneath until the iframe has loaded.
|
|
320
|
+
*/
|
|
321
|
+
function restFrame(f, iframe, img) {
|
|
322
|
+
iframe.closest(".frame")?.classList.toggle("live-frame", showsLive(f));
|
|
323
|
+
if (showsLive(f)) {
|
|
324
|
+
iframe.setAttribute("sandbox", "allow-scripts allow-same-origin allow-forms");
|
|
325
|
+
iframe.setAttribute("referrerpolicy", "no-referrer");
|
|
326
|
+
iframe.src = liveSrc(f.route);
|
|
327
|
+
} else {
|
|
328
|
+
iframe.setAttribute("sandbox", "");
|
|
329
|
+
iframe.src = f.html;
|
|
330
|
+
}
|
|
331
|
+
iframe.addEventListener("load", () => { iframe.classList.add("ready"); if (showsLive(f) && img) img.style.visibility = "hidden"; });
|
|
332
|
+
}
|
|
333
|
+
|
|
297
334
|
// layout: frames at their canvas positions
|
|
298
335
|
const frames = manifest.frames;
|
|
299
336
|
const minX = Math.min(...frames.map((f) => f.x)), minY = Math.min(...frames.map((f) => f.y));
|
|
@@ -303,12 +340,12 @@ button:focus-visible { outline: 2px solid #3b82f6; outline-offset: 1px; }
|
|
|
303
340
|
const el = document.createElement("div");
|
|
304
341
|
el.className = "frame";
|
|
305
342
|
el.style.cssText = "left:" + (f.x - minX) + "px;top:" + (f.y - minY) + "px;width:" + f.width + "px;height:" + f.height + "px";
|
|
306
|
-
el.innerHTML = '<div class="frame-label"><b>' + esc(f.name) + "</b>" + f.width + " \xD7 " + Math.round(f.height) + (manifest.live ? '<button class="live-btn" title="Open the live site at this width"><svg width="11" height="11" viewBox="0 0 24 24" fill="currentColor"><path d="M7 4.5v15l13-7.5z"/></svg>View live</button>' : "") + "</div>" +
|
|
307
|
-
'<div class="frame-box" style="width:' + f.width + "px;height:" + f.height + 'px"><img alt="" src="' + esc(f.image) + '"><iframe
|
|
308
|
-
|
|
309
|
-
iframe.addEventListener("load", () => iframe.classList.add("ready"));
|
|
343
|
+
el.innerHTML = '<div class="frame-label"><b>' + esc(f.name) + "</b>" + f.width + " \xD7 " + Math.round(f.height) + (manifest.live ? '<button class="live-btn" title="Open the live site at this width"><svg width="11" height="11" viewBox="0 0 24 24" fill="currentColor"><path d="M7 4.5v15l13-7.5z"/></svg>View live</button>' : "") + '<span class="interact-tag">Interacting<button type="button" class="interact-done">Done</button></span>' + "</div>" +
|
|
344
|
+
'<div class="frame-box" style="width:' + f.width + "px;height:" + f.height + 'px"><img alt="" src="' + esc(f.image) + '"><iframe title="' + esc(f.name) + '" scrolling="no" loading="lazy"></iframe></div>';
|
|
345
|
+
restFrame(f, el.querySelector("iframe"), el.querySelector("img"));
|
|
310
346
|
const liveBtn = el.querySelector(".live-btn");
|
|
311
347
|
if (liveBtn) liveBtn.onclick = (e) => { e.stopPropagation(); openLive(f); };
|
|
348
|
+
el.querySelector(".interact-done").onclick = (e) => { e.stopPropagation(); setInteracting(null); };
|
|
312
349
|
el.dataset.name = f.name;
|
|
313
350
|
world.appendChild(el);
|
|
314
351
|
labels.push(el.querySelector(".frame-label"));
|
|
@@ -367,7 +404,7 @@ button:focus-visible { outline: 2px solid #3b82f6; outline-offset: 1px; }
|
|
|
367
404
|
let moved = false, downAt = null;
|
|
368
405
|
stage.addEventListener("pointerdown", (e) => {
|
|
369
406
|
// pins and View live are buttons: they take their own clicks
|
|
370
|
-
if (e.target.closest && e.target.closest(".pin, .live-btn")) return;
|
|
407
|
+
if (e.target.closest && e.target.closest(".pin, .live-btn, .interact-done")) return;
|
|
371
408
|
stage.setPointerCapture(e.pointerId);
|
|
372
409
|
moved = false;
|
|
373
410
|
downAt = { x: e.clientX, y: e.clientY };
|
|
@@ -397,6 +434,16 @@ button:focus-visible { outline: 2px solid #3b82f6; outline-offset: 1px; }
|
|
|
397
434
|
stage.addEventListener("pointerup", end);
|
|
398
435
|
stage.addEventListener("pointercancel", end);
|
|
399
436
|
|
|
437
|
+
// the frame that has the mouse (its own scripts, clicks and forms), or null
|
|
438
|
+
let interacting = null;
|
|
439
|
+
function setInteracting(frameEl) {
|
|
440
|
+
if (interacting) interacting.classList.remove("interacting");
|
|
441
|
+
interacting = frameEl;
|
|
442
|
+
if (frameEl) frameEl.classList.add("interacting");
|
|
443
|
+
}
|
|
444
|
+
// the live session swaps frames: whatever had the mouse goes back to the canvas
|
|
445
|
+
addEventListener("tc:frames-swapped", () => setInteracting(null));
|
|
446
|
+
|
|
400
447
|
// double click (or double tap) a frame: read it at full width; elsewhere: everything in view
|
|
401
448
|
let lastTap = 0;
|
|
402
449
|
const zoomToFrameAt = (cx, cy) => {
|
|
@@ -415,6 +462,12 @@ button:focus-visible { outline: 2px solid #3b82f6; outline-offset: 1px; }
|
|
|
415
462
|
downAt = null;
|
|
416
463
|
// comment mode: a tap on a frame starts a comment there
|
|
417
464
|
if (commenting) return startComment(e.clientX, e.clientY);
|
|
465
|
+
// a real-site frame: one click hands it the mouse; a click elsewhere takes it back
|
|
466
|
+
// the stage captured the pointer: look under it rather than at e.target
|
|
467
|
+
const under = document.elementFromPoint(e.clientX, e.clientY);
|
|
468
|
+
const hit = under && under.closest ? under.closest(".frame.live-frame") : null;
|
|
469
|
+
if (hit && !hit.classList.contains("interacting")) { setInteracting(hit); lastTap = 0; return; }
|
|
470
|
+
if (!hit && interacting) { setInteracting(null); return; }
|
|
418
471
|
const now = Date.now();
|
|
419
472
|
if (now - lastTap < 320) { zoomToFrameAt(e.clientX, e.clientY); lastTap = 0; }
|
|
420
473
|
else lastTap = now;
|
|
@@ -427,7 +480,7 @@ button:focus-visible { outline: 2px solid #3b82f6; outline-offset: 1px; }
|
|
|
427
480
|
addEventListener("keydown", (e) => {
|
|
428
481
|
if (liveOpen()) { if (e.key === "Escape") closeLive(); return; }
|
|
429
482
|
if (e.target.closest && e.target.closest("input, textarea, select")) return;
|
|
430
|
-
if (e.key === "Escape") { closePop(); setCommenting(false); return; }
|
|
483
|
+
if (e.key === "Escape") { closePop(); setCommenting(false); setInteracting(null); return; }
|
|
431
484
|
if ((e.key === "c" || e.key === "C") && canComment && !e.metaKey && !e.ctrlKey) { setCommenting(!commenting); return; }
|
|
432
485
|
if (e.key === "+" || e.key === "=") zoomAt(z * 1.25, ...center());
|
|
433
486
|
else if (e.key === "-") zoomAt(z / 1.25, ...center());
|
|
@@ -465,7 +518,7 @@ button:focus-visible { outline: 2px solid #3b82f6; outline-offset: 1px; }
|
|
|
465
518
|
iframe.title = "Live site: " + f.name;
|
|
466
519
|
iframe.setAttribute("sandbox", "allow-scripts allow-same-origin allow-forms allow-popups allow-popups-to-escape-sandbox");
|
|
467
520
|
iframe.setAttribute("referrerpolicy", "no-referrer");
|
|
468
|
-
iframe.src = manifest.live.url;
|
|
521
|
+
iframe.src = f.route ? liveSrc(f.route) : manifest.live.url;
|
|
469
522
|
liveDevice.appendChild(iframe);
|
|
470
523
|
document.getElementById("live-name").textContent = f.name;
|
|
471
524
|
document.getElementById("live-tab").href = manifest.live.url;
|
|
@@ -516,6 +569,7 @@ button:focus-visible { outline: 2px solid #3b82f6; outline-offset: 1px; }
|
|
|
516
569
|
function setCommenting(on) {
|
|
517
570
|
if (!canComment) return;
|
|
518
571
|
commenting = on;
|
|
572
|
+
if (on) setInteracting(null);
|
|
519
573
|
stage.classList.toggle("commenting", on);
|
|
520
574
|
const b = document.getElementById("comment");
|
|
521
575
|
b.classList.toggle("on", on);
|
|
@@ -819,15 +873,17 @@ button:focus-visible { outline: 2px solid #3b82f6; outline-offset: 1px; }
|
|
|
819
873
|
next.setAttribute("sandbox", "allow-scripts allow-same-origin allow-forms");
|
|
820
874
|
next.setAttribute("referrerpolicy", "no-referrer");
|
|
821
875
|
next.src = sessionFrame(f);
|
|
876
|
+
frameEl.classList.add("live-frame");
|
|
877
|
+
next.addEventListener("load", () => { next.classList.add("ready"); if (img) img.style.visibility = "hidden"; });
|
|
822
878
|
} else {
|
|
823
|
-
next.setAttribute("sandbox", "");
|
|
824
879
|
next.loading = "lazy";
|
|
825
|
-
next.src = f.html;
|
|
826
880
|
if (img) img.style.visibility = "";
|
|
827
881
|
}
|
|
828
|
-
next.addEventListener("load", () => { next.classList.add("ready"); if (on && img) img.style.visibility = "hidden"; });
|
|
829
882
|
old.replaceWith(next);
|
|
883
|
+
// in the page now: restFrame marks the frame as a real-site one or not
|
|
884
|
+
if (!on) restFrame(f, next, img);
|
|
830
885
|
}
|
|
886
|
+
dispatchEvent(new Event("tc:frames-swapped"));
|
|
831
887
|
renderRoom();
|
|
832
888
|
toast(on ? "Live: you're seeing the design as the studio works on it" : "The live session ended: back to the published version");
|
|
833
889
|
}
|
package/dist/cli.js
CHANGED
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
responseHeaders,
|
|
23
23
|
safePath,
|
|
24
24
|
viewerHtml
|
|
25
|
-
} from "./chunk-
|
|
25
|
+
} from "./chunk-56Z2KSBF.js";
|
|
26
26
|
import {
|
|
27
27
|
DEVICES,
|
|
28
28
|
Workspace,
|
|
@@ -39,12 +39,13 @@ import {
|
|
|
39
39
|
outlineDoc,
|
|
40
40
|
readDesignTokens,
|
|
41
41
|
readTokenData,
|
|
42
|
+
routeOf,
|
|
42
43
|
shadcnAdd,
|
|
43
44
|
shadcnRegistry,
|
|
44
45
|
shadcnStatus,
|
|
45
46
|
syncComponentRegistry,
|
|
46
47
|
writeCompare
|
|
47
|
-
} from "./chunk-
|
|
48
|
+
} from "./chunk-PLJRUWCS.js";
|
|
48
49
|
import {
|
|
49
50
|
EditError,
|
|
50
51
|
addArgs,
|
|
@@ -144,7 +145,8 @@ async function createSnapshot(ws, shots, canvas, opts = {}) {
|
|
|
144
145
|
used.add(slug);
|
|
145
146
|
fs.writeFileSync(path.join(dir, "frames", `${slug}.html`), await freeze(raw, assets));
|
|
146
147
|
fs.writeFileSync(path.join(dir, "frames", `${slug}.png`), raw.png);
|
|
147
|
-
|
|
148
|
+
const route = f.page && ws.config.framework === "next" ? routeOf(ws.config, f.page) : void 0;
|
|
149
|
+
out.push({ name: f.frameName, x: f.x, y: f.y, width: raw.width, height: f.height ?? raw.height, theme: f.theme, device: f.device, html: `frames/${slug}.html`, image: `frames/${slug}.png`, ...route ? { route } : {} });
|
|
148
150
|
}
|
|
149
151
|
const manifest = {
|
|
150
152
|
format: 1,
|
package/dist/core/index.js
CHANGED
package/dist/share/index.d.ts
CHANGED
|
@@ -22,6 +22,8 @@ interface ShareFrame {
|
|
|
22
22
|
/** paths inside the snapshot */
|
|
23
23
|
html: string;
|
|
24
24
|
image: string;
|
|
25
|
+
/** a frame linked to a page: its URL in the app (e.g. "/pricing"), so the live site can show it */
|
|
26
|
+
route?: string;
|
|
25
27
|
}
|
|
26
28
|
interface ShareManifest {
|
|
27
29
|
format: 1;
|
package/dist/share/index.js
CHANGED
package/package.json
CHANGED