pub const PLAYGROUND_HTML: &str = r##"<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Labelize Playground</title>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #0f0f11;
--surface: #1a1a1f;
--surface2: #24242b;
--border: #2e2e38;
--accent: #5b8dee;
--accent-hover: #7aa3f5;
--text: #e2e2e8;
--text-dim: #888896;
--error: #e05555;
--success: #4dbd74;
--radius: 8px;
--font-mono: "JetBrains Mono","Fira Code","Cascadia Code",Consolas,monospace;
--font-ui: system-ui,-apple-system,"Segoe UI",sans-serif;
}
html, body { height: 100%; }
body {
background: var(--bg); color: var(--text);
font-family: var(--font-ui); font-size: 14px;
display: flex; flex-direction: column; overflow: hidden;
}
header {
flex-shrink: 0; background: var(--surface);
border-bottom: 1px solid var(--border);
padding: 10px 20px; display: flex; align-items: center; gap: 14px;
}
.logo { display: flex; align-items: center; gap: 8px; }
.logo-icon {
width: 28px; height: 28px; background: var(--accent);
border-radius: 6px; display: flex; align-items: center;
justify-content: center; font-size: 14px; font-weight: 800; color: #fff;
}
.logo-text { font-size: 16px; font-weight: 700; color: var(--text); }
.tagline {
color: var(--text-dim); font-size: 13px;
border-left: 1px solid var(--border); padding-left: 14px;
}
.header-spacer { flex: 1; }
.badge {
font-size: 11px; color: var(--text-dim); background: var(--surface2);
border: 1px solid var(--border); border-radius: 4px; padding: 2px 7px;
}
main {
flex: 1; display: grid; grid-template-columns: 1fr 1fr; overflow: hidden;
}
/* ── Editor panel ── */
.editor-panel {
display: flex; flex-direction: column;
border-right: 1px solid var(--border); overflow: hidden;
}
.panel-header {
flex-shrink: 0; padding: 7px 14px; background: var(--surface);
border-bottom: 1px solid var(--border); font-size: 11px; font-weight: 600;
text-transform: uppercase; letter-spacing: 0.8px; color: var(--text-dim);
display: flex; align-items: center; gap: 6px;
}
.panel-header .dot { width: 7px; height: 7px; border-radius: 50%; background: var(--accent); }
#zpl-input {
flex: 1; background: var(--bg); color: var(--text);
font-family: var(--font-mono); font-size: 13px; line-height: 1.6;
padding: 16px; border: none; outline: none; resize: none;
tab-size: 2; overflow: auto; caret-color: var(--accent);
}
#zpl-input::selection { background: rgba(91,141,238,0.28); }
/* ── Settings bar ── */
.settings-bar {
flex-shrink: 0; background: var(--surface);
border-top: 1px solid var(--border);
padding: 9px 14px; display: flex; flex-wrap: wrap; gap: 10px; align-items: center;
}
.sg { display: flex; align-items: center; gap: 5px; }
.sg label {
font-size: 11px; font-weight: 600; text-transform: uppercase;
letter-spacing: 0.5px; color: var(--text-dim); white-space: nowrap;
}
.sg input, .sg select {
background: var(--surface2); border: 1px solid var(--border);
border-radius: 5px; color: var(--text); font-size: 12px;
font-family: var(--font-ui); padding: 4px 7px; outline: none;
transition: border-color 0.15s;
}
.sg input:focus, .sg select:focus { border-color: var(--accent); }
.sg input[type="number"] { width: 58px; }
.size-sep { color: var(--text-dim); font-size: 13px; }
.unit-label { font-size: 11px; color: var(--text-dim); }
#open-file-btn {
background: var(--surface2); color: var(--text-dim);
border: 1px solid var(--border); border-radius: 6px;
padding: 6px 13px; font-size: 12px; font-weight: 600;
font-family: var(--font-ui); cursor: pointer;
display: flex; align-items: center; gap: 6px;
transition: border-color 0.15s, color 0.15s; white-space: nowrap;
}
#open-file-btn:hover { border-color: var(--accent); color: var(--accent); }
#file-input { display: none; }
#render-btn {
margin-left: auto; background: var(--accent); color: #fff;
border: none; border-radius: 6px; padding: 7px 18px;
font-size: 13px; font-weight: 600; font-family: var(--font-ui);
cursor: pointer; display: flex; align-items: center; gap: 6px;
transition: background 0.15s, transform 0.1s; white-space: nowrap;
}
#render-btn:hover:not(:disabled) { background: var(--accent-hover); }
#render-btn:active:not(:disabled) { transform: scale(0.97); }
#render-btn:disabled { opacity: 0.55; cursor: not-allowed; }
.shortcut {
font-size: 10px; opacity: 0.7;
background: rgba(255,255,255,0.15); border-radius: 3px; padding: 1px 4px;
}
/* ── Preview panel ── */
.preview-panel {
display: flex; flex-direction: column;
background: var(--bg); overflow: hidden; position: relative;
}
#preview-scroll {
flex: 1; overflow-y: auto;
display: flex; flex-direction: column; align-items: center;
padding: 20px 20px 0; gap: 16px;
}
.empty-state { margin: auto; text-align: center; color: var(--text-dim); }
.empty-state svg { opacity: 0.2; margin-bottom: 12px; display: block; margin-left: auto; margin-right: auto; }
.empty-state p { font-size: 13px; }
#loading {
display: none; position: absolute; inset: 0;
background: rgba(15,15,17,0.72); align-items: center;
justify-content: center; flex-direction: column; gap: 12px; z-index: 10;
}
#loading.active { display: flex; }
.spinner {
width: 32px; height: 32px; border: 3px solid var(--border);
border-top-color: var(--accent); border-radius: 50%;
animation: spin 0.7s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
.loading-text { color: var(--text-dim); font-size: 13px; }
#error-banner {
display: none; width: 100%;
background: rgba(224,85,85,0.10); border: 1px solid var(--error);
border-radius: var(--radius); padding: 12px 16px;
color: var(--error); font-size: 12px; font-family: var(--font-mono);
word-break: break-word; white-space: pre-wrap;
}
#error-banner.visible { display: block; }
#preview-img {
display: none; max-width: 100%;
border: 1px solid var(--border); border-radius: 4px;
box-shadow: 0 4px 24px rgba(0,0,0,0.4); background: #fff;
}
#preview-img.visible { display: block; }
/* ── Download bar ── */
#download-bar {
display: none; width: 100%;
padding: 14px 0 20px; margin-top: 4px;
border-top: 1px solid var(--border);
gap: 10px; justify-content: center; align-items: center; flex-wrap: wrap;
}
#download-bar.visible { display: flex; }
.dl-btn {
display: inline-flex; align-items: center; gap: 7px;
padding: 8px 20px; border-radius: 6px; font-size: 13px;
font-weight: 600; font-family: var(--font-ui);
cursor: pointer; border: none; transition: background 0.15s, opacity 0.15s;
text-decoration: none;
}
.dl-btn-png {
background: var(--surface2); color: var(--text); border: 1px solid var(--border);
}
.dl-btn-png:hover { border-color: var(--accent); color: var(--accent); }
.dl-btn-pdf { background: var(--accent); color: #fff; }
.dl-btn-pdf:hover:not(:disabled) { background: var(--accent-hover); }
.dl-btn-pdf:disabled { opacity: 0.55; cursor: not-allowed; }
.mini-spinner {
display: none; width: 13px; height: 13px;
border: 2px solid rgba(255,255,255,0.3); border-top-color: #fff;
border-radius: 50%; animation: spin 0.7s linear infinite;
}
.dl-btn-pdf.loading .mini-spinner { display: inline-block; }
.dl-btn-pdf.loading .dl-icon-pdf { display: none; }
/* ── Status bar ── */
.status-bar {
flex-shrink: 0; background: var(--surface);
border-top: 1px solid var(--border);
padding: 4px 14px; font-size: 11px; color: var(--text-dim);
display: flex; gap: 16px; align-items: center;
}
.status-ok { color: var(--success); }
.status-err { color: var(--error); }
</style>
</head>
<body>
<header>
<div class="logo">
<span class="logo-icon">L</span>
<span class="logo-text">Labelize</span>
</div>
<span class="tagline">ZPL & EPL Label Playground</span>
<span class="header-spacer"></span>
<span class="badge">v0.5</span>
</header>
<main>
<!-- ── Left: Editor + Settings ── -->
<div class="editor-panel">
<div class="panel-header">
<span class="dot"></span>ZPL / EPL Code
</div>
<textarea id="zpl-input" spellcheck="false" autocomplete="off" autocorrect="off" autocapitalize="off">^XA
^FX Top section with logo, name and address.
^CF0,60
^FO50,50^GB100,100,100^FS
^FO75,75^FR^GB100,100,100^FS
^FO93,93^GB40,40,40^FS
^FO220,50^FDIntershipping, Inc.^FS
^CF0,30
^FO220,115^FD1000 Shipping Lane^FS
^FO220,155^FDShelbyville TN 38102^FS
^FO220,195^FDUnited States (USA)^FS
^FO50,250^GB700,3,3^FS
^FX Second section with recipient address and permit information.
^CFA,30
^FO50,300^FDJohn Doe^FS
^FO50,340^FD100 Main Street^FS
^FO50,380^FDSpringfield TN 39021^FS
^FO50,420^FDUnited States (USA)^FS
^CFA,15
^FO600,300^GB150,150,3^FS
^FO638,340^FDPermit^FS
^FO638,390^FD123456^FS
^FO50,500^GB700,3,3^FS
^FX Third section with bar code.
^BY5,2,270
^FO100,550^BC^FD12345678^FS
^FO100,850^BY2,3,60^BQ,,2^FDQA,https://github.com/GOODBOY008/labelize^FS
^XZ</textarea>
<div class="settings-bar">
<div class="sg">
<label for="fmt">Format</label>
<select id="fmt">
<option value="zpl" selected>ZPL</option>
<option value="epl">EPL</option>
</select>
</div>
<div class="sg">
<label for="size-preset">Size</label>
<select id="size-preset">
<option value="4x6" selected>4 × 6 in</option>
<option value="4x4">4 × 4 in</option>
<option value="4x3">4 × 3 in</option>
<option value="2x4">2 × 4 in</option>
<option value="2x2">2 × 2 in</option>
<option value="3.5x1.5">3.5 × 1.5 in</option>
<option value="custom">Custom…</option>
</select>
</div>
<div class="sg" id="custom-size" style="display:none">
<label for="width-in">W</label>
<input id="width-in" type="number" value="4" min="0.5" max="15" step="0.1">
<span class="size-sep">×</span>
<label for="height-in">H</label>
<input id="height-in" type="number" value="6" min="0.5" max="15" step="0.1">
<span class="unit-label">in</span>
</div>
<div class="sg">
<label for="dpmm">dpmm</label>
<select id="dpmm">
<option value="6">6</option>
<option value="8" selected>8</option>
<option value="12">12</option>
<option value="24">24</option>
</select>
</div>
<input id="file-input" type="file" accept=".zpl,.epl">
<button id="open-file-btn">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2">
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/>
</svg>
Open File
</button>
<button id="render-btn">
▶ Render <span class="shortcut">Ctrl+⏎</span>
</button>
</div>
</div>
<!-- ── Right: Preview ── -->
<div class="preview-panel">
<div class="panel-header">
<span class="dot" style="background:var(--success)"></span>Preview
<span id="status-text" style="margin-left:auto;font-size:11px;text-transform:none;letter-spacing:0;font-weight:400"></span>
</div>
<div id="loading">
<div class="spinner"></div>
<span class="loading-text">Rendering…</span>
</div>
<div id="preview-scroll">
<div class="empty-state" id="empty-state">
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.2">
<rect x="3" y="3" width="18" height="18" rx="2"/>
<line x1="3" y1="9" x2="21" y2="9"/>
<line x1="9" y1="21" x2="9" y2="9"/>
</svg>
<p>Press <strong>Render</strong> to preview your label</p>
<p style="font-size:11px;margin-top:6px;opacity:0.55">Ctrl+Enter</p>
</div>
<div id="error-banner"></div>
<img id="preview-img" alt="Rendered label">
<!-- Download bar — appears after a successful render -->
<div id="download-bar">
<a id="dl-png" class="dl-btn dl-btn-png" href="#" download="label.png">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
<polyline points="7 10 12 15 17 10"/>
<line x1="12" y1="15" x2="12" y2="3"/>
</svg>
Download PNG
</a>
<button id="dl-pdf" class="dl-btn dl-btn-pdf">
<svg class="dl-icon-pdf" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
<polyline points="7 10 12 15 17 10"/>
<line x1="12" y1="15" x2="12" y2="3"/>
</svg>
<span class="mini-spinner"></span>
Download PDF
</button>
</div>
</div><!-- /preview-scroll -->
<div class="status-bar">
<span id="status-size"></span>
<span id="status-time"></span>
</div>
</div>
</main>
<script>
(function () {
"use strict";
var btn = document.getElementById("render-btn");
var openFileBtn = document.getElementById("open-file-btn");
var fileInput = document.getElementById("file-input");
var input = document.getElementById("zpl-input");
var loading = document.getElementById("loading");
var emptyState = document.getElementById("empty-state");
var errBanner = document.getElementById("error-banner");
var previewImg = document.getElementById("preview-img");
var dlBar = document.getElementById("download-bar");
var dlPng = document.getElementById("dl-png");
var dlPdf = document.getElementById("dl-pdf");
var sizePreset = document.getElementById("size-preset");
var customSize = document.getElementById("custom-size");
var widthIn = document.getElementById("width-in");
var heightIn = document.getElementById("height-in");
var statusText = document.getElementById("status-text");
var statusSize = document.getElementById("status-size");
var statusTime = document.getElementById("status-time");
var pngBlobUrl = null;
var SIZE_PRESETS = {
"4x6": [4, 6],
"4x4": [4, 4],
"4x3": [4, 3],
"2x4": [2, 4],
"2x2": [2, 2],
"3.5x1.5": [3.5, 1.5]
};
sizePreset.addEventListener("change", function () {
var isCustom = this.value === "custom";
customSize.style.display = isCustom ? "flex" : "none";
if (!isCustom) {
var wh = SIZE_PRESETS[this.value];
widthIn.value = wh[0];
heightIn.value = wh[1];
}
});
function getParams() {
var fmt = document.getElementById("fmt").value;
var dpmm = parseInt(document.getElementById("dpmm").value, 10) || 8;
var w_in = parseFloat(widthIn.value) || 4;
var h_in = parseFloat(heightIn.value) || 6;
var w_mm = +(w_in * 25.4).toFixed(2);
var h_mm = +(h_in * 25.4).toFixed(2);
return { fmt: fmt, dpmm: dpmm, w_mm: w_mm, h_mm: h_mm };
}
function buildUrl(p, output) {
var u = "/convert?width=" + p.w_mm + "&height=" + p.h_mm + "&dpmm=" + p.dpmm;
if (output) u += "&output=" + output;
return u;
}
function ctFor(fmt) {
return fmt === "epl" ? "application/epl" : "application/zpl";
}
function clearPreview() {
errBanner.classList.remove("visible");
previewImg.classList.remove("visible");
dlBar.classList.remove("visible");
emptyState.style.display = "none";
if (pngBlobUrl) { URL.revokeObjectURL(pngBlobUrl); pngBlobUrl = null; }
}
function showError(msg) {
clearPreview();
errBanner.textContent = msg;
errBanner.classList.add("visible");
statusText.textContent = "Error";
statusText.className = "status-err";
}
/* ── Render (PNG) ── */
function render() {
var zpl = input.value.trim();
if (!zpl) { showError("Editor is empty — paste some ZPL or EPL first."); return; }
var params = getParams();
clearPreview();
loading.classList.add("active");
btn.disabled = true;
statusText.textContent = "Rendering\u2026";
statusText.className = "";
statusSize.textContent = "";
statusTime.textContent = "";
var t0 = performance.now();
fetch(buildUrl(params, null), {
method: "POST",
headers: { "Content-Type": ctFor(params.fmt) },
body: zpl
})
.then(function (res) {
var elapsed = Math.round(performance.now() - t0);
loading.classList.remove("active");
btn.disabled = false;
if (!res.ok) {
return res.text().then(function (txt) {
showError("Server error " + res.status + ": " + txt);
});
}
return res.blob().then(function (blob) {
pngBlobUrl = URL.createObjectURL(blob);
dlPng.href = pngBlobUrl;
previewImg.src = pngBlobUrl;
previewImg.classList.add("visible");
dlBar.classList.add("visible");
statusSize.textContent = "PNG " + (blob.size / 1024).toFixed(1) + " KB";
statusTime.textContent = elapsed + " ms";
statusText.textContent = "OK";
statusText.className = "status-ok";
});
})
.catch(function (err) {
loading.classList.remove("active");
btn.disabled = false;
showError("Network error: " + err.message);
});
}
/* ── PDF download (lazy) ── */
dlPdf.addEventListener("click", function () {
var zpl = input.value.trim();
if (!zpl) return;
var params = getParams();
dlPdf.classList.add("loading");
dlPdf.disabled = true;
fetch(buildUrl(params, "pdf"), {
method: "POST",
headers: { "Content-Type": ctFor(params.fmt) },
body: zpl
})
.then(function (res) {
dlPdf.classList.remove("loading");
dlPdf.disabled = false;
if (!res.ok) {
return res.text().then(function (txt) {
showError("PDF error " + res.status + ": " + txt);
});
}
return res.blob().then(function (blob) {
var url = URL.createObjectURL(blob);
var a = document.createElement("a");
a.href = url;
a.download = "label.pdf";
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
setTimeout(function () { URL.revokeObjectURL(url); }, 10000);
});
})
.catch(function (err) {
dlPdf.classList.remove("loading");
dlPdf.disabled = false;
showError("PDF download error: " + err.message);
});
});
/* ── Open File ── */
openFileBtn.addEventListener("click", function () { fileInput.click(); });
fileInput.addEventListener("change", function () {
var file = this.files && this.files[0];
if (!file) return;
var ext = file.name.split(".").pop().toLowerCase();
var fmtSel = document.getElementById("fmt");
if (ext === "epl") fmtSel.value = "epl";
else fmtSel.value = "zpl";
var reader = new FileReader();
reader.onload = function (e) {
input.value = e.target.result;
input.focus();
};
reader.readAsText(file);
this.value = "";
});
btn.addEventListener("click", render);
input.addEventListener("keydown", function (e) {
if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) {
e.preventDefault();
render();
}
});
})();
</script>
</body>
</html>
"##;