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="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>" +
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 View live are buttons: they take their own clicks
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
@@ -22,7 +22,7 @@ import {
22
22
  responseHeaders,
23
23
  safePath,
24
24
  viewerHtml
25
- } from "./chunk-S5P2CHHZ.js";
25
+ } from "./chunk-7KJVGN4O.js";
26
26
  import {
27
27
  DEVICES,
28
28
  Workspace,
@@ -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 "View live" button: the real site in 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
@@ -6,7 +6,7 @@ import {
6
6
  responseHeaders,
7
7
  safePath,
8
8
  viewerHtml
9
- } from "../chunk-S5P2CHHZ.js";
9
+ } from "../chunk-7KJVGN4O.js";
10
10
  export {
11
11
  CHUNK,
12
12
  decodeFrame,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "truecanvas",
3
- "version": "0.6.3",
3
+ "version": "0.6.5",
4
4
  "description": "Design with your real React components: a Figma-like canvas for Next.js and Vite where layers are your .tsx and agents edit through MCP.",
5
5
  "license": "MIT",
6
6
  "type": "module",