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

wysiwyg-pdf

v0.1.6

Published

React + Konva WYSIWYG editor toolkit for print-ready (A4 portrait/landscape) templates in the browser.

Readme

wysiwyg-pdf

wysiwyg-pdf is a React + Konva WYSIWYG editor toolkit for building print-ready (A4 portrait/landscape) templates in the browser.

npm: https://www.npmjs.com/package/wysiwyg-pdf

Live Demo

It is designed as a set of composable editor building blocks:

  • Canvas editor (ReportKonvaEditor) for selection, transform, drag/drop, inline text editing, copy/paste
  • Toolbar (WysiwygEditorToolbar) for inserting elements and controlling zoom
  • Properties panel (WysiwygPropertiesPanel) for editing typography, colors, line styles, table structure, page background
  • Print/PDF layout (PrintLayout) to render a DOM representation that prints cleanly

If you are evaluating whether to adopt this package, the key question is:

  • Do you want a ready-to-integrate template editor UI where the persisted state is a plain JSON document (Doc), and printing is handled by your app (e.g., via react-to-print)?

Screenshots

What you get

  • Editable template document model (Doc with surfaces + nodes)
  • Element types: Text, Shapes, Line, Image, Table (see types exported from pdf-editor/types/wysiwyg)
  • Keyboard shortcuts: undo/redo, delete, select all, arrow-key move, copy/paste
  • A4-aware coordinate system (internal PT units with display scaling)
  • Printing support via a dedicated print DOM layout + print CSS

Non-goals (important for adoption decisions)

  • This is not a full “template management product” (no backend, no auth, no persistence API)
  • This is not a PDF renderer library; printing is done via the browser
  • Multi-page UI navigation is not provided as a complete workflow (the model supports pages; your app controls UX)

Installation

npm i wysiwyg-pdf

Peer dependencies

  • react
  • react-dom

Use versions compatible with this package’s peerDependencies.

Requirements & integration notes

1) i18n (host-provided translator)

This package does not ship an i18n library. Instead, you inject a translator function via I18nProvider.

t is optional; if omitted, the default translator returns fallback ?? key.

import { I18nProvider } from 'wysiwyg-pdf'

export function App() {
  return (
    <I18nProvider t={(key, fallback) => fallback ?? key}>
      {/* your editor UI */}
    </I18nProvider>
  )
}

If your app already uses react-i18next, you can bridge it:

import { I18nProvider } from 'wysiwyg-pdf'
import { useTranslation } from 'react-i18next'

export function App() {
  const { t } = useTranslation()
  return <I18nProvider t={(key, fallback) => t(key, fallback ?? key)}>{/* ... */}</I18nProvider>
}

2) Styling (Tailwind classes + theme CSS variables)

The UI relies heavily on:

  • Tailwind utility classes (layout, spacing, borders)
  • shadcn-style CSS variables such as --background, --foreground, --primary, --border, etc.

You should:

  • Use Tailwind in your host app, and ensure Tailwind scans this package for class usage
  • Define the required shadcn-style CSS variables in your global CSS (light/dark)

This package also ships a base stylesheet you can import (recommended):

import 'wysiwyg-pdf/styles.css'

Tailwind content/@source setup (host app)

Because this package uses Tailwind class strings at runtime, your host app’s Tailwind build must include this package in its scan targets.

Tailwind v3/v4 (tailwind.config):

// tailwind.config.{js,ts}
export default {
  content: [
    './src/**/*.{js,ts,jsx,tsx}',
    './node_modules/wysiwyg-pdf/dist/**/*.{js,cjs,mjs}',
  ],
}

Tailwind v4 (@source):

/* your app entry CSS */
@source "../node_modules/wysiwyg-pdf/dist/**/*.{js,cjs,mjs}";

CSS variables (host app)

Define shadcn-style tokens in your global CSS. Minimal example:

:root {
  --background: 0 0% 100%;
  --foreground: 210 20% 10%;
  --card: 0 0% 100%;
  --card-foreground: 210 20% 10%;
  --popover: 0 0% 100%;
  --popover-foreground: 210 20% 10%;
  --primary: 220 90% 45%;
  --primary-foreground: 0 0% 100%;
  --secondary: 0 0% 96%;
  --secondary-foreground: 210 20% 10%;
  --muted: 0 0% 96%;
  --muted-foreground: 210 10% 45%;
  --accent: 210 100% 26%;
  --accent-foreground: 0 0% 100%;
  --destructive: 0 75% 65%;
  --destructive-foreground: 0 0% 100%;
  --border: 210 16% 82%;
  --input: 210 16% 82%;
  --ring: 210 100% 26%;
  --radius: 0.5rem;
}

