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

@scopmx/markora-client

v1.5.0

Published

Fabric.js markup and drawing editor for React — shapes, text, PDF and image backgrounds, comments, version history, PDF/image/HTML export, and multi-user saving to @scopmx/markora-server.

Readme

@scopmx/markora-client

Fabric.js markup and drawing editor for React — shapes, text, PDF and image backgrounds, comments, version history, PDF/image/HTML export, and multi-user saving to @scopmx/markora-server.

This package succeeds @scopmx/markup-editor, whose code and full history it now holds; that package is no longer maintained.

Install

npm install @scopmx/markora-client fabric pdfjs-dist

Usage

import {
    DrawingEditor,
    UserProvider,
    ToastProvider,
    ToastContainer,
    SwitchUserModal,
} from "@scopmx/markora-client";
import "@scopmx/markora-client/styles.css";

export function EditorPage() {
    return (
        <UserProvider>
            <ToastProvider>
                <DrawingEditor
                    drawingId={null}
                    loadToken={0}
                    onDrawingCreated={() => {}}
                    onDirtyChange={() => {}}
                    onMetaUpdated={() => {}}
                    onCommentsChange={() => {}}
                    onSelectionChange={() => {}}
                    rightTab="style"
                    onRightTabChange={() => {}}
                    commentsPanelProps={{/* ... */}}
                    historyPanelProps={{/* ... */}}
                />
                <SwitchUserModal />
                <ToastContainer />
            </ToastProvider>
        </UserProvider>
    );
}

Exports

  • Components: DrawingEditor, EditorHeader, ConfirmDialog, SwitchUserModal, ToastContainer
  • Context: UserProvider, useCurrentUser, ToastProvider, useToast
  • Services: drawing/version CRUD via versionService functions
  • Types: DrawingEditorProps, DrawingEditorHandle, models, panel props, etc.

Use a ref on DrawingEditor to access DrawingEditorHandle (saveVersion, restoreVersionById, etc.).

Saving to a Markora server

By default drawings are kept in the browser (IndexedDB). To save them to a backend running @scopmx/markora-server, install createMarkoraStorage before the editor mounts:

import { createMarkoraStorage, setDrawingStorage } from "@scopmx/markora-client";

const storage = createMarkoraStorage({
    baseUrl: "/api/markora", // where the project mounted the server router
    // Your own HTTP function, so login tokens and session handling stay yours.
    // Resolve with { data: responseBody }; reject with error.response.{status,data}.
    request: ({ method, url, data, params }) => axios.request({ method, url, data, params }),
    uploadFile: (file, fileName, { timeout }) => myUpload(file, fileName, timeout),
    onConflict: ({ by }) => showReloadBanner(by),
    overrides: {
        updateDrawingTitle: (id, title) => api.rename(id, title),
        deleteDrawing: (id) => api.remove(id),
    },
});
setDrawingStorage(storage);

// While the editor is open:
useEffect(
    () =>
        storage.watch(drawingId, {
            onPresence: (others) => setOthers(others), // [{ id, name }]
            onRemoteChange: (change) => setRemoteChange(change), // { by, lostMarks } or null
        }),
    [drawingId],
);

Show "Priya changed this drawing · Show changes" when onRemoteChange reports a change and the editor has nothing unsaved (onDirtyChange(false)); the button reloads the editor by bumping its loadToken.

When two people save at once:

  • Comments and replies always merge.
  • HTML drawings merge mark by mark.
  • On image, PDF and 3D drawings, marks merge when only one person changed marks, or when both editors are the same size (the editor stores those marks in screen coordinates).
  • Both changed the same mark → the last save wins for that mark. One deleted it → the delete wins, and lostMarks counts the changes that were dropped.
  • When a merge is not safe, the person's unsaved work is saved as a "conflict copy" version in History, onConflict fires, the save throws MarkoraConflictError, and later saves are refused until the editor reloads.

The connector lives in src/markora/; its tests are test/integration/markora*.test.ts.

Props worth setting

