truecanvas 0.1.0
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/LICENSE +21 -0
- package/README.md +265 -0
- package/dist/catalog-worker.js +7 -0
- package/dist/chunk-753N4QRO.js +317 -0
- package/dist/chunk-J6CJBZO6.js +23 -0
- package/dist/chunk-WB35FXGP.js +3366 -0
- package/dist/cli.js +2931 -0
- package/dist/core/index.d.ts +820 -0
- package/dist/core/index.js +71 -0
- package/dist/editor/assets/controls-BQYSgX53.css +1 -0
- package/dist/editor/assets/controls-BYFjuGCq.js +9 -0
- package/dist/editor/assets/hub-C4_700Hv.js +2 -0
- package/dist/editor/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
- package/dist/editor/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
- package/dist/editor/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
- package/dist/editor/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
- package/dist/editor/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
- package/dist/editor/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
- package/dist/editor/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
- package/dist/editor/assets/main-CfAJbDf1.js +25 -0
- package/dist/editor/assets/react-DB-4Zxce.js +1 -0
- package/dist/editor/favicon.svg +3 -0
- package/dist/editor/hub.html +16 -0
- package/dist/editor/index.html +16 -0
- package/dist/next/index.d.ts +11 -0
- package/dist/next/index.js +83 -0
- package/dist/next/loader.cjs +16210 -0
- package/dist/runtime/host.d.ts +23 -0
- package/dist/runtime/host.js +412 -0
- package/dist/runtime/index.d.ts +42 -0
- package/dist/runtime/index.js +11 -0
- package/package.json +108 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { ComponentType } from 'react';
|
|
3
|
+
|
|
4
|
+
interface DarkModeConfig {
|
|
5
|
+
strategy: "class" | "attribute";
|
|
6
|
+
value: string;
|
|
7
|
+
attribute?: string;
|
|
8
|
+
lightValue?: string;
|
|
9
|
+
}
|
|
10
|
+
type Theme = "light" | "dark" | "system";
|
|
11
|
+
interface HostProps {
|
|
12
|
+
canvas: string;
|
|
13
|
+
frame: string | null;
|
|
14
|
+
theme: Theme;
|
|
15
|
+
darkMode: DarkModeConfig;
|
|
16
|
+
canvases: Record<string, ComponentType>;
|
|
17
|
+
/** catalog components, for single-component previews (Assets thumbnails) */
|
|
18
|
+
components?: Record<string, ComponentType<Record<string, unknown>>>;
|
|
19
|
+
}
|
|
20
|
+
/** Renders one frame of one canvas inside the editor's iframe. */
|
|
21
|
+
declare function TruecanvasHost({ canvas, frame, theme: initialTheme, darkMode, canvases, components }: HostProps): react.JSX.Element;
|
|
22
|
+
|
|
23
|
+
export { type DarkModeConfig, type HostProps, TruecanvasHost };
|
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import {
|
|
3
|
+
HostContext
|
|
4
|
+
} from "../chunk-J6CJBZO6.js";
|
|
5
|
+
|
|
6
|
+
// src/runtime/host.tsx
|
|
7
|
+
import { Component, Suspense, useEffect, useState } from "react";
|
|
8
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
9
|
+
var HIDE_CHROME = `
|
|
10
|
+
html, body { margin: 0 !important; }
|
|
11
|
+
html:not([data-tc-play]), html:not([data-tc-play]) body { overflow: hidden !important; }
|
|
12
|
+
html[data-tc-play] { overflow-y: auto !important; overflow-x: hidden !important; }
|
|
13
|
+
[data-tc-hidden] { display: none !important; }
|
|
14
|
+
nextjs-portal, [data-nextjs-toast], #__next-build-watcher { display: none !important; }
|
|
15
|
+
`;
|
|
16
|
+
function TruecanvasHost({ canvas, frame, theme: initialTheme, darkMode, canvases, components }) {
|
|
17
|
+
const [theme, setTheme] = useState(initialTheme);
|
|
18
|
+
const [replay, setReplay] = useState(0);
|
|
19
|
+
useEffect(() => applyTheme(theme, darkMode), [theme, darkMode]);
|
|
20
|
+
useEffect(() => startBridge({ frame, setTheme, replay: () => setReplay((n) => n + 1) }), [frame]);
|
|
21
|
+
useEffect(() => stillForScreenshots(), []);
|
|
22
|
+
if (canvas === "__preview") return /* @__PURE__ */ jsx(Preview, { components: components ?? {} });
|
|
23
|
+
const Canvas = canvases[canvas];
|
|
24
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
25
|
+
/* @__PURE__ */ jsx("style", { children: HIDE_CHROME }),
|
|
26
|
+
!Canvas ? /* @__PURE__ */ jsx(Notice, { title: `Canvas \u201C${canvas}\u201D not found`, body: "It may still be compiling. Truecanvas regenerates the route when canvas files are added." }) : /* @__PURE__ */ jsx(HostContext.Provider, { value: { frame }, children: /* @__PURE__ */ jsx(Boundary, { onError: (m) => post({ type: "tc:error", message: m }), children: /* @__PURE__ */ jsx(Suspense, { fallback: null, children: /* @__PURE__ */ jsx(Canvas, {}, replay) }) }) })
|
|
27
|
+
] });
|
|
28
|
+
}
|
|
29
|
+
function Preview({ components }) {
|
|
30
|
+
const [query, setQuery] = useState(null);
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
const q = new URLSearchParams(window.location.search);
|
|
33
|
+
let props = {};
|
|
34
|
+
try {
|
|
35
|
+
props = JSON.parse(q.get("props") ?? "{}");
|
|
36
|
+
} catch {
|
|
37
|
+
props = {};
|
|
38
|
+
}
|
|
39
|
+
setQuery({ name: q.get("component") ?? "", props });
|
|
40
|
+
}, []);
|
|
41
|
+
if (!query) return null;
|
|
42
|
+
const Comp = components[query.name];
|
|
43
|
+
if (!Comp) return /* @__PURE__ */ jsx(Notice, { title: `Component \u201C${query.name}\u201D not found`, body: "The component registry may still be generating." });
|
|
44
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
45
|
+
/* @__PURE__ */ jsx("style", { children: HIDE_CHROME }),
|
|
46
|
+
/* @__PURE__ */ jsx("div", { "data-tc-frame": "preview", style: { display: "inline-block", padding: 16, minWidth: 40 }, children: /* @__PURE__ */ jsx(Boundary, { onError: () => {
|
|
47
|
+
}, children: /* @__PURE__ */ jsx(Suspense, { fallback: null, children: /* @__PURE__ */ jsx(Comp, { ...query.props }) }) }) })
|
|
48
|
+
] });
|
|
49
|
+
}
|
|
50
|
+
function Notice({ title, body }) {
|
|
51
|
+
return /* @__PURE__ */ jsxs("div", { "data-tc-frame": "", style: { font: "13px/1.5 system-ui, sans-serif", padding: 24, color: "#b42318", background: "#fff5f5", minHeight: 120 }, children: [
|
|
52
|
+
/* @__PURE__ */ jsx("strong", { style: { display: "block", marginBottom: 4 }, children: title }),
|
|
53
|
+
/* @__PURE__ */ jsx("span", { style: { whiteSpace: "pre-wrap", color: "#7a271a" }, children: body })
|
|
54
|
+
] });
|
|
55
|
+
}
|
|
56
|
+
var Boundary = class extends Component {
|
|
57
|
+
state = { error: null };
|
|
58
|
+
static getDerivedStateFromError(error) {
|
|
59
|
+
return { error };
|
|
60
|
+
}
|
|
61
|
+
componentDidCatch(error) {
|
|
62
|
+
this.props.onError(error.message);
|
|
63
|
+
}
|
|
64
|
+
componentDidMount() {
|
|
65
|
+
post({ type: "tc:error", message: null });
|
|
66
|
+
}
|
|
67
|
+
componentDidUpdate(_, prev) {
|
|
68
|
+
if (prev.error && !this.state.error) post({ type: "tc:error", message: null });
|
|
69
|
+
}
|
|
70
|
+
render() {
|
|
71
|
+
if (this.state.error) return /* @__PURE__ */ jsx(Notice, { title: "This frame threw while rendering", body: this.state.error.message });
|
|
72
|
+
return this.props.children;
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
function applyTheme(theme, dm) {
|
|
76
|
+
const media = window.matchMedia("(prefers-color-scheme: dark)");
|
|
77
|
+
const set = () => {
|
|
78
|
+
const dark = theme === "dark" || theme === "system" && media.matches;
|
|
79
|
+
const el = document.documentElement;
|
|
80
|
+
if (dm.strategy === "class") el.classList.toggle(dm.value, dark);
|
|
81
|
+
else el.setAttribute(dm.attribute ?? "data-theme", dark ? dm.value : dm.lightValue ?? "light");
|
|
82
|
+
el.style.colorScheme = dark ? "dark" : "light";
|
|
83
|
+
};
|
|
84
|
+
set();
|
|
85
|
+
if (theme !== "system") return;
|
|
86
|
+
media.addEventListener("change", set);
|
|
87
|
+
return () => media.removeEventListener("change", set);
|
|
88
|
+
}
|
|
89
|
+
var editorOrigin = (() => {
|
|
90
|
+
if (typeof window === "undefined") return null;
|
|
91
|
+
try {
|
|
92
|
+
const raw = new URLSearchParams(window.location.search).get("editor");
|
|
93
|
+
if (!raw) return null;
|
|
94
|
+
const u = new URL(raw);
|
|
95
|
+
return ["localhost", "127.0.0.1", "[::1]"].includes(u.hostname) ? u.origin : null;
|
|
96
|
+
} catch {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
})();
|
|
100
|
+
function post(msg) {
|
|
101
|
+
if (window.parent !== window && editorOrigin) window.parent.postMessage(msg, editorOrigin);
|
|
102
|
+
}
|
|
103
|
+
function frameRoot() {
|
|
104
|
+
return document.querySelector("[data-tc-frame]");
|
|
105
|
+
}
|
|
106
|
+
function tcId(f) {
|
|
107
|
+
const id = f.memoizedProps?.["data-tc"];
|
|
108
|
+
return typeof id === "string" ? id : null;
|
|
109
|
+
}
|
|
110
|
+
function currentRoot() {
|
|
111
|
+
for (const node of [document, document.documentElement, document.body, document.getElementById("__next")]) {
|
|
112
|
+
if (!node) continue;
|
|
113
|
+
for (const key in node) {
|
|
114
|
+
if (key.startsWith("__reactContainer$")) {
|
|
115
|
+
const f = node[key];
|
|
116
|
+
return f.stateNode?.current ?? f;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
function indexTree() {
|
|
123
|
+
const byHost = /* @__PURE__ */ new Map();
|
|
124
|
+
const byId = /* @__PURE__ */ new Map();
|
|
125
|
+
const root = currentRoot();
|
|
126
|
+
const frameEl = frameRoot();
|
|
127
|
+
if (!root || !frameEl) return { byHost, byId };
|
|
128
|
+
const stack = [{ f: root, chain: [], inside: false }];
|
|
129
|
+
while (stack.length) {
|
|
130
|
+
const { f, chain, inside: parentInside } = stack.pop();
|
|
131
|
+
const inside = parentInside || f.stateNode === frameEl;
|
|
132
|
+
let next = chain;
|
|
133
|
+
if (inside) {
|
|
134
|
+
const id = tcId(f);
|
|
135
|
+
if (id && chain[chain.length - 1] !== id) {
|
|
136
|
+
next = [...chain, id];
|
|
137
|
+
const list = byId.get(id) ?? [];
|
|
138
|
+
list.push(f);
|
|
139
|
+
byId.set(id, list);
|
|
140
|
+
}
|
|
141
|
+
if ((f.tag === 5 || f.tag === 6) && next.length) byHost.set(f.stateNode, next);
|
|
142
|
+
}
|
|
143
|
+
for (let c = f.child; c; c = c.sibling) stack.push({ f: c, chain: next, inside });
|
|
144
|
+
}
|
|
145
|
+
return { byHost, byId };
|
|
146
|
+
}
|
|
147
|
+
function chainAt(el, idx) {
|
|
148
|
+
for (let n = el; n; n = n.parentNode) {
|
|
149
|
+
const chain = idx.byHost.get(n);
|
|
150
|
+
if (chain) return [...chain].reverse();
|
|
151
|
+
}
|
|
152
|
+
return [];
|
|
153
|
+
}
|
|
154
|
+
function hostNodes(f, out) {
|
|
155
|
+
if (f.tag === 5 || f.tag === 6) {
|
|
156
|
+
out.push(f.stateNode);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
for (let c = f.child; c; c = c.sibling) hostNodes(c, out);
|
|
160
|
+
}
|
|
161
|
+
function rectOfNode(node) {
|
|
162
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
163
|
+
const range = document.createRange();
|
|
164
|
+
range.selectNodeContents(node);
|
|
165
|
+
const r2 = range.getBoundingClientRect();
|
|
166
|
+
return r2.width || r2.height ? r2 : null;
|
|
167
|
+
}
|
|
168
|
+
const el = node;
|
|
169
|
+
const r = el.getBoundingClientRect();
|
|
170
|
+
if (r.width || r.height) return r;
|
|
171
|
+
let acc = null;
|
|
172
|
+
for (const child of Array.from(el.childNodes)) acc = union(acc, rectOfNode(child));
|
|
173
|
+
return acc;
|
|
174
|
+
}
|
|
175
|
+
function union(a, b) {
|
|
176
|
+
if (!a) return b && { x: b.x, y: b.y, width: b.width, height: b.height };
|
|
177
|
+
if (!b) return a;
|
|
178
|
+
const x = Math.min(a.x, b.x);
|
|
179
|
+
const y = Math.min(a.y, b.y);
|
|
180
|
+
return { x, y, width: Math.max(a.x + a.width, b.x + b.width) - x, height: Math.max(a.y + a.height, b.y + b.height) - y };
|
|
181
|
+
}
|
|
182
|
+
function rectOf(id, idx) {
|
|
183
|
+
let acc = null;
|
|
184
|
+
for (const f of idx.byId.get(id) ?? []) {
|
|
185
|
+
const nodes = [];
|
|
186
|
+
hostNodes(f, nodes);
|
|
187
|
+
for (const n of nodes) acc = union(acc, rectOfNode(n));
|
|
188
|
+
}
|
|
189
|
+
return acc && { x: Math.round(acc.x * 10) / 10, y: Math.round(acc.y * 10) / 10, width: Math.round(acc.width * 10) / 10, height: Math.round(acc.height * 10) / 10 };
|
|
190
|
+
}
|
|
191
|
+
var timeOffset = 0;
|
|
192
|
+
var clockPatched = false;
|
|
193
|
+
var settleTimers = [];
|
|
194
|
+
function patchClock() {
|
|
195
|
+
if (clockPatched) return;
|
|
196
|
+
clockPatched = true;
|
|
197
|
+
const realNow = performance.now.bind(performance);
|
|
198
|
+
performance.now = () => realNow() + timeOffset;
|
|
199
|
+
}
|
|
200
|
+
function settle() {
|
|
201
|
+
patchClock();
|
|
202
|
+
timeOffset += 6e4;
|
|
203
|
+
for (let pass = 0; pass < 4; pass++) {
|
|
204
|
+
const queue = [...held.values()];
|
|
205
|
+
held = /* @__PURE__ */ new Map();
|
|
206
|
+
for (const cb of queue) {
|
|
207
|
+
try {
|
|
208
|
+
cb(performance.now());
|
|
209
|
+
} catch {
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
for (const a of document.getAnimations()) {
|
|
214
|
+
try {
|
|
215
|
+
const end = a.effect?.getComputedTiming().endTime;
|
|
216
|
+
if (typeof end === "number" && Number.isFinite(end)) a.finish();
|
|
217
|
+
else a.pause();
|
|
218
|
+
} catch {
|
|
219
|
+
a.pause();
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
function scheduleSettle(done, quick = false) {
|
|
224
|
+
for (const t of settleTimers) clearTimeout(t);
|
|
225
|
+
settleTimers = (quick ? [0, 250, 700] : [0, 300, 1e3, 2500]).map(
|
|
226
|
+
(ms, i, all) => setTimeout(() => {
|
|
227
|
+
settle();
|
|
228
|
+
if (i === all.length - 1) done?.();
|
|
229
|
+
}, ms)
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
function stillForScreenshots() {
|
|
233
|
+
if (new URLSearchParams(window.location.search).get("still") !== "1") return;
|
|
234
|
+
const w = window;
|
|
235
|
+
setTimeout(() => {
|
|
236
|
+
setMotionPaused(true, () => w.__tcStill = true, true);
|
|
237
|
+
}, 400);
|
|
238
|
+
}
|
|
239
|
+
if (typeof window !== "undefined" && "IntersectionObserver" in window && !window.__tcIO) {
|
|
240
|
+
window.__tcIO = true;
|
|
241
|
+
const Native = window.IntersectionObserver;
|
|
242
|
+
window.IntersectionObserver = class extends Native {
|
|
243
|
+
constructor(callback, options = {}) {
|
|
244
|
+
super(callback, options.root == null ? { ...options, root: document } : options);
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
var remountWatch = null;
|
|
249
|
+
var realRaf = null;
|
|
250
|
+
var realCancel = null;
|
|
251
|
+
var held = /* @__PURE__ */ new Map();
|
|
252
|
+
var heldSeq = -1;
|
|
253
|
+
var PAUSE_STYLE_ID = "tc-pause-motion";
|
|
254
|
+
function raf(cb) {
|
|
255
|
+
return (realRaf ?? requestAnimationFrame)(cb);
|
|
256
|
+
}
|
|
257
|
+
function setMotionPaused(paused, settled, quick = false) {
|
|
258
|
+
realRaf ??= window.requestAnimationFrame.bind(window);
|
|
259
|
+
realCancel ??= window.cancelAnimationFrame.bind(window);
|
|
260
|
+
if (paused) {
|
|
261
|
+
window.requestAnimationFrame = (cb) => {
|
|
262
|
+
const id = heldSeq--;
|
|
263
|
+
held.set(id, cb);
|
|
264
|
+
return id;
|
|
265
|
+
};
|
|
266
|
+
window.cancelAnimationFrame = (id) => id < 0 ? void held.delete(id) : realCancel(id);
|
|
267
|
+
for (const a of document.getAnimations()) a.pause();
|
|
268
|
+
if (!document.getElementById(PAUSE_STYLE_ID)) {
|
|
269
|
+
const style = document.createElement("style");
|
|
270
|
+
style.id = PAUSE_STYLE_ID;
|
|
271
|
+
style.textContent = "*,*::before,*::after{animation-play-state:paused!important}";
|
|
272
|
+
document.head.appendChild(style);
|
|
273
|
+
}
|
|
274
|
+
scheduleSettle(settled, quick);
|
|
275
|
+
remountWatch?.disconnect();
|
|
276
|
+
let pending = null;
|
|
277
|
+
remountWatch = new MutationObserver(() => {
|
|
278
|
+
if (pending) clearTimeout(pending);
|
|
279
|
+
pending = setTimeout(() => {
|
|
280
|
+
pending = null;
|
|
281
|
+
settle();
|
|
282
|
+
setTimeout(settle, 300);
|
|
283
|
+
}, 120);
|
|
284
|
+
});
|
|
285
|
+
remountWatch.observe(document.body, { childList: true, subtree: true });
|
|
286
|
+
} else {
|
|
287
|
+
remountWatch?.disconnect();
|
|
288
|
+
remountWatch = null;
|
|
289
|
+
for (const t of settleTimers) clearTimeout(t);
|
|
290
|
+
settleTimers = [];
|
|
291
|
+
window.requestAnimationFrame = realRaf;
|
|
292
|
+
window.cancelAnimationFrame = realCancel;
|
|
293
|
+
const queue = [...held.values()];
|
|
294
|
+
held = /* @__PURE__ */ new Map();
|
|
295
|
+
for (const cb of queue) realRaf(cb);
|
|
296
|
+
document.getElementById(PAUSE_STYLE_ID)?.remove();
|
|
297
|
+
for (const a of document.getAnimations()) if (a.playState === "paused") a.play();
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
var hiddenIds = /* @__PURE__ */ new Set();
|
|
301
|
+
function applyHidden() {
|
|
302
|
+
const wanted = /* @__PURE__ */ new Set();
|
|
303
|
+
if (hiddenIds.size) {
|
|
304
|
+
const idx = indexTree();
|
|
305
|
+
for (const id of hiddenIds) {
|
|
306
|
+
for (const f of idx.byId.get(id) ?? []) {
|
|
307
|
+
const nodes = [];
|
|
308
|
+
hostNodes(f, nodes);
|
|
309
|
+
for (const n of nodes) if (n.nodeType === Node.ELEMENT_NODE) wanted.add(n);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
for (const el of Array.from(document.querySelectorAll("[data-tc-hidden]"))) if (!wanted.has(el)) el.removeAttribute("data-tc-hidden");
|
|
314
|
+
for (const el of wanted) if (!el.hasAttribute("data-tc-hidden")) el.setAttribute("data-tc-hidden", "");
|
|
315
|
+
}
|
|
316
|
+
var playing = false;
|
|
317
|
+
function startBridge({ frame, setTheme, replay }) {
|
|
318
|
+
if (window.parent === window || !editorOrigin) return;
|
|
319
|
+
const onMessage = (e) => {
|
|
320
|
+
if (e.source !== window.parent || e.origin !== editorOrigin) return;
|
|
321
|
+
const msg = e.data;
|
|
322
|
+
switch (msg?.type) {
|
|
323
|
+
case "tc:hit": {
|
|
324
|
+
const idx = indexTree();
|
|
325
|
+
const chain = chainAt(document.elementFromPoint(msg.x, msg.y), idx);
|
|
326
|
+
const rects = {};
|
|
327
|
+
for (const id of chain) rects[id] = rectOf(id, idx);
|
|
328
|
+
post({ type: "tc:hit:res", req: msg.req, chain, rects });
|
|
329
|
+
break;
|
|
330
|
+
}
|
|
331
|
+
case "tc:rects": {
|
|
332
|
+
const idx = indexTree();
|
|
333
|
+
const rects = {};
|
|
334
|
+
for (const id of msg.ids) rects[id] = rectOf(id, idx);
|
|
335
|
+
post({ type: "tc:rects:res", req: msg.req, rects });
|
|
336
|
+
break;
|
|
337
|
+
}
|
|
338
|
+
case "tc:theme":
|
|
339
|
+
setTheme(msg.theme);
|
|
340
|
+
break;
|
|
341
|
+
case "tc:motion":
|
|
342
|
+
setMotionPaused(!!msg.paused);
|
|
343
|
+
break;
|
|
344
|
+
case "tc:hidden":
|
|
345
|
+
hiddenIds = new Set(msg.ids);
|
|
346
|
+
applyHidden();
|
|
347
|
+
break;
|
|
348
|
+
case "tc:play":
|
|
349
|
+
playing = !!msg.on;
|
|
350
|
+
document.documentElement.toggleAttribute("data-tc-play", playing);
|
|
351
|
+
document.documentElement.scrollTop = 0;
|
|
352
|
+
if (playing) replay();
|
|
353
|
+
break;
|
|
354
|
+
case "tc:replay":
|
|
355
|
+
document.documentElement.scrollTop = 0;
|
|
356
|
+
replay();
|
|
357
|
+
break;
|
|
358
|
+
}
|
|
359
|
+
};
|
|
360
|
+
window.addEventListener("message", onMessage);
|
|
361
|
+
let rafId = 0;
|
|
362
|
+
const notify = () => {
|
|
363
|
+
if (rafId) return;
|
|
364
|
+
rafId = raf(() => {
|
|
365
|
+
rafId = 0;
|
|
366
|
+
const root2 = frameRoot();
|
|
367
|
+
post({ type: "tc:layout", frame, height: root2 ? Math.ceil(root2.getBoundingClientRect().height) : 0, ready: !!root2 });
|
|
368
|
+
});
|
|
369
|
+
};
|
|
370
|
+
const ro = new ResizeObserver(notify);
|
|
371
|
+
const mo = new MutationObserver((records) => {
|
|
372
|
+
const root2 = frameRoot();
|
|
373
|
+
if (root2) ro.observe(root2);
|
|
374
|
+
if (records.every((r) => r.type === "attributes" && r.attributeName === "data-tc-hidden")) return;
|
|
375
|
+
if (hiddenIds.size) applyHidden();
|
|
376
|
+
notify();
|
|
377
|
+
});
|
|
378
|
+
mo.observe(document.body, { subtree: true, childList: true, attributes: true, characterData: true });
|
|
379
|
+
const root = frameRoot();
|
|
380
|
+
if (root) ro.observe(root);
|
|
381
|
+
document.fonts?.ready.then(notify);
|
|
382
|
+
notify();
|
|
383
|
+
const onWheel = (e) => {
|
|
384
|
+
if (playing && !(e.ctrlKey || e.metaKey)) return;
|
|
385
|
+
e.preventDefault();
|
|
386
|
+
post({ type: "tc:wheel", dx: e.deltaX, dy: e.deltaY, mode: e.deltaMode, ctrl: e.ctrlKey || e.metaKey, x: e.clientX, y: e.clientY });
|
|
387
|
+
};
|
|
388
|
+
const onKey = (e) => {
|
|
389
|
+
const t = e.target;
|
|
390
|
+
if (t && (t.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(t.tagName))) return;
|
|
391
|
+
if (e.key === "Escape" || e.metaKey || e.ctrlKey || /^[vhfp]$/i.test(e.key)) {
|
|
392
|
+
post({ type: "tc:key", key: e.key, code: e.code, meta: e.metaKey, ctrl: e.ctrlKey, shift: e.shiftKey, alt: e.altKey });
|
|
393
|
+
}
|
|
394
|
+
};
|
|
395
|
+
const onError = (e) => post({ type: "tc:error", message: e.message });
|
|
396
|
+
window.addEventListener("wheel", onWheel, { passive: false });
|
|
397
|
+
window.addEventListener("keydown", onKey);
|
|
398
|
+
window.addEventListener("error", onError);
|
|
399
|
+
post({ type: "tc:ready", frame });
|
|
400
|
+
return () => {
|
|
401
|
+
window.removeEventListener("message", onMessage);
|
|
402
|
+
window.removeEventListener("wheel", onWheel);
|
|
403
|
+
window.removeEventListener("keydown", onKey);
|
|
404
|
+
window.removeEventListener("error", onError);
|
|
405
|
+
ro.disconnect();
|
|
406
|
+
mo.disconnect();
|
|
407
|
+
(realCancel ?? cancelAnimationFrame)(rafId);
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
export {
|
|
411
|
+
TruecanvasHost
|
|
412
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { ReactNode } from 'react';
|
|
3
|
+
|
|
4
|
+
/** Set by the render route: which frame this document is rendering. */
|
|
5
|
+
declare const HostContext: react.Context<{
|
|
6
|
+
frame: string | null;
|
|
7
|
+
} | null>;
|
|
8
|
+
interface CanvasProps {
|
|
9
|
+
children?: ReactNode;
|
|
10
|
+
}
|
|
11
|
+
/** Root of a canvas file. Its children are <Frame>s. */
|
|
12
|
+
declare function Canvas({ children }: CanvasProps): react.JSX.Element;
|
|
13
|
+
interface FrameProps {
|
|
14
|
+
/** Unique name, shown above the frame on the canvas. */
|
|
15
|
+
name: string;
|
|
16
|
+
/** Position on the canvas, in px. */
|
|
17
|
+
x?: number;
|
|
18
|
+
y?: number;
|
|
19
|
+
/** Frame width in px: the viewport width your components see. */
|
|
20
|
+
width: number;
|
|
21
|
+
/** Fixed height in px. Omit to hug the content. */
|
|
22
|
+
height?: number;
|
|
23
|
+
/** Force a theme for this frame. Omit to inherit the canvas theme. */
|
|
24
|
+
theme?: "light" | "dark";
|
|
25
|
+
/** Device preset (e.g. "iphone-16"): device chrome in the editor, mobile emulation in screenshots. */
|
|
26
|
+
device?: string;
|
|
27
|
+
/** Linked frame: the App Router page it shows (project-relative). Editing its layers edits that page. */
|
|
28
|
+
page?: string;
|
|
29
|
+
/** Exploration of this page file: "Apply to page" writes the frame back to it. */
|
|
30
|
+
from?: string;
|
|
31
|
+
/** Main component frame, "components/button.tsx#Button": its layers are that component's JSX. */
|
|
32
|
+
component?: string;
|
|
33
|
+
children?: ReactNode;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* An artboard. Inside the editor each frame renders in its own iframe at its
|
|
37
|
+
* own width, so media queries, portals and fixed positioning behave like the
|
|
38
|
+
* real app. Rendered anywhere else it is just a sized box.
|
|
39
|
+
*/
|
|
40
|
+
declare function Frame({ name, width, height, children }: FrameProps): react.JSX.Element | null;
|
|
41
|
+
|
|
42
|
+
export { Canvas, type CanvasProps, Frame, type FrameProps, HostContext };
|
package/package.json
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "truecanvas",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Design with your real React components: a Figma-like canvas for Next.js where layers are your .tsx and agents edit through MCP.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"truecanvas": "dist/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/runtime/index.d.ts",
|
|
13
|
+
"default": "./dist/runtime/index.js"
|
|
14
|
+
},
|
|
15
|
+
"./next": {
|
|
16
|
+
"types": "./dist/next/index.d.ts",
|
|
17
|
+
"default": "./dist/next/index.js"
|
|
18
|
+
},
|
|
19
|
+
"./core": {
|
|
20
|
+
"types": "./dist/core/index.d.ts",
|
|
21
|
+
"default": "./dist/core/index.js"
|
|
22
|
+
},
|
|
23
|
+
"./host": {
|
|
24
|
+
"types": "./dist/runtime/host.d.ts",
|
|
25
|
+
"default": "./dist/runtime/host.js"
|
|
26
|
+
},
|
|
27
|
+
"./loader": "./dist/next/loader.cjs",
|
|
28
|
+
"./package.json": "./package.json"
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"dist",
|
|
32
|
+
"README.md",
|
|
33
|
+
"LICENSE"
|
|
34
|
+
],
|
|
35
|
+
"scripts": {
|
|
36
|
+
"build": "rm -rf dist && tsup && vite build --config editor/vite.config.ts",
|
|
37
|
+
"build:lib": "tsup",
|
|
38
|
+
"build:editor": "vite build --config editor/vite.config.ts",
|
|
39
|
+
"dev:editor": "vite --config editor/vite.config.ts",
|
|
40
|
+
"typecheck": "tsc --noEmit -p tsconfig.json && tsc --noEmit -p editor/tsconfig.json",
|
|
41
|
+
"test": "node --test test/*.test.mjs",
|
|
42
|
+
"prepublishOnly": "pnpm build && pnpm test",
|
|
43
|
+
"prepack": "node ../../scripts/sync-readme.mjs"
|
|
44
|
+
},
|
|
45
|
+
"peerDependencies": {
|
|
46
|
+
"next": ">=15.0",
|
|
47
|
+
"react": ">=19"
|
|
48
|
+
},
|
|
49
|
+
"peerDependenciesMeta": {
|
|
50
|
+
"next": {
|
|
51
|
+
"optional": true
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
"dependencies": {
|
|
55
|
+
"@babel/parser": "^7.29.9",
|
|
56
|
+
"@modelcontextprotocol/sdk": "^1.32.0",
|
|
57
|
+
"launch-editor": "^2.14.1",
|
|
58
|
+
"magic-string": "^1.4.2",
|
|
59
|
+
"playwright-core": "^1.63.0",
|
|
60
|
+
"prettier": "^3.9.9",
|
|
61
|
+
"tinyglobby": "^0.2.17",
|
|
62
|
+
"typescript": "^6.0.3",
|
|
63
|
+
"zod": "^4.6.5"
|
|
64
|
+
},
|
|
65
|
+
"devDependencies": {
|
|
66
|
+
"@babel/types": "^7.29.0",
|
|
67
|
+
"@fontsource-variable/inter": "^5.3.0",
|
|
68
|
+
"@types/node": "^24.0.0",
|
|
69
|
+
"@types/react": "^19.2.0",
|
|
70
|
+
"@types/react-dom": "^19.2.0",
|
|
71
|
+
"@vitejs/plugin-react": "^6.1.1",
|
|
72
|
+
"lucide-react": "^1.52.0",
|
|
73
|
+
"react": "^19.3.0",
|
|
74
|
+
"react-dom": "^19.3.0",
|
|
75
|
+
"tsup": "^8.5.1",
|
|
76
|
+
"vite": "^8.3.2",
|
|
77
|
+
"zustand": "^5.0.15"
|
|
78
|
+
},
|
|
79
|
+
"keywords": [
|
|
80
|
+
"design",
|
|
81
|
+
"canvas",
|
|
82
|
+
"figma",
|
|
83
|
+
"react",
|
|
84
|
+
"nextjs",
|
|
85
|
+
"tailwind",
|
|
86
|
+
"mcp",
|
|
87
|
+
"agents",
|
|
88
|
+
"claude",
|
|
89
|
+
"cursor",
|
|
90
|
+
"design-system",
|
|
91
|
+
"components"
|
|
92
|
+
],
|
|
93
|
+
"engines": {
|
|
94
|
+
"node": ">=20"
|
|
95
|
+
},
|
|
96
|
+
"homepage": "https://truecanvas.dev",
|
|
97
|
+
"repository": {
|
|
98
|
+
"type": "git",
|
|
99
|
+
"url": "git+https://github.com/Nathandona/truecanvas.git",
|
|
100
|
+
"directory": "packages/truecanvas"
|
|
101
|
+
},
|
|
102
|
+
"bugs": {
|
|
103
|
+
"url": "https://github.com/Nathandona/truecanvas/issues"
|
|
104
|
+
},
|
|
105
|
+
"publishConfig": {
|
|
106
|
+
"access": "public"
|
|
107
|
+
}
|
|
108
|
+
}
|