import { coAuthorsOf } from '@pdsjs/git/authors'; import { Avatar } from '#/components/atoms/avatar.jsx'; import { describeAuthor, initial } from '#/lib/authors.js'; import { identicon } from '#/lib/avatars.js'; import { cn } from '#/lib/utils.js'; /** How many faces a pile shows before it counts the rest instead. */ const FACES = 3; /** * The author stands in front and everyone credited beside them tucks in * behind, so the face a reader looks for first is the one that is whole. * These are classes rather than a style, because the avatar takes a class * name and nothing else. The pile isolates them, so a face never rises * above a menu that opens over it. */ const DEPTH = ['z-30', 'z-20', 'z-10']; /** * Everyone a commit credits, in the order a reader meets them: the author it * names first, then anyone its message credits in a `Co-authored-by` trailer. * @param {{ident: unknown, message: unknown}} commit */ function creditedBy(ident, message) { const idents = [ident, ...coAuthorsOf(message)].filter(Boolean); const people = []; const seen = new Set(); for (const one of idents) { const author = describeAuthor(one); const key = author.name.toLowerCase(); if (seen.has(key)) continue; seen.add(key); people.push(author); } return people; } /** * The faces alone, for a row that has no width for the names: a commit on a * timeline, where the message is what the reader came for and the face says * whose it is. * * An author with no atproto account keeps whatever the account calls them, * which is how a tool that writes commits gets a face without having a repo * of its own. */ export function AuthorFaces({ ident, message, className }) { const people = creditedBy(ident, message); if (people.length === 0) return null; return ( person.title).join('\n')} > {people.slice(0, FACES).map((person, index) => ( ))} ); } /** * One face in a pile, drawn over the one behind it. * * An author the account has claimed draws the pattern their DID draws * everywhere else, so one person wears one face. An author claimed by nobody * has no identity to draw from and keeps their initial. */ function Face({ person, index }) { return ( {initial(person.name)} } // The face in front already carries a border, and that is the edge // between it and the one behind. A ring drawn here would have to guess // the colour behind the pile, which differs from one band to the next. className={cn( 'relative size-4 shrink-0', DEPTH[index], index > 0 && '-ml-1.5', )} /> ); } /** * Everyone a commit credits, faces and names. * * The faces overlap, and the names read as a sentence, because a commit two * people wrote is one piece of work rather than two rows. */ export function AuthorFacepile({ ident, message, className }) { const people = creditedBy(ident, message); if (people.length === 0) return null; const shown = people.slice(0, FACES); const named = people.map((person) => person.name); // "Ada", "Ada and Bo", "Ada, Bo and Cy": the last two join with "and", the // rest with commas, which is how the names would be read aloud. const sentence = named.length > 1 ? `${named.slice(0, -1).join(', ')} and ${named[named.length - 1]}` : named[0]; return ( person.title).join('\n')} > {shown.map((person, index) => ( ))} {sentence} {people.length > shown.length && ( {' '} and {people.length - shown.length} more )} ); }