chadtmiller.com/pds.js
An atproto PDS in JavaScript, for Cloudflare Workers and Node
0
0
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 (
<>
<Popover.Root open={popoverOpen} onOpenChange={setPopoverOpen}>
{/* 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. */}
<Popover.Trigger
nativeButton={false}
render={<span />}
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"
>
<CircleCheckIcon className={cn('size-4 shrink-0', className)} />
</Popover.Trigger>
<Popover.Portal>
<Popover.Positioner
side="top"
align="center"
sideOffset={8}
className="z-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. */}
<Popover.Popup
onClick={(event) => event.stopPropagation()}
className="max-w-[17rem] rounded-2xl border border-border bg-card p-4 text-left shadow-xl outline-none"
>
<Popover.Title className="mb-1.5 flex items-center gap-1.5 font-semibold text-[13px] text-foreground">
<CircleCheckIcon className="size-3.5 text-key" />
Published lexicon
</Popover.Title>
<Popover.Description className="text-[12.5px] text-muted-foreground leading-relaxed">
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.
</Popover.Description>
<div className="mt-3 flex items-center gap-3">
<button
type="button"
onClick={() => {
setPopoverOpen(false);
setSchemaOpen(true);
}}
className="inline-flex items-center gap-1.5 rounded-lg bg-accent px-2.5 py-1.5 font-medium text-[12.5px] text-foreground outline-none transition-colors hover:bg-muted focus-visible:ring-[3px] focus-visible:ring-ring/50"
>
View schema
</button>
<a
href="https://atproto.com/guides/lexicon"
target="_blank"
rel="noreferrer"
className="font-medium text-[12.5px] text-key hover:underline"
>
Learn more →
</a>
</div>
</Popover.Popup>
</Popover.Positioner>
</Popover.Portal>
</Popover.Root>
{nsid && (
<SchemaDialog
nsid={nsid}
open={schemaOpen}
onOpenChange={setSchemaOpen}
/>
)}
</>
);
}
/**
* 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 (
<BaseDialog.Root open={open} onOpenChange={onOpenChange}>
<BaseDialog.Portal>
<BaseDialog.Backdrop
onClick={(event) => event.stopPropagation()}
className="fixed inset-0 z-40 bg-black/60 backdrop-blur-sm"
/>
<BaseDialog.Popup
onClick={(event) => 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"
>
<div className="mb-4 flex items-start gap-3">
<div className="min-w-0">
<BaseDialog.Title className="break-all font-extrabold text-[16px] text-foreground tracking-tight">
{nsid}
</BaseDialog.Title>
<p className="mt-0.5 text-[12.5px] text-muted-foreground">
What this kind of record can hold
</p>
</div>
<div className="flex-1" />
<BaseDialog.Close className="grid size-8 shrink-0 place-items-center rounded-xl text-faint outline-none transition-colors hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50 [&_svg]:size-4">
<XIcon />
</BaseDialog.Close>
</div>
<div className="overflow-y-auto">
{isFetching && !lexicon ? (
<p className="py-8 text-center text-[13px] text-muted-foreground">
Loading…
</p>
) : isError || !lexicon ? (
<p className="py-8 text-center text-[13px] text-muted-foreground">
This schema couldn't be loaded right now.
</p>
) : (
<>
{description && (
<p className="mb-4 text-[13px] text-muted-foreground leading-relaxed">
{description}
</p>
)}
{fields.length ? (
<dl className="flex flex-col gap-3">
{fields.map((name) => (
<SchemaField
key={name}
name={name}
field={properties[name]}
required={required.has(name)}
/>
))}
</dl>
) : (
<p className="py-6 text-center text-[13px] text-muted-foreground">
This record type doesn't define named fields.
</p>
)}
{/* Friendly by default, raw on demand: the exact schema for
anyone who wants it, tucked behind a disclosure. */}
<div className="mt-5 border-border border-t pt-4">
<button
type="button"
onClick={() => setShowRaw((v) => !v)}
className="inline-flex items-center gap-1.5 rounded-md font-medium text-[12.5px] text-muted-foreground outline-none transition-colors hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50"
>
<ChevronRightIcon
className={cn(
'size-3.5 transition-transform',
showRaw && 'rotate-90',
)}
/>
{showRaw
? 'Hide technical details'
: 'Show technical details'}
</button>
{showRaw && (
<pre className="mt-3 overflow-auto rounded-2xl border border-border bg-muted p-4 font-mono text-[11px] text-foreground leading-relaxed">
{JSON.stringify(lexicon, null, 2)}
</pre>
)}
</div>
</>
)}
</div>
</BaseDialog.Popup>
</BaseDialog.Portal>
</BaseDialog.Root>
);
}
/**
* 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<string, unknown>} props.field
* @param {boolean} props.required
*/
function SchemaField({ name, field, required }) {
const { label, Icon } = describeType(field);
return (
<div className="rounded-2xl border border-border bg-muted/40 px-3.5 py-3">
<div className="flex flex-wrap items-center gap-x-2 gap-y-1">
<dt className="font-semibold text-[13.5px] text-foreground">{name}</dt>
<span className="inline-flex items-center gap-1 rounded-md bg-key/10 px-1.5 py-0.5 font-medium text-[11px] text-key">
<Icon className="size-3" />
{label}
</span>
{required && (
<span className="text-[11px] text-muted-foreground">Required</span>
)}
</div>
{typeof field?.description === 'string' && field.description && (
<dd className="mt-1 text-[12.5px] text-muted-foreground leading-relaxed">
{field.description}
</dd>
)}
</div>
);
}
/**
* 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<string, unknown>} [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,
};
}
}