npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@kopjra/pdf-sentinel

v1.8.0

Published

A React and Preact component library for embedding and interacting with PDF documents using EmbedPDF.

Readme

pdf-sentinel Documentation

A powerful React/Preact library for viewing and interacting with PDF forms. Built on top of embed-pdf-viewer, pdf-sentinel provides a complete solution for rendering PDF documents with form fields, custom toolbars, internationalization, and signature management.

Table of Contents

Installation

npm install @kopjra/pdf-sentinel

Quick Start

import { usePdfSentinel } from "@kopjra/pdf-sentinel";
import "@kopjra/pdf-sentinel/style.css";

function MyPdfViewer() {
    const { PdfSentinelContainer, Toolbar, PdfSentinel } = usePdfSentinel({
        pdf: "/path/to/document.pdf",
        name: "My Document",
    });

    return (
        <div style={{ height: "100vh" }}>
            <PdfSentinelContainer>
                <Toolbar />
                <PdfSentinel />
            </PdfSentinelContainer>
        </div>
    );
}

Features

Core Functionality

  • 📄 PDF Viewing - Render PDF documents with zoom, scroll, and navigation
  • 📋 Form Fields - Support for text inputs, checkboxes, radio buttons, and signature fields
  • ✍️ Form Validation - Built-in validation with error highlighting and auto-scroll to errors
  • 🎨 Toolbar - Customizable toolbar with zoom, page navigation, and download
  • 🌍 Internationalization - Multi-language support with custom translation overrides
  • 🔧 Custom Buttons - Extend the toolbar with custom buttons in left/center/right areas
  • 📝 Signatures - Native conditional signature support with automatic group coloring
  • 🎯 Responsive - Adaptive layout that responds to zoom and scroll events

Toolbar Features

  • Zoom in/out and fit-to-width
  • Page navigation and page count display
  • PDF download capability
  • Custom button support with spacers
  • Multi-language tooltips and labels

Form Features

  • Text field rendering with readonly support
  • Checkbox field rendering with custom styling
  • Radio button field rendering with custom styling
  • Signature field rendering with conditional group coloring
  • Form state management
  • Field validation with error styling
  • Initial values and readonly properties

API Reference

usePdfSentinel Hook

The main hook for initializing and controlling the PDF viewer.

Signature

function usePdfSentinel(options: PdfSentinelOptions): PdfSentinelResult

Options

interface PdfSentinelOptions {
    /** PDF document URL or ArrayBuffer */
    pdf: string | ArrayBuffer;
    
    /** Document name/identifier */
    name: string;
    
    /** Initial form field values */
    initialValues?: InitialValuesType;
    
    /** Hide the default toolbar */
    hideToolbar?: boolean;
    
    /** Callback when document finishes loading */
    onDocumentLoaded?: () => void;
    
    /** Locale code (en, it, es, fr, de) */
    locale?: string;
    
    /** Custom translations for any locale */
    translations?: Record<string, Record<string, string>>;
    
    /** Custom toolbar buttons */
    toolbarButtons?: ToolbarItem[];
    
    /** Conditional signature configurations */
    conditionalSignatures?: ConditionalSignature[];
}

Return Value

interface PdfSentinelResult {
    /** Container component that wraps the PDF viewer and toolbar */
    PdfSentinelContainer: React.ComponentType<{children: React.ReactNode}>;
    
    /** Toolbar component - should be rendered inside PdfSentinelContainer */
    Toolbar: React.ComponentType<{}>;
    
    /** Main PDF viewer component - should be rendered inside PdfSentinelContainer */
    PdfSentinel: React.ComponentType<{}>;
    
    /** Current form state */
    form: UsePdfFormsResult | null;
    
    /** Current page number (1-indexed) */
    currentPage: number | null;
    
    /** Total number of pages */
    totalPages: number | null;
    
    /** Whether PDF is loading */
    loadingPdf: boolean;
    
    /** Whether there was a loading error */
    loadingError: boolean;
    
    /** PDF engine instance */
    engine: PdfEngine<Blob> | null;
    
    /** Whether PDF has been closed */
    closedPdf: boolean;
    
    /** Helper methods */
    helpers: {
        zoomIn: () => void;
        zoomOut: () => void;
        resetZoom: () => void;
        scrollToPage: (pageNumber: number) => void;
        scrollToPosition: (position: { x: number; y: number; behavior?: ScrollBehavior }) => void;
        download: () => void;
    };
}

Types

InitialValuesType

type InitialValuesType = Record<string, {
    value: string | boolean;
    readonly?: boolean;
}>;

