import { Dialog as BaseDialog } from '@base-ui/react/dialog'; import { Popover } from '@base-ui/react/popover'; import { useQuery } from '@tanstack/react-query'; import { AtSignIcon, BinaryIcon, CalendarIcon, ChevronRightIcon, CircleCheckIcon, CircleDotIcon, CircleHelpIcon, ClockIcon, FingerprintIcon, FolderIcon, GlobeIcon, HashIcon, LanguagesIcon, LayersIcon, LinkIcon, ListIcon, PaperclipIcon, ToggleLeftIcon, TypeIcon, UserIcon, XIcon, } from 'lucide-react'; import { useState } from 'react'; import { get } from '#/lib/api.js'; import { queryKeys } from '#/lib/query.js'; import { cn } from '#/lib/utils.js'; /** * A check shown beside a collection NSID when that record type's lexicon is * published via the protocol. Clicking it opens a popover explaining, in plain * terms, what that means, with a button to view the schema itself. A popover * (click) rather than a hover tooltip so it works on touch, stays open to be * read, and can hold actions. * * Pass `published` when the caller already knows it (the app page resolves every * collection in one batch); pass `nsid` alone to have it resolve that one NSID. * Controlled callers must pass a real boolean even while their own data is * still loading: an undefined prop flips the component into resolving the NSID * itself, competing with the caller's request. Renders nothing unless the * lexicon is published. * * @param {object} props * @param {string} [props.nsid] - Collection NSID to resolve when uncontrolled. * @param {boolean} [props.published] - Known published state (controlled). * @param {string} [props.className] - Extra classes for the check icon (e.g. size). */ export function PublishedCheck({ nsid, published, className }) { const [popoverOpen, setPopoverOpen] = useState(false); const [schemaOpen, setSchemaOpen] = useState(false); const { data } = useQuery({ queryKey: queryKeys.lexiconStatus(nsid ? [nsid] : []), queryFn: () => get(`/lexicon-status?collections=${encodeURIComponent(nsid)}`), enabled: published === undefined && Boolean(nsid), staleTime: 5 * 60_000, }); const isPublished = published !== undefined ? published : Boolean(nsid && data?.published?.[nsid]); if (!isPublished) return null; return ( <> {/* A span, not a button: this can sit inside a row that is itself a link, where a nested button would be invalid. preventDefault stops the link from navigating and stopPropagation stops its click handler, so the check opens the popover instead of opening the record list. */} } aria-label="What does this check mean?" onClick={(event) => { event.stopPropagation(); event.preventDefault(); }} className="inline-flex cursor-pointer rounded-full text-key outline-none transition-colors hover:text-key/80 focus-visible:ring-[3px] focus-visible:ring-ring/50" > {/* This popup portals to the body but, in React, still bubbles events to its ancestors — which can include a row that is a link. Stop clicks here so nothing inside triggers that row's navigation. */} event.stopPropagation()} className="max-w-[17rem] rounded-2xl border border-border bg-card p-4 text-left shadow-xl outline-none" > Published lexicon These records follow an openly published definition called a lexicon. Any app on the AT Protocol network can look up what they are and how they're structured.
Learn more →
{nsid && ( )} ); } /** * A modal that shows a published lexicon in plain terms — the record's fields, * each with a friendly type, whether it's required, and its description — * fetched when it opens. Not raw JSON: this is for people reading their own * data, not implementing against it. Controlled, and — like the popover — it * stops click propagation so it can live inside a row that is a link without * triggering its navigation. * * @param {object} props * @param {string} props.nsid * @param {boolean} props.open * @param {(open: boolean) => void} props.onOpenChange */ function SchemaDialog({ nsid, open, onOpenChange }) { const [showRaw, setShowRaw] = useState(false); const { data, isFetching, isError } = useQuery({ queryKey: queryKeys.lexiconSchema(nsid), queryFn: () => get(`/lexicon-schema?collection=${encodeURIComponent(nsid)}`), enabled: open, staleTime: 5 * 60_000, }); const lexicon = data?.lexicon ?? null; const main = lexicon?.defs?.main; const description = main?.description || lexicon?.description || ''; const record = main?.record; const properties = record?.properties || {}; const required = new Set(record?.required || []); const fields = Object.keys(properties); return ( event.stopPropagation()} className="fixed inset-0 z-40 bg-black/60 backdrop-blur-sm" /> event.stopPropagation()} className="fixed top-1/2 left-1/2 z-50 flex max-h-[85vh] w-[calc(100vw-2rem)] max-w-lg -translate-x-1/2 -translate-y-1/2 flex-col overflow-hidden rounded-3xl border border-border bg-card p-6 shadow-2xl outline-none" >
{nsid}

