Claude-skill-registry document-patterns
Document and PDF patterns for Ballee using @kit/documents for document management and @kit/pdf-core for PDF generation. Use when working with file uploads, document viewers, or generating PDFs.
install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/document-patterns" ~/.claude/skills/majiayu000-claude-skill-registry-document-patterns && rm -rf "$T"
manifest:
skills/data/document-patterns/SKILL.mdsource content
Document & PDF Patterns
@kit/documents Package
The
@kit/documents package provides a complete document management system with types, components, hooks, and server actions.
Exports
import type { Document, DocumentWithUrl, EditableDocument } from '@kit/documents/types'; import { FileTypeCategory, ThumbnailSizes } from '@kit/documents/constants'; import { getFileCategory, formatFileSize, downloadDocument } from '@kit/documents/utils'; import { useDocumentViewer, useDocumentDownload, useReactPdf } from '@kit/documents/hooks'; import { DocumentList, DocumentViewerDialog, SortableDocumentList } from '@kit/documents/components'; import { getDocumentSignedUrlAction } from '@kit/documents/server';
Core Types
// Base document interface interface Document { id: string; fileName: string; fileSize: number; mimeType: string; storagePath: string; bucket: string; uploadedAt?: string | null; description?: string | null; } // Document with signed URL for display interface DocumentWithUrl extends Document { signedUrl: string; thumbnailUrl?: string | null; } // Editable document with title/order for CRUD lists interface EditableDocument extends Document { title: string; displayOrder?: number | null; documentType?: string; } interface EditableDocumentWithUrl extends EditableDocument { signedUrl: string; thumbnailUrl?: string | null; } // For document updates interface DocumentUpdateInput { title?: string; description?: string; } // For reordering documents interface DocumentReorderItem { id: string; displayOrder: number; } // For upload dialogs interface DocumentUploadInput { file: File; title: string; documentType?: string; description?: string; }
Constants
// File type categories import { FileTypeCategory, getFileCategory } from '@kit/documents/constants'; FileTypeCategory.IMAGE // 'image' FileTypeCategory.PDF // 'pdf' FileTypeCategory.VIDEO // 'video' FileTypeCategory.DOCUMENT // 'document' FileTypeCategory.SPREADSHEET // 'spreadsheet' FileTypeCategory.OTHER // 'other' const category = getFileCategory('image/jpeg'); // 'image' // Thumbnail sizes for image transforms import { ThumbnailSizes } from '@kit/documents/constants'; ThumbnailSizes.SMALL // { width: 100, height: 100, quality: 80 } ThumbnailSizes.MEDIUM // { width: 200, height: 200, quality: 80 } ThumbnailSizes.LARGE // { width: 400, height: 400, quality: 85 } ThumbnailSizes.XLARGE // { width: 800, height: 800, quality: 90 }
Components
Display Components:
import { FileTypeIcon, // Icon based on MIME type DocumentThumbnail, // Thumbnail with fallback icon DocumentCard, // Card view with actions DocumentRow, // List view row DocumentList, // Simple list wrapper } from '@kit/documents/components';
Viewer Components:
import { ImageViewer, // Image preview with pan/zoom PdfViewer, // PDF preview using react-pdf DocumentNavigator, // Navigation between documents DocumentViewerDialog, // Full modal viewer with keyboard shortcuts } from '@kit/documents/components';
Editable Components:
import { EditableDocumentCard, // Card with edit/delete actions SortableDocumentList, // Full CRUD list with reordering DocumentEditDialog, // Edit metadata dialog DocumentUploadDialog, // Upload new document dialog } from '@kit/documents/components';
Hooks
import { useDocumentUrl, // Fetch signed URL for a document useDocumentDownload, // Download document utility useDocumentViewer, // Manage viewer state (current doc, zoom, fullscreen) useDocumentEdit, // Manage edit dialog state useDocumentReorder, // Manage document ordering useReactPdf, // PDF viewer utilities } from '@kit/documents/hooks';
Server Actions
import { getDocumentSignedUrlAction } from '@kit/documents/server'; // Get a signed URL for viewing/downloading const result = await getDocumentSignedUrlAction({ bucket: 'venue-documents', path: 'venue-123/photo.jpg', expiresIn: 3600, // Optional: seconds (default 3600) download: false, // Optional: force download transform: { // Optional: image transforms width: 400, height: 400, quality: 85, }, }); if (result.success) { const url = result.url; }
@kit/pdf-core Package
Shared PDF generation utilities, styles, and components using
@react-pdf/renderer.
Exports
// Utilities import { sanitizeForPdf, // Deep sanitize Supabase data for PDF nodeStreamToWebStream, // Convert Node stream to Web stream getPdfResponseHeaders, // Get standard PDF response headers } from '@kit/pdf-core'; // Style tokens import { pdfColors, // Brand colors pdfFonts, // Font families pdfFontSizes, // Size scale pdfSpacing, // Spacing scale pdfPage, // Page dimensions } from '@kit/pdf-core'; // Pre-built style objects import { headerStyles, footerStyles, sectionStyles, tableStyles, badgeStyles, textStyles, } from '@kit/pdf-core'; // Components import { PdfHeader, // Document header with logo PdfFooter, // Page footer with numbers PdfSection, // Titled section wrapper PdfTable, // Data table PdfBadge, // Status badge PdfBadgeList, // List of badges PdfInfoSection, // Key-value info display PdfFieldRow, // Form field row PdfNotes, // Notes section PdfTotals, // Totals section PdfBankSection, // Bank details section PdfLegend, // Legend/key section } from '@kit/pdf-core/components';
PDF Template Pattern
// packages/features/my-feature/src/templates/pdf/my-template.tsx import React from 'react'; import { Document, Page, View, Text } from '@react-pdf/renderer'; import { PdfHeader, PdfFooter, PdfSection, PdfTable, pageStyles, pdfColors, } from '@kit/pdf-core'; interface MyPdfProps { data: { title: string; items: Array<{ name: string; value: number }>; }; } export function MyPdfTemplate({ data }: MyPdfProps) { return ( <Document> <Page size="A4" style={pageStyles.page}> <PdfHeader title={data.title} /> <View style={pageStyles.content}> <PdfSection title="Items"> <PdfTable columns={[ { header: 'Name', accessor: 'name', width: '70%' }, { header: 'Value', accessor: 'value', width: '30%', align: 'right' }, ]} data={data.items} /> </PdfSection> </View> <PdfFooter /> </Page> </Document> ); }
Document Management Patterns
Basic Read-Only Document List
import { DocumentList, DocumentViewerDialog } from '@kit/documents/components'; import { useDocumentViewer } from '@kit/documents/hooks'; import type { DocumentWithUrl } from '@kit/documents/types'; interface Props { documents: DocumentWithUrl[]; } export function DocumentGallery({ documents }: Props) { const viewer = useDocumentViewer(documents); return ( <> <DocumentList documents={documents} onView={(doc) => viewer.open(doc)} onDownload={(doc) => window.open(doc.signedUrl)} /> <DocumentViewerDialog documents={documents} currentIndex={viewer.currentIndex} isOpen={viewer.isOpen} onClose={viewer.close} onNavigate={viewer.navigate} zoom={viewer.zoom} onZoomChange={viewer.setZoom} /> </> ); }
Editable Document List with CRUD
import { SortableDocumentList } from '@kit/documents/components'; import type { EditableDocumentWithUrl, DocumentUploadInput } from '@kit/documents/types'; interface Props { documents: EditableDocumentWithUrl[]; documentTypes: Array<{ value: string; label: string }>; onUpload: (input: DocumentUploadInput) => Promise<void>; onEdit: (id: string, data: { title?: string; description?: string }) => Promise<void>; onDelete: (id: string) => Promise<void>; onReorder: (items: Array<{ id: string; displayOrder: number }>) => Promise<void>; } export function VenueDocumentsList({ documents, documentTypes, onUpload, onEdit, onDelete, onReorder, }: Props) { return ( <SortableDocumentList documents={documents} documentTypes={documentTypes} onUpload={onUpload} onEdit={onEdit} onDelete={onDelete} onReorder={onReorder} emptyMessage="No documents uploaded yet" /> ); }
Adapter Pattern (Database to Component Types)
When your database schema doesn't match
@kit/documents types exactly, create an adapter:
// adapters/venue-document-adapter.ts import type { EditableDocumentWithUrl, Document } from '@kit/documents/types'; interface VenueDocument { id: string; venue_id: string; title: string; document_type: string | null; storage_path: string; file_name: string; file_size: number; mime_type: string; display_order: number | null; created_at: string; } export function toEditableDocument( doc: VenueDocument, signedUrl: string, thumbnailUrl?: string, ): EditableDocumentWithUrl { return { id: doc.id, title: doc.title, fileName: doc.file_name, fileSize: doc.file_size, mimeType: doc.mime_type, storagePath: doc.storage_path, bucket: 'venue-documents', displayOrder: doc.display_order, documentType: doc.document_type ?? undefined, uploadedAt: doc.created_at, signedUrl, thumbnailUrl, }; }
PDF Generation Patterns
CRITICAL: Use API Routes for PDF generation, not Server Actions. See
api-patterns skill for the complete pattern.
Quick Reference
// 1. Fetch data with RLS-protected client const data = await fetchData(client, id); // 2. Sanitize for PDF (REQUIRED!) import { sanitizeForPdf } from '@kit/pdf-core'; const safeData = sanitizeForPdf(data); // 3. Render to stream import { renderToStream } from '@react-pdf/renderer'; const stream = await renderToStream(<MyPdfTemplate data={safeData} />); // 4. Return streaming response import { nodeStreamToWebStream, getPdfResponseHeaders } from '@kit/pdf-core'; return new Response(nodeStreamToWebStream(stream), { headers: getPdfResponseHeaders('my-document.pdf'), });
Existing PDF Routes
| Route | Purpose |
|---|---|
| Admin hire order documents |
| Dancer resume/CV |
| Cast assignment sheets |
Unified Storage Library (@kit/shared/storage)
The unified storage library provides centralized utilities for storage URLs, path extraction, and thumbnail generation.
Exports
import { // URL Service StorageUrlService, createStorageUrlService, StorageBuckets, SignedUrlExpiry, type StorageBucket, type SignedUrlOptions, // Path Service StoragePathService, extractStoragePath, generatePublicThumbnailUrl, // Constants ThumbnailSizes, type ThumbnailSize, } from '@kit/shared/storage';
Storage Buckets
// Account/Profile StorageBuckets.ACCOUNT_IMAGE // 'account_image' StorageBuckets.PROFILE_MEDIA // 'profile-media' (public) StorageBuckets.DANCER_MEDIA // 'dancer-media' // Documents StorageBuckets.VENUE_DOCUMENTS // 'venue-documents' StorageBuckets.PRODUCTION_DOCUMENTS // 'production-documents' StorageBuckets.LEGAL_DOCUMENTS // 'legal-documents' StorageBuckets.REIMBURSEMENT_DOCUMENTS // 'reimbursement-documents' // Legal/Compliance StorageBuckets.CONTRACTS // 'contracts' StorageBuckets.IDENTITY_DOCUMENTS // 'identity-documents' StorageBuckets.INVOICE_PDFS // 'invoice-pdfs'
Signed URL Expiry Times
SignedUrlExpiry.IMMEDIATE_DISPLAY // 3600 (1 hour) - UI display SignedUrlExpiry.DOWNLOAD // 86400 (24 hours) - download links SignedUrlExpiry.PROFILE_PHOTO // 604800 (7 days) - cached URLs SignedUrlExpiry.ADMIN_REVIEW // 86400 (24 hours) - admin views SignedUrlExpiry.MAX // 604800 (7 days max) SignedUrlExpiry.MIN // 60 (minimum)
StorageUrlService
import { StorageUrlService, StorageBuckets, SignedUrlExpiry } from '@kit/shared/storage'; const service = new StorageUrlService(client); // Single signed URL const result = await service.getSignedUrl( StorageBuckets.VENUE_DOCUMENTS, 'venue-123/document.pdf', { expiresIn: SignedUrlExpiry.DOWNLOAD } ); // Batch signed URLs (more efficient for multiple files) const results = await service.getBatchSignedUrls( StorageBuckets.VENUE_DOCUMENTS, ['path1.pdf', 'path2.pdf'], { expiresIn: SignedUrlExpiry.IMMEDIATE_DISPLAY } ); // Enrich items with signed URLs const docsWithUrls = await service.enrichWithSignedUrls( StorageBuckets.VENUE_DOCUMENTS, documents, (doc) => doc.storage_path, (doc, url) => ({ ...doc, signedUrl: url }), ); // With thumbnail transform const result = await service.getSignedUrl( StorageBuckets.DANCER_MEDIA, 'user-123/photo.jpg', { expiresIn: SignedUrlExpiry.IMMEDIATE_DISPLAY, transform: ThumbnailSizes.MEDIUM, } );
StoragePathService (Path Extraction)
Extract storage paths from signed URLs, public URLs, or bucket-prefixed paths:
import { StoragePathService, extractStoragePath, StorageBuckets } from '@kit/shared/storage'; // Extract from signed URL const path = extractStoragePath( 'https://xxx.supabase.co/storage/v1/object/sign/identity-documents/user-123/doc.pdf?token=...', StorageBuckets.IDENTITY_DOCUMENTS ); // Result: 'user-123/doc.pdf' // Extract from bucket-prefixed path const path2 = extractStoragePath('identity-documents/user-123/doc.pdf'); // Result: 'user-123/doc.pdf' // Detect bucket from path const bucket = StoragePathService.detectBucket(url); // Result: 'identity-documents' // Validate path const result = StoragePathService.validate(storagePath); if (result.isSuccess) { // Use validated path }
Thumbnail Generation
import { ThumbnailSizes, generatePublicThumbnailUrl } from '@kit/shared/storage'; // Preset sizes ThumbnailSizes.SMALL // { width: 100, height: 100, quality: 80 } ThumbnailSizes.MEDIUM // { width: 200, height: 200, quality: 80 } ThumbnailSizes.LARGE // { width: 400, height: 400, quality: 85 } ThumbnailSizes.XLARGE // { width: 800, height: 800, quality: 90 } // For signed URLs - use transform option const result = await service.getSignedUrl(bucket, path, { transform: ThumbnailSizes.MEDIUM, }); // For public URLs - use generatePublicThumbnailUrl const thumbnailUrl = generatePublicThumbnailUrl(publicUrl, { width: 200, height: 200, quality: 80, });
URL Storage Best Practice
CRITICAL: Always store raw storage paths in the database, never signed URLs (they expire).
// ✅ CORRECT - Store raw path await client.from('documents').insert({ storage_path: 'user-123/photo.jpg', // Raw path only }); // ❌ WRONG - Storing signed URL (will expire!) await client.from('documents').insert({ file_url: signedUrl, // This will break after expiry! }); // Generate signed URLs on-demand when displaying const result = await service.getSignedUrl(bucket, doc.storage_path);
Anti-Patterns
// ❌ WRONG - Server Action for PDF generation 'use server'; import { renderToBuffer } from '@react-pdf/renderer'; export async function generatePdf(data) { return await renderToBuffer(<MyPdf data={data} />); // hasOwnProperty error! } // ✅ CORRECT - Use API Route (see api-patterns skill) // ❌ WRONG - Raw Supabase data to PDF const data = await client.from('table').select('*').single(); return <MyPdf data={data} />; // May fail with undefined/null // ✅ CORRECT - Sanitize first import { sanitizeForPdf } from '@kit/pdf-core'; const safeData = sanitizeForPdf(data); return <MyPdf data={safeData} />; // ❌ WRONG - Hardcoded bucket names await client.storage.from('venue-documents').upload(...); // ✅ CORRECT - Use constants import { StorageBuckets } from '@kit/shared/storage'; await client.storage.from(StorageBuckets.VENUE_DOCUMENTS).upload(...);
Related Skills
| Need | Skill |
|---|---|
| Storage service patterns | |
| PDF API routes | |
| UI components | |
| Database migrations for document tables | |
| RLS policies for document access | |