.dark {
  --background: 210 20% 10%;
  --foreground: 210 30% 96%;
  --card: 210 20% 10%;
  --card-foreground: 210 30% 96%;
  --popover: 210 20% 10%;
  --popover-foreground: 210 30% 96%;
  --primary: 200 95% 42%;
  --primary-foreground: 210 20% 10%;
  --secondary: 210 20% 16%;
  --secondary-foreground: 210 30% 96%;
  --muted: 210 20% 16%;
  --muted-foreground: 210 15% 60%;
  --accent: 210 85% 42%;
  --accent-foreground: 210 20% 10%;
  --destructive: 0 75% 38%;
  --destructive-foreground: 210 30% 96%;
  --border: 210 20% 30%;
  --input: 210 20% 30%;
  --ring: 210 85% 42%;
}

And in your Tailwind config, map the tokens (shadcn convention):

// tailwind.config.{js,ts}
export default {
  theme: {
    extend: {
      colors: {
        border: 'hsl(var(--border))',
        input: 'hsl(var(--input))',
        ring: 'hsl(var(--ring))',
        background: 'hsl(var(--background))',
        foreground: 'hsl(var(--foreground))',
        primary: {
          DEFAULT: 'hsl(var(--primary))',
          foreground: 'hsl(var(--primary-foreground))',
        },
        secondary: {
          DEFAULT: 'hsl(var(--secondary))',
          foreground: 'hsl(var(--secondary-foreground))',
        },
        destructive: {
          DEFAULT: 'hsl(var(--destructive))',
          foreground: 'hsl(var(--destructive-foreground))',
        },
        muted: {
          DEFAULT: 'hsl(var(--muted))',
          foreground: 'hsl(var(--muted-foreground))',
        },
        accent: {
          DEFAULT: 'hsl(var(--accent))',
          foreground: 'hsl(var(--accent-foreground))',
        },
        popover: {
          DEFAULT: 'hsl(var(--popover))',
          foreground: 'hsl(var(--popover-foreground))',
        },
        card: {
          DEFAULT: 'hsl(var(--card))',
          foreground: 'hsl(var(--card-foreground))',
        },
      },
      borderRadius: {
        lg: 'var(--radius)',
        md: 'calc(var(--radius) - 2px)',
        sm: 'calc(var(--radius) - 4px)',
      },
    },
  },
}

3) Print CSS import

PrintLayout imports a print.css internally. Your bundler must support resolving CSS imports from dependencies.

If your bundler requires an explicit import, you can also import it directly:

import 'wysiwyg-pdf/print.css'

Quick start (minimal editor shell)

This is a minimal composition based on wysiwyg-pdf/example/src/App.tsx.

import { useRef, useState } from 'react';
import {
  ReportKonvaEditor,
  WysiwygEditorToolbar,
  WysiwygPropertiesPanel,
  type Doc,
  type ReportKonvaEditorHandle,
  useReportHistory,
} from 'wysiwyg-pdf';

const INITIAL_DOC: Doc = {
  v: 1,
  id: 'doc-1',
  title: 'New Template',
  unit: 'pt',
  surfaces: [
    {
      id: 'page-1',
      type: 'page',
      w: 595.28,
      h: 841.89,
      margin: { t: 0, r: 0, b: 0, l: 0 },
      bg: '#ffffff',
    },
  ],
  nodes: [],
};

export function EditorPage() {
  const [zoom, setZoom] = useState(100);
  const [selectedElementId, setSelectedElementId] = useState<string | null>(null);
  const [selectedCell, setSelectedCell] = useState<{
    elementId: string;
    row: number;
    col: number;
  } | null>(null);

  const { document: doc, setDocument, undo, redo } = useReportHistory(INITIAL_DOC);
  const editorRef = useRef<ReportKonvaEditorHandle>(null);

  const currentPageId = doc.surfaces[0]?.id;

  return (
    <div className="flex h-screen w-screen overflow-hidden">
      <div className="w-16 border-r">
        <WysiwygEditorToolbar
          zoom={zoom}
          onZoomChange={setZoom}
          templateDoc={doc}
          onTemplateChange={setDocument}
          onSelectElement={(id) => setSelectedElementId(id)}
          currentPageId={currentPageId}
        />
      </div>

      <div className="flex-1 overflow-hidden">
        <ReportKonvaEditor
          ref={editorRef}
          templateDoc={doc}
          zoom={zoom / 100}
          selectedElementId={selectedElementId || undefined}
          onElementSelect={(el) => setSelectedElementId(el?.id ?? null)}
          onTemplateChange={setDocument}
          currentPageId={currentPageId}
          onSelectedCellChange={setSelectedCell}
          onUndo={undo}
          onRedo={redo}
        />
      </div>

      <div className="w-72 border-l overflow-hidden">
        <WysiwygPropertiesPanel
          templateDoc={doc}
          selectedElementId={selectedElementId}
          onTemplateChange={setDocument}
          currentPageId={currentPageId}
          selectedCell={selectedCell}
        />
      </div>
    </div>
  );
}

Printing / “Save as PDF” (react-to-print)

PrintLayout renders a print-optimized DOM. Pair it with react-to-print.

import { useRef } from 'react';
import { PrintLayout, type Doc } from 'wysiwyg-pdf';
import { useReactToPrint } from 'react-to-print';

