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

@pixldocs/canvas-renderer

v0.5.210

Published

Client-side template renderer for Pixldocs — React component + imperative API for rendering templates in any web app

Downloads

15,002

Readme

@pixldocs/canvas-renderer

Client-side template renderer for Pixldocs — render templates in any web app with 1:1 visual parity with the Pixldocs editor. No server round-trip needed for previews.

Installation

npm install @pixldocs/canvas-renderer fabric react react-dom

Public package — no auth token or .npmrc needed.


Quick Start: Build an App From a Workspace

Got a Pixldocs workspace full of published templates (e.g. social-media posts, invitations, certificates)? You can spin up a standalone app that lists and renders them with just the workspace ID + Pixldocs anon key — no per-template wiring needed.

import {
  listPublishedTemplates,
  PixldocsRenderer,
} from '@pixldocs/canvas-renderer';

const SUPABASE_URL = 'https://ttvtjhxjxuxdeybcnjkd.supabase.co';
const SUPABASE_ANON_KEY = 'eyJ...'; // Pixldocs publishable key (safe in browser)
const WORKSPACE_ID = 'your-workspace-uuid';

// 1. List all published templates owned by a workspace
const templates = await listPublishedTemplates({
  workspaceId: WORKSPACE_ID,
  supabaseUrl: SUPABASE_URL,
  supabaseAnonKey: SUPABASE_ANON_KEY,
  // category: 'social-media',  // optional filter
});

// templates: [{ id, name, thumbnail_url, category, price, ... }, ...]

// 2. Render a chosen template (form-bound or static)
const renderer = new PixldocsRenderer({
  supabaseUrl: SUPABASE_URL,
  supabaseAnonKey: SUPABASE_ANON_KEY,
});

const pages = await renderer.renderFromForm({
  templateId: templates[0].id,
  sectionState: { /* user inputs (optional for static templates) */ },
});

Two ways to source templates

| Approach | Best for | API | |----------|----------|-----| | By workspace | Multi-template apps (template gallery, social-media post maker, certificate maker) — pick from a curated workspace | listPublishedTemplates({ workspaceId }) | | By form schema | Single-purpose apps where one form drives many template variants (e.g. BioMaker — one form, many biodata designs) | Resolve directly via renderFromForm({ templateId, formSchemaId, sectionState }) |

Both work with the same anon key and the same renderer — choose whichever matches your product shape.


Templates Catalog API

listPublishedTemplates(options)

Returns every published template belonging to a workspace. Uses public RLS (no auth needed beyond the anon key).

const templates = await listPublishedTemplates({
  workspaceId: 'uuid',
  supabaseUrl: '...',
  supabaseAnonKey: '...',
  category: 'social-media',  // optional
  limit: 200,                 // optional (default 200)
  offset: 0,                  // optional pagination
});

Each item: { id, name, description, category, thumbnail_url, preview_images, price, download_count, workspace_id, sort_order, created_at, updated_at }.

getPublishedTemplate({ templateId, supabaseUrl, supabaseAnonKey })

Fetch a single published template's catalog row (without the heavy config JSON — use the renderer for that).


Two Rendering Approaches

Approach A: Live Canvas (interactive)

Renders a visible <canvas> element in your page. Best for editors, interactive tools, or when you need real-time canvas manipulation.

import { PixldocsPreview } from '@pixldocs/canvas-renderer';

<PixldocsPreview
  templateId="your-template-id"
  formSchemaId="your-schema-id"
  sectionState={sectionState}
  supabaseUrl="https://xxx.supabase.co"
  supabaseAnonKey="eyJ..."
  pageIndex={0}
  onReady={() => console.log('Canvas ready')}
/>

Pros: Real-time updates, no re-render flicker
Cons: Heavier DOM footprint, canvas stays in memory

Approach B: Hidden Canvas → Image (recommended for previews)

Renders off-screen using a hidden canvas, captures the result as a data URL, and displays it as a lightweight <img>. Best for preview panels, template pickers, and multi-page views.

import { useState, useEffect } from 'react';
import { PixldocsRenderer } from '@pixldocs/canvas-renderer';

const renderer = new PixldocsRenderer({
  supabaseUrl: 'https://xxx.supabase.co',
  supabaseAnonKey: 'eyJ...',
});

