feat(browse): extension chip under tree icon + archive refs in hovercard

Two small surface upgrades on file rows:

- Tree icon column now stacks the Lucide glyph on top of a small
  uppercase extension chip (PDF, DOCX, YAML, etc.). File type reads
  at a glance without expanding the row. Folders and zips skip the
  chip — their glyph already carries enough.
- Hovercard on a ZDDC-parseable file gains two clickable references
  in the .archive section:
    Latest         → /<project>/.archive/<tracking>.html
    This revision  → /<project>/.archive/<tracking>_<rev>.html
  Both forms are dispatcher-canonicalised to project-root, so the
  link works from any depth. Folders that parse (transmittal folders)
  get just the Latest link.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
ZDDC 2026-05-18 08:18:01 -05:00
parent c2423f8873
commit 4497ebdf99
3 changed files with 77 additions and 7 deletions

View file

@ -361,15 +361,17 @@ body {
.tree-name__icon { .tree-name__icon {
flex-shrink: 0; flex-shrink: 0;
/* Fixed-width column keeps label alignment consistent regardless /* Stacked column glyph on top, extension chip below for files.
of which symbol the row picks. Height matches one line of label Wider min-width than the 1em glyph itself so common extensions
text so the icon anchors to the title row on two-line layouts. */ (pdf/docx/xlsx/json) don't push the label sideways. Height
width: 1.2em; grows with content; flex-start anchors to the title-line. */
height: 1.2em; min-width: 2.2em;
display: inline-flex; display: inline-flex;
flex-direction: column;
align-items: center; align-items: center;
justify-content: center; justify-content: flex-start;
color: var(--text-muted); color: var(--text-muted);
gap: 1px;
} }
.tree-name__icon svg { .tree-name__icon svg {
@ -378,6 +380,19 @@ body {
display: block; 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 /* Folder rows get the primary accent so directories stand out from
files at a glance same convention as macOS Finder / GNOME Files. */ files at a glance same convention as macOS Finder / GNOME Files. */
.tree-row[data-isdir="true"] .tree-name__icon, .tree-row[data-isdir="true"] .tree-name__icon,
@ -951,6 +966,17 @@ body {
font-size: 0.78rem; 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 /* Separator stretches across both grid columns. Bleed into the
card's padding so it visually reads as a divider, not a hairline. */ card's padding so it visually reads as a divider, not a hairline. */
.tree-hovercard__sep { .tree-hovercard__sep {

View file

@ -108,6 +108,32 @@
if (parsed.revision) html += kv('Revision', parsed.revision, true); if (parsed.revision) html += kv('Revision', parsed.revision, true);
if (parsed.status) html += kv('Status', parsed.status, true); if (parsed.status) html += kv('Status', parsed.status, true);
if (parsed.title) html += kv('Title', parsed.title); if (parsed.title) html += kv('Title', parsed.title);
// Archive references — the /<project>/.archive/<tracking>.html
// URL is the latest issued version (highest base rev), and
// /<project>/.archive/<tracking>_<rev>.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 += '<div class="tree-hovercard__sep"></div>'; html += '<div class="tree-hovercard__sep"></div>';
} else if (node.displayName) { } else if (node.displayName) {
// Operator-supplied display name — only useful as info if // Operator-supplied display name — only useful as info if
@ -136,6 +162,18 @@
+ '">' + escapeHtml(val) + '</span>'; + '">' + escapeHtml(val) + '</span>';
} }
// kvLink — value rendered as an <a> 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 '<span class="tree-hovercard__key">' + escapeHtml(key) + '</span>'
+ '<span class="tree-hovercard__val tree-hovercard__val--mono">'
+ '<a href="' + escapeHtml(href) + '" target="_blank" rel="noopener">'
+ escapeHtml(label)
+ '</a>'
+ '</span>';
}
function render(node) { function render(node) {
var z = window.zddc; var z = window.zddc;
var parsed = z var parsed = z

View file

@ -368,6 +368,12 @@
var virtualHint = node.virtual var virtualHint = node.virtual
? '<span class="tree-name__hint">(empty)</span>' ? '<span class="tree-name__hint">(empty)</span>'
: ''; : '';
// 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)
? '<span class="tree-name__ext">' + escapeHtml(String(node.ext)) + '</span>'
: '';
return '' return ''
+ '<div class="tree-row ' + (visuallyExpanded ? 'expanded' : '') + selected + virtualCls + '<div class="tree-row ' + (visuallyExpanded ? 'expanded' : '') + selected + virtualCls
+ '" data-id="' + node.id + '" data-id="' + node.id
@ -377,7 +383,7 @@
+ ' style="padding-left:' + indent + 'rem"' + ' style="padding-left:' + indent + 'rem"'
+ ' role="treeitem" tabindex="-1">' + ' role="treeitem" tabindex="-1">'
+ '<span class="' + chevronClass + '">' + chevronGlyph + '</span>' + '<span class="' + chevronClass + '">' + chevronGlyph + '</span>'
+ '<span class="tree-name__icon">' + iconChar + '</span>' + '<span class="tree-name__icon">' + iconChar + extChip + '</span>'
+ labelHtml(node) + labelHtml(node)
+ virtualHint + virtualHint
+ '</div>'; + '</div>';