What this kind of record can hold

{isFetching && !lexicon ? (

Loading…

) : isError || !lexicon ? (

This schema couldn't be loaded right now.

) : ( <> {description && (

{description}

)} {fields.length ? (
{fields.map((name) => ( ))}
) : (

This record type doesn't define named fields.

)} {/* Friendly by default, raw on demand: the exact schema for anyone who wants it, tucked behind a disclosure. */}
{showRaw && (
                      {JSON.stringify(lexicon, null, 2)}
                    
)}
)}
); } /** * One field of a record: its name, a plain-English type, a Required marker, and * its description if the lexicon gives one. * * @param {object} props * @param {string} props.name * @param {Record} props.field * @param {boolean} props.required */ function SchemaField({ name, field, required }) { const { label, Icon } = describeType(field); return (
{name}
{label} {required && ( Required )}
{typeof field?.description === 'string' && field.description && (
{field.description}
)}
); } /** * A friendly, non-technical name and matching icon for a lexicon field's type — * so the field list reads without protocol vocabulary and scans by icon. * @param {Record} [field] * @returns {{ label: string, Icon: typeof TypeIcon }} */ function describeType(field) { if (!field || typeof field !== 'object') return { label: 'Value', Icon: CircleHelpIcon }; switch (field.type) { case 'string': { const byFormat = { datetime: { label: 'Date & time', Icon: CalendarIcon }, uri: { label: 'Link', Icon: GlobeIcon }, 'at-uri': { label: 'Record link', Icon: LinkIcon }, did: { label: 'Account', Icon: UserIcon }, 'at-identifier': { label: 'Account', Icon: UserIcon }, handle: { label: 'Handle', Icon: AtSignIcon }, language: { label: 'Language', Icon: LanguagesIcon }, cid: { label: 'Content hash', Icon: FingerprintIcon }, tid: { label: 'Timestamp id', Icon: ClockIcon }, }; if (typeof field.format === 'string' && byFormat[field.format]) return byFormat[field.format]; if (Array.isArray(field.knownValues) || Array.isArray(field.enum)) return { label: 'Option', Icon: CircleDotIcon }; return { label: 'Text', Icon: TypeIcon }; } case 'integer': case 'number': return { label: 'Number', Icon: HashIcon }; case 'boolean': return { label: 'Yes / no', Icon: ToggleLeftIcon }; case 'blob': return { label: 'File', Icon: PaperclipIcon }; case 'bytes': return { label: 'Binary data', Icon: BinaryIcon }; case 'array': return { label: `List of ${describeType(field.items).label.toLowerCase()}`, Icon: ListIcon, }; case 'union': return { label: 'One of several types', Icon: LayersIcon }; case 'ref': return { label: 'Linked record', Icon: LinkIcon }; case 'object': return { label: 'Group of fields', Icon: FolderIcon }; case 'unknown': return { label: 'Any data', Icon: CircleHelpIcon }; default: return { label: typeof field.type === 'string' ? field.type.charAt(0).toUpperCase() + field.type.slice(1) : 'Value', Icon: CircleHelpIcon, }; } }