export function PrintButton({
  doc,
  orientation,
}: {
  doc: Doc;
  orientation: 'portrait' | 'landscape';
}) {
  const printRef = useRef<HTMLDivElement>(null);

  const print = useReactToPrint({
    contentRef: printRef,
    pageStyle: `
      @page {
        size: A4 ${orientation};
        margin: 0;
      }
    `,
  } as any);

  return (
    <>
      <div style={{ display: 'none' }}>
        <PrintLayout ref={printRef} doc={doc} orientation={orientation} />
      </div>
      <button type="button" onClick={() => print()}>
        Print / Save as PDF
      </button>
    </>
  );
}

Optional: data binding support via schema

The properties panel can receive an optional schema?: IDataSchema.

  • IDataSchema is defined in src/types/schema.ts
  • Text elements can carry a binding field (field binding)
  • Table elements can be used for “repeater-like” content (your app defines runtime semantics)

Public API (high-level)

From src/index.ts, typical consumers use:

  • ReportEditor (pre-composed editor shell)
  • ReportKonvaEditor / ReportKonvaEditorHandle
  • WysiwygEditorToolbar
  • WysiwygPropertiesPanel
  • PrintLayout
  • EditorHeader
  • ShortcutHelpModal
  • useReportHistory
  • Types: Doc, Surface, UnifiedNode, PageSize, IDataSchema

This package also exports bed-layout related components (e.g., BedLayoutEditor, BedPrintLayout).

Component Customization

You can customize the editor components to fit your application's needs or build your own variants.

Header Customization (EditorHeader)

The EditorHeader component supports customization via props:

  • orientationOptions: Define your own set of orientation choices (e.g., 'Square').
  • children: Render custom buttons or actions (e.g., Theme Toggle, Save status).
<EditorHeader
  // ... other props
  orientationOptions={[
    { label: 'Portrait', value: 'portrait' },
    { label: 'Landscape', value: 'landscape' },
    { label: 'Square', value: 'square' },
  ]}
>
  <button onClick={myCustomAction}>My Action</button>
</EditorHeader>

You can also wrap it in your own component (like BedLayoutHeader does) to preset these options.

Toolbar Customization

The toolbar (WysiwygEditorToolbar or BedToolbar) is simply a consumer of the editor state. If you need a fully custom toolbar:

  1. Create your own component.
  2. Use the state handlers provided by useReportHistory or useBedEditorHistory (e.g., setDocument, undo, redo).
  3. Manage tool state (e.g., activeTool) in your parent page and pass it to your toolbar.

Property Panel Customization

The property panel (WysiwygPropertiesPanel) updates the selected element's attributes. To customize it:

  • You can create a copy of the panel and add/remove fields.
  • Or, if you just need to support new element types, extend the rendering logic for those types.
  • The panel receives selectedElement and onChange. You can wrap it or conditional render different panels based on selectedElement.type.

I18n Overrides

If your application uses different translation keys or if you want to override specific labels without setting up a full i18next resource bundle, you can use the i18nOverrides prop.

This prop is supported by EditorHeader, WysiwygEditorToolbar, BedToolbar, WysiwygPropertiesPanel, and PropertyPanel (BedLayout).

<EditorHeader
  // ...
  i18nOverrides={{
    'editor_orientation': 'Page Orientation', // Override specific key
    'orientations_portrait': 'Vertical',
    'save': 'Save Changes',
  }}
/>

<WysiwygPropertiesPanel
  // ...
  i18nOverrides={{
    'properties_layout': 'Layout Settings',
    'properties_text_align': 'Alignment',
  }}
/>

Common keys you might want to override:

  • Header: editor_orientation, save, back, toolbar_undo, toolbar_redo
  • Toolbar: toolbar_text, toolbar_image, toolbar_shape, toolbar_line
  • Properties: properties_layout, properties_font, color, position

Development (in this repository)

An interactive reference app is available under wysiwyg-pdf/example.

pnpm install
pnpm -C wysiwyg-pdf/example dev

Packaging notes (for maintainers)

If you publish this to npm, ensure you have a proper build output (e.g., dist/) and configure main/module/types/exports accordingly. Also ensure CSS assets (e.g., print.css) are included in the published files.

Testing

This project uses Vitest for unit testing.

Prerequisites

  • Node.js: v18.18 or higher
  • Package Manager: npm (recommended for standard environments)

Running Tests

To run all tests:

npm test

To run tests in watch mode:

npm run test:watch

To check code coverage:

npm run coverage

[!NOTE] If you encounter vitest: not found, ensure you have run npm install to install all dependencies. If you are using npm test -- --runInBand, make sure your environment recognizes the local binaries in node_modules/.bin.

License

Deployment to Cloudflare Workers

An example deployment configuration is provided in example/backend.

It supports:

  • Cloudflare Workers (Hono + D1 + R2)
  • Unified Deployment (Frontend + Backend as single unit)
  • Periodic Cleanup (Cron Triggers)

See example/backend/wrangler.toml for configuration.

cd example/backend
pnpm run deploy:all # Deploys both frontend and backend