truecanvas 0.6.3 → 0.6.5
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; }
|
|
@@ -309,6 +319,7 @@ button:focus-visible { outline: 2px solid #3b82f6; outline-offset: 1px; }
|
|
|
309
319
|
* image stays underneath until the iframe has loaded.
|
|
310
320
|
*/
|
|
311
321
|
function restFrame(f, iframe, img) {
|
|
322
|
+
iframe.closest(".frame")?.classList.toggle("live-frame", showsLive(f));
|
|
312
323
|
if (showsLive(f)) {
|
|
313
324
|
iframe.setAttribute("sandbox", "allow-scripts allow-same-origin allow-forms");
|
|
314
325
|
iframe.setAttribute("referrerpolicy", "no-referrer");
|
|
@@ -329,11 +340,12 @@ button:focus-visible { outline: 2px solid #3b82f6; outline-offset: 1px; }
|
|
|
329
340
|
const el = document.createElement("div");
|
|
330
341
|
el.className = "frame";
|
|
331
342
|
el.style.cssText = "left:" + (f.x - minX) + "px;top:" + (f.y - minY) + "px;width:" + f.width + "px;height:" + f.height + "px";
|
|
332
|
-
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="
|
|
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="The real site in a screen-sized window: it scrolls like on a real screen, so scroll animations and pinned sections behave as they will for visitors"><svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7"/></svg>Full screen</button>' : "") + '<span class="interact-tag">Interacting<button type="button" class="interact-done">Done</button></span>' + "</div>" +
|
|
333
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>';
|
|
334
345
|
restFrame(f, el.querySelector("iframe"), el.querySelector("img"));
|
|
335
346
|
const liveBtn = el.querySelector(".live-btn");
|
|
336
347
|
if (liveBtn) liveBtn.onclick = (e) => { e.stopPropagation(); openLive(f); };
|
|
348
|
+
el.querySelector(".interact-done").onclick = (e) => { e.stopPropagation(); setInteracting(null); };
|
|
337
349
|
el.dataset.name = f.name;
|
|
338
350
|
world.appendChild(el);
|
|
339
351
|
labels.push(el.querySelector(".frame-label"));
|
|
@@ -386,13 +398,31 @@ button:focus-visible { outline: 2px solid #3b82f6; outline-offset: 1px; }
|
|
|
386
398
|
else { x -= e.deltaX; y -= e.deltaY; apply(); }
|
|
387
399
|
}, { passive: false });
|
|
388
400
|
|
|
401
|
+
// a real-site frame that has the mouse forwards its wheel and Esc (the review
|
|
402
|
+
// site adds a small bridge to those pages): the canvas keeps scrolling
|
|
403
|
+
addEventListener("message", (e) => {
|
|
404
|
+
const m = e.data;
|
|
405
|
+
if (!m || m.__tc !== 1) return;
|
|
406
|
+
const iframe = [...world.querySelectorAll(".frame-box iframe")].find((f) => f.contentWindow === e.source);
|
|
407
|
+
if (!iframe) return;
|
|
408
|
+
if (m.t === "escape") { setInteracting(null); return; }
|
|
409
|
+
if (m.t !== "wheel") return;
|
|
410
|
+
const unit = m.dm === 1 ? 16 : m.dm === 2 ? innerHeight : 1;
|
|
411
|
+
const dx = (+m.dx || 0) * unit, dy = (+m.dy || 0) * unit;
|
|
412
|
+
if (m.zoom) {
|
|
413
|
+
// the frame is drawn at the canvas zoom: its coordinates scale with it
|
|
414
|
+
const r = iframe.getBoundingClientRect();
|
|
415
|
+
zoomAt(z * Math.exp(-dy * 0.01), r.left + (+m.x || 0) * z, r.top + (+m.y || 0) * z);
|
|
416
|
+
} else { x -= dx; y -= dy; apply(); }
|
|
417
|
+
});
|
|
418
|
+
|
|
389
419
|
// drag to pan, two fingers to pinch
|
|
390
420
|
const pointers = new Map();
|
|
391
421
|
let pinch = null;
|
|
392
422
|
let moved = false, downAt = null;
|
|
393
423
|
stage.addEventListener("pointerdown", (e) => {
|
|
394
|
-
// pins and
|
|
395
|
-
if (e.target.closest && e.target.closest(".pin, .live-btn")) return;
|
|
424
|
+
// pins, Full screen and Done are buttons: they take their own clicks
|
|
425
|
+
if (e.target.closest && e.target.closest(".pin, .live-btn, .interact-done")) return;
|
|
396
426
|
stage.setPointerCapture(e.pointerId);
|
|
397
427
|
moved = false;
|
|
398
428
|
downAt = { x: e.clientX, y: e.clientY };
|
|
@@ -422,6 +452,16 @@ button:focus-visible { outline: 2px solid #3b82f6; outline-offset: 1px; }
|
|
|
422
452
|
stage.addEventListener("pointerup", end);
|
|
423
453
|
stage.addEventListener("pointercancel", end);
|
|
424
454
|
|
|
455
|
+
// the frame that has the mouse (its own scripts, clicks and forms), or null
|
|
456
|
+
let interacting = null;
|
|
457
|
+
function setInteracting(frameEl) {
|
|
458
|
+
if (interacting) interacting.classList.remove("interacting");
|
|
459
|
+
interacting = frameEl;
|
|
460
|
+
if (frameEl) frameEl.classList.add("interacting");
|
|
461
|
+
}
|
|
462
|
+
// the live session swaps frames: whatever had the mouse goes back to the canvas
|
|
463
|
+
addEventListener("tc:frames-swapped", () => setInteracting(null));
|
|
464
|
+
|
|
425
465
|
// double click (or double tap) a frame: read it at full width; elsewhere: everything in view
|
|
426
466
|
let lastTap = 0;
|
|
427
467
|
const zoomToFrameAt = (cx, cy) => {
|
|
@@ -440,6 +480,12 @@ button:focus-visible { outline: 2px solid #3b82f6; outline-offset: 1px; }
|
|
|
440
480
|
downAt = null;
|
|
441
481
|
// comment mode: a tap on a frame starts a comment there
|
|
442
482
|
if (commenting) return startComment(e.clientX, e.clientY);
|
|
483
|
+
// a real-site frame: one click hands it the mouse; a click elsewhere takes it back
|
|
484
|
+
// the stage captured the pointer: look under it rather than at e.target
|
|
485
|
+
const under = document.elementFromPoint(e.clientX, e.clientY);
|
|
486
|
+
const hit = under && under.closest ? under.closest(".frame.live-frame") : null;
|
|
487
|
+
if (hit && !hit.classList.contains("interacting")) { setInteracting(hit); lastTap = 0; return; }
|
|
488
|
+
if (!hit && interacting) { setInteracting(null); return; }
|
|
443
489
|
const now = Date.now();
|
|
444
490
|
if (now - lastTap < 320) { zoomToFrameAt(e.clientX, e.clientY); lastTap = 0; }
|
|
445
491
|
else lastTap = now;
|
|
@@ -452,7 +498,7 @@ button:focus-visible { outline: 2px solid #3b82f6; outline-offset: 1px; }
|
|
|
452
498
|
addEventListener("keydown", (e) => {
|
|
453
499
|
if (liveOpen()) { if (e.key === "Escape") closeLive(); return; }
|
|
454
500
|
if (e.target.closest && e.target.closest("input, textarea, select")) return;
|
|
455
|
-
if (e.key === "Escape") { closePop(); setCommenting(false); return; }
|
|
501
|
+
if (e.key === "Escape") { closePop(); setCommenting(false); setInteracting(null); return; }
|
|
456
502
|
if ((e.key === "c" || e.key === "C") && canComment && !e.metaKey && !e.ctrlKey) { setCommenting(!commenting); return; }
|
|
457
503
|
if (e.key === "+" || e.key === "=") zoomAt(z * 1.25, ...center());
|
|
458
504
|
else if (e.key === "-") zoomAt(z / 1.25, ...center());
|
|
@@ -541,6 +587,7 @@ button:focus-visible { outline: 2px solid #3b82f6; outline-offset: 1px; }
|
|
|
541
587
|
function setCommenting(on) {
|
|
542
588
|
if (!canComment) return;
|
|
543
589
|
commenting = on;
|
|
590
|
+
if (on) setInteracting(null);
|
|
544
591
|
stage.classList.toggle("commenting", on);
|
|
545
592
|
const b = document.getElementById("comment");
|
|
546
593
|
b.classList.toggle("on", on);
|
|
@@ -844,14 +891,17 @@ button:focus-visible { outline: 2px solid #3b82f6; outline-offset: 1px; }
|
|
|
844
891
|
next.setAttribute("sandbox", "allow-scripts allow-same-origin allow-forms");
|
|
845
892
|
next.setAttribute("referrerpolicy", "no-referrer");
|
|
846
893
|
next.src = sessionFrame(f);
|
|
894
|
+
frameEl.classList.add("live-frame");
|
|
847
895
|
next.addEventListener("load", () => { next.classList.add("ready"); if (img) img.style.visibility = "hidden"; });
|
|
848
896
|
} else {
|
|
849
897
|
next.loading = "lazy";
|
|
850
898
|
if (img) img.style.visibility = "";
|
|
851
|
-
restFrame(f, next, img);
|
|
852
899
|
}
|
|
853
900
|
old.replaceWith(next);
|
|
901
|
+
// in the page now: restFrame marks the frame as a real-site one or not
|
|
902
|
+
if (!on) restFrame(f, next, img);
|
|
854
903
|
}
|
|
904
|
+
dispatchEvent(new Event("tc:frames-swapped"));
|
|
855
905
|
renderRoom();
|
|
856
906
|
toast(on ? "Live: you're seeing the design as the studio works on it" : "The live session ended: back to the published version");
|
|
857
907
|
}
|
package/dist/cli.js
CHANGED
package/dist/share/index.d.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* frozen frames out at their canvas positions, with pan and zoom (drag,
|
|
4
4
|
* wheel, pinch, keys). Frames are sandboxed iframes without scripts, under a
|
|
5
5
|
* transparent layer that takes every pointer event. When the version has a
|
|
6
|
-
* live site, each frame gets a "
|
|
6
|
+
* live site, each frame gets a "Full screen" button: the real site in a
|
|
7
7
|
* device-sized window, scripts and animations included, on its own origin.
|
|
8
8
|
* Self-contained: no
|
|
9
9
|
* build step, no dependencies, so the same file works locally and on a
|
package/dist/share/index.js
CHANGED
package/package.json
CHANGED