Spaces:
Running
Running
Add live web preview: code bars with copy + sandboxed iframe preview for HTML/CSS/JS
Browse files- index.html +223 -2
index.html
CHANGED
|
@@ -327,10 +327,11 @@
|
|
| 327 |
background: #050505 !important;
|
| 328 |
border: 1px solid var(--border) !important;
|
| 329 |
border-left: 2px solid var(--green) !important;
|
| 330 |
-
border-radius: 6px !important;
|
| 331 |
padding: 14px 16px !important;
|
| 332 |
overflow-x: auto;
|
| 333 |
-
margin: 12px 0 !important;
|
|
|
|
| 334 |
}
|
| 335 |
|
| 336 |
.msg-body pre code {
|
|
@@ -342,6 +343,115 @@
|
|
| 342 |
line-height: 1.7 !important;
|
| 343 |
}
|
| 344 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 345 |
.msg-body blockquote {
|
| 346 |
border-left: 2px solid var(--green);
|
| 347 |
padding-left: 14px;
|
|
@@ -911,12 +1021,123 @@ function makeRenderer(el) {
|
|
| 911 |
finish() {
|
| 912 |
el.id = "";
|
| 913 |
el.innerHTML = marked.parse(buf);
|
|
|
|
| 914 |
scrollBot();
|
| 915 |
return buf;
|
| 916 |
}
|
| 917 |
};
|
| 918 |
}
|
| 919 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 920 |
/* ββ Send ββ */
|
| 921 |
window.handleSend = async function() {
|
| 922 |
if (isGenerating) return;
|
|
|
|
| 327 |
background: #050505 !important;
|
| 328 |
border: 1px solid var(--border) !important;
|
| 329 |
border-left: 2px solid var(--green) !important;
|
| 330 |
+
border-radius: 6px 6px 0 0 !important;
|
| 331 |
padding: 14px 16px !important;
|
| 332 |
overflow-x: auto;
|
| 333 |
+
margin: 12px 0 0 !important;
|
| 334 |
+
position: relative;
|
| 335 |
}
|
| 336 |
|
| 337 |
.msg-body pre code {
|
|
|
|
| 343 |
line-height: 1.7 !important;
|
| 344 |
}
|
| 345 |
|
| 346 |
+
/* Code block action bar */
|
| 347 |
+
.code-bar {
|
| 348 |
+
display: flex;
|
| 349 |
+
align-items: center;
|
| 350 |
+
justify-content: space-between;
|
| 351 |
+
background: #080808;
|
| 352 |
+
border: 1px solid var(--border);
|
| 353 |
+
border-top: none;
|
| 354 |
+
border-radius: 0 0 6px 6px;
|
| 355 |
+
padding: 5px 12px;
|
| 356 |
+
margin: 0 0 12px;
|
| 357 |
+
gap: 8px;
|
| 358 |
+
}
|
| 359 |
+
|
| 360 |
+
.code-lang {
|
| 361 |
+
font-size: 10px;
|
| 362 |
+
color: var(--text-muted);
|
| 363 |
+
letter-spacing: 0.06em;
|
| 364 |
+
text-transform: uppercase;
|
| 365 |
+
font-family: var(--mono);
|
| 366 |
+
}
|
| 367 |
+
|
| 368 |
+
.code-actions { display: flex; gap: 6px; }
|
| 369 |
+
|
| 370 |
+
.code-btn {
|
| 371 |
+
background: transparent;
|
| 372 |
+
border: 1px solid var(--border);
|
| 373 |
+
border-radius: 4px;
|
| 374 |
+
color: var(--text-dim);
|
| 375 |
+
font-family: var(--mono);
|
| 376 |
+
font-size: 10px;
|
| 377 |
+
padding: 3px 9px;
|
| 378 |
+
cursor: pointer;
|
| 379 |
+
transition: all 0.15s;
|
| 380 |
+
letter-spacing: 0.04em;
|
| 381 |
+
}
|
| 382 |
+
.code-btn:hover {
|
| 383 |
+
border-color: var(--border-hi);
|
| 384 |
+
color: var(--green);
|
| 385 |
+
background: var(--green-dim);
|
| 386 |
+
}
|
| 387 |
+
.code-btn.active {
|
| 388 |
+
border-color: var(--green);
|
| 389 |
+
color: var(--green);
|
| 390 |
+
background: var(--green-dim);
|
| 391 |
+
}
|
| 392 |
+
|
| 393 |
+
/* Live preview iframe panel */
|
| 394 |
+
.preview-panel {
|
| 395 |
+
display: none;
|
| 396 |
+
border: 1px solid var(--border-hi);
|
| 397 |
+
border-top: 2px solid var(--green);
|
| 398 |
+
border-radius: 0 0 8px 8px;
|
| 399 |
+
margin: -12px 0 12px;
|
| 400 |
+
overflow: hidden;
|
| 401 |
+
background: #fff;
|
| 402 |
+
position: relative;
|
| 403 |
+
}
|
| 404 |
+
.preview-panel.open { display: block; }
|
| 405 |
+
|
| 406 |
+
.preview-topbar {
|
| 407 |
+
display: flex;
|
| 408 |
+
align-items: center;
|
| 409 |
+
justify-content: space-between;
|
| 410 |
+
padding: 5px 12px;
|
| 411 |
+
background: #0d0d0d;
|
| 412 |
+
border-bottom: 1px solid var(--border);
|
| 413 |
+
}
|
| 414 |
+
|
| 415 |
+
.preview-url {
|
| 416 |
+
font-size: 10px;
|
| 417 |
+
color: var(--text-muted);
|
| 418 |
+
font-family: var(--mono);
|
| 419 |
+
letter-spacing: 0.03em;
|
| 420 |
+
}
|
| 421 |
+
|
| 422 |
+
.preview-resize {
|
| 423 |
+
display: flex;
|
| 424 |
+
gap: 4px;
|
| 425 |
+
}
|
| 426 |
+
|
| 427 |
+
.resize-btn {
|
| 428 |
+
background: transparent;
|
| 429 |
+
border: 1px solid var(--border);
|
| 430 |
+
border-radius: 3px;
|
| 431 |
+
color: var(--text-muted);
|
| 432 |
+
font-family: var(--mono);
|
| 433 |
+
font-size: 9px;
|
| 434 |
+
padding: 2px 7px;
|
| 435 |
+
cursor: pointer;
|
| 436 |
+
transition: all 0.15s;
|
| 437 |
+
}
|
| 438 |
+
.resize-btn:hover { color: var(--green); border-color: var(--border-hi); }
|
| 439 |
+
.resize-btn.active { color: var(--green); border-color: var(--green); background: var(--green-dim); }
|
| 440 |
+
|
| 441 |
+
.preview-frame-wrap {
|
| 442 |
+
width: 100%;
|
| 443 |
+
overflow: hidden;
|
| 444 |
+
transition: height 0.3s ease;
|
| 445 |
+
}
|
| 446 |
+
|
| 447 |
+
.preview-iframe {
|
| 448 |
+
width: 100%;
|
| 449 |
+
height: 100%;
|
| 450 |
+
border: none;
|
| 451 |
+
display: block;
|
| 452 |
+
background: #fff;
|
| 453 |
+
}
|
| 454 |
+
|
| 455 |
.msg-body blockquote {
|
| 456 |
border-left: 2px solid var(--green);
|
| 457 |
padding-left: 14px;
|
|
|
|
| 1021 |
finish() {
|
| 1022 |
el.id = "";
|
| 1023 |
el.innerHTML = marked.parse(buf);
|
| 1024 |
+
injectCodeBars(el);
|
| 1025 |
scrollBot();
|
| 1026 |
return buf;
|
| 1027 |
}
|
| 1028 |
};
|
| 1029 |
}
|
| 1030 |
|
| 1031 |
+
/* ββ Code bar + live preview injector ββ */
|
| 1032 |
+
function injectCodeBars(container) {
|
| 1033 |
+
const pres = container.querySelectorAll("pre");
|
| 1034 |
+
pres.forEach(pre => {
|
| 1035 |
+
// Avoid double-processing
|
| 1036 |
+
if (pre.nextElementSibling && pre.nextElementSibling.classList.contains("code-bar")) return;
|
| 1037 |
+
|
| 1038 |
+
const codeEl = pre.querySelector("code");
|
| 1039 |
+
const rawCode = codeEl ? codeEl.textContent : pre.textContent;
|
| 1040 |
+
|
| 1041 |
+
// Detect language from class (e.g. language-html)
|
| 1042 |
+
const cls = codeEl ? codeEl.className : "";
|
| 1043 |
+
const langMatch = cls.match(/language-(\w+)/);
|
| 1044 |
+
const lang = langMatch ? langMatch[1].toLowerCase() : "";
|
| 1045 |
+
|
| 1046 |
+
// Determine if this is a web-previewable block
|
| 1047 |
+
const isHtml = lang === "html" || lang === "svg";
|
| 1048 |
+
const isPreviewable = isHtml ||
|
| 1049 |
+
(!lang && rawCode.trim().startsWith("<") && rawCode.includes("</"));
|
| 1050 |
+
|
| 1051 |
+
// Build action bar
|
| 1052 |
+
const bar = document.createElement("div");
|
| 1053 |
+
bar.className = "code-bar";
|
| 1054 |
+
|
| 1055 |
+
let previewBtn = "";
|
| 1056 |
+
if (isPreviewable || lang === "css" || lang === "js" || lang === "javascript") {
|
| 1057 |
+
previewBtn = `<button class="code-btn preview-toggle" title="Render preview">βΆ Preview</button>`;
|
| 1058 |
+
}
|
| 1059 |
+
|
| 1060 |
+
bar.innerHTML = `
|
| 1061 |
+
<span class="code-lang">${lang || "code"}</span>
|
| 1062 |
+
<div class="code-actions">
|
| 1063 |
+
${previewBtn}
|
| 1064 |
+
<button class="code-btn copy-btn" title="Copy">Copy</button>
|
| 1065 |
+
</div>
|
| 1066 |
+
`;
|
| 1067 |
+
|
| 1068 |
+
pre.after(bar);
|
| 1069 |
+
|
| 1070 |
+
// Copy button
|
| 1071 |
+
const copyBtn = bar.querySelector(".copy-btn");
|
| 1072 |
+
copyBtn.addEventListener("click", () => {
|
| 1073 |
+
navigator.clipboard.writeText(rawCode).then(() => {
|
| 1074 |
+
copyBtn.textContent = "Copied!";
|
| 1075 |
+
copyBtn.classList.add("active");
|
| 1076 |
+
setTimeout(() => { copyBtn.textContent = "Copy"; copyBtn.classList.remove("active"); }, 1800);
|
| 1077 |
+
});
|
| 1078 |
+
});
|
| 1079 |
+
|
| 1080 |
+
// Preview toggle
|
| 1081 |
+
const pvBtn = bar.querySelector(".preview-toggle");
|
| 1082 |
+
if (!pvBtn) return;
|
| 1083 |
+
|
| 1084 |
+
// Build preview panel (hidden by default)
|
| 1085 |
+
const panel = document.createElement("div");
|
| 1086 |
+
panel.className = "preview-panel";
|
| 1087 |
+
|
| 1088 |
+
// Compose the HTML to render
|
| 1089 |
+
let renderSrc = rawCode;
|
| 1090 |
+
if (lang === "css") {
|
| 1091 |
+
renderSrc = `<!DOCTYPE html><html><head><style>${rawCode}</style></head><body><p style='font-family:sans-serif;padding:20px;color:#333'>CSS preview β add HTML to see layout</p></body></html>`;
|
| 1092 |
+
} else if (lang === "js" || lang === "javascript") {
|
| 1093 |
+
renderSrc = `<!DOCTYPE html><html><body><script>${rawCode}<\/script></body></html>`;
|
| 1094 |
+
}
|
| 1095 |
+
|
| 1096 |
+
panel.innerHTML = `
|
| 1097 |
+
<div class="preview-topbar">
|
| 1098 |
+
<span class="preview-url">β£ live preview β sandbox</span>
|
| 1099 |
+
<div class="preview-resize">
|
| 1100 |
+
<button class="resize-btn" data-h="260">S</button>
|
| 1101 |
+
<button class="resize-btn active" data-h="420">M</button>
|
| 1102 |
+
<button class="resize-btn" data-h="620">L</button>
|
| 1103 |
+
<button class="resize-btn" data-h="900">XL</button>
|
| 1104 |
+
</div>
|
| 1105 |
+
</div>
|
| 1106 |
+
<div class="preview-frame-wrap" style="height:420px">
|
| 1107 |
+
<iframe class="preview-iframe"
|
| 1108 |
+
sandbox="allow-scripts allow-forms allow-modals"
|
| 1109 |
+
srcdoc=""
|
| 1110 |
+
></iframe>
|
| 1111 |
+
</div>
|
| 1112 |
+
`;
|
| 1113 |
+
|
| 1114 |
+
bar.after(panel);
|
| 1115 |
+
|
| 1116 |
+
// Resize buttons
|
| 1117 |
+
panel.querySelectorAll(".resize-btn").forEach(rb => {
|
| 1118 |
+
rb.addEventListener("click", () => {
|
| 1119 |
+
panel.querySelectorAll(".resize-btn").forEach(b => b.classList.remove("active"));
|
| 1120 |
+
rb.classList.add("active");
|
| 1121 |
+
panel.querySelector(".preview-frame-wrap").style.height = rb.dataset.h + "px";
|
| 1122 |
+
});
|
| 1123 |
+
});
|
| 1124 |
+
|
| 1125 |
+
let loaded = false;
|
| 1126 |
+
pvBtn.addEventListener("click", () => {
|
| 1127 |
+
const isOpen = panel.classList.toggle("open");
|
| 1128 |
+
pvBtn.textContent = isOpen ? "βΌ Hide" : "βΆ Preview";
|
| 1129 |
+
pvBtn.classList.toggle("active", isOpen);
|
| 1130 |
+
|
| 1131 |
+
if (isOpen && !loaded) {
|
| 1132 |
+
loaded = true;
|
| 1133 |
+
const iframe = panel.querySelector(".preview-iframe");
|
| 1134 |
+
iframe.srcdoc = renderSrc;
|
| 1135 |
+
}
|
| 1136 |
+
scrollBot();
|
| 1137 |
+
});
|
| 1138 |
+
});
|
| 1139 |
+
}
|
| 1140 |
+
|
| 1141 |
/* ββ Send ββ */
|
| 1142 |
window.handleSend = async function() {
|
| 1143 |
if (isGenerating) return;
|