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

@domternal-pro/extension-columns

v1.0.0

Published

Notion-style multi-column layout for Domternal: slash creation, drag-to-create, resizable columns, self-healing structure

Readme

@domternal-pro/extension-columns

Notion-style multi-column layout for Domternal: a columnList block holds two or more column children with proportional widths, created from the slash menu or by dragging a block to another block's edge, resized by dragging the divider, and kept structurally sound by a self-healing watchdog.

Links

Documentation    •    Live examples

Licensing

This is a commercial package distributed through the public npm registry. A DOM-connected editor surface remains fully featured without a key for internal evaluation and shows a small evaluation badge. Guarded headless, standalone, server and worker operations have no badge surface, so they require an explicit evaluation mode or production mode with a covered DMP2 Commercial Key. Evaluation does not permit production use.

Keyless headless evaluation is explicit and remains fully functional:

import { configureProLicense } from '@domternal-pro/core';

configureProLicense({ mode: 'evaluation' });

Production uses the signed Commercial Key from the Order confirmation:

import { configureProLicense } from '@domternal-pro/core';

configureProLicense({
  mode: 'production',
  key: 'DMP2...',
});

A Commercial DMP2 Key carries exactly one signed Product Line. Every registered commercial package in an ordinary release must carry that same Product Line. A security-only release must carry it in both package metadata and its separate DMS1 certificate. Missing, conflicting or mismatched Product Line metadata remains uncovered and does not activate production. DMS1 never changes the key or grants another Product Line. Evaluation, Internal and legacy DMP1 keys do not use this Commercial Product Line field.

The signed key payload is not encrypted and may be readable in delivered browser code. Permitted embedding is expected, but the raw key must remain within the Customer scope allowed by the license. DMP2 omits Customer identity, contact details, Order reference, plan, price, separate payment and paid-through fields, and Developer limits. Commercial entitlement and per-key identifiers are random and opaque; the public status API does not expose them.

License validation runs entirely offline and never contacts Domternal: there is no online activation, license telemetry, metering, seat-counting or revocation request. A DMP2 Evaluation Key can remove visible evaluation notices only in explicitly selected evaluation mode and only through its signed UTC expiry day; it can never activate production mode. In production mode it remains visibly badged or marked. A DMP1 key is recognized only for legacy diagnostics: it leaves editor badges and marked-output notices in place and never authorizes guarded production use. A DMP2 Commercial Key does not activate before its signed not-before day. For ordinary and renewal keys that day is the Subscription start; an exceptional surviving-rights or remedy key can use the later decision or issuance day required by the license. During its paid Subscription and 14-day grace period, ordinary package coverage stops at its signed ordinaryReleaseThrough day; grace does not extend that boundary. After grace, a key without signed technical post-grace eligibility reports subscription-expired. A technically eligible key can continue coverage through ordinaryReleaseThrough, qualifying DMS1 releases and exact Section 10 remedy days, while the public status exposes a compatible derived fallbackThrough equal to ordinaryReleaseThrough. That technical marker does not prove 12 paid months or define the surviving legal scope: the Agreement and accepted Order define the legal scope, while the key and minimum durable entitlement state support later recognition of that scope, including whether it covers Qualified Builds, discontinuation final retained builds or another express surviving right. Post-expiry use is runtime-only for the exact permitted artifacts. Development, modification, rebuilding and new Applications require an active Subscription, subject only to the Agreement's narrow security, intellectual-property and replacement-key exceptions. A key-only reproduction changes no other code, dependency or feature and inherits the original build status. The contractual post-expiry Developer limit remains as a final derived value in the minimum durable entitlement state while it is relevant; supporting Order, payment and calculation records follow their separate retention periods; the runtime does not count people or expose that limit. Rights from different expiries remain attached to their own Commercial Keys. Coverage therefore uses both baked package release dates and the current UTC day reported by the runtime. Guarded headless production activates only when that Commercial Key covers every registered Pro package. Each JavaScript context that runs guarded headless work needs its own configuration. Activation is technical, not the license itself: permitted use is defined by LICENSE.md. See Installation and licensing for the complete setup.

