Skip to content

Commit d774947

Browse files
committed
Add visual polish to struct packing playground
Cross-highlighting between field rows and slot segments, taller slot bars with hover effects, staggered fade-in animations, basic Solidity syntax highlighting in generated code, field row hover states, and a slot reduction delta badge in side-by-side view.
1 parent fde13fe commit d774947

File tree

1 file changed

+118
-11
lines changed

1 file changed

+118
-11
lines changed

playground.html

Lines changed: 118 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,12 @@
7676

7777
/* ── Field list ── */
7878
.field-list { display: flex; flex-direction: column; gap: 6px; margin-bottom: 14px; }
79-
.field-row { background: var(--bg-0); border: 1px solid var(--border); border-radius: 8px; overflow: hidden; transition: border-color 0.2s; }
79+
.field-row {
80+
background: var(--bg-0); border: 1px solid var(--border); border-radius: 8px; overflow: hidden;
81+
transition: border-color 0.2s, background 0.15s, box-shadow 0.2s;
82+
animation: fadeUp 0.3s ease both;
83+
}
84+
.field-row:hover { border-color: var(--border-focus); background: var(--bg-1); }
8085
.field-row.has-error { border-color: rgba(251,113,133,0.3); }
8186
.field-main { display: flex; align-items: center; gap: 6px; padding: 8px 10px; }
8287
.field-color { width: 4px; height: 28px; border-radius: 2px; flex-shrink: 0; }
@@ -166,21 +171,23 @@
166171
.slot-count-good { background: var(--green-ghost); color: var(--green); }
167172

168173
.slot-group { display: flex; flex-direction: column; gap: 10px; }
169-
.slot-item { }
174+
.slot-item { animation: fadeUp 0.3s ease both; }
170175
.slot-header { font-family: 'Azeret Mono', monospace; font-size: 0.6rem; color: var(--fg-2); margin-bottom: 4px; }
171-
.slot-bar { display: flex; height: 44px; border-radius: 6px; overflow: hidden; background: var(--bg-0); border: 1px solid var(--border); }
176+
.slot-bar { display: flex; height: 52px; border-radius: 6px; overflow: hidden; background: var(--bg-0); border: 1px solid var(--border); }
172177
.slot-seg {
173178
display: flex; flex-direction: column; align-items: center; justify-content: center;
174-
min-width: 0; overflow: hidden; position: relative; transition: flex 0.3s ease;
179+
min-width: 0; overflow: hidden; position: relative;
180+
transition: flex 0.3s ease, filter 0.2s, opacity 0.2s, transform 0.15s;
175181
}
182+
.slot-seg:hover { filter: brightness(1.3); box-shadow: inset 0 0 8px rgba(255,255,255,0.06); }
176183
.slot-seg-name {
177184
font-size: 0.56rem; font-weight: 600; letter-spacing: 0.04em; opacity: 0.9;
178185
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%; padding: 0 4px;
179186
}
180187
.slot-seg-bits { font-family: 'Azeret Mono', monospace; font-size: 0.65rem; font-weight: 700; }
181188
.slot-seg-quant { font-size: 0.5rem; opacity: 0.6; position: absolute; top: 2px; right: 3px; }
182189
.slot-waste {
183-
background: repeating-linear-gradient(-45deg, transparent, transparent 3px, rgba(255,255,255,0.02) 3px, rgba(255,255,255,0.02) 6px);
190+
background: repeating-linear-gradient(-45deg, transparent, transparent 3px, rgba(255,255,255,0.035) 3px, rgba(255,255,255,0.035) 6px);
184191
color: var(--fg-2);
185192
}
186193
.empty-msg { font-size: 0.78rem; color: var(--fg-2); text-align: center; padding: 24px 0; }
@@ -221,6 +228,25 @@
221228
}
222229
.copy-btn:hover { background: var(--accent); color: var(--bg-0); }
223230

