|
| 1 | +const els = { |
| 2 | + label: document.getElementById("b-label"), |
| 3 | + message: document.getElementById("b-message"), |
| 4 | + style: document.getElementById("b-style"), |
| 5 | + labelColor: document.getElementById("b-labelcolor"), |
| 6 | + swatches: document.getElementById("b-swatches"), |
| 7 | + preview: document.getElementById("b-preview"), |
| 8 | + md: document.getElementById("snippet-md"), |
| 9 | + html: document.getElementById("snippet-html"), |
| 10 | +}; |
| 11 | + |
| 12 | +const SWATCHES = [ |
| 13 | + { name: "bytes pink", value: "#EC4899" }, |
| 14 | + { name: "brand purple", value: "#7C4DFF" }, |
| 15 | + { name: "success green", value: "#2da44e" }, |
| 16 | + { name: "H# red", value: "#9c1120" }, |
| 17 | + { name: "Hacker Lang purple", value: "#8250df" }, |
| 18 | + { name: "HackerScript gray", value: "#8a8a94" }, |
| 19 | + { name: "info blue", value: "#0969da" }, |
| 20 | + { name: "warning orange", value: "#FB923C" }, |
| 21 | +]; |
| 22 | + |
| 23 | +let messageColor = SWATCHES[0].value; |
| 24 | + |
| 25 | +function buildSwatches() { |
| 26 | + els.swatches.innerHTML = SWATCHES.map( |
| 27 | + (s, i) => |
| 28 | + `<span class="swatch ${i === 0 ? "is-active" : ""}" data-color="${s.value}" style="background:${s.value}" title="${s.name}"></span>` |
| 29 | + ).join(""); |
| 30 | + els.swatches.querySelectorAll(".swatch").forEach((el) => { |
| 31 | + el.addEventListener("click", () => { |
| 32 | + messageColor = el.dataset.color; |
| 33 | + els.swatches.querySelectorAll(".swatch").forEach((s) => s.classList.remove("is-active")); |
| 34 | + el.classList.add("is-active"); |
| 35 | + update(); |
| 36 | + }); |
| 37 | + }); |
| 38 | +} |
| 39 | + |
| 40 | +/** Measures text width in the badge's font so segments size correctly, like shields.io. */ |
| 41 | +function textWidth(text, fontSize = 11) { |
| 42 | + const canvas = textWidth._canvas || (textWidth._canvas = document.createElement("canvas")); |
| 43 | + const ctx = canvas.getContext("2d"); |
| 44 | + ctx.font = `bold ${fontSize}px Verdana, Geneva, sans-serif`; |
| 45 | + return ctx.measureText(text).width; |
| 46 | +} |
| 47 | + |
| 48 | +function escapeXml(s) { |
| 49 | + return String(s).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">"); |
| 50 | +} |
| 51 | + |
| 52 | +function buildSvg({ label, message, style, labelColor, messageColor }) { |
| 53 | + const pad = 10; |
| 54 | + const labelW = Math.round(textWidth(label) + pad * 2); |
| 55 | + const messageW = Math.round(textWidth(message) + pad * 2); |
| 56 | + const height = 20; |
| 57 | + const totalW = labelW + messageW; |
| 58 | + const radius = style === "flat-square" ? 0 : 3; |
| 59 | + const gloss = |
| 60 | + style === "plastic" |
| 61 | + ? `<linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#fff" stop-opacity=".2"/><stop offset=".1" stop-color="#aaa" stop-opacity=".1"/><stop offset=".9" stop-color="#000" stop-opacity=".1"/><stop offset="1" stop-color="#000" stop-opacity=".2"/></linearGradient>` |
| 62 | + : ""; |
| 63 | + |
| 64 | + return `<svg xmlns="http://www.w3.org/2000/svg" width="${totalW}" height="${height}" role="img" aria-label="${escapeXml(label)}: ${escapeXml(message)}"> |
| 65 | + <defs>${gloss}</defs> |
| 66 | + <clipPath id="r"><rect width="${totalW}" height="${height}" rx="${radius}" fill="#fff"/></clipPath> |
| 67 | + <g clip-path="url(#r)"> |
| 68 | + <rect width="${labelW}" height="${height}" fill="${labelColor}"/> |
| 69 | + <rect x="${labelW}" width="${messageW}" height="${height}" fill="${messageColor}"/> |
| 70 | + ${gloss ? `<rect width="${totalW}" height="${height}" fill="url(#s)"/>` : ""} |
| 71 | + </g> |
| 72 | + <g fill="#fff" text-anchor="middle" font-family="Verdana, Geneva, sans-serif" font-size="11" font-weight="bold"> |
| 73 | + <text x="${labelW / 2}" y="14">${escapeXml(label)}</text> |
| 74 | + <text x="${labelW + messageW / 2}" y="14">${escapeXml(message)}</text> |
| 75 | + </g> |
| 76 | +</svg>`; |
| 77 | +} |
| 78 | + |
| 79 | +function update() { |
| 80 | + const label = els.label.value.trim() || "label"; |
| 81 | + const message = els.message.value.trim() || "message"; |
| 82 | + const style = els.style.value; |
| 83 | + const labelColor = els.labelColor.value.trim() || "#4a4a58"; |
| 84 | + |
| 85 | + const svg = buildSvg({ label, message, style, labelColor, messageColor }); |
| 86 | + const dataUri = `data:image/svg+xml;base64,${btoa(unescape(encodeURIComponent(svg)))}`; |
| 87 | + |
| 88 | + els.preview.innerHTML = `<img src="${dataUri}" alt="${label}: ${message}" width="${Math.round( |
| 89 | + (textWidth(label) + 20) + (textWidth(message) + 20) |
| 90 | + )}" height="20" />`; |
| 91 | + |
| 92 | + els.md.value = `[](https://bytes-repository.github.io/index.html)`; |
| 93 | + els.html.value = `<a href="https://bytes-repository.github.io/index.html"><img src="${dataUri}" alt="${label}: ${message}" /></a>`; |
| 94 | +} |
| 95 | + |
| 96 | +document.querySelectorAll("[data-copy-target]").forEach((btn) => { |
| 97 | + btn.addEventListener("click", async () => { |
| 98 | + const target = document.getElementById(btn.dataset.copyTarget); |
| 99 | + try { |
| 100 | + await navigator.clipboard.writeText(target.value); |
| 101 | + const original = btn.textContent; |
| 102 | + btn.textContent = "Copied"; |
| 103 | + setTimeout(() => (btn.textContent = original), 1300); |
| 104 | + } catch { |
| 105 | + target.select(); |
| 106 | + } |
| 107 | + }); |
| 108 | +}); |
| 109 | + |
| 110 | +[els.label, els.message, els.style, els.labelColor].forEach((el) => el.addEventListener("input", update)); |
| 111 | + |
| 112 | +buildSwatches(); |
| 113 | +update(); |
0 commit comments