Tools for the Atmosphere tools.slices.network
quickslice atproto html

fix(lexicon-explorer): improve string truncation for nested JSON

- Use indent-aware truncation that accounts for nesting depth
- Cap width calculation at card max-width (700px)
- Use different values for mobile vs desktop
- Hide "/ to focus" hint on mobile

+29 -13
+29 -13
lexicon-explorer.html
··· 460 460 .json-string-truncated { 461 461 cursor: pointer; 462 462 border-bottom: 1px dashed var(--json-string); 463 - display: inline-block; 464 - max-width: 100%; 465 - white-space: nowrap; 466 - overflow: hidden; 467 - text-overflow: ellipsis; 468 - vertical-align: bottom; 469 463 } 470 464 471 465 .json-string-truncated[data-expanded="true"] { 472 - white-space: pre-wrap; 473 466 word-break: break-word; 474 467 } 475 468 ··· 605 598 return div.innerHTML; 606 599 } 607 600 608 - const STRING_TRUNCATE_LENGTH = 80; 601 + function getTruncateLength(indent = 0) { 602 + // Account for indentation depth (2 spaces per level) 603 + // Cap at card max-width (700px) 604 + const cardWidth = Math.min(window.innerWidth, 700); 605 + const isMobile = window.innerWidth < 600; 606 + const charWidth = isMobile ? 12 : 10; 607 + const padding = isMobile ? 120 : 140; 608 + const indentPx = indent * (isMobile ? 24 : 20); 609 + const available = cardWidth - padding - indentPx; 610 + return Math.max(6, Math.floor(available / charWidth)); 611 + } 609 612 610 613 function toggleString(element) { 611 614 const isExpanded = element.dataset.expanded === 'true'; 612 - element.dataset.expanded = isExpanded ? 'false' : 'true'; 615 + if (isExpanded) { 616 + // Collapse: show truncated 617 + element.textContent = element.dataset.truncated; 618 + element.dataset.expanded = 'false'; 619 + } else { 620 + // Expand: show full 621 + element.textContent = element.dataset.full; 622 + element.dataset.expanded = 'true'; 623 + } 613 624 } 614 625 615 626 function highlightJson(obj, indent = 0) { ··· 628 639 } 629 640 630 641 if (typeof obj === 'string') { 631 - if (obj.length > STRING_TRUNCATE_LENGTH) { 632 - const escaped = escapeHtml(obj); 633 - return `<span class="json-string json-string-truncated" data-expanded="false" onclick="toggleString(this)">"${escaped}"</span>`; 642 + const truncateLen = getTruncateLength(indent); 643 + if (obj.length > truncateLen) { 644 + const truncatedText = `"${obj.slice(0, truncateLen)}…"`; 645 + const fullText = `"${obj}"`; 646 + // Escape for HTML attribute (double-encode quotes) 647 + const truncatedAttr = truncatedText.replace(/&/g, '&amp;').replace(/"/g, '&quot;'); 648 + const fullAttr = fullText.replace(/&/g, '&amp;').replace(/"/g, '&quot;'); 649 + return `<span class="json-string json-string-truncated" data-expanded="false" data-truncated="${truncatedAttr}" data-full="${fullAttr}" onclick="toggleString(this)">${escapeHtml(truncatedText)}</span>`; 634 650 } 635 651 return `<span class="json-string">"${escapeHtml(obj)}"</span>`; 636 652 } ··· 754 770 <span class="search-icon">🔍</span> 755 771 <input type="text" 756 772 class="search-input" 757 - placeholder="Search lexicons... (/ to focus)" 773 + placeholder="${window.innerWidth >= 600 ? 'Search lexicons... (/ to focus)' : 'Search lexicons...'}" 758 774 value="${escapeHtml(state.searchQuery)}" 759 775 oninput="handleSearchInput(this.value)" 760 776 onkeydown="handleSearchKeydown(event)"