truecanvas 0.6.3 → 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; }
@@ -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="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>" +
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"));
@@ -392,7 +404,7 @@ button:focus-visible { outline: 2px solid #3b82f6; outline-offset: 1px; }
392
404
  let moved = false, downAt = null;
393
405
  stage.addEventListener("pointerdown", (e) => {
394
406
  // pins and View live are buttons: they take their own clicks
395
- 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;
396
408
  stage.setPointerCapture(e.pointerId);
397
409
  moved = false;
398
410
  downAt = { x: e.clientX, y: e.clientY };
@@ -422,6 +434,16 @@ button:focus-visible { outline: 2px solid #3b82f6; outline-offset: 1px; }
422
434
  stage.addEventListener("pointerup", end);
423
435
  stage.addEventListener("pointercancel", end);
424
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
+
425
447
  // double click (or double tap) a frame: read it at full width; elsewhere: everything in view
426
448
  let lastTap = 0;
427
449
  const zoomToFrameAt = (cx, cy) => {
@@ -440,6 +462,12 @@ button:focus-visible { outline: 2px solid #3b82f6; outline-offset: 1px; }
440
462
  downAt = null;
441
463
  // comment mode: a tap on a frame starts a comment there
442
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; }
443
471
  const now = Date.now();
444
472
  if (now - lastTap < 320) { zoomToFrameAt(e.clientX, e.clientY); lastTap = 0; }
445
473
  else lastTap = now;
@@ -452,7 +480,7 @@ button:focus-visible { outline: 2px solid #3b82f6; outline-offset: 1px; }
452
480
  addEventListener("keydown", (e) => {
453
481
  if (liveOpen()) { if (e.key === "Escape") closeLive(); return; }
454
482
  if (e.target.closest && e.target.closest("input, textarea, select")) return;
455
- if (e.key === "Escape") { closePop(); setCommenting(false); return; }
483
+ if (e.key === "Escape") { closePop(); setCommenting(false); setInteracting(null); return; }
456
484
  if ((e.key === "c" || e.key === "C") && canComment && !e.metaKey && !e.ctrlKey) { setCommenting(!commenting); return; }
457
485
  if (e.key === "+" || e.key === "=") zoomAt(z * 1.25, ...center());
458
486
  else if (e.key === "-") zoomAt(z / 1.25, ...center());
@@ -541,6 +569,7 @@ button:focus-visible { outline: 2px solid #3b82f6; outline-offset: 1px; }
541
569
  function setCommenting(on) {
542
570
  if (!canComment) return;
543
571
  commenting = on;
572
+ if (on) setInteracting(null);
544
573
  stage.classList.toggle("commenting", on);
545
574
  const b = document.getElementById("comment");
546
575
  b.classList.toggle("on", on);
@@ -844,14 +873,17 @@ button:focus-visible { outline: 2px solid #3b82f6; outline-offset: 1px; }
844
873
  next.setAttribute("sandbox", "allow-scripts allow-same-origin allow-forms");
845
874
  next.setAttribute("referrerpolicy", "no-referrer");
846
875
  next.src = sessionFrame(f);
876
+ frameEl.classList.add("live-frame");
847
877
  next.addEventListener("load", () => { next.classList.add("ready"); if (img) img.style.visibility = "hidden"; });
848
878
  } else {
849
879
  next.loading = "lazy";
850
880
  if (img) img.style.visibility = "";
851
- restFrame(f, next, img);
852
881
  }
853
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);
854
885
  }
886
+ dispatchEvent(new Event("tc:frames-swapped"));
855
887
  renderRoom();
856
888
  toast(on ? "Live: you're seeing the design as the studio works on it" : "The live session ended: back to the published version");
857
889
  }
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-56Z2KSBF.js";
26
26
  import {
27
27
  DEVICES,
28
28
  Workspace,
@@ -6,7 +6,7 @@ import {
6
6
  responseHeaders,
7
7
  safePath,
8
8
  viewerHtml
9
- } from "../chunk-S5P2CHHZ.js";
9
+ } from "../chunk-56Z2KSBF.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.4",
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",