function TemplatePreview({ templateId, formSchemaId, sectionState, themeId }) {
  const [pages, setPages] = useState([]);
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    let cancelled = false;
    async function render() {
      setLoading(true);
      try {
        const results = await renderer.renderFromForm({
          templateId,
          formSchemaId,
          sectionState,
          themeId,
        });
        if (!cancelled) setPages(results);
      } catch (err) {
        console.error('Render failed:', err);
      } finally {
        if (!cancelled) setLoading(false);
      }
    }
    render();
    return () => { cancelled = true; };
  }, [templateId, formSchemaId, sectionState, themeId]);

  if (loading) return <div>Generating preview...</div>;

  return (
    <div>
      {pages.map((page, i) => (
        <img key={i} src={page.dataUrl} width={page.width} height={page.height} alt={`Page ${i + 1}`} />
      ))}
    </div>
  );
}

Pros: Lightweight DOM (just <img> tags), canvas is disposed after capture, fast for multi-page
Cons: Not interactive, requires re-render to update

Approach C: Vector PDF Export

Renders all pages as SVGs and assembles them into a vector PDF using jsPDF + svg2pdf.js. Best for print-quality exports.

import { PixldocsRenderer } from '@pixldocs/canvas-renderer';

const renderer = new PixldocsRenderer({
  supabaseUrl: 'https://xxx.supabase.co',
  supabaseAnonKey: 'eyJ...',
});

// From form data (V2 sectionState)
const { blob, totalPages } = await renderer.renderPdfFromForm({
  templateId: 'your-template-id',
  formSchemaId: 'your-schema-id',
  sectionState: { section_abc: { name: 'John' } },
  title: 'My Document',
});

// Download the PDF
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'document.pdf';
a.click();
URL.revokeObjectURL(url);

// Or from a pre-resolved config
const result = await renderer.renderPdf(templateConfig, { title: 'My Doc' });

Pros: Vector output (infinite zoom), small file size, print-quality
Cons: No interactivity, requires jsPDF/svg2pdf.js (bundled as dependencies)


API Reference

Form schema shape (V2 sectionState)

sectionState accepts entries for both repeatable sections and repeatable pages. Both look the same on the wire — an array of entry objects keyed by the section/page id:

sectionState = {
  // single section
  personal: { name: 'Jane' },

  // repeatable section (e.g. Experience)
  experience: [
    { title: 'Designer', company: 'Acme' },
    { title: 'Lead',      company: 'Beta' },
  ],

  // repeatable PAGE (e.g. Photo Page) — same shape as above, but at render
  // time the bound template page is cloned once per entry.
  photo_page: [
    { caption: 'Cover' },
    { caption: 'Back'  },
  ],
}

Fetch the schema from the Pixldocs form API to discover what keys/fields are expected. The response exposes repeatable pages as a dedicated schema.repeatablePages[] array (each entry mirrors a repeatable section: id, label, order, templateKeyPrefix, minEntries, maxEntries, entryFields).

const res = await fetch(
  `${SUPABASE_URL}/functions/v1/form-api?form_schema_id=${SCHEMA_ID}&action=schema`,
  { headers: { apikey: SUPABASE_ANON_KEY } },
).then((r) => r.json());

res.schema.sections         // single + repeatable sections
res.schema.repeatablePages  // repeatable pages (clone-per-entry)

PixldocsPreview (React Component)

| Prop | Type | Default | Description | |------|------|---------|-------------| | config | TemplateConfig | — | Pre-resolved template config (Mode 1) | | templateId | string | — | Template UUID (Mode 2) | | formSchemaId | string | — | Form schema UUID (Mode 2) | | sectionState | SectionFormState | — | V2 section state data (Mode 2) | | themeId | string | 'default' | Theme variant ID | | supabaseUrl | string | — | Supabase URL (Mode 2) | | supabaseAnonKey | string | — | Supabase anon key (Mode 2) | | pageIndex | number | 0 | Page index to render | | zoom | number | auto-fit | Zoom/scale factor | | imageProxyUrl | string | — | CORS proxy URL for external images | | onReady | () => void | — | Called when rendering completes | | onError | (error: Error) => void | — | Called on error |

PixldocsRenderer (Imperative API)

const renderer = new PixldocsRenderer({
  supabaseUrl: string,
  supabaseAnonKey: string,
  imageProxyUrl?: string,   // CORS proxy for external images
  pixelRatio?: number,       // default: 2
  assetWaitTimeoutMs?: number,    // default: 15000; use ~30000 for multi-photo PDF exports
  assetWaitEarlyExitMs?: number,  // default: 1500; ignored for strict PDF image-ID waits
  maxImageEdgePx?: number,        // auto: 2048 for user-uploaded PDF photos; 0 disables
  debug?: boolean,                // logs image wait/SVG completeness diagnostics
});