commentMaxLength

Defaults to 100 characters. Most hosts will want more — for example 2000–4000.

Comments are stored inside the drawing snapshot, not in a table of their own, so size this against your snapshot storage limit rather than against a text column.

Shell height

The editor shell fills the viewport by default. To embed it inside a page, set --me-shell-height on or above the editor rather than overriding its height:

.my-editor-wrapper {
    --me-shell-height: 720px;
}

Any CSS length works, including calc(100svh - 64px) for a page with a header of its own. Unset, it falls back to 100svh.

Security notes for hosts

The editor runs in the browser, so everything below is the host's responsibility — none of it can be enforced from here.

  • Comment edit and delete are only author-checked in the browser. The check compares the comment's authorId with the current user and hides the controls; it is not an authorisation boundary. A comment with no authorId at all — which is what snapshots saved by older versions contain — is treated as editable by anyone.
  • Comments live in the drawing snapshot. A user who can save a snapshot can save one containing any comment text, author name or authorId. If authorship matters, your server has to enforce it when a snapshot is written, by comparing the incoming comments against the ones it already stored.
  • Uploaded HTML is isolated from the host origin by default. The editor loads it in an iframe with sandbox="allow-scripts" and no allow-same-origin, via srcdoc. Guest scripts cannot read this page's localStorage, cookies or DOM. Pass htmlIsolation="legacy" only if you need the 1.x blob URL, which is same-origin with the host and can steal session tokens.
  • To give a prototype its own real origin (so localStorage inside the page still works), host sandbox.html on a different origin and pass that URL as htmlIsolationOrigin. A same-origin URL is ignored and falls back to unique-origin srcdoc. Or write HTML_SANDBOX_PAGE to a file on that origin.
  • Guest pages can still paint a fake UI inside the frame, and can still make uncredentialed network requests. Treat uploaded HTML as untrusted content.
  • Everything the editor sends to the adapter is client-controlled data. Snapshots, screen routes, screen snapshots and activity entries are all built in the page and must be validated server-side: check sizes, check that ids belong to the drawing being written, and do not echo stored HTML back into another user's page without treating it as untrusted.

Guides

Peer dependencies

  • react ^18 or ^19
  • react-dom ^18 or ^19
  • fabric ^7.4.0
  • pdfjs-dist ^6.0.227

fabric and pdfjs-dist are peers because there has to be exactly one copy of each. The editor writes fabric's ownDefaults and reads pdf.js's GlobalWorkerOptions, and both are module-level state belonging to one instance. With two copies installed, your fabric objects would not see the editor's defaults and any pdf.js worker configuration you set would apply to a pdf.js the editor never uses — with no error either way.

jspdf, jszip, html2canvas and three stay as regular dependencies: they are internal, and nothing you configure reaches them.

The pdf.js worker

PDF rendering needs a pdf.js worker. The editor ships one and uses it by default — nothing to configure.

The worker is served from the package, so a bundler that copies package assets (Vite, Next, webpack 5 with asset/resource) picks it up. If yours does not, serve node_modules/@scopmx/markora-client/dist/pdf.worker.min.js yourself and point the editor at it:

import { configurePdfWorker } from "@scopmx/markora-client";

configurePdfWorker({ workerSrc: "/assets/pdf.worker.min.js" });

Or let your bundler build the worker, which avoids a separate request:

const Worker = (await import("@scopmx/markora-client/pdf.worker?worker")).default;
configurePdfWorker({ workerPort: new Worker() });

Call it once, before the first PDF is opened. A workerPort wins over a workerSrc — that is pdf.js's rule.

Two things worth knowing:

  • The file is named .js, not .mjs. Many servers (nginx by default) have no MIME mapping for .mjs; browsers then refuse it as a module script and every PDF fails.
  • In the CommonJS build the bundled default cannot be located, because a CJS bundle has no way to resolve its own sibling asset. The editor logs an error telling you to call configurePdfWorker. The ESM build — what import resolves to, and what every modern bundler picks — works without it.