Redesign EXIF viewer and compact tools UI

EXIF Viewer:
- Card-based grid layout with categories (Camera, Image, Date/Time, Exposure, GPS, Other)
- Icons for each category
- Truncation for long values with full value on hover

Tools UI:
- Reduced padding from 1.25rem to 0.5rem on all tool panels
- Smaller fonts for labels (0.55rem) and values (0.7rem)
- Compact headers and action buttons
- Tighter grid gaps and card padding

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Aaron D. Lee
2026-01-11 16:50:59 -05:00
parent 4e3acfca20
commit 38bef32750
4 changed files with 143 additions and 31 deletions

View File

@@ -283,10 +283,8 @@
<span>Drop an image to view metadata</span>
</div>
<div id="exifData" class="d-none">
<div class="tool-exif-table">
<table>
<tbody id="exifTable"></tbody>
</table>
<div class="exif-grid" id="exifGrid">
<!-- Cards populated by JS -->
</div>
<div id="exifNoData" class="text-muted text-center py-3 d-none">
<i class="bi bi-inbox d-block mb-2"></i>
@@ -657,20 +655,70 @@ setupDropZone('exifZone', 'exifFile', async (file) => {
const data = await res.json();
if (data.success) {
const tbody = document.getElementById('exifTable');
const entries = Object.entries(data.exif).sort((a, b) => a[0].localeCompare(b[0]));
const grid = document.getElementById('exifGrid');
const entries = Object.entries(data.exif);
if (entries.length === 0) {
tbody.innerHTML = '';
grid.innerHTML = '';
document.getElementById('exifNoData').classList.remove('d-none');
document.getElementById('exifNoData').innerHTML = '<i class="bi bi-inbox d-block mb-2"></i>No metadata found';
} else {
document.getElementById('exifNoData').classList.add('d-none');
tbody.innerHTML = entries.map(([key, value]) => {
// Categorize EXIF fields
const categories = {
'Camera': ['Make', 'Model', 'Software', 'LensMake', 'LensModel', 'BodySerialNumber'],
'Image': ['ImageWidth', 'ImageLength', 'Orientation', 'ResolutionUnit', 'XResolution', 'YResolution', 'ColorSpace', 'ExifImageWidth', 'ExifImageHeight'],
'Date/Time': ['DateTime', 'DateTimeOriginal', 'DateTimeDigitized', 'SubsecTime', 'SubsecTimeOriginal', 'SubsecTimeDigitized', 'OffsetTime', 'OffsetTimeOriginal'],
'Exposure': ['ExposureTime', 'FNumber', 'ExposureProgram', 'ISOSpeedRatings', 'ExposureBiasValue', 'MaxApertureValue', 'MeteringMode', 'Flash', 'FocalLength', 'FocalLengthIn35mmFilm', 'WhiteBalance', 'ExposureMode', 'DigitalZoomRatio', 'SceneCaptureType', 'Contrast', 'Saturation', 'Sharpness'],
'GPS': ['GPSInfo', 'GPSLatitude', 'GPSLatitudeRef', 'GPSLongitude', 'GPSLongitudeRef', 'GPSAltitude', 'GPSAltitudeRef', 'GPSTimeStamp', 'GPSDateStamp'],
};
const categorized = {};
const other = [];
const allCategoryFields = new Set(Object.values(categories).flat());
entries.forEach(([key, value]) => {
let found = false;
for (const [cat, fields] of Object.entries(categories)) {
if (fields.includes(key)) {
if (!categorized[cat]) categorized[cat] = [];
categorized[cat].push([key, value]);
found = true;
break;
}
}
if (!found) other.push([key, value]);
});
// Render cards
let html = '';
const renderCard = ([key, value]) => {
let displayVal = typeof value === 'object' ? JSON.stringify(value) : String(value);
if (displayVal.length > 40) displayVal = displayVal.substring(0, 37) + '...';
return `<tr><th>${key}</th><td title="${String(value)}">${displayVal}</td></tr>`;
}).join('');
const needsTruncate = displayVal.length > 60;
if (needsTruncate) displayVal = displayVal.substring(0, 57) + '...';
const fullVal = typeof value === 'object' ? JSON.stringify(value) : String(value);
return `<div class="exif-card" title="${fullVal.replace(/"/g, '&quot;')}">
<div class="exif-card-label">${key}</div>
<div class="exif-card-value${needsTruncate ? ' truncated' : ''}">${displayVal}</div>
</div>`;
};
// Render each category
for (const [cat, fields] of Object.entries(categories)) {
if (categorized[cat] && categorized[cat].length > 0) {
html += `<div class="exif-category"><i class="bi bi-${cat === 'Camera' ? 'camera' : cat === 'Image' ? 'image' : cat === 'Date/Time' ? 'clock' : cat === 'Exposure' ? 'aperture' : cat === 'GPS' ? 'geo-alt' : 'tag'} me-1"></i>${cat}</div>`;
html += categorized[cat].map(renderCard).join('');
}
}
// Render other fields
if (other.length > 0) {
html += `<div class="exif-category"><i class="bi bi-three-dots me-1"></i>Other</div>`;
html += other.map(renderCard).join('');
}
grid.innerHTML = html;
}
document.getElementById('exifEmpty').classList.add('d-none');