Initial form field values. Each key is a field name.

Example:

const initialValues: InitialValuesType = {
    "fullName": { value: "John Doe", readonly: false },
    "signature": { value: true, readonly: true },
    "agreeToTerms": { value: true },
};

ConditionalSignature

interface ConditionalSignature {
    index: string;
    fieldNames: string[];
}

Defines a group of signature fields that are conditional on the same value.

Example:

const conditionalSignatures: ConditionalSignature[] = [
    { index: "1", fieldNames: ["signature1", "signature2"] },
    { index: "2", fieldNames: ["signature3", "signature4"] },
];

UsePdfFormsResult

interface UsePdfFormsResult {
    fields: Field[];
    values: Record<string, string | boolean>;
    setValue: (name: string, value: string | boolean) => void;
    validate: () => { valid: boolean; errors: Record<string, string> };
    reload: () => void;
    errors?: Record<string, string>;
    showErrors: boolean;
    scrollToConditionalSignature: (index: string) => Generator;
}

Represents the current form state.

Field

interface Field {
    name: string;
    type: "text" | "checkbox" | "signature" | "radiobutton";
    pageIndex: number;
    bbox: { x: number; y: number; width: number; height: number };
    radioValue?: string;
    readonly?: boolean;
    required?: boolean;
    conditionalIndex?: string;
}

Represents a single form field.

Toolbar Customization

ToolbarItem

type ToolbarItem = ToolbarButton | ToolbarSpacer;

ToolbarButton

interface ToolbarButton {
    id: string;
    label: string;
    icon?: string;           // Font Awesome icon class
    title?: string;          // Tooltip text
    onClick: () => void;
    area: "left" | "right";
    disabled?: boolean;
    type: "button";
}

ToolbarSpacer

interface ToolbarSpacer {
    id: string;
    type: "spacer";
    area: "left" | "right";
}

Internationalization

Supported Locales

  • en - English (default)
  • it - Italian
  • es - Spanish
  • fr - French
  • de - German

Default Translation Keys

{
    zoomIn: string;
    zoomOut: string;
    resetZoom: string;
    download: string;
    pageNumberTitle: string;
    signature: string;
}

Custom Translations

Override or add translations for any locale:

const translations = {
    en: {
        zoomIn: "Enlarge",
        zoomOut: "Shrink",
        customKey: "Custom value",
    },
    it: {
        customKey: "Valore personalizzato",
    },
};

Examples

Basic PDF Viewer

import { usePdfSentinel } from "@kopjra/pdf-sentinel";

export function BasicViewer() {
    const { PdfSentinelContainer, Toolbar, PdfSentinel } = usePdfSentinel({
        pdf: "https://example.com/document.pdf",
        name: "Document",
    });

    return (
        <div style={{ height: "600px" }}>
            <PdfSentinelContainer>
                <Toolbar />
                <PdfSentinel />
            </PdfSentinelContainer>
        </div>
    );
}

PDF with Form Fields

import { usePdfSentinel } from "@kopjra/pdf-sentinel";

export function FormViewer() {
    const { PdfSentinelContainer, Toolbar, PdfSentinel, form } = usePdfSentinel({
        pdf: "/document.pdf",
        name: "Form Document",
        initialValues: {
            "firstName": { value: "John" },
            "lastName": { value: "Doe" },
        },
    });

    const handleValidate = () => {
        if (form?.validate()) {
            console.log("Form is valid, values:", form.values);
        }
    };

    return (
        <>
            <div style={{ height: "600px" }}>
                <PdfSentinelContainer>
                    <Toolbar />
                    <PdfSentinel />
                </PdfSentinelContainer>
            </div>
            <button onClick={handleValidate}>Validate</button>
        </>
    );
}

Custom Toolbar Buttons

import { usePdfSentinel, ToolbarItem } from "@kopjra/pdf-sentinel";

export function CustomToolbar() {
    const customButtons: ToolbarItem[] = [
        {
            id: "print-btn",
            label: "Print",
            icon: "fal fa-print",
            title: "Print document",
            area: "left",
            type: "button",
            onClick: () => window.print(),
        },
        {
            id: "spacer",
            type: "spacer",
            area: "left",
        },
        {
            id: "share-btn",
            label: "Share",
            icon: "fal fa-share",
            title: "Share document",
            area: "right",
            type: "button",
            onClick: () => {
                console.log("Share clicked");
            },
        },
    ];

    const { PdfSentinelContainer, Toolbar, PdfSentinel } = usePdfSentinel({
        pdf: "/document.pdf",
        name: "Document",
        toolbarButtons: customButtons,
    });

    return (
        <div style={{ height: "600px" }}>
            <PdfSentinelContainer>
                <Toolbar />
                <PdfSentinel />
            </PdfSentinelContainer>
        </div>
    );
}

