refactor: update scoreboard layout to use flexbox for improved responsiveness and clarity

This commit is contained in:
Sarto
2026-05-03 11:35:31 +03:30
parent 6e062dbc73
commit 6a6255bd31
2 changed files with 89 additions and 477 deletions
+89 -83
View File
@@ -1594,9 +1594,57 @@
background: var(--card-bg)
}
/* Bank-row action buttons (+ / ✕). Sized 32×32 with a real
border so they're easy to tap on touch screens; spaced apart
so a stray tap on "+" doesn't hit "✕". */
/* Bank scoreboard rows — vertical stack of cards, two columns:
a "main" block with address + a wrapped stats line, and a
fixed-width actions column on the trailing edge. The action
buttons stay visible on phone widths because the main column
flexes/wraps instead of pushing the actions off-screen. */
.rb-rows {
display: flex;
flex-direction: column;
gap: 6px
}
.rb-row {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 10px;
border: 1px solid var(--border);
border-radius: 10px;
background: var(--card-bg)
}
.rb-row-main {
flex: 1;
min-width: 0
}
.rb-row-addr {
font-family: monospace;
font-size: 13px;
color: var(--text);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap
}
.rb-row-stats {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 3px;
font-size: 11px;
color: var(--text-dim)
}
.rb-row-actions {
display: flex;
align-items: center;
gap: 6px;
flex-shrink: 0
}
.rb-row-btn {
display: inline-flex;
align-items: center;
@@ -1613,10 +1661,6 @@
transition: background 120ms, border-color 120ms, color 120ms
}
.rb-row-btn + .rb-row-btn {
margin-inline-start: 8px
}
.rb-row-add {
color: var(--accent)
}
@@ -2376,8 +2420,6 @@
border-radius: 2px
}
</style>
<!-- Lazy-loadable; only used when the share modal asks for QR. -->
<script src="/static/qrcode.js" defer></script>
</head>
<body>
@@ -2656,13 +2698,8 @@
<label data-i18n="share_uri">Share URI</label>
<textarea id="shareModalUri" readonly rows="3" style="width:100%;font-family:monospace;font-size:12px;resize:vertical;box-sizing:border-box"></textarea>
</div>
<div id="shareModalQrWrap" style="display:none;text-align:center;margin-top:8px">
<div id="shareModalQr" style="display:inline-block;background:#fff;padding:10px;border-radius:8px"></div>
<div style="font-size:11px;color:var(--text-dim);margin-top:6px" data-i18n="share_qr_hint">Scan with another device to import</div>
</div>
<div class="modal-actions">
<button class="btn btn-flat" onclick="closeShareModal()" data-i18n="close">Close</button>
<button class="btn btn-outline" id="shareModalQrBtn" onclick="toggleShareModalQr()" data-i18n="show_qr">Show QR</button>
<button class="btn btn-primary" onclick="copyShareModalUri()" data-i18n="copy">Copy</button>
</div>
</div>
@@ -2992,9 +3029,6 @@
share_source: 'منبع',
profile_name: 'نام پروفایل',
share_uri: 'پیوند اشتراک',
show_qr: 'نمایش QR',
hide_qr: 'پنهان کردن QR',
share_qr_hint: 'برای ایمپورت با دستگاه دیگر اسکن کن',
paste: 'چسباندن',
clipboard_blocked: 'دسترسی به کلیپ‌بورد ممکن نیست',
resolver_list_switch_failed: 'تغییر لیست ممکن نشد',
@@ -3205,9 +3239,6 @@
share_source: 'Source',
profile_name: 'Profile name',
share_uri: 'Share URI',
show_qr: 'Show QR',
hide_qr: 'Hide QR',
share_qr_hint: 'Scan with another device to import',
paste: 'Paste',
clipboard_blocked: 'Clipboard access blocked',
resolver_list_switch_failed: 'Could not switch list',
@@ -4073,6 +4104,9 @@
var menu = document.getElementById('resolverTabMenu');
if (!menu) return;
if (!menu.hidden) { hideResolverTabMenu(); return; }
// Defensive: drop any leftover outside-click listener from a
// previous open before installing a new one.
document.removeEventListener('click', _resolverTabMenuOutside);
// Use viewport coordinates from getBoundingClientRect — pairs
// with position: fixed in CSS. Anchor the menu's top edge just
// below the ⋮ button, and align horizontally so the menu hangs
@@ -4123,6 +4157,12 @@
var bankAddPickerAddr = '';
function openBankAddPicker(anchor, addr) {
// Strip any pending outside-click listener from a previous
// open. Without this, clicking + on a second row while the
// menu is still open lets the stale listener fire on the new
// click and snap-close the freshly-opened menu — looks like
// "+ doesn't work" from the user's seat.
hideBankAddMenu();
bankAddPickerAddr = addr;
var menu = document.getElementById('bankAddMenu');
if (!menu) return;
@@ -4520,9 +4560,6 @@
var p = (profiles && profiles.profiles || []).find(function (x) { return x.id === id });
if (!p) return;
document.getElementById('shareModalProfileName').textContent = p.nickname || p.config.domain;
document.getElementById('shareModalQrWrap').style.display = 'none';
var qrBtn = document.getElementById('shareModalQrBtn');
if (qrBtn) qrBtn.textContent = t('show_qr') || 'Show QR';
// Make sure rlState is loaded — the user may not have opened
// the Bank modal yet, in which case rlState is its empty
// default and the source dropdown would only have "Bank".
@@ -4601,10 +4638,6 @@
var uri = getShareModalUri();
var ta = document.getElementById('shareModalUri');
if (ta) ta.value = uri || (t('no_config') || '');
// If QR is currently visible, keep it in sync with the URI.
if (document.getElementById('shareModalQrWrap').style.display === 'block') {
renderShareModalQr(uri);
}
}
function copyShareModalUri() {
@@ -4617,34 +4650,6 @@
});
}
function toggleShareModalQr() {
var wrap = document.getElementById('shareModalQrWrap');
var btn = document.getElementById('shareModalQrBtn');
if (!wrap) return;
if (wrap.style.display === 'block') {
wrap.style.display = 'none';
if (btn) btn.textContent = t('show_qr') || 'Show QR';
} else {
wrap.style.display = 'block';
if (btn) btn.textContent = t('hide_qr') || 'Hide QR';
renderShareModalQr(getShareModalUri());
}
}
function renderShareModalQr(uri) {
var box = document.getElementById('shareModalQr');
if (!box) return;
if (!uri) { box.innerHTML = ''; return }
try {
if (typeof qrEncodeSvg === 'function') {
box.innerHTML = qrEncodeSvg(uri, 6);
} else {
box.innerHTML = '<span style="color:#000;font-size:12px;padding:8px">QR not available</span>';
}
} catch (e) {
box.innerHTML = '<span style="color:#a00;font-size:12px;padding:8px">' + esc(e.message || 'QR error') + '</span>';
}
}
async function activateProfile(id) {
if (id === activeProfileId) { closeProfiles(); return }
@@ -7463,43 +7468,39 @@
var resolversRefreshTimer = null;
function _buildScoreboardTable(board, showRemove, removeFromBank) {
if (!board.length) return '<div style="color:var(--text-dim)">' + t('no_active_resolvers') + '</div>';
var h = '<table style="width:100%;border-collapse:collapse;font-size:12px">';
h += '<thead><tr style="border-bottom:2px solid var(--border);text-align:left">';
h += '<th style="padding:6px 8px">Resolver</th>';
h += '<th style="padding:6px 8px;text-align:right">' + t('resolver_speed') + '</th>';
h += '<th style="padding:6px 8px;text-align:right">' + t('resolver_score') + '</th>';
h += '<th style="padding:6px 8px;text-align:center">\u2705</th>';
h += '<th style="padding:6px 8px;text-align:center">\u274C</th>';
if (showRemove) h += '<th style="padding:6px 8px"></th>';
h += '</tr></thead><tbody>';
// Row-card layout — left holds resolver address + a stats line
// (speed · score · ✅ · ❌), right holds the action buttons.
// Replaces the old 6-column table that overflowed off the right
// edge on phone widths and hid the × button.
var h = '<div class="rb-rows">';
for (var i = 0; i < board.length; i++) {
var b = board[i];
var scoreColor = b.score >= 0.5 ? 'var(--success)' : b.score >= 0.15 ? 'var(--text)' : 'var(--error)';
h += '<tr style="border-bottom:1px solid var(--border)">';
h += '<td style="padding:5px 8px;font-family:monospace">' + esc(b.addr);
if (b.active !== undefined && b.active) h += ' <span style="color:var(--success);font-size:10px">\u25CF</span>';
h += '</td>';
h += '<td style="padding:5px 8px;text-align:right">' + (b.avgMs > 0 ? Math.round(b.avgMs) + 'ms' : '-') + '</td>';
h += '<td style="padding:5px 8px;text-align:right;color:' + scoreColor + ';font-weight:600">' + b.score.toFixed(2) + '</td>';
h += '<td style="padding:5px 8px;text-align:center;color:var(--success)">' + b.success + '</td>';
h += '<td style="padding:5px 8px;text-align:center;color:var(--error)">' + b.failure + '</td>';
var dot = (b.active !== undefined && b.active)
? ' <span style="color:var(--success);font-size:10px"></span>' : '';
h += '<div class="rb-row">';
h += '<div class="rb-row-main">';
h += '<div class="rb-row-addr">' + esc(b.addr) + dot + '</div>';
h += '<div class="rb-row-stats">';
h += '<span>' + (b.avgMs > 0 ? Math.round(b.avgMs) + 'ms' : '-') + '</span>';
h += '<span style="color:' + scoreColor + ';font-weight:600">' + b.score.toFixed(2) + '</span>';
h += '<span style="color:var(--success)">✅ ' + b.success + '</span>';
h += '<span style="color:var(--error)">❌ ' + b.failure + '</span>';
h += '</div></div>';
if (showRemove) {
var fn = removeFromBank ? 'removeResolverFromBank' : 'removeResolver';
h += '<td style="padding:5px 8px;text-align:center;white-space:nowrap">';
// Bank tab gets the add-to-list "+" alongside the remove
// "✕". Both buttons sized for touch: 32×32, 8px gap, full
// border so they read as tap targets rather than glyphs.
h += '<div class="rb-row-actions">';
if (removeFromBank) {
h += '<button class="rb-row-btn rb-row-add" onclick="openBankAddPicker(this,\'' + esc(b.addr) + '\')" '
+ 'data-i18n-title="add_to_list" title="Add to list" aria-label="Add to list">&#43;</button>';
}
h += '<button class="rb-row-btn rb-row-del" onclick="' + fn + '(\'' + esc(b.addr) + '\')" '
+ 'title="Remove" aria-label="Remove">&times;</button>';
h += '</td>';
h += '</div>';
}
h += '</tr>';
h += '</div>';
}
h += '</tbody></table>';
h += '</div>';
return h;
}
async function _fetchActiveBoard() {
@@ -7624,11 +7625,16 @@
}
function copyResolversList() {
var panelId = currentResolverTab === 'active' ? 'resolverPanelActive' : 'resolverBankListEl';
var rows = document.querySelectorAll('#' + panelId + ' tbody tr');
// New row layout uses .rb-row-addr divs in place of <td>.
var addrs = document.querySelectorAll('#' + panelId + ' .rb-row-addr');
var lines = [];
rows.forEach(function (tr) {
var cells = tr.querySelectorAll('td');
if (cells.length > 0) lines.push(cells[0].textContent.trim());
addrs.forEach(function (el) {
// Strip the trailing active-dot span if present.
var clone = el.cloneNode(true);
var dot = clone.querySelector('span');
if (dot) dot.remove();
var t = clone.textContent.trim();
if (t) lines.push(t);
});
if (!lines.length) { showToast(t('no_active_resolvers')); return }
navigator.clipboard.writeText(lines.join('\n')).then(function () { showToast(t('copied')) });
-394
View File
@@ -1,394 +0,0 @@
// Minimal QR encoder: byte mode + L (low) error correction, versions
// 110. Renders to SVG. Used by the share modal so the offline app
// can produce a scannable code without a CDN dependency.
//
// Spec: ISO/IEC 18004:2015, simplified for our subset.
// - Byte mode only (UTF-8 input).
// - Low-level error correction (~7%).
// - Versions 110 (max ~230 ASCII chars).
// - Mask 0 only (skip the 8-mask penalty search).
//
// Exposes window.qrEncodeSvg(text, scale=4) -> string (SVG markup).
(function (global) {
// ===== Galois field GF(256) tables =====
var EXP = new Array(256);
var LOG = new Array(256);
(function init() {
var x = 1;
for (var i = 0; i < 255; i++) {
EXP[i] = x;
LOG[x] = i;
x <<= 1;
if (x & 0x100) x ^= 0x11d;
}
EXP[255] = EXP[0];
})();
function gfMul(a, b) {
if (a === 0 || b === 0) return 0;
return EXP[(LOG[a] + LOG[b]) % 255];
}
function rsPoly(degree) {
var poly = [1];
for (var i = 0; i < degree; i++) {
var next = new Array(poly.length + 1);
for (var k = 0; k < next.length; k++) next[k] = 0;
for (var j = 0; j < poly.length; j++) {
next[j] ^= poly[j];
next[j + 1] ^= gfMul(poly[j], EXP[i]);
}
poly = next;
}
return poly;
}
function rsEncode(data, ecCount) {
var poly = rsPoly(ecCount);
var buf = data.slice();
for (var i = 0; i < ecCount; i++) buf.push(0);
for (var i = 0; i < data.length; i++) {
var coef = buf[i];
if (coef !== 0) {
for (var j = 0; j < poly.length; j++) {
buf[i + j] ^= gfMul(poly[j], coef);
}
}
}
return buf.slice(data.length);
}
// ===== Version capacity tables (L correction, byte mode) =====
// {size, ecPerBlock, groups: [[blockCount, dataBytesPerBlock], ...]}
var VER = [
{ size: 21, ec: 7, groups: [[1, 19]] },
{ size: 25, ec: 10, groups: [[1, 34]] },
{ size: 29, ec: 15, groups: [[1, 55]] },
{ size: 33, ec: 20, groups: [[1, 80]] },
{ size: 37, ec: 26, groups: [[1, 108]] },
{ size: 41, ec: 18, groups: [[2, 68]] },
{ size: 45, ec: 20, groups: [[2, 78]] },
{ size: 49, ec: 24, groups: [[2, 97]] },
{ size: 53, ec: 30, groups: [[2, 116]] },
{ size: 57, ec: 18, groups: [[2, 68], [2, 69]] }
];
// Alignment-pattern centre coordinates for each version.
var ALIGN = [
[], [6, 18], [6, 22], [6, 26], [6, 30],
[6, 34], [6, 22, 38], [6, 24, 42], [6, 26, 46], [6, 28, 50]
];
function totalDataBytes(info) {
var t = 0;
for (var i = 0; i < info.groups.length; i++) {
t += info.groups[i][0] * info.groups[i][1];
}
return t;
}
function pickVersion(byteLen) {
for (var v = 0; v < VER.length; v++) {
var info = VER[v];
var ccBits = (v + 1) <= 9 ? 8 : 16;
var maxBytes = totalDataBytes(info) - 2 - Math.ceil(ccBits / 8);
if (byteLen <= maxBytes) return v + 1;
}
return -1;
}
function utf8Bytes(s) {
var bytes = [];
for (var i = 0; i < s.length; i++) {
var c = s.charCodeAt(i);
if (c < 0x80) {
bytes.push(c);
} else if (c < 0x800) {
bytes.push(0xc0 | (c >> 6));
bytes.push(0x80 | (c & 0x3f));
} else if (c < 0xd800 || c >= 0xe000) {
bytes.push(0xe0 | (c >> 12));
bytes.push(0x80 | ((c >> 6) & 0x3f));
bytes.push(0x80 | (c & 0x3f));
} else {
var c2 = s.charCodeAt(++i);
var cp = 0x10000 + (((c & 0x3ff) << 10) | (c2 & 0x3ff));
bytes.push(0xf0 | (cp >> 18));
bytes.push(0x80 | ((cp >> 12) & 0x3f));
bytes.push(0x80 | ((cp >> 6) & 0x3f));
bytes.push(0x80 | (cp & 0x3f));
}
}
return bytes;
}
function buildBitstream(bytes, version, info) {
var ccBits = version <= 9 ? 8 : 16;
var totalBytes = totalDataBytes(info);
var totalBits = totalBytes * 8;
var bits = [];
function pushBits(val, n) {
for (var i = n - 1; i >= 0; i--) bits.push((val >> i) & 1);
}
pushBits(0x4, 4); // byte mode
pushBits(bytes.length, ccBits);
for (var i = 0; i < bytes.length; i++) pushBits(bytes[i], 8);
var term = Math.min(4, totalBits - bits.length);
for (var i = 0; i < term; i++) bits.push(0);
while (bits.length % 8 !== 0) bits.push(0);
var out = [];
for (var i = 0; i < bits.length; i += 8) {
var b = 0;
for (var j = 0; j < 8; j++) b = (b << 1) | bits[i + j];
out.push(b);
}
var pad = [0xec, 0x11];
while (out.length < totalBytes) out.push(pad[out.length % 2]);
return out;
}
function buildBlocks(dataBytes, info) {
var data = [];
var ec = [];
var idx = 0;
for (var g = 0; g < info.groups.length; g++) {
var count = info.groups[g][0];
var perBlock = info.groups[g][1];
for (var i = 0; i < count; i++) {
var slice = dataBytes.slice(idx, idx + perBlock);
idx += perBlock;
data.push(slice);
ec.push(rsEncode(slice, info.ec));
}
}
return { data: data, ec: ec };
}
function interleave(blocks) {
var maxData = 0;
for (var i = 0; i < blocks.data.length; i++) {
if (blocks.data[i].length > maxData) maxData = blocks.data[i].length;
}
var out = [];
for (var i = 0; i < maxData; i++) {
for (var j = 0; j < blocks.data.length; j++) {
if (i < blocks.data[j].length) out.push(blocks.data[j][i]);
}
}
var ecLen = blocks.ec[0].length;
for (var i = 0; i < ecLen; i++) {
for (var j = 0; j < blocks.ec.length; j++) out.push(blocks.ec[j][i]);
}
return out;
}
function makeGrid(size) {
var m = new Array(size);
var f = new Array(size);
for (var i = 0; i < size; i++) {
m[i] = new Array(size);
f[i] = new Array(size);
for (var j = 0; j < size; j++) { m[i][j] = 0; f[i][j] = false; }
}
return { m: m, f: f, size: size };
}
function placeFinder(g, r, c) {
for (var dr = -1; dr <= 7; dr++) {
for (var dc = -1; dc <= 7; dc++) {
var rr = r + dr, cc = c + dc;
if (rr < 0 || cc < 0 || rr >= g.size || cc >= g.size) continue;
var v = 0;
if (dr >= 0 && dr <= 6 && dc >= 0 && dc <= 6) {
if (dr === 0 || dr === 6 || dc === 0 || dc === 6) v = 1;
else if (dr >= 2 && dr <= 4 && dc >= 2 && dc <= 4) v = 1;
}
g.m[rr][cc] = v;
g.f[rr][cc] = true;
}
}
}
function placeAlignment(g, r, c) {
for (var dr = -2; dr <= 2; dr++) {
for (var dc = -2; dc <= 2; dc++) {
var rr = r + dr, cc = c + dc;
if (rr < 0 || cc < 0 || rr >= g.size || cc >= g.size) continue;
var v = 0;
if (dr === -2 || dr === 2 || dc === -2 || dc === 2) v = 1;
else if (dr === 0 && dc === 0) v = 1;
g.m[rr][cc] = v;
g.f[rr][cc] = true;
}
}
}
function placeFunctionPatterns(g, version) {
var size = g.size;
placeFinder(g, 0, 0);
placeFinder(g, 0, size - 7);
placeFinder(g, size - 7, 0);
// Timing
for (var i = 8; i < size - 8; i++) {
g.m[6][i] = (i % 2 === 0) ? 1 : 0;
g.m[i][6] = (i % 2 === 0) ? 1 : 0;
g.f[6][i] = true;
g.f[i][6] = true;
}
// Alignment
var pos = ALIGN[version - 1];
for (var i = 0; i < pos.length; i++) {
for (var j = 0; j < pos.length; j++) {
var r = pos[i], c = pos[j];
if (r === 6 && c === 6) continue;
if (r === 6 && c === size - 7) continue;
if (r === size - 7 && c === 6) continue;
placeAlignment(g, r, c);
}
}
// Reserve format info (filled later)
for (var i = 0; i < 9; i++) {
g.f[8][i] = true;
g.f[i][8] = true;
}
for (var i = 0; i < 8; i++) {
g.f[size - 1 - i][8] = true;
g.f[8][size - 1 - i] = true;
}
// Dark module
g.m[size - 8][8] = 1;
g.f[size - 8][8] = true;
// Reserve version info (v ≥ 7)
if (version >= 7) {
for (var i = 0; i < 6; i++) {
for (var j = size - 11; j <= size - 9; j++) {
g.f[i][j] = true;
g.f[j][i] = true;
}
}
}
}
// Mask 0: (i + j) % 2 === 0
function maskBit(r, c) { return ((r + c) % 2) === 0 ? 1 : 0; }
function placeData(g, bits) {
var size = g.size;
var bitIdx = 0;
var dir = -1;
var col = size - 1;
while (col > 0) {
if (col === 6) col--;
var row = (dir === -1) ? size - 1 : 0;
for (var k = 0; k < size; k++) {
for (var x = 0; x < 2; x++) {
var c = col - x;
if (!g.f[row][c]) {
var bit = (bitIdx < bits.length) ? bits[bitIdx] : 0;
g.m[row][c] = bit ^ maskBit(row, c);
bitIdx++;
}
}
row += dir;
}
col -= 2;
dir = -dir;
}
}
// Format info: ECC level L = 01, mask = 000 → 5 bits = 0b01000 → 0x08.
// Append BCH(15,5) parity, mask with 0x5412.
function formatInfoBits() {
var data = 0x08; // L + mask 0
var rem = data << 10;
var poly = 0x537;
for (var i = 14; i >= 10; i--) {
if ((rem >> i) & 1) rem ^= poly << (i - 10);
}
var bits = ((data << 10) | rem) ^ 0x5412;
return bits & 0x7fff;
}
function placeFormatInfo(g) {
var bits = formatInfoBits();
var size = g.size;
function get(i) { return (bits >> i) & 1; }
// Top-left horizontal
for (var i = 0; i < 6; i++) g.m[8][i] = get(i);
g.m[8][7] = get(6);
g.m[8][8] = get(7);
g.m[7][8] = get(8);
for (var i = 9; i < 15; i++) g.m[14 - i][8] = get(i);
// Bottom + right
for (var i = 0; i < 7; i++) g.m[size - 1 - i][8] = get(i);
for (var i = 7; i < 15; i++) g.m[8][size - 15 + i] = get(i);
}
// Version info BCH(18,6) for v ≥ 7.
function versionInfoBits(version) {
var rem = version << 12;
var poly = 0x1f25;
for (var i = 17; i >= 12; i--) {
if ((rem >> i) & 1) rem ^= poly << (i - 12);
}
return (version << 12) | rem;
}
function placeVersionInfo(g, version) {
if (version < 7) return;
var bits = versionInfoBits(version);
var size = g.size;
for (var i = 0; i < 18; i++) {
var bit = (bits >> i) & 1;
var a = Math.floor(i / 3);
var b = (i % 3) + size - 11;
g.m[a][b] = bit;
g.m[b][a] = bit;
}
}
function renderSvg(matrix, scale) {
var size = matrix.length;
var quiet = 4;
var dim = (size + quiet * 2) * scale;
var parts = [];
parts.push('<svg xmlns="http://www.w3.org/2000/svg" width="' + dim + '" height="' + dim + '" viewBox="0 0 ' + (size + quiet * 2) + ' ' + (size + quiet * 2) + '" shape-rendering="crispEdges">');
parts.push('<rect width="100%" height="100%" fill="#fff"/>');
var path = '';
for (var r = 0; r < size; r++) {
for (var c = 0; c < size; c++) {
if (matrix[r][c]) {
path += 'M' + (c + quiet) + ',' + (r + quiet) + 'h1v1h-1z';
}
}
}
parts.push('<path d="' + path + '" fill="#000"/>');
parts.push('</svg>');
return parts.join('');
}
global.qrEncodeSvg = function (text, scale) {
scale = scale || 4;
var bytes = utf8Bytes(text || '');
var version = pickVersion(bytes.length);
if (version < 0) throw new Error('text too long for QR (max ~230 chars)');
var info = VER[version - 1];
var dataBytes = buildBitstream(bytes, version, info);
var blocks = buildBlocks(dataBytes, info);
var bytestream = interleave(blocks);
var bits = [];
for (var i = 0; i < bytestream.length; i++) {
for (var j = 7; j >= 0; j--) bits.push((bytestream[i] >> j) & 1);
}
var g = makeGrid(info.size);
placeFunctionPatterns(g, version);
placeData(g, bits);
placeFormatInfo(g);
placeVersionInfo(g, version);
return renderSvg(g.m, scale);
};
})(window);