231+
/* ── Animations ── */
232+
@keyframes fadeUp {
233+
from { opacity: 0; transform: translateY(14px); }
234+
to { opacity: 1; transform: translateY(0); }
235+
}
236+
237+
/* ── Cross-highlighting ── */
238+
.field-row.highlight { border-color: var(--border-focus); box-shadow: 0 0 0 1px var(--border-focus); }
239+
.slot-seg.highlight { filter: brightness(1.4); box-shadow: inset 0 0 12px rgba(255,255,255,0.08); z-index: 1; }
240+
.slot-seg.dimmed { opacity: 0.3; }
241+
242+
/* ── Delta indicator ── */
243+
.viz-delta-indicator {
244+
grid-column: 1 / -1; justify-self: center;
245+
font-family: 'Azeret Mono', monospace; font-size: 0.72rem; font-weight: 700;
246+
color: var(--green); background: var(--green-ghost); border: 1px solid rgba(52,211,153,0.18);
247+
padding: 4px 14px; border-radius: 999px; letter-spacing: 0.02em;
248+
}
249+
224250
/* ── Responsive ── */
225251
@media (max-width: 859px) {
226252
.playground { padding: 12px; }
@@ -419,6 +445,8 @@
419445
const err = fieldError(f);
420446
const row = document.createElement('div');
421447
row.className = 'field-row' + (err ? ' has-error' : '');
448+
row.dataset.fieldId = f.id;
449+
row.style.animationDelay = (i * 0.04) + 's';
422450

423451
let html = '<div class="field-main">';
424452
html += '<div class="field-color" style="background:' + c.text + '"></div>';
@@ -484,15 +512,15 @@
484512
html += '<div class="slot-group">';
485513

486514
slots.forEach((slot, si) => {
487-
html += '<div class="slot-item">';
515+
html += '<div class="slot-item" style="animation-delay:' + (si * 0.05) + 's">';
488516
html += '<div class="slot-header">Slot ' + si + ' (' + slot.used + '/256 bits)</div>';
489517
html += '<div class="slot-bar">';
490518

491519
slot.segs.forEach(seg => {
492520
const c = fieldColor(seg.idx);
493521
const isNarrow = seg.bits < 32;
494522
const showQuant = isQuantized && seg.field.quantized;
495-
html += '<div class="slot-seg" style="flex:' + seg.bits + ';background:' + c.bg + ';border-left:1px solid ' + c.border + ';border-right:1px solid ' + c.border + ';color:' + c.text + '" title="' + escAttr(seg.field.name) + ': ' + seg.bits + ' bits' + (showQuant ? ' (quantized)' : '') + '">';
523+
html += '<div class="slot-seg" data-field-id="' + seg.field.id + '" style="flex:' + seg.bits + ';background:' + c.bg + ';border-left:1px solid ' + c.border + ';border-right:1px solid ' + c.border + ';color:' + c.text + '" title="' + escAttr(seg.field.name) + ': ' + seg.bits + ' bits' + (showQuant ? ' (quantized)' : '') + '">';
496524
if (showQuant) html += '<span class="slot-seg-quant">&gt;&gt;</span>';
497525
if (!isNarrow) html += '<span class="slot-seg-name">' + escAttr(seg.field.name) + '</span>';
498526
html += '<span class="slot-seg-bits">' + seg.bits + '</span>';
@@ -523,10 +551,15 @@
523551
} else if (viewMode === 'quantized') {
524552
panel.innerHTML = '<div class="viz-container">' + renderSlotDiagram(quantSlots, 'Quantized', true) + '</div>';
525553
} else {
526-
panel.innerHTML = '<div class="viz-side-by-side">' +
554+
let sideHtml = '<div class="viz-side-by-side">' +
527555
renderSlotDiagram(origSlots, 'Original', false) +
528-
renderSlotDiagram(quantSlots, 'Quantized', true) +
529-
'</div>';
556+
renderSlotDiagram(quantSlots, 'Quantized', true);
557+
const delta = origSlots.length - quantSlots.length;
558+
if (delta > 0 && fields.some(f => f.quantized && !fieldError(f))) {
559+
sideHtml += '<div class="viz-delta-indicator">-' + delta + ' slot' + (delta !== 1 ? 's' : '') + '</div>';
560+
}
561+
sideHtml += '</div>';
562+
panel.innerHTML = sideHtml;
530563
}
531564
}
532565

@@ -569,6 +602,47 @@
569602
panel.innerHTML = html;
570603
}
571604

605+
// ── Syntax highlighting ──
606+
function escHtml(s) {
607+
return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
608+
}
609+
610+
function highlightSolidity(code) {
611+
// Process line by line so comments are handled cleanly
612+
return code.split('\n').map(line => {
613+
// Full-line comments
614+
const commentMatch = line.match(/^(\s*)(\/\/.*)$/);
615+
if (commentMatch) {
616+
return escHtml(commentMatch[1]) + '<span style="color:var(--fg-2)">' + escHtml(commentMatch[2]) + '</span>';
617+
}
618+
// Lines with trailing comments
619+
const trailingMatch = line.match(/^(.*?)(\/\/.*)$/);
620+
let codePart = trailingMatch ? trailingMatch[1] : line;
621+
let commentPart = trailingMatch ? trailingMatch[2] : '';
622+
623+
let result = highlightCodePart(codePart);
624+
if (commentPart) {
625+
result += '<span style="color:var(--fg-2)">' + escHtml(commentPart) + '</span>';
626+
}
627+
return result;
628+
}).join('\n');
629+
}
630+
631+
function highlightCodePart(code) {
632+
const escaped = escHtml(code);
633+
// Tokenize: keywords, types, strings, hex literals, everything else
634+
return escaped.replace(
635+
/\b(import|from|struct|private|immutable|using|for|global)\b|\b(uint\d+|bool|Quant|QuantizationLib)\b|(&quot;[^&]*?&quot;)|\b(0x[0-9A-Fa-f]+)\b/g,
636+
(match, kw, typ, str, hex) => {
637+
if (kw) return '<span style="color:var(--accent)">' + kw + '</span>';
638+
if (typ) return '<span style="color:var(--green)">' + typ + '</span>';
639+
if (str) return '<span style="color:var(--amber)">' + str + '</span>';
640+
if (hex) return '<span style="color:var(--amber)">' + hex + '</span>';
641+
return match;
642+
}
643+
);
644+
}
645+
572646
// ── Generate Solidity code ──
573647
function renderCode() {
574648
const codeEl = el('codeOut');
@@ -635,7 +709,7 @@
635709
});
636710
}
637711

