truecanvas 0.5.0 → 0.6.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/README.md +6 -0
- package/dist/chunk-D565QDII.js +814 -0
- package/dist/chunk-LKFOLM3L.js +54 -0
- package/dist/chunk-NAK4DQWO.js +1005 -0
- package/dist/{chunk-DV76O5SZ.js → chunk-OPKROZI2.js} +261 -4
- package/dist/chunk-OZOV4Y6C.js +45 -0
- package/dist/{chunk-A4QIAVTG.js → chunk-QJ5M3G4E.js} +88 -852
- package/dist/{chunk-TSHD2S36.js → chunk-VWKLYIY6.js} +1 -1
- package/dist/cli.js +702 -936
- package/dist/core/index.js +14 -11
- package/dist/editor/assets/{Menu-BJ3OmqzH.css → Menu-CACFFH7S.css} +1 -1
- package/dist/editor/assets/hub-B_TP_af8.js +2 -0
- package/dist/editor/assets/main-Dj60SnCH.js +26 -0
- package/dist/editor/hub.html +3 -3
- package/dist/editor/index.html +3 -3
- package/dist/hub.d.ts +88 -0
- package/dist/hub.js +15 -0
- package/dist/share/index.d.ts +73 -1
- package/dist/share/index.js +13 -1
- package/dist/vite/index.js +3 -2
- package/package.json +5 -1
- package/dist/chunk-PBKX3SBC.js +0 -97
- package/dist/editor/assets/hub-B5hqZCM2.js +0 -2
- package/dist/editor/assets/main-woTyWmya.js +0 -26
- /package/dist/editor/assets/{Menu-C0Ti-6T2.js → Menu-CZr87VA3.js} +0 -0
|
@@ -40,6 +40,10 @@ header { position: fixed; top: 12px; left: 12px; right: 12px; display: flex; ali
|
|
|
40
40
|
background: var(--panel); backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px); border: 1px solid var(--line); box-shadow: 0 1px 2px rgba(0,0,0,.05); }
|
|
41
41
|
.brand img { height: 20px; width: auto; display: block; }
|
|
42
42
|
.brand span { font-weight: 600; letter-spacing: -.01em; }
|
|
43
|
+
.brand .who { font-weight: 450; color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 180px; }
|
|
44
|
+
.brand form { display: contents; }
|
|
45
|
+
.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; }
|
|
46
|
+
.brand .who-btn:hover { background: var(--line); }
|
|
43
47
|
.title { font-weight: 550; }
|
|
44
48
|
.meta { color: var(--muted); }
|
|
45
49
|
.vselect { gap: 4px; color: var(--muted); padding: 0 6px 0 8px; margin-right: -6px; max-width: 46vw; }
|
|
@@ -120,6 +124,7 @@ button:focus-visible { outline: 2px solid #3b82f6; outline-offset: 1px; }
|
|
|
120
124
|
@media (max-width: 640px) {
|
|
121
125
|
.meta, .sep, .made, .vdate { display: none; } header { top: 8px; left: 8px; right: 8px; gap: 8px; }
|
|
122
126
|
.brand:has(img) span { display: none; }
|
|
127
|
+
.brand .who { display: none; }
|
|
123
128
|
header .pill { min-width: 0; padding: 0 10px; }
|
|
124
129
|
header .pill:last-child { flex: 1 1 auto; justify-content: flex-end; }
|
|
125
130
|
.title { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
@@ -141,13 +146,26 @@ button:focus-visible { outline: 2px solid #3b82f6; outline-offset: 1px; }
|
|
|
141
146
|
.live-device { position: relative; overflow: hidden; border-radius: 10px; background: #fff; box-shadow: 0 30px 80px -20px rgba(0,0,0,.6); animation: pop-in .2s var(--ease-out); }
|
|
142
147
|
.live-device iframe { position: absolute; left: 0; top: 0; border: 0; transform-origin: 0 0; background: #fff; }
|
|
143
148
|
@media (max-width: 640px) { .live-bar .meta { display: none; } }
|
|
144
|
-
|
|
149
|
+
/* the room: who's here, their cursors, a live session */
|
|
150
|
+
#room { gap: 8px; padding: 0 10px 0 12px; }
|
|
151
|
+
.live-badge { display: inline-flex; align-items: center; gap: 6px; font-weight: 650; font-size: 12px; color: #dc2626; letter-spacing: .01em; }
|
|
152
|
+
.live-badge::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: #dc2626; box-shadow: 0 0 0 3px rgba(220,38,38,.18); }
|
|
153
|
+
.faces { display: flex; }
|
|
154
|
+
.face { width: 24px; height: 24px; border-radius: 50%; display: grid; place-items: center; color: #fff; font-size: 10.5px; font-weight: 650;
|
|
155
|
+
border: 2px solid var(--panel-solid); margin-left: -6px; }
|
|
156
|
+
.face:first-child { margin-left: 0; }
|
|
157
|
+
.rcursor { position: absolute; width: 0; height: 0; transform-origin: 0 0; z-index: 3; pointer-events: none; transition: left .09s linear, top .09s linear; }
|
|
158
|
+
.rcursor svg { position: absolute; left: -2px; top: -2px; display: block; }
|
|
159
|
+
.rcursor span { position: absolute; left: 13px; top: 15px; white-space: nowrap; padding: 2px 7px; border-radius: 7px; color: #fff; font-size: 11.5px; font-weight: 600;
|
|
160
|
+
box-shadow: 0 2px 6px rgba(0,0,0,.18); }
|
|
161
|
+
@media (prefers-reduced-motion: reduce) { .frame-box iframe { transition: none; } #live, .live-device { animation: none; } .rcursor { transition: none; } }
|
|
145
162
|
</style>
|
|
146
163
|
</head>
|
|
147
164
|
<body>
|
|
148
165
|
<div id="stage" aria-label="Design canvas"><div id="world"></div></div>
|
|
149
166
|
<header>
|
|
150
167
|
<div class="pill brand" id="brand"><span>Design review</span></div>
|
|
168
|
+
<div class="pill" id="room" hidden aria-live="polite"></div>
|
|
151
169
|
<div class="pill"><span class="title" id="title"></span><span class="sep"></span><span class="meta" id="meta"></span><button id="list" hidden aria-label="Comments" title="Comments"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 5h16v11H9l-5 4z"/></svg><span class="count" id="count">0</span></button></div>
|
|
152
170
|
</header>
|
|
153
171
|
<aside id="panel" hidden aria-label="Comments"><div class="panel-head"><span>Comments</span><button id="panel-close" aria-label="Close">\u2715</button></div><div class="panel-list" id="panel-list"></div></aside>
|
|
@@ -267,6 +285,14 @@ button:focus-visible { outline: 2px solid #3b82f6; outline-offset: 1px; }
|
|
|
267
285
|
if (manifest.brand) {
|
|
268
286
|
document.getElementById("brand").innerHTML = (manifest.brand.logo ? '<img alt="" src="' + esc(manifest.brand.logo) + '">' : "") + (manifest.brand.name ? "<span>" + esc(manifest.brand.name) + "</span>" : "");
|
|
269
287
|
}
|
|
288
|
+
// signed in: who's commenting, and sign out. On review sites with sign-in, a way in for the studio and invitees.
|
|
289
|
+
if (manifest.viewer || manifest.signIn) {
|
|
290
|
+
const brandEl = document.getElementById("brand");
|
|
291
|
+
const here = location.pathname.replace(/\\/+$/, "");
|
|
292
|
+
brandEl.insertAdjacentHTML("beforeend", '<span class="sep"></span>' + (manifest.viewer
|
|
293
|
+
? '<span class="who" title="' + esc(manifest.viewer.email) + '">' + esc(manifest.viewer.name) + '</span><form method="post" action="/signout"><input type="hidden" name="next" value="' + esc(here) + '"><button class="who-btn" type="submit">Sign out</button></form>'
|
|
294
|
+
: '<a class="who-btn" href="/signin?next=' + encodeURIComponent(here) + '">Sign in</a>'));
|
|
295
|
+
}
|
|
270
296
|
|
|
271
297
|
// layout: frames at their canvas positions
|
|
272
298
|
const frames = manifest.frames;
|
|
@@ -297,7 +323,7 @@ button:focus-visible { outline: 2px solid #3b82f6; outline-offset: 1px; }
|
|
|
297
323
|
zoomLabel.textContent = Math.round(z * 100) + "%";
|
|
298
324
|
// labels keep a readable size at any zoom
|
|
299
325
|
for (const l of labels) l.style.transform = "scale(" + 1 / z + ")";
|
|
300
|
-
for (const p of world.querySelectorAll(".pin")) p.style.transform = "scale(" + 1 / z + ")";
|
|
326
|
+
for (const p of world.querySelectorAll(".pin, .rcursor")) p.style.transform = "scale(" + 1 / z + ")";
|
|
301
327
|
if (typeof placePop === "function") placePop();
|
|
302
328
|
};
|
|
303
329
|
const zoomAt = (nz, cx, cy) => {
|
|
@@ -467,7 +493,8 @@ button:focus-visible { outline: 2px solid #3b82f6; outline-offset: 1px; }
|
|
|
467
493
|
var popAnchor = null;
|
|
468
494
|
var pop = document.getElementById("pop");
|
|
469
495
|
const studioName = (manifest.brand && manifest.brand.name) || "Studio";
|
|
470
|
-
|
|
496
|
+
// signed in: comments carry the account's name, nothing to type
|
|
497
|
+
const storedName = () => { if (manifest.viewer) return manifest.viewer.name; try { return localStorage.getItem("tc-review-name") || ""; } catch { return ""; } };
|
|
471
498
|
const saveName = (n) => { try { localStorage.setItem("tc-review-name", n); } catch {} };
|
|
472
499
|
const ago = (t) => {
|
|
473
500
|
const s = Math.round((Date.now() - t) / 1000);
|
|
@@ -716,6 +743,156 @@ button:focus-visible { outline: 2px solid #3b82f6; outline-offset: 1px; }
|
|
|
716
743
|
setInterval(() => document.visibilityState === "visible" && refresh(), 15000);
|
|
717
744
|
addEventListener("visibilitychange", () => document.visibilityState === "visible" && refresh());
|
|
718
745
|
}
|
|
746
|
+
|
|
747
|
+
// ---------- the link's room: who's here, their cursors, comments as they come, live sessions ----------
|
|
748
|
+
if (manifest.room && "WebSocket" in window) {
|
|
749
|
+
const people = new Map();
|
|
750
|
+
const roomPill = document.getElementById("room");
|
|
751
|
+
let me = null, live = false, route = null, sock = null, retry = 1000, ping = 0;
|
|
752
|
+
// a session shows the app as it is now: only on the latest version
|
|
753
|
+
const canLive = !!manifest.session && (!manifest.versions || !manifest.versions.length || manifest.versions[0].id === manifest.id);
|
|
754
|
+
const initials = (n) => n.split(" ").filter(Boolean).slice(0, 2).map((w) => w[0].toUpperCase()).join("") || "?";
|
|
755
|
+
|
|
756
|
+
function renderRoom() {
|
|
757
|
+
const others = [...people.values()];
|
|
758
|
+
roomPill.hidden = !live && !others.length;
|
|
759
|
+
roomPill.textContent = "";
|
|
760
|
+
if (live) roomPill.appendChild(el("span", "live-badge", "Live"));
|
|
761
|
+
const faces = el("span", "faces");
|
|
762
|
+
for (const p of others.slice(0, 5)) {
|
|
763
|
+
const f = el("span", "face", initials(p.name));
|
|
764
|
+
f.style.background = p.color;
|
|
765
|
+
f.title = p.name + (p.kind === "studio" ? " (" + studioName + ")" : "");
|
|
766
|
+
faces.appendChild(f);
|
|
767
|
+
}
|
|
768
|
+
if (others.length > 5) faces.appendChild(el("span", "face", "+" + (others.length - 5))).style.background = "#8b8a85";
|
|
769
|
+
if (others.length) roomPill.appendChild(faces);
|
|
770
|
+
roomPill.setAttribute("aria-label", (live ? "Live session. " : "") + (others.length ? others.map((p) => p.name).join(", ") + " here" : ""));
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
function cursorOf(p) {
|
|
774
|
+
if (p.el) return p.el;
|
|
775
|
+
p.el = el("div", "rcursor");
|
|
776
|
+
p.el.innerHTML = '<svg width="18" height="18" viewBox="0 0 18 18" aria-hidden="true"><path d="M2.2 1.6 15.1 8.3c.7.4.6 1.4-.2 1.6l-5.3 1.3-2.6 4.8c-.4.7-1.4.6-1.6-.2L1.1 2.7c-.2-.8.5-1.4 1.1-1.1Z" stroke="#fff" stroke-width="1.5" stroke-linejoin="round"/></svg>';
|
|
777
|
+
p.el.querySelector("path").setAttribute("fill", p.color);
|
|
778
|
+
const name = el("span", null, p.name);
|
|
779
|
+
name.style.background = p.color;
|
|
780
|
+
p.el.appendChild(name);
|
|
781
|
+
p.el.style.transform = "scale(" + 1 / z + ")";
|
|
782
|
+
world.appendChild(p.el);
|
|
783
|
+
return p.el;
|
|
784
|
+
}
|
|
785
|
+
function moveCursor(p, cx, cy) {
|
|
786
|
+
if (cx == null || cy == null) { if (p.el) p.el.hidden = true; return; }
|
|
787
|
+
const c = cursorOf(p);
|
|
788
|
+
c.hidden = false;
|
|
789
|
+
c.style.left = cx - minX + "px";
|
|
790
|
+
c.style.top = cy - minY + "px";
|
|
791
|
+
}
|
|
792
|
+
const add = (person) => { if (person.id !== me) people.set(person.id, Object.assign(people.get(person.id) || {}, person)); };
|
|
793
|
+
const remove = (id) => { const p = people.get(id); if (p && p.el) p.el.remove(); people.delete(id); };
|
|
794
|
+
|
|
795
|
+
// live frames: the studio's app, served from the link's session host, at each frame's size
|
|
796
|
+
function sessionFrame(f) {
|
|
797
|
+
const home = new URL(manifest.session.url);
|
|
798
|
+
const url = new URL((route || "/truecanvas/") + encodeURIComponent(manifest.canvas), home);
|
|
799
|
+
url.searchParams.set("frame", f.name);
|
|
800
|
+
if (f.theme) url.searchParams.set("theme", f.theme);
|
|
801
|
+
const token = home.searchParams.get("tc_access");
|
|
802
|
+
if (token) url.searchParams.set("tc_access", token);
|
|
803
|
+
return url.href;
|
|
804
|
+
}
|
|
805
|
+
function setLive(on) {
|
|
806
|
+
on = !!on && canLive;
|
|
807
|
+
if (on === live) return renderRoom();
|
|
808
|
+
live = on;
|
|
809
|
+
for (const frameEl of world.querySelectorAll(".frame")) {
|
|
810
|
+
const f = frameOf(frameEl.dataset.name);
|
|
811
|
+
const box = frameEl.querySelector(".frame-box");
|
|
812
|
+
const old = box.querySelector("iframe");
|
|
813
|
+
const img = box.querySelector("img");
|
|
814
|
+
const next = document.createElement("iframe");
|
|
815
|
+
next.title = f.name;
|
|
816
|
+
next.setAttribute("scrolling", "no");
|
|
817
|
+
if (on) {
|
|
818
|
+
// its own origin: the app's scripts run there, never on the review site
|
|
819
|
+
next.setAttribute("sandbox", "allow-scripts allow-same-origin allow-forms");
|
|
820
|
+
next.setAttribute("referrerpolicy", "no-referrer");
|
|
821
|
+
next.src = sessionFrame(f);
|
|
822
|
+
} else {
|
|
823
|
+
next.setAttribute("sandbox", "");
|
|
824
|
+
next.loading = "lazy";
|
|
825
|
+
next.src = f.html;
|
|
826
|
+
if (img) img.style.visibility = "";
|
|
827
|
+
}
|
|
828
|
+
next.addEventListener("load", () => { next.classList.add("ready"); if (on && img) img.style.visibility = "hidden"; });
|
|
829
|
+
old.replaceWith(next);
|
|
830
|
+
}
|
|
831
|
+
renderRoom();
|
|
832
|
+
toast(on ? "Live: you're seeing the design as the studio works on it" : "The live session ended: back to the published version");
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
function onEvent(e) {
|
|
836
|
+
if (e.t === "hello") {
|
|
837
|
+
me = e.you.id;
|
|
838
|
+
for (const id of [...people.keys()]) remove(id);
|
|
839
|
+
for (const p of e.people) add(p);
|
|
840
|
+
route = e.route;
|
|
841
|
+
setLive(e.live);
|
|
842
|
+
} else if (e.t === "join") { add(e.person); renderRoom(); }
|
|
843
|
+
else if (e.t === "leave") { remove(e.id); renderRoom(); }
|
|
844
|
+
else if (e.t === "cursor") { const p = people.get(e.id); if (p) moveCursor(p, e.x, e.y); }
|
|
845
|
+
else if (e.t === "state") { route = e.route || route; setLive(e.live); }
|
|
846
|
+
else if (e.t === "comments" && canComment) refresh();
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
function connect() {
|
|
850
|
+
const url = new URL("room", document.baseURI);
|
|
851
|
+
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
852
|
+
const typed = storedName();
|
|
853
|
+
if (!manifest.viewer && typed) url.searchParams.set("name", typed);
|
|
854
|
+
const s = new WebSocket(url.href);
|
|
855
|
+
sock = s;
|
|
856
|
+
s.onopen = () => {
|
|
857
|
+
retry = 1000;
|
|
858
|
+
clearInterval(ping);
|
|
859
|
+
// idle sockets are dropped after about 100 s: the room answers "ping" without waking up
|
|
860
|
+
ping = setInterval(() => s.readyState === 1 && s.send("ping"), 30000);
|
|
861
|
+
};
|
|
862
|
+
s.onmessage = (m) => {
|
|
863
|
+
if (m.data === "pong") return;
|
|
864
|
+
let e;
|
|
865
|
+
try { e = JSON.parse(m.data); } catch { return; }
|
|
866
|
+
onEvent(e);
|
|
867
|
+
};
|
|
868
|
+
s.onclose = () => {
|
|
869
|
+
clearInterval(ping);
|
|
870
|
+
if (sock !== s) return;
|
|
871
|
+
sock = null;
|
|
872
|
+
for (const id of [...people.keys()]) remove(id);
|
|
873
|
+
renderRoom();
|
|
874
|
+
setTimeout(connect, retry);
|
|
875
|
+
retry = Math.min(retry * 2, 15000);
|
|
876
|
+
};
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
// my cursor, in canvas coordinates, at most 20 times a second
|
|
880
|
+
let next = null, timer = 0, last = 0;
|
|
881
|
+
const flush = () => {
|
|
882
|
+
timer = 0;
|
|
883
|
+
if (!next || !sock || sock.readyState !== 1) return;
|
|
884
|
+
last = Date.now();
|
|
885
|
+
sock.send(JSON.stringify({ t: "cursor", x: next.x, y: next.y }));
|
|
886
|
+
next = null;
|
|
887
|
+
};
|
|
888
|
+
const sendCursor = (cx, cy) => {
|
|
889
|
+
next = { x: cx, y: cy };
|
|
890
|
+
if (!timer) timer = setTimeout(flush, Math.max(0, 50 - (Date.now() - last)));
|
|
891
|
+
};
|
|
892
|
+
stage.addEventListener("pointermove", (e) => sendCursor((e.clientX - x) / z + minX, (e.clientY - y) / z + minY));
|
|
893
|
+
stage.addEventListener("pointerleave", () => sendCursor(null, null));
|
|
894
|
+
connect();
|
|
895
|
+
}
|
|
719
896
|
})();
|
|
720
897
|
</script>
|
|
721
898
|
</body>
|
|
@@ -723,6 +900,86 @@ button:focus-visible { outline: 2px solid #3b82f6; outline-offset: 1px; }
|
|
|
723
900
|
`;
|
|
724
901
|
}
|
|
725
902
|
|
|
903
|
+
// src/share/wire.ts
|
|
904
|
+
var CHUNK = 256 * 1024;
|
|
905
|
+
var enc = new TextEncoder();
|
|
906
|
+
var dec = new TextDecoder();
|
|
907
|
+
function encodeFrame(header, payload) {
|
|
908
|
+
const head = enc.encode(JSON.stringify(header));
|
|
909
|
+
const out = new Uint8Array(4 + head.length + (payload?.length ?? 0));
|
|
910
|
+
new DataView(out.buffer).setUint32(0, head.length);
|
|
911
|
+
out.set(head, 4);
|
|
912
|
+
if (payload) out.set(payload, 4 + head.length);
|
|
913
|
+
return out;
|
|
914
|
+
}
|
|
915
|
+
function decodeFrame(data) {
|
|
916
|
+
const bytes = data instanceof Uint8Array ? data : new Uint8Array(data);
|
|
917
|
+
if (bytes.length < 4) throw new Error("Short tunnel frame");
|
|
918
|
+
const size = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength).getUint32(0);
|
|
919
|
+
if (size > bytes.length - 4) throw new Error("Bad tunnel frame");
|
|
920
|
+
const header = JSON.parse(dec.decode(bytes.subarray(4, 4 + size)));
|
|
921
|
+
if (!header || typeof header.t !== "string" || typeof header.id !== "string") throw new Error("Bad tunnel frame");
|
|
922
|
+
return { header, payload: bytes.subarray(4 + size) };
|
|
923
|
+
}
|
|
924
|
+
function safePath(path) {
|
|
925
|
+
if (typeof path !== "string" || path.length > 8192) return null;
|
|
926
|
+
if (!path.startsWith("/") || path.startsWith("//") || /[\\\u0000-\u001f\u007f]/.test(path)) return null;
|
|
927
|
+
if (/^\/(%2f|%5c)/i.test(path)) return null;
|
|
928
|
+
return path;
|
|
929
|
+
}
|
|
930
|
+
var DROP_REQUEST = /* @__PURE__ */ new Set([
|
|
931
|
+
"host",
|
|
932
|
+
"connection",
|
|
933
|
+
"keep-alive",
|
|
934
|
+
"upgrade",
|
|
935
|
+
"transfer-encoding",
|
|
936
|
+
"te",
|
|
937
|
+
"trailer",
|
|
938
|
+
"proxy-authorization",
|
|
939
|
+
"proxy-connection",
|
|
940
|
+
"content-length",
|
|
941
|
+
"accept-encoding",
|
|
942
|
+
"origin",
|
|
943
|
+
"referer",
|
|
944
|
+
"authorization",
|
|
945
|
+
"cdn-loop",
|
|
946
|
+
"x-real-ip",
|
|
947
|
+
"true-client-ip",
|
|
948
|
+
"sec-websocket-key",
|
|
949
|
+
"sec-websocket-version",
|
|
950
|
+
"sec-websocket-extensions",
|
|
951
|
+
"sec-websocket-protocol"
|
|
952
|
+
]);
|
|
953
|
+
function requestHeaders(headers) {
|
|
954
|
+
const out = [];
|
|
955
|
+
for (const [k, v] of headers) {
|
|
956
|
+
const key = k.toLowerCase();
|
|
957
|
+
if (DROP_REQUEST.has(key) || key.startsWith("cf-") || key.startsWith("x-forwarded-") || key.startsWith("sec-fetch-") || key.startsWith("x-tc-")) continue;
|
|
958
|
+
if (key === "cookie") {
|
|
959
|
+
const kept = v.split(";").map((c) => c.trim()).filter((c) => c && !c.startsWith("tc_"));
|
|
960
|
+
if (kept.length) out.push(["cookie", kept.join("; ")]);
|
|
961
|
+
continue;
|
|
962
|
+
}
|
|
963
|
+
out.push([key, v]);
|
|
964
|
+
}
|
|
965
|
+
return out;
|
|
966
|
+
}
|
|
967
|
+
var DROP_RESPONSE = /* @__PURE__ */ new Set(["connection", "keep-alive", "transfer-encoding", "content-encoding", "content-length", "upgrade", "trailer", "alt-svc"]);
|
|
968
|
+
function responseHeaders(headers) {
|
|
969
|
+
const out = [];
|
|
970
|
+
for (const [k, v] of headers) {
|
|
971
|
+
const key = k.toLowerCase();
|
|
972
|
+
if (!DROP_RESPONSE.has(key)) out.push([key, v]);
|
|
973
|
+
}
|
|
974
|
+
return out;
|
|
975
|
+
}
|
|
976
|
+
|
|
726
977
|
export {
|
|
727
|
-
viewerHtml
|
|
978
|
+
viewerHtml,
|
|
979
|
+
CHUNK,
|
|
980
|
+
encodeFrame,
|
|
981
|
+
decodeFrame,
|
|
982
|
+
safePath,
|
|
983
|
+
requestHeaders,
|
|
984
|
+
responseHeaders
|
|
728
985
|
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// src/core/config.ts
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
function loadConfig(root, overrides = {}) {
|
|
5
|
+
let file = {};
|
|
6
|
+
const configPath = path.join(root, "truecanvas.config.json");
|
|
7
|
+
if (fs.existsSync(configPath)) file = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
8
|
+
let deps = {};
|
|
9
|
+
try {
|
|
10
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8"));
|
|
11
|
+
deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
12
|
+
} catch {
|
|
13
|
+
deps = {};
|
|
14
|
+
}
|
|
15
|
+
const framework = file.framework ?? detectFramework(deps);
|
|
16
|
+
const vite = framework === "vite";
|
|
17
|
+
const appDir = file.appDir ?? (vite ? fs.existsSync(path.join(root, "src")) ? "src" : "." : fs.existsSync(path.join(root, "src/app")) ? "src/app" : "app");
|
|
18
|
+
const srcPrefix = appDir === "src" || appDir.startsWith("src/") ? "src/" : "";
|
|
19
|
+
const knownLibraries = ["@paper-design/shaders-react"].filter((l) => l in deps);
|
|
20
|
+
return {
|
|
21
|
+
root,
|
|
22
|
+
framework,
|
|
23
|
+
appUrl: vite ? "http://localhost:5173" : "http://localhost:3000",
|
|
24
|
+
port: 4800,
|
|
25
|
+
canvasDir: `${srcPrefix}canvas`,
|
|
26
|
+
appDir,
|
|
27
|
+
routeDir: vite ? ".truecanvas" : `${appDir}/truecanvas`,
|
|
28
|
+
components: [`${srcPrefix}components/**/*.tsx`],
|
|
29
|
+
darkMode: { strategy: "class", value: "dark" },
|
|
30
|
+
...file,
|
|
31
|
+
libraries: [.../* @__PURE__ */ new Set([...knownLibraries, ...file.libraries ?? []])],
|
|
32
|
+
...stripUndefined(overrides)
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
function stripUndefined(o) {
|
|
36
|
+
return Object.fromEntries(Object.entries(o).filter(([, v]) => v !== void 0));
|
|
37
|
+
}
|
|
38
|
+
function detectFramework(deps) {
|
|
39
|
+
return !("next" in deps) && "vite" in deps ? "vite" : "next";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export {
|
|
43
|
+
loadConfig,
|
|
44
|
+
detectFramework
|
|
45
|
+
};
|