@freedom-code-compliance/annotations
v0.6.0
Published
Shared React SDK for file annotations (PDFs, images) — powers the FCC Plan Review Markup Editor (DEV-527) and future FCC markup features
Readme
@freedom-code-compliance/annotations
Shared React SDK for file annotations — PDFs and images with realtime sync, powering the FCC Plan Review Markup Editor (DEV-527) and future FCC markup features.
What's new in 0.2.1 (2026-04-21)
PDFAnnotationEditorgainsonPdfLoaded({ pageCount })for page navigator wiringPDFAnnotationEditorgainsrenderContextMenu(ctx)+onCreateCommentAtCursor(cursor)for host-driven right-click menus- New type export:
ContextMenuCallContext
What's new in 0.2.0 (2026-04-21)
- Labeled point annotations (
shape_type = 'point') with auto-derived{B|E|M|P}{ordinal}labels - Comments panel components (
CommentsPanel,CommentCard) with per-comment color override - Right-click context menu (
AnnotationContextMenu) for create / attach / move / detach / delete - Page navigator component (
PageNavigator) with pdfjs thumbnails + discipline density dots - New hook:
useFilePanelComments(planSetId)— labeled comments with server-computed ordinals useUpdateAnnotationpatch now acceptsissue_comment_idfor attach/detach/movedisciplineLetter()+resolveColor()utilities +DISCIPLINE_PALETTEconstant
Consumers must have applied the v2.1 backend migrations (fk column on file_annotations, color column on issue_comments, point in the shape_type check, new list_file_panel_comments RPC).
Install
npm install @freedom-code-compliance/annotationsUsage
import { createClient } from '@supabase/supabase-js';
import { AnnotationsProvider, useFileAnnotations } from '@freedom-code-compliance/annotations';
const supabase = createClient(URL, ANON_KEY);
<AnnotationsProvider supabase={supabase} currentUser={{ id: userId, role_code: 'private_provider' }}>
<MyMarkupView fileId={fileId} />
</AnnotationsProvider>
function MyMarkupView({ fileId }) {
const { data: annotations = [], isLoading } = useFileAnnotations(fileId);
// annotations stay live-synced across tabs/users via the file:{fileId} channel.
return <pre>{JSON.stringify(annotations, null, 2)}</pre>;
}PDF.js worker setup (one-time, before mounting <PDFAnnotationEditor>)
This package does NOT auto-configure the PDF.js worker to keep the bundle small. Consumer apps must set pdfjs.GlobalWorkerOptions.workerSrc once during app initialization.
Vite
// In your app entry (e.g. main.tsx)
import { pdfjs } from 'react-pdf';
import workerSrc from 'pdfjs-dist/build/pdf.worker.min.mjs?url';
pdfjs.GlobalWorkerOptions.workerSrc = workerSrc;Next.js
// In your app entry or a top-level layout client component
'use client';
import { pdfjs } from 'react-pdf';
pdfjs.GlobalWorkerOptions.workerSrc = `//unpkg.com/pdfjs-dist@${pdfjs.version}/build/pdf.worker.min.mjs`;Webpack 5
import { pdfjs } from 'react-pdf';
pdfjs.GlobalWorkerOptions.workerSrc = new URL(
'pdfjs-dist/build/pdf.worker.min.mjs',
import.meta.url
).toString();If not configured, <PDFAnnotationEditor> will log a one-time warning to the console on first mount. The PDF will fail to render without the worker.
Realtime sync
useFileAnnotations subscribes to the file:{fileId} private broadcast channel automatically. Create / update / delete events from other clients update the cache within ~200-500ms.
- Opt out: pass
{ subscribe: false }for read-only previews, static exports, or tests that don't need live sync. - Auth: subscribing requires a signed-in Supabase session. The hook calls
supabase.realtime.setAuth()internally on mount and on token rotation (onAuthStateChange). - Private channel: subscription is gated by RLS on
realtime.messages— the caller must have SELECT access to the underlyingpublic.filesrow. - Reconnect: on every
SUBSCRIBEDstatus (including after a disconnect), the hook invalidates the query once to resync any broadcasts missed during downtime.
Mutation hooks
Three mutation hooks are available — all call Phase 1's SECURITY INVOKER RPCs and update the local TanStack cache optimistically:
const create = useCreateAnnotation(fileId);
const update = useUpdateAnnotation(fileId);
const del = useDeleteAnnotation(fileId);
await create.mutateAsync({
file_id: fileId,
page: 1,
shape_type: 'rect',
geometry: { x: 0.1, y: 0.1, w: 0.2, h: 0.2 },
is_internal: false,
});
await update.mutateAsync({ id, patch: { style: { color: '#2563eb' } } });
await del.mutateAsync(id);Optimistic rows get replaced by real rows from the RPC response; broadcasts from your own mutations are idempotent no-ops (already in cache).
See docs/superpowers/specs/2026-04-20-plan-review-markup-editor-design.md in fcc-vault for the full design.
Peer dependencies
- React 18+
- @supabase/supabase-js 2+
- @tanstack/react-query 5+
- Zod 3.22+
Build
npm install
npm run buildOutputs to dist/.
