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

@molecule/app-feature-canvas-react

v1.0.2

Published

Shared pan/zoom/select/transform infrastructure + node/edge primitives + pointer-event utilities for whiteboard / mind-map / design-canvas / presentation slide-canvas wrappers

Readme

@molecule/app-feature-canvas-react

Auto-generated, AI-first package reference for the molecule.dev ecosystem. It is written to be read by coding agents as much as by people, and is generated from this package's source — edit src/index.ts JSDoc, not this file.

React canvas primitives — the SHARED BASE for canvas-family wrappers (whiteboard, mind-map, design-canvas, presentation slide-canvas).

This package provides only the generic mechanics every canvas variant uses: pan/zoom/select/transform infrastructure, node/edge primitives, pointer-event utilities, and pure coordinate-transform helpers.

Domain-specific behavior (whiteboard drawing tools, mind-map auto- layout, design-canvas vector ops) lives in the wrapper packages, which consume this base as a peer dependency.

Exports:

  • <CanvasSurface> — pan/zoom container; children render in canvas-coordinate-space. Wheel zooms around the cursor; primary drag on the empty surface pans.
  • <CanvasNode> — generic positioned + draggable + resizable wrapper that lives inside the surface's canvas-space layer.
  • <CanvasEdge> — generic edge between two canvas-space points, with 'line', 'bezier', or 'orthogonal' geometry.
  • useCanvasViewport() — viewport state hook with panBy / zoomBy helpers and optional clamping.
  • useCanvasSelection() — selection-set hook with idiomatic toggles.
  • screenToCanvas, canvasToScreen, clampViewport, fitToBounds — pure coordinate-transform helpers.
  • buildEdgePath — pure SVG path builder used by <CanvasEdge>.
  • CanvasViewport, Point, Size, Bounds, ViewportLimits, CanvasEdgeKind, CanvasItemId, CanvasDragInfo, CanvasResizeInfo types.

Quick Start

import {
  CanvasSurface,
  CanvasNode,
  CanvasEdge,
  useCanvasViewport,
  useCanvasSelection,
  type CanvasViewport,
} from '@molecule/app-feature-canvas-react'

function Demo() {
  const { viewport, setViewport } = useCanvasViewport()
  const { selected, toggle } = useCanvasSelection()
  return (
    <CanvasSurface viewport={viewport} onViewportChange={setViewport} width={800} height={600}>
      <CanvasNode
        id="a"
        position={{ x: 100, y: 100 }}
        size={{ width: 80, height: 40 }}
        selected={selected.has('a')}
        onSelect={(id) => id && toggle(id)}
      />
      <CanvasEdge from={{ x: 180, y: 120 }} to={{ x: 280, y: 200 }} kind="bezier" />
    </CanvasSurface>
  )
}

Type

feature

Installation

npm install @molecule/app-feature-canvas-react @molecule/app-react @molecule/app-ui react
npm install -D @types/react

API

Interfaces

Bounds

An axis-aligned rectangle in canvas-space (top-left origin, +x right, +y down). Used for selection regions, content bounds, fitting, etc.

interface Bounds {
  /** Left edge in canvas units. */
  x: number
  /** Top edge in canvas units. */
  y: number
  /** Width in canvas units. */
  width: number
  /** Height in canvas units. */
  height: number
}

CanvasDragInfo

Drag callback payload for <CanvasNode>. delta is the canvas-space displacement applied since the previous onDrag call within the same gesture. position is the new canvas-space top-left of the node.

interface CanvasDragInfo {
  /** New canvas-space top-left position of the node. */
  position: Point
  /** Canvas-space delta since the previous drag tick. */
  delta: Point
  /** `true` on the very first move of a gesture. */
  start: boolean
  /** `true` on the final pointer-up of a gesture. */
  end: boolean
}

CanvasEdgeProps

<CanvasEdge> props.

interface CanvasEdgeProps {
  /** Edge starting point in canvas-space. */
  from: Point
  /** Edge ending point in canvas-space. */
  to: Point
  /**
   * Edge geometry. `'line'` is a straight segment, `'bezier'` is a
   * cubic with horizontal-first handles, `'orthogonal'` is a
   * right-angle path that goes horizontal-then-vertical from `from`.
   * Defaults to `'line'`.
   */
  kind?: CanvasEdgeKind
  /** Stroke width in canvas units. Defaults to `2`. */
  strokeWidth?: number
  /** SVG `stroke` attribute (color). Defaults to `'currentColor'`. */
  stroke?: string
  /** Optional aria-label override (defaults to a translated label). */
  ariaLabel?: string
  /** Extra classes merged onto the wrapper. */
  className?: string
}

