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 sandbox title="' + esc(f.name) + '" scrolling="no" loading="lazy" src="' + esc(f.html) + '"></iframe></div>';
308
- const iframe = el.querySelector("iframe");
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
  }
@@ -3180,6 +3180,7 @@ function addVitePlugin(src) {
3180
3180
  export {
3181
3181
  listRoutes,
3182
3182
  layoutFiles,
3183
+ routeOf,
3183
3184
  componentsDir,
3184
3185
  DEVICES,
3185
3186
  findDevice,
package/dist/cli.js CHANGED
@@ -22,7 +22,7 @@ import {
22
22
  responseHeaders,
23
23
  safePath,
24
24
  viewerHtml
25
- } from "./chunk-OPKROZI2.js";
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-QJ5M3G4E.js";
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
- 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` });
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,
@@ -20,7 +20,7 @@ import {
20
20
  viteStylesheets,
21
21
  wrapConfigExport,
22
22
  writeCompare
23
- } from "../chunk-QJ5M3G4E.js";
23
+ } from "../chunk-PLJRUWCS.js";
24
24
  import {
25
25
  CanvasEditor,
26
26
  EditError,
@@ -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;
@@ -6,7 +6,7 @@ import {
6
6
  responseHeaders,
7
7
  safePath,
8
8
  viewerHtml
9
- } from "../chunk-OPKROZI2.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.2",
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",