For BioMaker-style PDFs with user-uploaded profile/gallery photos, use the live SVG path and do not set forcePerElementPdf: true. Large data:image/* photos are normalized before the hidden Fabric canvas mounts (default cap: 2048px edge and ~2MB encoded data URL) so svg2pdf does not corrupt/drop multi-megapixel uploads.

renderer.renderFromForm(options)

Renders all pages from a V2 sectionState payload (same format as the server /render-from-form API).

const results = await renderer.renderFromForm({
  templateId: 'uuid',
  formSchemaId: 'uuid',
  sectionState: { section_abc: { name: 'John' }, section_def: [...] },
  themeId: 'default',       // optional
  format: 'png',            // 'png' | 'jpeg' | 'webp'
  quality: 0.92,            // for jpeg/webp
  scale: 1,                 // scale multiplier
});
// Returns: RenderResult[] — one per page
// Each: { dataUrl, width, height, pixelWidth, pixelHeight }

renderer.render(config, options?)

Renders a single page from a pre-resolved template config.

const { dataUrl, width, height } = await renderer.render(templateConfig, { pageIndex: 0 });

renderer.renderAllPages(config, options?)

Renders all pages from a pre-resolved config.

const pages = await renderer.renderAllPages(templateConfig);

Data Resolution Utilities

Resolve template data without rendering — useful for inspecting configs or building custom pipelines.

import { resolveFromForm, resolveTemplateData } from '@pixldocs/canvas-renderer';

// V2: resolve from sectionState
const { config, totalPages } = await resolveFromForm({
  templateId: 'uuid',
  formSchemaId: 'uuid',
  sectionState: { ... },
  supabaseUrl: '...', supabaseAnonKey: '...',
});

// Legacy: resolve with flat formData
const { config } = await resolveTemplateData({
  templateId: 'uuid',
  formData: { name: 'Jane' },
  supabaseUrl: '...', supabaseAnonKey: '...',
});

Peer Dependencies

  • fabric ^6.0.0
  • react ^18.0.0 || ^19.0.0
  • react-dom ^18.0.0 || ^19.0.0

License

UNLICENSED — Private package for Pixldocs ecosystem only.


Preview Blur (anti-screenshot)

Set previewBlur: true on any text or image element in your config to redact it in watermarked previews — useful for protecting paid content (e.g. biodata reference rows) from screenshot theft.

The element renders normally underneath, and a translucent frosted-glass overlay rectangle is appended on top at the element's absolute bounds, so layout/wrap stays identical but the content is visually obscured. The overlay auto-tints (light glass on dark text, dark glass on light text) and is rendered with no stroke, no shadow, and square corners so it sits flush against neighboring content. In the live web preview the overlay is upgraded to a CSS backdrop-filter: blur() rectangle for a real frosted-glass effect; exported PNG/PDF use the translucent rect (the closest pure-vector approximation).

Preview-blur is gated identically to the watermark: it only runs when watermark === true (or, by default, when template.price > 0 and the user hasn't paid). Clean / paid downloads always render the original content.

Live backdrop-filter overlay in <PixldocsPreview>

The React <PixldocsPreview> component automatically renders a real CSS backdrop-filter: blur() frosted-glass overlay on top of every element marked previewBlur: true. Bounds are auto-derived from the config (groups respected), so no extra wiring is required.

Defaults: blur(5px) saturate(130%) with a rgba(255,255,255,0.12) tint, square corners, no stroke, no shadow — identical to the in-app preview on pixldocs.com.

Opt-out or tune via props:

<PixldocsPreview
  config={config}
  // disable entirely
  frostedBlur={false}
  // or tune
  frostedBlurOptions={{ blurPx: 6, saturatePct: 140, background: 'rgba(255,255,255,0.15)' }}
/>

The imperative PNG / PDF export paths still use the static translucent rectangle (the closest pure-vector approximation), unchanged.

// In your template config
{
  id: 'reference-1-name',
  type: 'text',
  text: 'John Doe',
  previewBlur: true,   // ← redacted in preview, full in paid download
  ...
}

No API change is required on the consumer side — the same renderFromForm, renderPdfFromForm, etc. respect the flag automatically.

Example: per-field blur for a biodata form (use page)

On pixldocs.com's Use page for the Biodata preset, every form field renders a small 🔒 Blur in preview checkbox next to it. Toggling it redacts only that field's text in the watermarked preview — perfect for hiding reference rows, phone numbers, or addresses behind the paywall while leaving the rest of the biodata visible. Paid / clean downloads always render the original content.

Under the hood the flow is:

  1. Keep a Set<string> of "blurred field ids" in component state — each id encodes the field's path (single field, repeatable entry, or nested repeatable entry), e.g. __bioPreviewBlur__:nested:family:1:siblings:2:name.
  2. Resolve each blurred field id to the exact cloned element ids in the paginated config (a single repeatable field may map to N cloned elements, one per entry).
  3. Pass that set to injectPreviewBlur as extraElementExactIds (or extraElementBaseIds if you only know the source/base id), then render the returned config like any other.
import { injectPreviewBlur } from '@pixldocs/canvas-renderer';

// `blurredElementIds` = the resolved Set<string> of exact cloned element ids
// you built from the user's checkbox state.
const previewConfig = injectPreviewBlur(paginatedConfig, {
  extraElementExactIds: blurredElementIds,
});

// Then render `previewConfig` with <PixldocsPreview config={previewConfig} />
// or `renderer.renderAllPages(previewConfig)`. The live React preview will
// additionally upgrade the static rectangles to real CSS
// `backdrop-filter: blur()` overlays automatically.

This is exactly how the Biodata preset on pixldocs.com wires its per-field 🔒 checkboxes — no template authoring required, the user picks at runtime which fields stay visible in the watermarked preview.

Shortcut: pass blur field ids straight to <PixldocsPreview> (v0.5.186+)

If you're using the React <PixldocsPreview> component (rather than the imperative renderer), you don't need to call injectPreviewBlur yourself. Just pass the field ids directly as a prop:

import { PixldocsPreview } from '@pixldocs/canvas-renderer';

// Source / base ids straight from your form schema.
// Every cloned instance across pages + repeatable entries is auto-matched.
const blurFieldIds = ['text-reference-name', 'text-phone', 'text-address'];

<PixldocsPreview
  templateId={templateId}
  formSchemaId={formSchemaId}
  sectionState={sectionState}
  supabaseUrl={SUPABASE_URL}
  supabaseAnonKey={SUPABASE_ANON_KEY}
  blurFieldIds={blurFieldIds}
/>
  • blurFieldIds: matched by base id — cloned suffixes (_1, _2_3, …) are stripped before comparison, so one entry blurs every clone of that element across all pages / repeatable entries.
  • blurElementExactIds: optional companion prop for targeting a single specific clone (no base-id stripping).
  • The preview still respects any previewBlur: true flags already baked into the resolved config — blurFieldIds is purely additive.
  • This only affects the live React preview overlay. For watermarked downloads (PNG/PDF) you still want to run injectPreviewBlur(...) on the resolved config before handing it to the imperative renderer, so the blur is baked into the exported pixels.

Biodata teaser previews — blur by flat form key (v0.5.187+, hardened in v0.5.189, pagination-safe in v0.5.190)

Form-driven apps (BioMaker, the pixldocs.com Use page, etc.) already speak the flat form-key language produced by applyFormDataToConfig — e.g.

field_<treeNodeId>_<entryIdx1>_<fieldKey>
field_<parentTree>_<pi>_field_<childTree>_<ci>_<fieldKey>     // nested

Instead of reverse-engineering config.__cloneIdMap keys or the __cN / _eN clone suffixes on canvas element ids, you can pass those flat keys straight to <PixldocsPreview> and the package resolves them:

import {
  PixldocsPreview,
  buildTeaserBlurFlatKeys,
} from '@pixldocs/canvas-renderer';

// e.g. for a biodata teaser: blur every detail-row VALUE after row 3.
const blurFlatFormKeys = buildTeaserBlurFlatKeys(sectionState, schema, {
  afterRow: 3,
  bindings: 'value',
});

<PixldocsPreview
  config={displayConfig}
  pageIndex={pageIndex}
  frostedBlur
  blurFlatFormKeys={blurFlatFormKeys}
  // Optional — defaults to { bindings: 'value' } so only `*_value` keys
  // are honoured. Pass 'all' to blur labels / titles too. `pageIndex` is
  // forwarded into the resolver so verification scopes to the page
  // currently being rendered (recommended for stacked multi-page previews).
  blurFlatFormKeyOptions={{ bindings: 'value', pageIndex }}
/>

Notes:

  • Matching is done on config.__cloneIdMap and handles all alias variants (grp-…_4_value, …_4_field_detail_section_N_field_N_value, with or without the wrapper field_ prefix). As of v0.5.189 every resolved id is then verified against the actual rendered page tree (config.pages[*]) — stale or alias map entries that don't match a real element are dropped silently.
  • As of v0.5.190 applyContentBoundsPagination rewrites __cloneIdMap after pagination so flat keys for overflow rows that moved to continuation pages still resolve to ids that exist on the tree (clones inherit __sourceId from the original element). If the map is still stale for any reason, the resolver also falls back to walking the page tree and matching __sourceId / __baseNodeId against the raw resolved ids — the same strategy the theming pipeline uses to track paginated clones. End result: frosted blur for row N appears on whichever page row N actually renders on, even when content-bounds pagination moved it.
  • Pass pageIndex in blurFlatFormKeyOptions to scope verification to a single page — clones that landed on a different page will not contribute overlays to this page. This is the recommended pattern for stacked multi-page previews. Default is 'all' (verify across every page).
  • Use skipTreeVerification: true only if you fully trust your __cloneIdMap (e.g. you just built the config in-process and haven't mutated the tree since).
  • Defaults to bindings: 'value', i.e. only flat keys whose last segment is value resolve. This is exactly the biodata teaser pattern: blur row values, leave labels / section titles / full-name / photo sharp. Switch to 'all' if you want labels blurred too.
  • This only drives the live preview overlay. For the watermarked PNG/PDF download path, resolve the same keys to exact ids and bake them in (see below).

buildTeaserBlurFlatKeys(sectionState, sections, options)

Emits the flat form-keys for every repeatable-section entry after afterRow (1-indexed). Walks nested repeatables recursively.

  • sectionState — the same SectionFormState you'd hand to resolveFromForm / <PixldocsPreview sectionState={…}>.
  • sectionsInferredSection[] for the active form schema (you already have these from inferFormSchemaFromTemplate or from the form_schemas row converted via fromFormDefSections).
  • options.afterRow — keep the first N entries sharp; blur entries N+1, N+2, ….
  • options.bindings'value' (default) emits only _value keys (the canonical biodata teaser pattern); 'all' emits every entry field key.

Returned keys can be fed straight into the blurFlatFormKeys prop above and into resolveBlurElementExactIdsFromFlatFormKeys for the watermarked download path below.

Watermarked downloads — same resolver + injectPreviewBlur

import {
  injectPreviewBlur,
  resolveBlurElementExactIdsFromFlatFormKeys,
} from '@pixldocs/canvas-renderer';

const exactIds = resolveBlurElementExactIdsFromFlatFormKeys(
  resolvedConfig,
  blurFlatFormKeys,
  // { bindings: 'value', pageIndex: 'all' } by default. For the
  // download path you usually want 'all' since the export covers
  // every page.
);

const exportConfig = injectPreviewBlur(resolvedConfig, {
  extraElementExactIds: new Set(exactIds),
});

// Hand `exportConfig` to renderer.renderAllPages / renderPdf / …

Same resolution path as the live preview — so the on-screen overlay and the downloaded PNG/PDF blur exactly the same elements.

Parity note

The pixldocs.com Use page (the in-app paid-template preview, including the Biodata preset's 🔒 per-field checkboxes) and external consumers (BioMaker, etc.) both call the same resolveBlurElementExactIdsFromFlatFormKeys function — there is now exactly one resolver shared across the entire stack. Anything that blurs correctly in one place blurs identically in the other; visual/UX bugs introduced in one pipeline cannot diverge silently from the other.

Reliable PDF photo embedding across browsers (v0.5.191+)

Form-driven apps that embed user-uploaded photos (biodatas, resumes, ID cards…) historically hit a Safari/iOS bug where the downloaded PDF rendered without the photos even though the on-screen preview was fine. The svg2pdf fast path internally re-rasterises data:image/* sources through a tainted offscreen canvas roundtrip, which Safari silently drops for large camera-roll JPEGs.

v0.5.191 fixes this with two new options:

const renderer = new PixldocsRenderer({
  supabaseUrl, supabaseAnonKey,
  // Keep the live SVG path. Use true only as a manual escape hatch because
  // the per-element/config-space path can drift from live Fabric layout.
  forcePerElementPdf: 'auto',         // true | false | 'auto' (default)
  // Normalize data:image/* photos before mounting into Fabric. The default
  // auto value is 2048 and also recompresses oversized encoded data URLs.
  maxImageEdgePx: 2048,               // default auto; 0 disables
});

const pdf = await renderer.renderPdfFromForm({
  templateId, formSchemaId, sectionState, title,
  // Per-call overrides also accepted:
  // forcePerElementPdf: 'auto',
  // maxImageEdgePx: 2400,
});

Defaults: forcePerElementPdf: 'auto' keeps the live SVG path. Any resolved config containing data:image/(jpeg|png|webp) sources is normalized before PDF mounting so large camera/gallery uploads behave consistently across browsers and server renderers.

Hosts that previously monkey-patched captureFabricCanvasSvgForPdf to throw (the unofficial BioMaker workaround) can delete that patch after upgrading.