CanvasNodeProps

<CanvasNode> props.

interface CanvasNodeProps {
  /** Optional id, surfaced on `data-canvas-node-id` and via `onSelect`. */
  id?: CanvasItemId
  /** Canvas-space top-left position of the node. */
  position: Point
  /** Optional canvas-space size. If omitted, the node sizes to content. */
  size?: Size
  /** Whether the node is currently selected (drives data attributes only). */
  selected?: boolean
  /**
   * Called when the user activates the node (pointerdown that doesn't
   * become a drag). Receives the node id (if set) and the original
   * pointer event so consumers can read modifier keys for additive
   * selection.
   */
  onSelect?: (id: CanvasItemId | undefined, e: ReactPointerEvent<HTMLDivElement>) => void
  /**
   * Called continuously during a drag gesture. The base does NOT
   * mutate `position` itself — consumers update their own state from
   * `info.position` and feed it back through props.
   *
   * Drag deltas are computed in CANVAS-space using the surface's
   * current zoom (read from the parent surface's `data-canvas-zoom`).
   */
  onDrag?: (info: CanvasDragInfo) => void
  /**
   * Called continuously during a resize gesture from the SE corner.
   * `info.size` is canvas-space; `info.position` matches the original
   * top-left (resize-from-SE doesn't shift the origin).
   */
  onResize?: (info: CanvasResizeInfo) => void
  /** Optional aria-label override (defaults to a translated label). */
  ariaLabel?: string
  /** Children render inside the node, in canvas-space. */
  children?: ReactNode
  /** Extra classes merged onto the wrapper. */
  className?: string
}

CanvasResizeInfo

Resize callback payload for <CanvasNode>. size is the new canvas-space size; position is the new canvas-space top-left (resize from a non-bottom-right handle moves the origin too).

interface CanvasResizeInfo {
  /** New canvas-space top-left position of the node. */
  position: Point
  /** New canvas-space size of the node. */
  size: Size
  /** `true` on the final pointer-up of a gesture. */
  end: boolean
}

CanvasSurfaceProps

<CanvasSurface> props.

interface CanvasSurfaceProps {
  /**
   * Controlled viewport. If omitted, the surface manages its own
   * viewport state via {@link useCanvasViewport}.
   */
  viewport?: CanvasViewport
  /**
   * Initial viewport (uncontrolled mode only). Ignored when `viewport`
   * is supplied.
   */
  initialViewport?: CanvasViewport
  /**
   * Called whenever the viewport changes (pan, zoom, or external set).
   * Required when `viewport` is supplied (controlled mode).
   */
  onViewportChange?: (next: CanvasViewport) => void
  /** Optional clamping limits applied on every viewport update. */
  limits?: ViewportLimits
  /**
   * Wheel-zoom factor per scroll tick. Multiplied for zoom-in (deltaY < 0)
   * and divided for zoom-out (deltaY > 0). Defaults to `1.1`.
   */
  zoomFactor?: number
  /**
   * If `true`, dragging anywhere on the surface pans (children that
   * stop propagation on pointerdown will still capture their own
   * gestures — the typical pattern for nodes). Defaults to `true`.
   */
  panOnDrag?: boolean
  /**
   * Called when an empty-area pointerdown fires. Useful for clearing
   * selection on background clicks.
   */
  onBackgroundPointerDown?: (e: ReactPointerEvent<HTMLDivElement>) => void
  /** Width of the surface in CSS pixels. */
  width: number
  /** Height of the surface in CSS pixels. */
  height: number
  /** Optional aria-label override (defaults to a translated label). */
  ariaLabel?: string
  /** Children render in canvas-coordinate-space (transformed by viewport). */
  children?: ReactNode
  /** Extra classes merged onto the outer wrapper. */
  className?: string
}

CanvasViewport

Viewport state — the camera over the canvas. (x, y) is the canvas-space coordinate that maps to the surface's top-left corner. zoom is the scale factor (1 = identity, 2 = 2x zoom-in, 0.5 = zoom-out).

