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 ? ', ' : ''}
{inner}