truecanvas 0.6.5 → 0.6.7
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.
- package/README.md +2 -2
- package/dist/{chunk-7KJVGN4O.js → chunk-O6YS366Z.js} +52 -6
- package/dist/{chunk-FJO64PGM.js → chunk-S5PJFVUC.js} +14 -3
- package/dist/cli.js +396 -174
- package/dist/editor/assets/{Menu-ikkNcA3s.css → Menu-CjfwzDIj.css} +1 -1
- package/dist/editor/assets/hub-DT4g2GNr.js +2 -0
- package/dist/editor/assets/main-7P76Qbyg.js +27 -0
- package/dist/editor/hub.html +3 -3
- package/dist/editor/index.html +3 -3
- package/dist/hub.d.ts +1 -0
- package/dist/hub.js +1 -1
- package/dist/share/index.js +1 -1
- package/package.json +1 -1
- package/dist/editor/assets/hub-C_YkICvy.js +0 -2
- package/dist/editor/assets/main-Dx3BpXRE.js +0 -27
- /package/dist/editor/assets/{Menu-TTrH9CSM.js → Menu-D7nLRfM-.js} +0 -0
package/README.md
CHANGED
|
@@ -116,9 +116,9 @@ The **desktop app** (Linux AppImage and deb, from the [releases](https://github.
|
|
|
116
116
|
|
|
117
117
|
**Share** (top right in the editor), `npx truecanvas share <canvas>`, or your agent's `share_canvas` renders every frame through your app and freezes it into static HTML and CSS: no scripts, so a link can never call your API or leak a session. It's published to your own review site as a new version of that page's link. Clients open it in any browser, with an optional password, and pin comments on the frames. Their threads sync into the canvas's comments while Truecanvas runs; your replies and your agent's (`reply_comment`, `resolve_comment`) go back to the link, under your studio's name. Comments you start in Truecanvas stay internal. `--revoke` takes a link down and `--delete` removes it with its comments.
|
|
118
118
|
|
|
119
|
-
|
|
119
|
+
On a review site that hosts live sites (the Cloudflare one), each share also includes **the real site**: Truecanvas builds your app as static pages (a copy, so your dev server keeps running), and page frames show the actual page, scrollable and clickable, animations included. Clients click a page to interact with it, and **Full screen** opens it at a real screen size. Sharing again only uploads the files that changed. Pages that need a server (API routes, middleware) can't be built this way: the share then keeps frozen frames and says why. `--frozen` leaves the real site out; `--live <dir|url>` uses another build folder or the URL where the app runs.
|
|
120
120
|
|
|
121
|
-
On the Cloudflare review site, **
|
|
121
|
+
On the Cloudflare review site, **Live session** in the Share dialog, `npx truecanvas share <canvas> --session` or your agent's `start_session` opens a **live session**: everyone with access to the link sees the canvas live from your app, updating as you edit, with each other's cursors and comments arriving as they're written. Stop it and the link shows the latest published version again.
|
|
122
122
|
|
|
123
123
|
On the Cloudflare review site with sign-in, `--invite client@company.com` emails your client an invitation: they sign in with a link, no password, and comment under their name. New links are for invited people and your studio; `--access password|public` changes that, and `--link-only` invites or changes access without a new version.
|
|
124
124
|
|
|
@@ -39,8 +39,11 @@ html, body { margin: 0; height: 100%; overflow: hidden; background: var(--bg); c
|
|
|
39
39
|
.frame.interacting .interact-tag { display: inline-flex; }
|
|
40
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; }
|
|
41
41
|
.frame-label { position: absolute; left: 0; bottom: 100%; padding-bottom: 6px; white-space: nowrap; color: var(--muted);
|
|
42
|
-
font-size: 12px; transform-origin: 0 100%; }
|
|
43
|
-
|
|
42
|
+
font-size: 12px; transform-origin: 0 100%; display: flex; align-items: center; overflow: hidden; }
|
|
43
|
+
/* within its frame's width: the name gives way last, the size first */
|
|
44
|
+
.frame-label b { color: var(--text); font-weight: 550; margin-right: 6px; flex: 0 1 auto; min-width: 24px; overflow: hidden; text-overflow: ellipsis; }
|
|
45
|
+
.frame-label .fsize { flex: 0 1000 auto; min-width: 0; overflow: hidden; }
|
|
46
|
+
.frame-label .live-btn, .frame-label .interact-tag { flex: none; }
|
|
44
47
|
.frame-box { position: relative; overflow: hidden; border-radius: 2px; background: #fff; box-shadow: var(--frame-shadow); }
|
|
45
48
|
.frame-box img, .frame-box iframe { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; display: block; }
|
|
46
49
|
.frame-box iframe { opacity: 0; transition: opacity .25s var(--ease-out); pointer-events: none; }
|
|
@@ -52,7 +55,7 @@ header { position: fixed; top: 12px; left: 12px; right: 12px; display: flex; ali
|
|
|
52
55
|
.brand span { font-weight: 600; letter-spacing: -.01em; }
|
|
53
56
|
.brand .who { font-weight: 450; color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 180px; }
|
|
54
57
|
.brand form { display: contents; }
|
|
55
|
-
.brand .who-btn, .brand a.who-btn { height: 28px; padding: 0 8px; border-radius: 8px; font-size: 13px; color: var(--text); text-decoration: none; display: inline-flex; align-items: center; }
|
|
58
|
+
.brand .who-btn, .brand a.who-btn { height: 28px; padding: 0 8px; border-radius: 8px; font-size: 13px; color: var(--text); text-decoration: none; display: inline-flex; align-items: center; white-space: nowrap; }
|
|
56
59
|
.brand .who-btn:hover { background: var(--line); }
|
|
57
60
|
.title { font-weight: 550; }
|
|
58
61
|
.meta { color: var(--muted); }
|
|
@@ -76,7 +79,8 @@ button:hover { background: var(--line); }
|
|
|
76
79
|
button:active { transform: scale(.96); }
|
|
77
80
|
button:focus-visible { outline: 2px solid #3b82f6; outline-offset: 1px; }
|
|
78
81
|
#zoom { min-width: 56px; }
|
|
79
|
-
.made { position: fixed; right:
|
|
82
|
+
.made { position: fixed; right: 12px; bottom: 14px; height: 26px; display: inline-flex; align-items: center; padding: 0 10px; border-radius: 8px;
|
|
83
|
+
background: var(--panel); backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px); border: 1px solid var(--line); color: var(--muted); font-size: 11.5px; text-decoration: none; }
|
|
80
84
|
.made:hover { color: var(--text); }
|
|
81
85
|
#empty { position: fixed; inset: 0; display: grid; place-items: center; color: var(--muted); }
|
|
82
86
|
/* comments */
|
|
@@ -141,6 +145,16 @@ button:focus-visible { outline: 2px solid #3b82f6; outline-offset: 1px; }
|
|
|
141
145
|
#pop { left: 8px !important; right: 8px; top: auto !important; bottom: 8px; width: auto; max-height: 60dvh; }
|
|
142
146
|
#panel { left: 8px; right: 8px; top: 56px; bottom: 68px; width: auto; }
|
|
143
147
|
}
|
|
148
|
+
/* first visit: how to review, once */
|
|
149
|
+
#welcome { position: fixed; z-index: 25; left: 50%; bottom: 66px; transform: translateX(-50%); width: min(360px, calc(100vw - 16px)); padding: 14px 16px 12px;
|
|
150
|
+
background: var(--panel-solid); border: 1px solid var(--line); border-radius: 14px; box-shadow: 0 18px 50px -12px rgba(0,0,0,.35); animation: pop-in .2s var(--ease-out); }
|
|
151
|
+
#welcome b { display: block; font-weight: 600; margin-bottom: 8px; }
|
|
152
|
+
#welcome ul { margin: 0; padding: 0; list-style: none; display: grid; gap: 7px; color: var(--muted); }
|
|
153
|
+
#welcome li { display: flex; gap: 9px; align-items: baseline; }
|
|
154
|
+
#welcome li::before { content: ""; flex: none; width: 5px; height: 5px; border-radius: 50%; background: var(--accent); transform: translateY(-2px); }
|
|
155
|
+
#welcome strong { font-weight: 600; color: var(--text); }
|
|
156
|
+
#welcome kbd { font: inherit; font-size: 11px; padding: 0 5px; border-radius: 5px; border: 1px solid var(--line); background: var(--field); color: var(--text); }
|
|
157
|
+
#welcome .welcome-actions { display: flex; justify-content: flex-end; margin-top: 10px; }
|
|
144
158
|
/* live site */
|
|
145
159
|
.live-btn { height: 22px; min-width: 0; margin-left: 8px; padding: 0 8px 0 6px; gap: 4px; border-radius: 6px; vertical-align: middle;
|
|
146
160
|
background: var(--accent-soft); color: var(--accent); font-size: 11.5px; font-weight: 600; }
|
|
@@ -340,7 +354,7 @@ button:focus-visible { outline: 2px solid #3b82f6; outline-offset: 1px; }
|
|
|
340
354
|
const el = document.createElement("div");
|
|
341
355
|
el.className = "frame";
|
|
342
356
|
el.style.cssText = "left:" + (f.x - minX) + "px;top:" + (f.y - minY) + "px;width:" + f.width + "px;height:" + f.height + "px";
|
|
343
|
-
el.innerHTML = '<div class="frame-label"><b>' + esc(f.name) +
|
|
357
|
+
el.innerHTML = '<div class="frame-label" data-w="' + f.width + '"><b title="' + esc(f.name) + '">' + esc(f.name) + '</b><span class="fsize">' + f.width + " \xD7 " + Math.round(f.height) + "</span>" + (showsLive(f) ? '<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>" +
|
|
344
358
|
'<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
359
|
restFrame(f, el.querySelector("iframe"), el.querySelector("img"));
|
|
346
360
|
const liveBtn = el.querySelector(".live-btn");
|
|
@@ -359,7 +373,10 @@ button:focus-visible { outline: 2px solid #3b82f6; outline-offset: 1px; }
|
|
|
359
373
|
world.style.transform = "translate(" + x + "px," + y + "px) scale(" + z + ")";
|
|
360
374
|
zoomLabel.textContent = Math.round(z * 100) + "%";
|
|
361
375
|
// labels keep a readable size at any zoom
|
|
362
|
-
for (const l of labels)
|
|
376
|
+
for (const l of labels) {
|
|
377
|
+
l.style.transform = "scale(" + 1 / z + ")";
|
|
378
|
+
l.style.maxWidth = Math.max(40, l.dataset.w * z) + "px";
|
|
379
|
+
}
|
|
363
380
|
for (const p of world.querySelectorAll(".pin, .rcursor")) p.style.transform = "scale(" + 1 / z + ")";
|
|
364
381
|
if (typeof placePop === "function") placePop();
|
|
365
382
|
};
|
|
@@ -806,6 +823,35 @@ button:focus-visible { outline: 2px solid #3b82f6; outline-offset: 1px; }
|
|
|
806
823
|
} catch {}
|
|
807
824
|
}
|
|
808
825
|
|
|
826
|
+
welcome();
|
|
827
|
+
/** The first time someone opens a review link: how it works, in three lines. */
|
|
828
|
+
function welcome() {
|
|
829
|
+
try {
|
|
830
|
+
if (localStorage.getItem("tc-review-welcomed")) return;
|
|
831
|
+
} catch {
|
|
832
|
+
return;
|
|
833
|
+
}
|
|
834
|
+
const touch = matchMedia("(pointer: coarse)").matches;
|
|
835
|
+
const tips = [
|
|
836
|
+
touch ? "Drag to move around, pinch to zoom." : "Scroll or drag to move around. Pinch or <kbd>Ctrl</kbd> + scroll to zoom.",
|
|
837
|
+
manifest.frames.some(showsLive) ? "Pages are the real site: " + (touch ? "tap one" : "click one") + " to scroll and try it." : "",
|
|
838
|
+
canComment ? "Press <strong>Comment</strong>" + (touch ? "" : " (or <kbd>C</kbd>)") + ", then " + (touch ? "tap" : "click") + " the design to leave a note." : "",
|
|
839
|
+
].filter(Boolean);
|
|
840
|
+
const card = document.createElement("div");
|
|
841
|
+
card.id = "welcome";
|
|
842
|
+
card.setAttribute("role", "dialog");
|
|
843
|
+
card.setAttribute("aria-label", "How to review");
|
|
844
|
+
card.innerHTML = "<b>How to review</b><ul>" + tips.map((t) => "<li><span>" + t + "</span></li>").join("") + '</ul><div class="welcome-actions"><button class="btn-primary">Got it</button></div>';
|
|
845
|
+
const done = () => {
|
|
846
|
+
card.remove();
|
|
847
|
+
try { localStorage.setItem("tc-review-welcomed", "1"); } catch {}
|
|
848
|
+
};
|
|
849
|
+
card.querySelector("button").onclick = done;
|
|
850
|
+
// the first comment or interaction counts as having read it
|
|
851
|
+
stage.addEventListener("pointerdown", () => setTimeout(done, 1200), { once: true });
|
|
852
|
+
document.body.appendChild(card);
|
|
853
|
+
}
|
|
854
|
+
|
|
809
855
|
if (canComment) {
|
|
810
856
|
for (const id of ["comment", "comment-sep", "list"]) document.getElementById(id).hidden = false;
|
|
811
857
|
document.getElementById("comment").onclick = () => setCommenting(!commenting);
|
|
@@ -210,16 +210,27 @@ function projectName(dir) {
|
|
|
210
210
|
}
|
|
211
211
|
function inspect(dir) {
|
|
212
212
|
const pkg = readPkg(dir);
|
|
213
|
-
if (!pkg) return { next: false, ready: false, canvases: 0, exists: fs3.existsSync(dir) };
|
|
213
|
+
if (!pkg) return { next: false, ready: false, canvases: 0, clientComments: 0, exists: fs3.existsSync(dir) };
|
|
214
214
|
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
215
215
|
let canvases = 0;
|
|
216
|
+
let clientComments = 0;
|
|
216
217
|
for (const d of ["canvas", "src/canvas"]) {
|
|
218
|
+
let files;
|
|
217
219
|
try {
|
|
218
|
-
|
|
220
|
+
files = fs3.readdirSync(path3.join(dir, d));
|
|
219
221
|
} catch {
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
canvases += files.filter((f) => f.endsWith(".canvas.tsx")).length;
|
|
225
|
+
for (const f of files.filter((f2) => f2.endsWith(".comments.json"))) {
|
|
226
|
+
try {
|
|
227
|
+
const { threads } = JSON.parse(fs3.readFileSync(path3.join(dir, d, f), "utf8"));
|
|
228
|
+
clientComments += (threads ?? []).filter((t) => !t.resolved && t.messages?.[0]?.author?.kind === "client").length;
|
|
229
|
+
} catch {
|
|
230
|
+
}
|
|
220
231
|
}
|
|
221
232
|
}
|
|
222
|
-
return { next: "next" in deps || "vite" in deps && "react" in deps, ready: "truecanvas" in deps, canvases, exists: true };
|
|
233
|
+
return { next: "next" in deps || "vite" in deps && "react" in deps, ready: "truecanvas" in deps, canvases, clientComments, exists: true };
|
|
223
234
|
}
|
|
224
235
|
function discover() {
|
|
225
236
|
const home = os3.homedir();
|