interface CanvasViewport {
  /** Canvas-space x at the surface's top-left corner. */
  x: number
  /** Canvas-space y at the surface's top-left corner. */
  y: number
  /** Scale factor (1 = identity, > 1 = zoomed in). */
  zoom: number
}

Point

A 2D point in either screen-space or canvas-space.

interface Point {
  /** X coordinate. */
  x: number
  /** Y coordinate. */
  y: number
}

Size

A 2D size in either screen-space or canvas-space.

interface Size {
  /** Width in the relevant coordinate space. */
  width: number
  /** Height in the relevant coordinate space. */
  height: number
}

UseCanvasSelectionResult

Result of {@link useCanvasSelection}.

interface UseCanvasSelectionResult {
  /** Read-only selected ids. */
  selected: ReadonlySet<CanvasItemId>
  /** `true` if `id` is in the selection. */
  isSelected: (id: CanvasItemId) => boolean
  /**
   * Replace the selection. If `additive` is true, `ids` are merged into
   * the existing selection; otherwise the selection is replaced.
   */
  select: (ids: readonly CanvasItemId[], additive?: boolean) => void
  /** Toggle a single id's membership. */
  toggle: (id: CanvasItemId) => void
  /** Remove a single id. */
  deselect: (id: CanvasItemId) => void
  /** Clear the entire selection. */
  clear: () => void
}

UseCanvasViewportResult

Result of {@link useCanvasViewport}.

interface UseCanvasViewportResult {
  /** Current viewport. */
  viewport: CanvasViewport
  /** Replace the viewport (clamped to `limits` if supplied). */
  setViewport: (next: CanvasViewport) => void
  /** Pan by a screen-space delta in pixels (interpreted via the current zoom). */
  panBy: (deltaX: number, deltaY: number) => void
  /**
   * Zoom around a screen-space focal point. `factor > 1` zooms in,
   * `< 1` zooms out. The focal point's canvas-space position is held
   * fixed across the zoom.
   */
  zoomBy: (factor: number, focalScreenX: number, focalScreenY: number) => void
  /** Reset to the initial viewport. */
  reset: () => void
}

ViewportLimits

Optional clamping limits for the viewport. All fields are optional; omitted limits are unbounded. bounds constrains where (x, y) may sit; minZoom / maxZoom clamp the zoom factor.

interface ViewportLimits {
  /** Allowed range for the viewport origin. Omit for unbounded. */
  bounds?: Bounds
  /** Minimum allowed zoom. Defaults to no minimum. */
  minZoom?: number
  /** Maximum allowed zoom. Defaults to no maximum. */
  maxZoom?: number
}

Types

CanvasEdgeKind

Kinds of edge geometry <CanvasEdge> knows how to draw.

type CanvasEdgeKind = 'line' | 'bezier' | 'orthogonal'

CanvasItemId

Identifier for a selectable item. Generic strings so consumers can use whatever id scheme matches their domain (uuid, slug, db id, etc.).

type CanvasItemId = string

Functions

buildEdgePath(from, to, kind)

Build the SVG path d attribute for one of the supported edge kinds.

function buildEdgePath(from: Point, to: Point, kind: CanvasEdgeKind): string
  • from — Edge start in canvas-space.
  • to — Edge end in canvas-space.
  • kind — Edge geometry.

Returns: SVG path data.

CanvasEdge(props)

