|
76 | 76 |
|
77 | 77 | /* ── Field list ── */ |
78 | 78 | .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); } |
80 | 85 | .field-row.has-error { border-color: rgba(251,113,133,0.3); } |
81 | 86 | .field-main { display: flex; align-items: center; gap: 6px; padding: 8px 10px; } |
82 | 87 | .field-color { width: 4px; height: 28px; border-radius: 2px; flex-shrink: 0; } |
|
166 | 171 | .slot-count-good { background: var(--green-ghost); color: var(--green); } |
167 | 172 |
|
168 | 173 | .slot-group { display: flex; flex-direction: column; gap: 10px; } |
169 | | - .slot-item { } |
| 174 | + .slot-item { animation: fadeUp 0.3s ease both; } |
170 | 175 | .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); } |
172 | 177 | .slot-seg { |
173 | 178 | 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; |
175 | 181 | } |
| 182 | + .slot-seg:hover { filter: brightness(1.3); box-shadow: inset 0 0 8px rgba(255,255,255,0.06); } |
176 | 183 | .slot-seg-name { |
177 | 184 | font-size: 0.56rem; font-weight: 600; letter-spacing: 0.04em; opacity: 0.9; |
178 | 185 | white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%; padding: 0 4px; |
179 | 186 | } |
180 | 187 | .slot-seg-bits { font-family: 'Azeret Mono', monospace; font-size: 0.65rem; font-weight: 700; } |
181 | 188 | .slot-seg-quant { font-size: 0.5rem; opacity: 0.6; position: absolute; top: 2px; right: 3px; } |
182 | 189 | .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); |
184 | 191 | color: var(--fg-2); |
185 | 192 | } |
186 | 193 | .empty-msg { font-size: 0.78rem; color: var(--fg-2); text-align: center; padding: 24px 0; } |
|
221 | 228 | } |
222 | 229 | .copy-btn:hover { background: var(--accent); color: var(--bg-0); } |
223 | 230 |
|
| 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 | + |
224 | 250 | /* ── Responsive ── */ |
225 | 251 | @media (max-width: 859px) { |
226 | 252 | .playground { padding: 12px; } |
|
419 | 445 | const err = fieldError(f); |
420 | 446 | const row = document.createElement('div'); |
421 | 447 | row.className = 'field-row' + (err ? ' has-error' : ''); |
| 448 | + row.dataset.fieldId = f.id; |
| 449 | + row.style.animationDelay = (i * 0.04) + 's'; |
422 | 450 |
|
423 | 451 | let html = '<div class="field-main">'; |
424 | 452 | html += '<div class="field-color" style="background:' + c.text + '"></div>'; |
|
484 | 512 | html += '<div class="slot-group">'; |
485 | 513 |
|
486 | 514 | slots.forEach((slot, si) => { |
487 | | - html += '<div class="slot-item">'; |
| 515 | + html += '<div class="slot-item" style="animation-delay:' + (si * 0.05) + 's">'; |
488 | 516 | html += '<div class="slot-header">Slot ' + si + ' (' + slot.used + '/256 bits)</div>'; |
489 | 517 | html += '<div class="slot-bar">'; |
490 | 518 |
|
491 | 519 | slot.segs.forEach(seg => { |
492 | 520 | const c = fieldColor(seg.idx); |
493 | 521 | const isNarrow = seg.bits < 32; |
494 | 522 | 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)' : '') + '">'; |
496 | 524 | if (showQuant) html += '<span class="slot-seg-quant">>></span>'; |
497 | 525 | if (!isNarrow) html += '<span class="slot-seg-name">' + escAttr(seg.field.name) + '</span>'; |
498 | 526 | html += '<span class="slot-seg-bits">' + seg.bits + '</span>'; |
|
523 | 551 | } else if (viewMode === 'quantized') { |
524 | 552 | panel.innerHTML = '<div class="viz-container">' + renderSlotDiagram(quantSlots, 'Quantized', true) + '</div>'; |
525 | 553 | } else { |
526 | | - panel.innerHTML = '<div class="viz-side-by-side">' + |
| 554 | + let sideHtml = '<div class="viz-side-by-side">' + |
527 | 555 | 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; |
530 | 563 | } |
531 | 564 | } |
532 | 565 |
|
|
569 | 602 | panel.innerHTML = html; |
570 | 603 | } |
571 | 604 |
|
| 605 | + // ── Syntax highlighting ── |
| 606 | + function escHtml(s) { |
| 607 | + return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>'); |
| 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|("[^&]*?")|\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 | + |
572 | 646 | // ── Generate Solidity code ── |
573 | 647 | function renderCode() { |
574 | 648 | const codeEl = el('codeOut'); |
|
635 | 709 | }); |
636 | 710 | } |
637 | 711 |
|
638 | | - codeEl.textContent = code; |
| 712 | + codeEl.innerHTML = highlightSolidity(code); |
639 | 713 | } |
640 | 714 |
|
641 | 715 | // ── Main render (right panel only) ── |
|
771 | 845 | }); |
772 | 846 | }); |
773 | 847 |
|
| 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 | + |
774 | 881 | // ── View tabs ── |
775 | 882 | el('viewTabs').addEventListener('click', e => { |
776 | 883 | const tab = e.target.closest('.view-tab'); |
|
0 commit comments