{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "bookstore", "title": "Bookstore RPC demo", "description": "Five live Book RPC flows: list, get, RHF stepper create with ISBN CEL, update masks with etags, and AutoForm delete.", "dependencies": [ "@buf/googleapis_googleapis.bufbuild_es", "@bufbuild/protobuf", "@connectrpc/connect", "@connectrpc/connect-query", "@tanstack/react-query", "react-hook-form" ], "registryDependencies": [ "@protoform/auto-form", "@protoform/protoform-demo-runtime" ], "files": [ { "path": "registry/base-nova/protoform/demo/bookstore/bookstore-demo.tsx", "content": "\"use client\";\n\nimport { createRouterTransport, type Transport } from \"@connectrpc/connect\";\nimport {\n skipToken,\n TransportProvider,\n useQuery,\n} from \"@connectrpc/connect-query\";\nimport { QueryClient, QueryClientProvider } from \"@tanstack/react-query\";\nimport { useState } from \"react\";\n\nimport {\n Alert,\n AlertDescription,\n AlertTitle,\n} from \"@/registry/base-nova/protoform/components/alert\";\nimport { Badge } from \"@/registry/base-nova/protoform/components/badge\";\nimport { Button } from \"@/registry/base-nova/protoform/components/button\";\nimport {\n Card,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n} from \"@/registry/base-nova/protoform/components/card\";\nimport {\n Field,\n FieldLabel,\n} from \"@/registry/base-nova/protoform/components/field\";\nimport { Input } from \"@/registry/base-nova/protoform/components/input\";\nimport {\n type Book,\n LibraryService,\n} from \"@/registry/base-nova/protoform/demo/runtime/gen/protoform/conformance/v1/aip_pb\";\n\nimport { CreateBookForm } from \"./create-book-form\";\nimport { DeleteBookForm } from \"./delete-book-form\";\nimport { createLibraryService } from \"./library-service\";\nimport { UpdateBookForm } from \"./update-book-form\";\n\nexport const client = \"only\";\n\ntype View = \"create\" | \"delete\" | \"detail\" | \"edit\" | \"list\";\n\ninterface BookstoreDemoProps {\n transport?: Transport;\n visitorId?: string;\n}\n\nconst VISITOR_KEY = \"protoform-bookstore-visitor\";\n\nfunction newVisitorId(): string {\n return `demo-${globalThis.crypto.randomUUID().toLowerCase()}`;\n}\n\nfunction initialVisitorId(provided?: string): string {\n if (provided) {\n return provided;\n }\n if (typeof sessionStorage === \"undefined\") {\n return newVisitorId();\n }\n const stored = sessionStorage.getItem(VISITOR_KEY);\n if (stored) {\n return stored;\n }\n const created = newVisitorId();\n sessionStorage.setItem(VISITOR_KEY, created);\n return created;\n}\n\nexport function BookstoreDemo({\n transport: providedTransport,\n visitorId: providedVisitorId,\n}: BookstoreDemoProps) {\n const [queryClient] = useState(() => new QueryClient());\n const [transport] = useState(\n () =>\n providedTransport ??\n createRouterTransport((router) =>\n router.service(LibraryService, createLibraryService())\n )\n );\n const [visitorId, setVisitorId] = useState(() =>\n initialVisitorId(providedVisitorId)\n );\n\n function resetLibrary() {\n const next = newVisitorId();\n if (typeof sessionStorage !== \"undefined\") {\n sessionStorage.setItem(VISITOR_KEY, next);\n }\n queryClient.clear();\n setVisitorId(next);\n }\n\n return (\n \n \n \n \n \n );\n}\n\nfunction BookstoreWorkspace({\n onReset,\n parent,\n}: {\n onReset: () => void;\n parent: string;\n}) {\n const [view, setView] = useState(\"list\");\n const [selectedName, setSelectedName] = useState();\n const [filter, setFilter] = useState(\"\");\n const list = useQuery(LibraryService.method.listBooks, { filter, parent });\n const detail = useQuery(\n LibraryService.method.getBook,\n selectedName ? { name: selectedName } : skipToken\n );\n\n function openBook(name: string) {\n setSelectedName(name);\n setView(\"detail\");\n }\n\n function returnToList() {\n setSelectedName(undefined);\n setView(\"list\");\n }\n\n function resetLibrary() {\n returnToList();\n setFilter(\"\");\n onReset();\n }\n\n function handleDeleted() {\n returnToList();\n void list.refetch();\n }\n\n const selected = detail.data;\n\n return (\n
\n
\n
\n
\n 5 RPCs\n Generated bindings\n Visitor-isolated state\n
\n

Protoform library

\n

\n Create with an RHF stepper, edit with a field mask and etag, and\n confirm deletion through AutoForm.\n

\n
\n \n
\n\n {view === \"list\" ? (\n
\n
\n
\n

\n Books\n

\n

\n Live ListBooks results from your temporary library.\n

\n
\n \n
\n \n Filter books\n setFilter(event.target.value)}\n placeholder=\"Title or ISBN\"\n type=\"search\"\n value={filter}\n />\n \n {list.isPending ? (\n

Loading books…

\n ) : list.error ? (\n \n Books not loaded\n \n {list.error.message}\n \n \n \n ) : list.data.books.length === 0 ? (\n

\n No books match this filter.\n

\n ) : (\n
\n {list.data.books.map((book) => (\n \n \n \n openBook(book.name)}\n type=\"button\"\n variant=\"link\"\n >\n {book.displayName}\n \n \n {book.isbn}\n \n \n

\n {book.note || \"No note yet.\"}\n

\n
\n
\n ))}\n
\n )}\n
\n ) : null}\n\n {view === \"create\" ? (\n openBook(name)}\n parent={parent}\n />\n ) : null}\n\n {view === \"detail\" ? (\n setView(\"delete\")}\n onEdit={() => setView(\"edit\")}\n onRetry={() => detail.refetch()}\n />\n ) : null}\n\n {view === \"edit\" && selected ? (\n setView(\"detail\")}\n onUpdated={() => setView(\"detail\")}\n />\n ) : null}\n\n {view === \"delete\" && selected ? (\n setView(\"detail\")}\n onDeleted={handleDeleted}\n />\n ) : null}\n
\n );\n}\n\nfunction BookDetail({\n book,\n error,\n isPending,\n onBack,\n onDelete,\n onEdit,\n onRetry,\n}: {\n book?: Book;\n error?: string;\n isPending: boolean;\n onBack: () => void;\n onDelete: () => void;\n onEdit: () => void;\n onRetry: () => void;\n}) {\n if (isPending) {\n return

Loading book…

;\n }\n if (error || !book) {\n return (\n \n Book not loaded\n \n {error ?? \"The book no longer exists.\"}\n \n \n \n );\n }\n\n return (\n
\n \n
\n

\n {book.displayName}\n

\n

{book.isbn}

\n

{book.note || \"No note yet.\"}

\n
\n
\n
\n
Resource name
\n
{book.name}
\n
\n
\n
Etag
\n
{book.etag}
\n
\n
\n
\n \n \n
\n
\n );\n}\n\nexport default BookstoreDemo;\n", "type": "registry:component", "target": "~/components/protoform-bookstore/bookstore-demo.tsx" }, { "path": "registry/base-nova/protoform/demo/bookstore/create-book-form.tsx", "content": "\"use client\";\n\nimport { create } from \"@bufbuild/protobuf\";\nimport {\n createConnectQueryKey,\n useMutation,\n useTransport,\n} from \"@connectrpc/connect-query\";\nimport { useQueryClient } from \"@tanstack/react-query\";\nimport { useState } from \"react\";\n\nimport {\n Alert,\n AlertDescription,\n AlertTitle,\n} from \"@/registry/base-nova/protoform/components/alert\";\nimport { Button } from \"@/registry/base-nova/protoform/components/button\";\nimport {\n Field,\n FieldDescription,\n FieldError,\n FieldLabel,\n} from \"@/registry/base-nova/protoform/components/field\";\nimport { Input } from \"@/registry/base-nova/protoform/components/input\";\nimport { Textarea } from \"@/registry/base-nova/protoform/components/textarea\";\nimport { useProtoForm } from \"@/registry/base-nova/protoform/hooks/use-proto-form\";\nimport { BookFormBinding } from \"@/registry/base-nova/protoform/demo/runtime/gen/protoform/conformance/v1/aip_form\";\nimport {\n BookSchema,\n LibraryService,\n} from \"@/registry/base-nova/protoform/demo/runtime/gen/protoform/conformance/v1/aip_pb\";\n\ninterface CreateBookFormProps {\n onCancel: () => void;\n onCreated: (name: string) => void;\n parent: string;\n}\n\nconst bookIdInputPattern = String.raw`[a-z][a-z0-9\\-]{2,62}[a-z0-9]`;\n\nexport function CreateBookForm({\n onCancel,\n onCreated,\n parent,\n}: CreateBookFormProps) {\n const [step, setStep] = useState<\"book\" | \"publishing\">(\"book\");\n const [bookId, setBookId] = useState(\"\");\n const queryClient = useQueryClient();\n const transport = useTransport();\n const mutation = useMutation(LibraryService.method.createBook);\n const form = useProtoForm(BookFormBinding.descriptor, {\n defaultValues: create(BookSchema, { displayName: \"\", isbn: \"\", note: \"\" }),\n mode: \"onChange\",\n serverPathPrefix: \"book\",\n });\n\n async function continueToPublishing() {\n const valid = await form.trigger([\"displayName\", \"isbn\"]);\n if (valid) {\n setStep(\"publishing\");\n }\n }\n\n async function submitBook() {\n if (!bookId.match(/^[a-z][a-z0-9-]{2,62}[a-z0-9]$/)) {\n return;\n }\n form.clearServerErrorContext();\n try {\n const response = await mutation.mutateAsync({\n book: form.createMessage(),\n bookId,\n parent,\n });\n void queryClient.invalidateQueries({\n queryKey: createConnectQueryKey({\n cardinality: undefined,\n input: { parent },\n schema: LibraryService.method.listBooks,\n transport,\n }),\n });\n onCreated(response.name);\n } catch (error) {\n form.setServerErrors(error);\n }\n }\n\n const titleError = form.formState.errors.displayName?.message;\n const isbnError = form.formState.errors.isbn?.message;\n const bookIdInvalid =\n bookId.length > 0 && !bookId.match(/^[a-z][a-z0-9-]{2,62}[a-z0-9]$/);\n\n return (\n
\n
\n

\n Step {step === \"book\" ? \"1\" : \"2\"} of 2\n

\n

\n Create a book\n

\n

\n React Hook Form state, generated Protoform binding, and protobuf CEL\n validation.\n

\n
\n\n {step === \"book\" ? (\n
\n \n Title\n \n {titleError ? {titleError} : null}\n \n \n ISBN-13\n \n {isbnError ? (\n {isbnError}\n ) : (\n \n Thirteen digits. The check digit is verified by the CEL rule in\n the proto.\n \n )}\n \n
\n \n \n
\n
\n ) : (\n \n \n Book id\n setBookId(event.target.value)}\n pattern={bookIdInputPattern}\n required\n value={bookId}\n />\n {bookIdInvalid ? (\n \n Use lowercase letters, numbers, and hyphens.\n \n ) : null}\n \n \n Note\n