Generic edge between two canvas-space points. Renders an absolutely- positioned SVG that spans the bounding box of the two endpoints (with a small overflow margin so curves and bezier handles aren't clipped).

Domain semantics (which nodes are connected, edge labels, arrowheads, directionality) live in wrapper packages — this base just draws the geometry.

function CanvasEdge(props: CanvasEdgeProps): JSX.Element
  • props — Component props.

Returns: The edge element.

CanvasNode(props)

Generic positioned + draggable + resizable wrapper. Renders into the canvas-coordinate-space layer of <CanvasSurface>.

The base is intentionally minimal — it owns:

  • absolute positioning at position (canvas units).
  • optional explicit size (canvas units).
  • onSelect on pointerdown (with e.stopPropagation() so the surface doesn't pan).
  • onDrag from any pointerdown on the node body.
  • onResize from a SE-corner handle (only rendered if onResize is supplied).

It does NOT own visual chrome — wrapper packages compose chrome on top via children.

function CanvasNode(props: CanvasNodeProps): JSX.Element
  • props — Component props.

Returns: The node element.

CanvasSurface(props)

Pan/zoom container — the shared base every canvas variant builds on.

Wheel scroll zooms around the cursor; primary-button drag on the empty surface pans; children render inside a CSS-transformed inner layer so they live in canvas-coordinate-space (use screenToCanvas / canvasToScreen to translate as needed).

Children that handle their own pointer gestures (e.g. <CanvasNode>) should call e.stopPropagation() on pointerdown to suppress surface pan.

Style is driven entirely by getClassMap(); inline styles are reserved for things ClassMap can't express (transforms, touch-action, viewport-derived offsets).

function CanvasSurface(props: CanvasSurfaceProps): JSX.Element
  • props — Component props.

Returns: The canvas surface element.

canvasToScreen(point, viewport)

Convert a canvas-space point into screen-space (relative to the surface's top-left). Inverse of {@link screenToCanvas}.

function canvasToScreen(point: Point, viewport: CanvasViewport): Point
  • point — Canvas-space point.
  • viewport — Current viewport state.

Returns: The same point expressed in screen-space pixels.

clampViewport(viewport, limits)

Clamp a viewport to optional limits. bounds (if provided) constrains where the viewport origin may sit; minZoom / maxZoom clamp zoom. Returns a new viewport — does not mutate the input.

If bounds is provided AND has positive width/height, the origin is clamped so the surface still overlaps the bounds when fully zoomed out. (Wrappers can layer richer rules on top via onViewportChange.)

function clampViewport(viewport: CanvasViewport, limits?: ViewportLimits): CanvasViewport
  • viewport — The desired viewport.
  • limits — Optional clamping limits.

Returns: A clamped viewport.

fitToBounds(bounds, surface, padding)

Compute the viewport that fits a canvas-space bounds rectangle into a surface of the given screen-space size, with optional padding (in screen-space pixels) around the content.

The returned viewport centers the content, but if either dimension is zero the center is well-defined (the bounds origin) and zoom is clamped to 1.

function fitToBounds(bounds: Bounds, surface: Size, padding?: number): CanvasViewport
  • bounds — Canvas-space content rectangle to fit.
  • surface — Screen-space size of the surface.
  • padding — Screen-space padding around the content (default 0).

Returns: A viewport that frames bounds inside the surface.

screenToCanvas(point, viewport)

Convert a screen-space point (relative to the surface's top-left) into canvas-space. Inverse of {@link canvasToScreen}.

function screenToCanvas(point: Point, viewport: CanvasViewport): Point
  • point — Screen-space point in pixels.
  • viewport — Current viewport state.

Returns: The same point expressed in canvas-space.

useCanvasSelection(options)

Manage a selection set of canvas item ids with idiomatic helpers. Pure JS state — no DOM coupling, no domain assumptions.

Controlled mode: pass value + onChange. Uncontrolled mode: omit both; the hook owns its own state seeded from initial.

function useCanvasSelection(options?: {
  initial?: readonly CanvasItemId[]
  value?: ReadonlySet<CanvasItemId>
  onChange?: (next: ReadonlySet<CanvasItemId>) => void
}): UseCanvasSelectionResult
  • options — Hook options.
  • options.initial — Initial selection set (default empty).
  • options.value — External selection (controlled mode).
  • options.onChange — External setter (controlled mode).

Returns: The selection set + helpers.

useCanvasViewport(options)

Manage canvas viewport state with optional clamping limits. May be used controlled (pass value + onChange) or uncontrolled (omit both — it manages its own state seeded from initial).

function useCanvasViewport(options?: {
  initial?: CanvasViewport
  limits?: ViewportLimits
  value?: CanvasViewport
  onChange?: (next: CanvasViewport) => void
}): UseCanvasViewportResult
  • options — Hook options.
  • options.initial — Initial viewport (default { x: 0, y: 0, zoom: 1 }).
  • options.limits — Optional clamping limits applied on every update.
  • options.value — External viewport state (controlled mode).
  • options.onChange — External setter (controlled mode).

Returns: The viewport state + helpers (panBy, zoomBy, reset).

Injection Notes

Requirements

Peer dependencies:

  • @molecule/app-react ^1.0.1
  • @molecule/app-ui ^1.0.1
  • react ^18.0.0 || ^19.0.0

Runtime Dependencies

  • @molecule/app-react
  • @molecule/app-ui
  • react

Translations

Translation strings are provided by @molecule/app-locales-feature-canvas.