papyr-react
v1.0.0
Published
React components, hooks, and utilities for building Papyr knowledge experiences
Maintainers
Readme
papyr-react
Components vs. Blocks
Papyr exposes two layers, similar to shadcn/ui:
- Components – focused building blocks such as
FileHierarchy,FileSearch,NoteViewer, andTableOfContents. - Blocks – opinionated compositions that wire multiple components together. The new
WorkspaceBlockusesDoubleSidebarLayoutto render a file hierarchy + main + outline surface with independent scrolling.
import {
WorkspaceBlock,
hydrateSearchIndex,
type WorkspaceBlockProps
} from 'papyr-react'
const searchIndex = hydrateSearchIndex(serializedIndex)
const workspaceProps: WorkspaceBlockProps = {
notes,
tree,
searchIndex,
graph,
initialSlug: notes[0]?.slug
}
export function App() {
return <WorkspaceBlock {...workspaceProps} />
}Blocks accept slots so you can swap any pane:
<WorkspaceBlock
{...workspaceProps}
rightSidebar={({ activeNote }) => (
<CustomMetadataPanel note={activeNote} />
)}
/>Use components directly when you need finer control, or drop in a block for a ready-made layout.
Hydrating build output
When you load JSON produced by papyr-core, hydrate the search index before
passing it to PapyrProvider:
import { hydrateSearchIndex } from 'papyr-react'
import { type SerializedSearchIndex } from 'papyr-core/runtime'
const serialized: SerializedSearchIndex = /* build output */
const searchIndex = hydrateSearchIndex(serialized)This accepts either the serialized payload or an already hydrated index, so it can be used safely in shared code paths.
Syncing with a router
Use useRoutableActiveNote to keep Papyr state in sync with your routing
library. Provide two callbacks: one to read the current slug from the router and
another to push updates when the user selects a different note.
import { useCallback } from 'react'
import { useNavigate, useParams } from 'react-router-dom'
import { useRoutableActiveNote } from 'papyr-react'
const navigate = useNavigate()
const { slug } = useParams<{ slug?: string }>()
const { activeNote, activeSlug, setActiveSlug } = useRoutableActiveNote(notes, {
getCurrentSlug: useCallback(() => slug ?? null, [slug]),
onSlugChange: useCallback(
(nextSlug) => {
navigate(nextSlug ? `/notes/${nextSlug}` : '/')
},
[navigate]
)
})The hook exposes the same API as useActiveNote, so existing components (e.g.
sidebar or note viewer) continue to work while the URL stays in sync.