638-
codeEl.textContent = code;
712+
codeEl.innerHTML = highlightSolidity(code);
639713
}
640714

641715
// ── Main render (right panel only) ──
@@ -771,6 +845,39 @@
771845
});
772846
});
773847

848+
// ── Cross-highlighting ──
849+
function clearHighlights() {
850+
document.querySelectorAll('.field-row.highlight').forEach(n => n.classList.remove('highlight'));
851+
document.querySelectorAll('.slot-seg.highlight, .slot-seg.dimmed').forEach(n => {
852+
n.classList.remove('highlight', 'dimmed');
853+
});
854+
}
855+
856+
el('fieldList').addEventListener('mouseover', e => {
857+
const row = e.target.closest('.field-row[data-field-id]');
858+
if (!row) return;
859+
clearHighlights();
860+
const fid = row.dataset.fieldId;
861+
row.classList.add('highlight');
862+
document.querySelectorAll('.slot-seg[data-field-id]').forEach(seg => {
863+
seg.classList.add(seg.dataset.fieldId === fid ? 'highlight' : 'dimmed');
864+
});
865+
});
866+
el('fieldList').addEventListener('mouseleave', clearHighlights);
867+
868+
el('vizPanel').addEventListener('mouseover', e => {
869+
const seg = e.target.closest('.slot-seg[data-field-id]');
870+
if (!seg) return;
871+
clearHighlights();
872+
const fid = seg.dataset.fieldId;
873+
document.querySelectorAll('.slot-seg[data-field-id]').forEach(s => {
874+
s.classList.add(s.dataset.fieldId === fid ? 'highlight' : 'dimmed');
875+
});
876+
const row = document.querySelector('.field-row[data-field-id="' + fid + '"]');
877+
if (row) row.classList.add('highlight');
878+
});
879+
el('vizPanel').addEventListener('mouseleave', clearHighlights);
880+
774881
// ── View tabs ──
775882
el('viewTabs').addEventListener('click', e => {
776883
const tab = e.target.closest('.view-tab');

0 commit comments

Comments
 (0)