import { Link } from '#/components/atoms/link.jsx'; import { dateTime } from '#/lib/format.js'; import { parseAtUri } from '#/lib/record-link.js'; import { cn } from '#/lib/utils.js'; // Structural bookkeeping a person never needs to read: type tags, content // hashes, and the byte-offset ranges that map rich text back onto its string. const HIDDEN = new Set(['$type', 'cid', 'facets']); const LABELS = { createdAt: 'Created', indexedAt: 'Indexed', updatedAt: 'Updated', displayName: 'Display name', avatar: 'Avatar', banner: 'Banner', uri: 'Link', }; const ISO_DATE = /^\d{4}-\d{2}-\d{2}T[\d:.]/; function label(key) { if (LABELS[key]) return LABELS[key]; const spaced = key .replace(/([a-z0-9])([A-Z])/g, '$1 $2') .replace(/[._]/g, ' '); return spaced.charAt(0).toUpperCase() + spaced.slice(1); } function humanBytes(bytes) { if (!Number.isFinite(bytes)) return null; const units = ['B', 'KB', 'MB', 'GB']; let value = bytes; let unit = 0; while (value >= 1024 && unit < units.length - 1) { value /= 1024; unit += 1; } const rounded = value >= 10 || unit === 0 ? Math.round(value) : value.toFixed(1); return `${rounded} ${units[unit]}`; } function isEmpty(value) { if (value === null || value === undefined || value === '') return true; if (Array.isArray(value)) return value.length === 0; if (typeof value === 'object') return fields(value).length === 0; return false; } function isBlob(value) { return Boolean(value) && typeof value === 'object' && value.$type === 'blob'; } function fields(object) { return Object.entries(object).filter( ([key, value]) => !HIDDEN.has(key) && !isEmpty(value), ); } /** * A record rendered as plain content rather than a code dump: text as prose, * dates in words, image blobs as thumbnails, everything else as labeled rows. * Structural plumbing is dropped; the raw record stays available alongside it. */ export function HumanValue({ value, blobBase, repoDid, recordHref, onNavigate, top, }) { const opts = { blobBase, repoDid, recordHref, onNavigate }; if (value === null || value === undefined) return null; if (typeof value === 'string') { if (ISO_DATE.test(value)) return {dateTime(value)}; if (value.startsWith('at://')) { const target = parseAtUri(value); // Only a record in this same repo can be opened here; another account's // records live on their own server, so those stay as plain text. if (recordHref && target && target.did === repoDid) return ( {value} ); return ( {value} ); } if (value.startsWith('did:')) return ( {value} ); return ( {value} ); } if (typeof value === 'number') return {value}; if (typeof value === 'boolean') return {value ? 'Yes' : 'No'}; if (isBlob(value)) { const link = value.ref?.$link; if ( blobBase && typeof link === 'string' && value.mimeType?.startsWith('image/') ) return ( ); const size = humanBytes(value.size); return ( {value.mimeType || 'File'} {size ? ` · ${size}` : ''} ); } if (Array.isArray(value)) { const items = value.filter((item) => !isEmpty(item)); if (!items.length) return null; // A list of plain values reads as one comma-separated line; a list of // objects stacks so each keeps its own labeled rows. const scalar = items.every( (item) => item === null || typeof item !== 'object', ); if (scalar) return ( {items.map((item, index) => ( // biome-ignore lint/suspicious/noArrayIndexKey: records have no ids {index > 0 ? ', ' : ''} ))} ); return (
{items.map((item, index) => ( // biome-ignore lint/suspicious/noArrayIndexKey: records have no ids
))}
); } const entries = fields(value); if (!entries.length) return null; return (
{entries.map(([key, inner]) => { // Lead content — a post's text and the like — reads as the body: the // same quiet caption as every field, but set larger. if (top && key === 'text' && typeof inner === 'string') return (

{inner}

); const object = inner && typeof inner === 'object' && !Array.isArray(inner); // Collapse a wrapper that holds a single value once its plumbing is // hidden (a reply's `root` reduces to just its link, and so on). if (object && !isBlob(inner)) { const inside = fields(inner); const flatten = inside.length === 1 && !( inside[0][1] && typeof inside[0][1] === 'object' && !isBlob(inside[0][1]) ); if (flatten) return ( ); return ( ); } // A list of plain values (langs, tags) belongs on one line under its // label; only a list of objects earns the indented grouping. if (Array.isArray(inner)) { const scalar = inner.every( (item) => item === null || typeof item !== 'object', ); return scalar ? ( ) : ( ); } return ( ); })}
); } // A field as a caption stacked over its value: no fixed label column, so short // values sit right under their name instead of stranding a gap across the row. function Row({ label: text, children }) { return (
{text}
{children}
); } // A caption over a bordered block, for the rare nested object or list of // objects where the indent shows what belongs together. function Group({ label: text, children }) { return (
{text}
{children}
); }