Security-only releases use a separate signed DMS1 Security Release Certificate embedded in the package set; DMS1 does not add fields to or modify the Customer's DMP2 key. During the paid Subscription and grace, DMS1 requires a signed feature-baseline Release Date on or before ordinaryReleaseThrough; after grace, the key must also have technical post-grace eligibility. An exact Section 10 replacement date does not establish DMS1 baseline eligibility. A key without post-grace eligibility is never revived after grace. DMS1 does not renew a Subscription, move an ordinary-release boundary, change a Developer limit, unlock features or grant another Product Line. Post-expiry use is limited to the narrow security replacement of an eligible artifact, with no unrelated code, dependency or feature change, and the result inherits that artifact's status. The fixed Product Line Security Support End Date ends the obligation to issue further Security Updates, subject to mandatory law. An eligible DMS1 update signed and released during that period may remain usable afterward only within the same existing or surviving production right and limits; continued use does not make the Product Line supported or create, restore, renew or expand a right. The controlling terms are in Section 8 of LICENSE.md and the public security policy.

Constructing Columns without an element, or with element: null, is guarded as true headless initialization. An explicitly supplied detached HTMLElement is accepted as a pending framework-wrapper mount, but every local document-changing transaction is reclassified against the live DOM connection, including generic ProseMirror dispatches, Columns commands, built-in paste, drop and resize paths, and automatic self-healing normalization. Recognized inbound Collaboration synchronization remains readable during pending construction and later detachment. If an inbound merge temporarily leaves a one-column layout while detached headless production is not authorized, Columns renders the received state and queues only its own structural repair. The watchdog repairs it after the editor attaches or headless use becomes authorized. Unrelated local appenders are dropped and local root transactions remain blocked, so inbound readability does not create a local authoring exemption. A pending editor that never connects therefore cannot author, and detaching an initialized editor cannot retain the connected-editor allowance. A connected editor remains fully functional for evaluation with the badge. Selection-only and metadata-only transactions do not require an authoring assertion. Pure layout math, parsing, serialization, flattening and data-migration helpers remain available without a production assertion so licensing cannot obstruct reading or moving document data.

Install

pnpm add @domternal-pro/core @domternal-pro/extension-columns

One copy of the core

@domternal/core must resolve to a single instance across your app. An extension belongs to the core that created it: Extension is its base class, the schema it contributes to is that copy's, and so are its plugin keys. Hand one to an editor built by another copy and nothing disagrees out loud, so this package checks at construction and refuses, naming the fix.

The free core performs the same check from its own side. This one exists because the peer range on @domternal/core is open-ended on purpose, so you may be holding a core released before that check existed. Full dedupe recipe per package manager and bundler: https://domternal.dev/v1/guides/single-prosemirror-copy/

Usage

import { Columns } from '@domternal-pro/extension-columns';
import '@domternal-pro/extension-columns/columns.css';

const editor = new Editor({
  extensions: [StarterKit, Columns],
});

Columns auto-registers the column child node and the core Gapcursor extension. Widths are per-column flex-grow factors (data-width), so inserting or removing a column never rewrites its siblings' widths. Columns never wrap: on narrow containers they shrink toward --dm-column-min-width and an overfull row scrolls horizontally, exactly like Notion's desktop app (see the opt-in phone stacking snippet below). Pasting Notion's HTML export keeps column layouts, including their widths.

Drag-to-create (with the free block handle)

With @domternal/extension-block-controls present, configure the block handle with the ready-made preset:

import { BlockHandle } from '@domternal/extension-block-controls';
import { Columns, columnsBlockHandleOptions } from '@domternal-pro/extension-columns';

BlockHandle.configure(columnsBlockHandleOptions);
// Merging your own options: { ...columnsBlockHandleOptions, hideDelay: 300 }