Multi-Language Support

import { usePdfSentinel } from "@kopjra/pdf-sentinel";
import { useState } from "react";

export function MultiLanguageViewer() {
    const [locale, setLocale] = useState("en");

    const customTranslations = {
        en: {
            zoomIn: "Zoom In",
            download: "Download PDF",
        },
        it: {
            zoomIn: "Ingrandisci",
            download: "Scarica PDF",
        },
    };

    const { PdfSentinelContainer, Toolbar, PdfSentinel } = usePdfSentinel({
        pdf: "/document.pdf",
        name: "Document",
        locale: locale,
        translations: customTranslations,
    });

    return (
        <div>
            <select onChange={(e) => setLocale(e.target.value)} value={locale}>
                <option value="en">English</option>
                <option value="it">Italiano</option>
            </select>
            <div style={{ height: "600px", marginTop: "10px" }}>
                <PdfSentinelContainer>
                    <Toolbar />
                    <PdfSentinel />
                </PdfSentinelContainer>
            </div>
        </div>
    );
}

Conditional Signatures with Groups

import { usePdfSentinel, ConditionalSignature } from "@kopjra/pdf-sentinel";

export function SignatureViewer() {
    const conditionalSignatures: ConditionalSignature[] = [
        { index: "1", fieldNames: ["authorizedBy", "authorizedDate"] },
        { index: "2", fieldNames: ["approvedBy", "approvedDate"] },
    ];

    const { PdfSentinelContainer, Toolbar, PdfSentinel } = usePdfSentinel({
        pdf: "/contract.pdf",
        name: "Contract",
        conditionalSignatures: conditionalSignatures,
    });

    return (
        <div style={{ height: "600px" }}>
            <PdfSentinelContainer>
                <Toolbar />
                <PdfSentinel />
            </PdfSentinelContainer>
        </div>
    );
}

Form Validation with Error Handling

import { usePdfSentinel } from "@kopjra/pdf-sentinel";

export function FormWithValidation() {
    const { PdfSentinelContainer, Toolbar, PdfSentinel, form } = usePdfSentinel({
        pdf: "/form.pdf",
        name: "Application Form",
        initialValues: {
            "email": { value: "" },
            "phone": { value: "" },
        },
    });

    const handleSubmit = () => {
        if (!form) return;

        // Validate required fields
        let isValid = true;
        const errors: Record<string, string> = {};

        if (!form.values["email"]) {
            errors["email"] = "Email is required";
            isValid = false;
        }

        if (!form.values["phone"]) {
            errors["phone"] = "Phone is required";
            isValid = false;
        }

        if (!isValid) {
            // Set errors and show them
            Object.entries(errors).forEach(([field, error]) => {
                form.setFieldError(field, error);
            });
        } else {
            console.log("Form submitted:", form.values);
        }
    };

    return (
        <>
            <div style={{ height: "600px" }}>
                <PdfSentinelContainer>
                    <Toolbar />
                    <PdfSentinel />
                </PdfSentinelContainer>
            </div>
            <button onClick={handleSubmit}>Submit</button>
        </>
    );
}

Using React and Preact Variants

To use the Preact variant use import from @kopjra/pdf-sentinel/preact instead of @kopjra/pdf-sentinel. The React is the default one, however you can also use the import @kopjra/pdf-sentinel/react for clarity.

// React variant
import { usePdfSentinel } from "@kopjra/pdf-sentinel/react";

// Preact variant (same import, different package)
import { usePdfSentinel } from "@kopjra/pdf-sentinel/preact";

export function Viewer() {
    const { PdfSentinelContainer, Toolbar, PdfSentinel } = usePdfSentinel({
        pdf: "/document.pdf",
        name: "Document",
    });

    return (
        <div style={{ height: "600px" }}>
            <PdfSentinelContainer>
                <Toolbar />
                <PdfSentinel />
            </PdfSentinelContainer>
        </div>
    );
}

The same code works for both React and Preact - the library automatically uses the correct variant based on your project configuration.

Browser Support

  • Chrome/Chromium (latest)
  • Firefox (latest)
  • Safari (latest)
  • Edge (latest)

Contributing

Contributions are welcome! Please ensure:

  1. TypeScript types are correctly defined
  2. All code follows the project style guide
  3. npm run typecheck passes
  4. Changes are tested in both React and Preact variants

License

ISC

2