diff --git a/browse/css/tree.css b/browse/css/tree.css index 28051d3..397faf4 100644 --- a/browse/css/tree.css +++ b/browse/css/tree.css @@ -361,15 +361,17 @@ body { .tree-name__icon { flex-shrink: 0; - /* Fixed-width column keeps label alignment consistent regardless - of which symbol the row picks. Height matches one line of label - text so the icon anchors to the title row on two-line layouts. */ - width: 1.2em; - height: 1.2em; + /* Stacked column — glyph on top, extension chip below for files. + Wider min-width than the 1em glyph itself so common extensions + (pdf/docx/xlsx/json) don't push the label sideways. Height + grows with content; flex-start anchors to the title-line. */ + min-width: 2.2em; display: inline-flex; + flex-direction: column; align-items: center; - justify-content: center; + justify-content: flex-start; color: var(--text-muted); + gap: 1px; } .tree-name__icon svg { @@ -378,6 +380,19 @@ body { display: block; } +.tree-name__ext { + font-size: 0.58rem; + line-height: 1; + color: var(--text-muted); + text-transform: uppercase; + letter-spacing: 0.04em; + font-weight: 600; + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + /* Folder rows get the primary accent so directories stand out from files at a glance — same convention as macOS Finder / GNOME Files. */ .tree-row[data-isdir="true"] .tree-name__icon, @@ -951,6 +966,17 @@ body { font-size: 0.78rem; } +/* Archive-reference links inside the hovercard pick up the primary + accent so they read as clickable, and stay inline with the mono + font when they sit inside a mono cell. */ +.tree-hovercard__val a { + color: var(--primary, #2868c8); + text-decoration: none; +} +.tree-hovercard__val a:hover { + text-decoration: underline; +} + /* Separator stretches across both grid columns. Bleed into the card's padding so it visually reads as a divider, not a hairline. */ .tree-hovercard__sep { diff --git a/browse/js/hovercard.js b/browse/js/hovercard.js index bbbcd61..3a8a7c8 100644 --- a/browse/js/hovercard.js +++ b/browse/js/hovercard.js @@ -108,6 +108,32 @@ if (parsed.revision) html += kv('Revision', parsed.revision, true); if (parsed.status) html += kv('Status', parsed.status, true); if (parsed.title) html += kv('Title', parsed.title); + + // Archive references — the //.archive/.html + // URL is the latest issued version (highest base rev), and + // //.archive/_.html pins the exact + // revision the user is currently hovering. The dispatcher + // canonicalises both forms to project-root so links work + // from any depth. + if (parsed.trackingNumber) { + var fullPath = tree ? tree.pathFor(node) : ''; + var rel = fullPath.replace(/^\/+|\/+$/g, ''); + var firstSeg = rel ? rel.split('/')[0] : ''; + if (firstSeg) { + var encProject = encodeURIComponent(firstSeg); + var encTracking = encodeURIComponent(parsed.trackingNumber); + var latestUrl = '/' + encProject + '/.archive/' + encTracking + '.html'; + var latestLbl = '.archive/' + parsed.trackingNumber + '.html'; + html += kvLink('Latest', latestUrl, latestLbl); + if (!node.isDir && parsed.revision) { + var encRev = encodeURIComponent(parsed.revision); + var inspectUrl = '/' + encProject + '/.archive/' + encTracking + '_' + encRev + '.html'; + var inspectLbl = '.archive/' + parsed.trackingNumber + '_' + parsed.revision + '.html'; + html += kvLink('This revision', inspectUrl, inspectLbl); + } + } + } + html += '
'; } else if (node.displayName) { // Operator-supplied display name — only useful as info if @@ -136,6 +162,18 @@ + '">' + escapeHtml(val) + ''; } + // kvLink — value rendered as an the user can click (opens in + // a new tab so the hover context isn't lost) or right-click to + // copy. Used for the .archive references on ZDDC files. + function kvLink(key, href, label) { + return '' + escapeHtml(key) + '' + + '' + + '' + + escapeHtml(label) + + '' + + ''; + } + function render(node) { var z = window.zddc; var parsed = z diff --git a/browse/js/tree.js b/browse/js/tree.js index 12ddfc8..c9265b2 100644 --- a/browse/js/tree.js +++ b/browse/js/tree.js @@ -368,6 +368,12 @@ var virtualHint = node.virtual ? '(empty)' : ''; + // Extension chip stacked under the file icon. Files with a + // non-empty ext get a small uppercase label; folders / zips + // skip it (the chevron + icon glyph carries enough info). + var extChip = (!node.isDir && !node.isZip && node.ext) + ? '' + escapeHtml(String(node.ext)) + '' + : ''; return '' + '
' + '' + chevronGlyph + '' - + '' + iconChar + '' + + '' + iconChar + extChip + '' + labelHtml(node) + virtualHint + '
';