The preset wires BOTH halves of the containment model: dropZoneProviders (dragging a block to another block's left or right edge creates a layout, a column boundary inserts a new column, and in-column gaps reorder) and nested.anchorContainers (blocks inside columns get their own per-block handle, anchored beside their column instead of the page gutter). They must ship together: anchorContainers alone makes blocks draggable OUT of columns with no way back in, and dropZoneProviders alone leaves blocks inside columns without handles. The inter-column gap is shared exactly like Notion's: hovering it instantly summons the handle of the block beside it (the gap is that column's handle home), while the resize divider appears after a short dwell and only over its own central band: lingering on the handle's buttons never lights it. Where both are up, the guide line stays grabbable between the buttons and both buttons keep their click targets beside it (on a single-line column that row is the whole divider, so without the band it would be unresizable).

Dragging never nests layouts (Notion parity). The slash command allows one nesting level. A drag is a single undo step, including the cleanup when the dragged block was its column's last one. List items travel with their own list kind (an ordered item stays ordered in its new column).

Commands

| Command | Effect | |---|---| | insertColumns({ columns? }) | Insert an empty layout (default 2 columns) at the cursor | | setColumns({ columns? }) | Wrap the selected block(s); the selection becomes column one | | unsetColumns() | Dissolve the layout back into sequential blocks | | addLayoutColumnBefore() / addLayoutColumnAfter() | Add an empty column beside the cursor column | | deleteLayoutColumn({ keepContent? }) | Delete the cursor column; keepContent: true merges its blocks into the neighbor instead (a single survivor unwraps) | | duplicateLayoutColumn() | Duplicate the cursor column (content and width) right after itself | | moveBlockToColumn({ dir }) | Move the current block into the adjacent column (visual left/right, RTL-aware) | | resizeLayoutColumn({ dir }) | Grow or shrink the cursor's column by one step ('grow' / 'shrink') | | resetColumnWidths() | Reset every column of the layout to equal widths |

The Layout prefix avoids colliding with the table's addColumnBefore/addColumnAfter/deleteColumn.

Keyboard

  • Mod-Alt-ArrowLeft / Mod-Alt-ArrowRight: move the current block between columns
  • Mod-Alt-Shift-ArrowRight / Mod-Alt-Shift-ArrowLeft: grow / shrink the current column (keyboard counterpart of the divider drag; no other editor has keyboard column resize)
  • Arrow keys and Backspace hop across column boundaries instead of merging content
  • Enter on a trailing empty paragraph, or ArrowDown at the end of the document, escapes below the layout
  • /2c through /5c, /col: slash menu creation
  • Double-click the resize divider to reset that pair to equal widths

Accessibility

  • The layout and every column expose role="group" with positional labels (Column 2 of 3), so screen readers announce entering and leaving columns while arrowing through content (WCAG 1.3.1).
  • Keyboard block moves and resizes announce their outcome (Moved block to column 2 of 3, Column widths 60% and 40%) through a polite live region (WCAG 4.1.3).
  • The resize divider's hit area is 24px wide (WCAG 2.5.8 target size; it narrows to a 16px band only across the rows of a shown block handle, whose buttons are the equivalent-size alternative there), and the keyboard resize covers WCAG 2.1.1 where a pointer is unavailable.
  • The resize guide and the drop indicator repaint with the OS Highlight color under Windows High Contrast (forced colors).
  • For WCAG 1.4.10 Reflow at narrow widths, apply the opt-in stacking snippet below. Reading order is linear per column either way (WCAG 1.3.2).

Paste from Notion

Copying blocks in the LIVE Notion app keeps column layouts on paste. Notion's own text/html clipboard flavor flattens layouts entirely, but the app also ships its internal block tree as JSON in a custom clipboard flavor that Chromium round-trips between pages. When that payload contains a column layout, the extension rebuilds it from the JSON, converting ratios to widths and a pragmatic subset of block types (paragraphs, headings, lists, to-dos, quotes, code, dividers, bold/italic/strike/code/link marks). Unknown block types degrade to their text instead of being dropped. Payloads without columns fall through to the default HTML path, which is the richer source for flat content.

Pasting from a Notion HTML EXPORT also keeps layouts, whether the whole page is copied or just a selection dragged across the columns: the parser accepts Notion's column-list/column markup, a paste normalizer rebuilds the wrapper browsers drop when serializing a selection from INSIDE a layout, and widths come from the data-notion-column-ratio attribute (the inline percentage is rewritten to pixels by the browser's clipboard, so the ratio is the reliable source).

Markdown

Markdown has no column construct, so serialization flattens each column's blocks sequentially. Register the shipped spec so the loss is reported through the warnings channel as a known lossy-structure conversion instead of the generic unknown-node fallback:

import { columnsMarkdownSpec } from '@domternal-pro/extension-columns';

Markdown.configure({ specs: columnsMarkdownSpec });

Migrating off columns

flattenColumnsJSON(docJSON) converts every layout into sequential blocks in plain document JSON: no schema, no editor, no DOM, usable in any Node script. Run it server-side over stored documents before removing the extension from a deployment (license downgrade, mixed-version fleets). A document that still contains columnList nodes cannot load without the Pro schema.

This data-exit helper never requires a production license mode and never blocks on license state. Reading, recovering or migrating your documents must remain possible even when the deployment is being downgraded or its key is unavailable.

Export

@domternal-pro/extension-export serializes columns side by side out of the box: PDF uses pdfmake's native columns element, DOCX renders a borderless fixed-layout table with percentage widths. No wiring needed. A custom nodeMappings.columnList still overrides the built-in. The mappers also stay importable from @domternal-pro/extension-export/mappers (a weightless subpath) for composing custom mappings, and the docx/pdfmake libraries keep loading lazily at export time.

Collaboration and versions

The layout is plain document structure: it syncs through @domternal-pro/extension-collaboration unmodified, and @domternal-pro/extension-version-history shows block-level diff badges inside columns out of the box (columnList/column are in its default diffTypes).

Theming

| Token | Default | Purpose | |---|---|---| | --dm-column-gap | 46px | Gutter between columns (Notion-true; fits the anchored block handle without covering the neighbour's text) | | --dm-column-min-width | 20px | Smallest a column shrinks to (columns never wrap; an overfull row scrolls). Set 46px for Notion's floor. Divider and keyboard resize clamp at it too, so stored widths always match the render. Use px, rem or em (not calc/percentages: the resize math reads the token) | | --dm-column-resize-guide | var(--dm-border-color) | Divider guide line (appears after a short hover dwell, Notion-style) | | --dm-column-resize-guide-active | var(--dm-accent) | Guide line while dragging (set it to the hover gray for Notion's monochrome look) | | --dm-accent | theme accent | Drag-to-create drop indicator |

Stacking columns on phones (opt-in)

Columns always stay side by side and shrink, exactly like Notion's desktop app on a narrow window. Notion's PHONE apps instead stack columns vertically. To mirror that in a mobile-targeted embed, add:

@media (max-width: 640px) {
  .dm-editor .dm-column-list {
    flex-wrap: wrap;
    /* No vertical gap between stacked columns: block margins separate
       them, matching Notion's stacked look. */
    gap: 0 var(--dm-column-gap, 46px);
  }
  .dm-editor .dm-column {
    min-width: 100%;
  }
}

Support

Report a defect that reproduces with synthetic content on the public tracker, using the Pro bug report template. Anything involving your own documents, your data or a collaboration update goes to email instead, and security vulnerabilities go to email only.

The Support and reporting issues policy explains where confirmed defects may be documented and how material notices are handled. Public channels are not a complete record of internal reports, investigations or other issues. Security vulnerabilities are handled through coordinated private disclosure.