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

@deepctrls/craftjs

v0.2.16

Published

DeepCtrls' fork of the Craft.js React page editor framework

Readme

@deepctrls/craftjs

A maintained fork of Craft.js, based on @craftjs/[email protected]. This package contains the core React page editor framework. It retains the upstream MIT license. The release includes its matching utilities internally so core and transaction validation always use the same implementation.

Install

npm install @deepctrls/craftjs
import { Editor, Frame, Element, useEditor, useNode } from '@deepctrls/craftjs';

React 16.8, 17, 18, and 19 are supported by the upstream peer dependency range.

Migrate an existing Craft.js project

To preserve existing imports and share the same core instance with integrations such as @craftjs/layers, install this fork under the original package name:

npm install @craftjs/core@npm:@deepctrls/[email protected]

Continue importing from @craftjs/core in that project. Choose either direct installation or the alias; do not install both core packages in one application, because their React contexts would be separate.

Changes in 0.2.14

  • Opt-in editing access for persisted layer locks and temporary subtree isolation.
  • Atomic synchronous transactions, checked after normalization and before state, history, or subscriber updates. Rejected batches leave no partial writes.
  • Live canvas selection/drag/drop checks, including locks changed during a drag.
  • Explicit document-load and application-authorized runtime update contexts.
  • Local utility runtime and declarations are included in the release tarball.
  • Port all three Designer @craftjs/[email protected] patch changes to maintained source: microtask DOM batching, batch setDOM, and serialization-free notifications.

Designer patch compatibility

Connector DOM registrations are collected per editor store and flushed in one queueMicrotask. For duplicate node IDs, the last DOM wins. Missing/deleted nodes are skipped. Internal store.actions.setDOM(id, dom) remains synchronous and also accepts store.actions.setDOM([[id, dom], ...]). Neither form adds history. Code reading a newly connected node's DOM must allow the microtask to flush.

onNodesChange now follows the Designer patch: each store notification invokes the current configured callback, including DOM, selection and option updates. The framework does not serialize the tree or compare document contents first. Rejected edits do not notify. This differs from upstream and fork 0.2.13, where the serialized document was compared when a callback was present. Callers that need persistence-only notifications must decide when to save; avoid unconditional store writes inside this callback, which can recursively notify again.

Dynamic callback changes through setOptions, unmount cleanup and StrictMode cleanup from the fork are retained. These patch changes apply independently of the optional editing-access feature below.

Editing access

The feature is disabled by default. Existing documents and programmatic actions retain their previous behavior until editAccess is enabled:

<Editor
  editAccess={{ documentRevision: 'page-42' }}
  onEditDenied={({ reason, nodeId }) => showEditMessage(reason, nodeId)}
>
  <Frame>{/* your document */}</Frame>
</Editor>

editAccess accepts true, false, or an EditAccessPolicy. It can be changed through the Editor prop or actions.setOptions. enabled: false also rejects user edits and history replay when this policy is enabled.

Locks and temporary isolation

actions.setEditorLock(nodeId, 'position');
actions.setEditorLock(nodeId, 'all');
actions.setEditorLock(nodeId, ''); // unlock

actions.setOptions((options) => {
  options.editAccess = { scope: [containerId], documentRevision: 'page-42' };
});

Locks are stored as node.data.custom.editorLock and survive serialization. position allows content and ordinary appearance edits, but prevents geometry and structure edits. all additionally prevents props, other custom metadata, and canvas selection. Ancestor locks apply to descendants, including linked nodes. A node can change its own lock; a locked ancestor must be unlocked first. The root cannot be locked.

scope is session configuration, not document data. Omit it to allow the full document; [] disables editing everywhere; entries include their subtrees. Layer panels can still inspect/select locked or out-of-scope nodes with actions.selectNode. Query access before enabling their edit controls:

const access = query.node(nodeId).getEditAccess({ operation: 'geometry' });
// { allowed, reason?, lockOwnerId? }
query.node(nodeId).getEditAccess({ operation: 'select', selectionSource: 'layer' });

Operations are select, props, geometry, structure, and lock. Geometry/structure checks conservatively protect locked descendants too. Thus changing a container's child order or inserting into it is rejected if it contains a locked descendant, including the root. Unlock the affected subtree before such structural edits. This protects flow-layout positions as well as absolute layouts.

Geometry and pipes

The default classifier covers top-level props.style positioning, sizes, transforms, spacing, flex/grid layout and related geometry. It inspects actual changed leaves: replacing a style object just to change color is allowed under a position lock. Component-specific fields need a classifier:

import { isDefaultGeometryProp } from '@craftjs/core';

const editAccess = {
  isGeometryProp: (path, node) =>
    ['points', 'startPoint', 'endPoint', 'rotation'].includes(String(path[0])) ||
    isDefaultGeometryProp(path, node),
};

Adapt these field names to your pipe schema. This framework provides the editing contract; pipe handle geometry, snapping, endpoint binding and panels remain application responsibilities. Custom drag tools should query access before showing handles, then write through the guarded actions.

getLock(node) can read a custom lock format. setEditorLock always writes the default custom.editorLock field; use setCustom if your storage format differs. All policy callbacks must be synchronous and free of side effects.

Transactions, history and delayed updates

actions.transact({ documentRevision: 'page-42' }, (tx) => {
  tx.setProp(selectedIds, (props) => { props.style.width = 200; });
  tx.setProp(labelId, (props) => { props.text = 'Updated'; });
});

One denied change rejects the entire transaction. It produces one history entry when accepted. The callback receives ordinary actions; nested transactions, history methods and setState are not exposed on tx. Use tx.setCustom for lock changes inside a transaction. The API returns void; rejection is reported through onEditDenied. Do not dispatch further actions from a denial callback.

Callbacks and recipes must finish synchronously. Capture the application's document revision for delayed dialogs and compare it by supplying it in the transaction context. Transactions with a stale revision are rejected; the framework does not generate or increment revisions for the application.

history.ignore(), merge() and throttle() do not bypass editing access. Undo/redo replay previously accepted entries in full, even if the current locks or isolation scope differ. Page-level enabled: false still blocks replay.

Loading and live data

actions.history.ignore().transact({ source: 'document-load' }, (tx) => {
  tx.deserialize(savedDocument);
});
actions.history.clear(); // clear the old document's undo stack when switching pages

Frame and Element initialization use document-load internally. Direct actions.deserialize remains a user edit when access is enabled. Document-load is a trusted host capability that bypasses editing locks; it must only receive validated application documents. This is an editor consistency feature, not a security boundary against JavaScript with access to the store.

source: 'derived-binding' and source: 'runtime-data' are denied by default. Use canApplySystemChange({ context, previousState, nextState, patches }) to authorize only the actual fields your application needs to update. For example:

const editAccess = {
  canApplySystemChange: ({ context, patches }) =>
    context.source === 'runtime-data' && patches.every(({ path }) =>
      path.length === 5 && path[0] === 'nodes' && path[2] === 'data' &&
      path[3] === 'props' && path[4] === 'liveValue'
    ),
};
actions.history.ignore().transact({ source: 'runtime-data' }, (tx) => {
  tx.setProp(sensorId, (props) => { props.liveValue = latestValue; });
});

Scope this authorization further by node/type as appropriate. This allows live values or authorized endpoint-following updates without allowing manual edits to locked geometry. Viewer data held outside the Craft document is unaffected. source: 'lock-control' limits a transaction to the default lock field only.

Changes in 0.2.13

  • Avoid full-tree serialization when no node-change callback is configured.
  • Support callbacks added or replaced at runtime with actions.setOptions.
  • Unsubscribe on unmount and during React StrictMode effect cleanup.

Development and publishing

Automatic publishing with GitHub OIDC

.github/workflows/release.yml publishes only @deepctrls/craftjs. Bump the stable version in scripts/logic-package.json and push to main, or run the workflow manually from the Actions page. Existing npm versions are verified but never republished. Registry failures stop the workflow rather than being treated as a missing version.

The verification job runs the release checks, complete Jest suite, builds, lint, and independent React 18/19 tarball checks. The publish job downloads that exact artifact and uses id-token: write with npm 11.20.0 to publish with provenance. No NPM_TOKEN or NODE_AUTH_TOKEN secret is required. The GitHub-hosted runner and workflow identity must match the package's Trusted Publisher configuration:

| Setting | Value | | --- | --- | | Provider | GitHub Actions | | Owner | hnldlsjzt | | Repository | craft.js | | Workflow filename | release.yml | | Environment | Leave empty |

A package maintainer can establish this one-time trust from an interactive terminal (npm may require 2FA for changing account/package settings):

npx --yes --registry=https://registry.npmjs.org/ [email protected] trust github @deepctrls/craftjs --repo=hnldlsjzt/craft.js --file=release.yml --allow-publish --yes --registry=https://registry.npmjs.org/

See npm Trusted Publishing.

Local build and manual publishing

The monorepo keeps the upstream workspace names so examples and internal dependencies continue to resolve. The release process builds the utilities and core, then stages the renamed core package with CommonJS, ES modules, and TypeScript declarations in release/logic-craftjs.

For the version preflight, build, exact-tarball verification, manual publish and consumer-update checklist, follow the manual release guide. Release metadata and the fork version are maintained in scripts/logic-package.json.

Documentation and license