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

@miracles-cn/react-signature-card

v0.20.2

Published

A React digital signature card component (手写签名卡片) with fixed ink, grid background and device detection, styled with styled-components.

Downloads

961

Readme

@miracles-cn/react-signature-card

手写签名卡片 React 组件 / A React digital signature card component — a white card with a fixed-black-ink signing area, grid background, clear button and input device badge (鼠标 / 触屏 / 手写板), as used in commitment-signing flows.

Built for React 17 with styled-components v6, written in strict TypeScript and developed with the Deno toolchain.

Features

  • Composable API: arrange the toolbar, clear button, device badge and signing canvas freely — nothing is hard-wired into a fixed layout. Headers and hint lines live outside the component, in your markup
  • Signature canvas with grid background and centered placeholder text
  • Fixed black ink strokes via Pointer Events (mouse / touch / stylus)
  • Device detection badge, showing 未检测 until the first stroke
  • Undo/redo per stroke, with canUndo/canRedo state for custom controls
  • Optional brush dynamics: strokes get bolder when drawing slowly (or under real stylus pressure) — off by default for fixed ink
  • Export as PNG, JPEG or SVG (exportToDataURL() on the ref and hook)
  • onChange delivers every format at once: PNG/JPEG/SVG data URLs plus the raw stroke matrix (path)
  • Live designer component: drag-and-drop, show/hide every widget, pick the toolbar's position relative to the canvas — and get the layout back as config
  • The designed config drives SignatureCard directly via its layout prop
  • Layout config also covers the button size and every built-in text — button labels, device badge and the canvas placeholder
  • DPI-aware crisp rendering and resize-safe redraw
  • useSignatureCard() hook for building fully custom controls
  • Publishes ESM + CJS + TypeScript types

Breaking changes: 0.2.0 removed the built-in hint line and made the toolbar composable; 0.4.0 changed onChange to receive a SignatureExportResult (every format at once) instead of a single data URL; 0.13.0 removed the <SignatureCard.Header> part entirely — render headers outside of the component.

Install

npm install @miracles-cn/react-signature-card

Peer dependencies: react@^17 and styled-components@^6.

Usage

The root <SignatureCard> owns the signing engine (state, imperative ref) and renders the white card; you compose the inside from its subcomponents. Headers and hint lines are rendered outside of the card, in your own markup:

import React, { useRef } from "react";
import { SignatureCard } from "@miracles-cn/react-signature-card";
import type { SignatureCardRef } from "@miracles-cn/react-signature-card";

export function CommitmentSign() {
  const ref = useRef<SignatureCardRef>(null);

  return (
    <section>
      {/* Hint line and header are intentionally not part of the component. */}
      <p style={{ margin: "0 0 8px", fontSize: 14 }}>
        ✍️ 请手动填写,签订后随承诺书一并反馈组织方
      </p>
      <p style={{ margin: "0 0 12px", fontSize: 15, fontWeight: 600 }}>
        ✍️ 请直接在签名区书写(固定黑色笔迹)
      </p>

      <SignatureCard
        ref={ref}
        onChange={(result) => {
          // result is null right after clearing
          if (result) console.log("signature png:", result.png);
        }}
      >
        <SignatureCard.Toolbar>
          <SignatureCard.ClearButton />
          <SignatureCard.DeviceStatus />
        </SignatureCard.Toolbar>
        <SignatureCard.Canvas height={480} />
      </SignatureCard>
    </section>
  );
}

Because the layout is composed, rearranging is trivial — e.g. toolbar below the canvas:

<SignatureCard>
  <SignatureCard.Canvas height={360} />
  <SignatureCard.Toolbar style={{ marginTop: 12 }}>
    <SignatureCard.DeviceStatus />
    <SignatureCard.ClearButton />
  </SignatureCard.Toolbar>
</SignatureCard>;

Parts

| Part | Purpose | Props | | ------------------------------ | ---------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | <SignatureCard> | White card frame + signing engine (state, ref) | penColor (#000000), penWidth (3), disabled (false), brush (false), layout (see the live designer), className, style, children, onSignStart, onChange, onClear | | <SignatureCard.Toolbar> | Flex row, space-between | children, className, style | | <SignatureCard.ClearButton> | Button wired to clear() | children (default 🗑️ 清空重签), className, style | | <SignatureCard.UndoButton> | Button wired to undo(); disabled when there is nothing to undo | children (default ↶ 撤销), className, style | | <SignatureCard.RedoButton> | Button wired to redo(); disabled when there is nothing to redo | children (default ↷ 重做), className, style | | <SignatureCard.DeviceStatus> | 当前设备 badge | label (default 当前设备:), names (Partial<DeviceNames>, default { mouse: "鼠标", touch: "触屏", pen: "手写板", unknown: "未检测" }), className, style | | <SignatureCard.Canvas> | Grid, input canvas, placeholder | height (480, numbers are px), grid (true, false or { size?: number, color?: string }, defaults 64 / #e4e9f0), placeholder (default 请在此区域签名(支持鼠标 / 触屏 / 手写板); null hides it), insetToolbar + insetSide (top/bottom/left/right) to dock a control inside the edge — the input surface shrinks around it so ink never runs beneath, className, style |

Callbacks on the root: onSignStart(device) with "mouse" | "touch" | "pen", onChange(result) after each stroke, undo, redo, and clearing — result carries every format (see below) and null means empty — and onClear().

Custom controls with useSignatureCard()

import { useSignatureCard } from "@miracles-cn/react-signature-card";

function MyClearButton() {
  const { clear, hasInk, disabled } = useSignatureCard();
  return (
    <button onClick={clear} disabled={disabled || !hasInk}>
      重写
    </button>
  );
}

The hook returns { device, hasInk, canUndo, canRedo, disabled, clear, undo, redo, isEmpty, toDataURL, exportToDataURL, getResult } and throws when used outside of <SignatureCard>.

Undo / redo

Every stroke is one history step. undo() removes the last stroke, redo() re-applies it, and starting a new stroke drops the redo history. Both fire onChange like any other change:

function HistoryButtons() {
  const { undo, redo, canUndo, canRedo } = useSignatureCard();
  return (
    <>
      <button onClick={() => undo()} disabled={!canUndo}>↶</button>
      <button onClick={() => redo()} disabled={!canRedo}>↷</button>
    </>
  );
}

or use the ready-made <SignatureCard.UndoButton /> / <SignatureCard.RedoButton />. Formal signing flows that only want 清空重签 can simply leave them out.

Exporting

Both the ref and useSignatureCard() expose exportToDataURL():

const png = ref.current?.exportToDataURL(); // transparent PNG
const jpg = ref.current?.exportToDataURL({ format: "jpeg", quality: 0.8 });
const svg = ref.current?.exportToDataURL({ format: "svg" }); // true vector
const onBlue = ref.current?.exportToDataURL({ background: "#e6f4ff" });

| Option | Default | Notes | | ------------ | ----------------------------------------- | --------------------------------------------------------------- | | format | "png" | "png", "jpeg" or "svg" | | quality | browser default (0.92) | JPEG only | | background | white for "jpeg", transparent otherwise | any CSS color; the strokes themselves are transparent by design |

JPEG has no alpha channel — a naive canvas export would turn the transparent background black, so white is composited in unless you pass another background. SVG is serialized from the recorded strokes (viewBox in CSS pixels), so it is real vector data and stays sharp at any size.

toDataURL(type?, quality?) remains available for compatibility.

Everything at once

onChange and getResult() hand you all formats in a single object:

<SignatureCard
  onChange={(result) => {
    if (!result) return; // cleared
    console.log(result.png); // transparent PNG data URL
    console.log(result.jpeg); // JPEG data URL (white background)
    console.log(result.svg); // vector SVG data URL
    console.log(result.path); // stroke matrix: Point[][]
  }}
/>;

const result = ref.current?.getResult();

result.path is the recorded stroke data — one array of points per stroke, in canvas coordinates:

[
  [{ "x": 10, "y": 20, "p": 1.2 }, { "x": 12, "y": 22, "p": 1.1 }],
  [{ "x": 40, "y": 8 }]
]

Convert to plain number pairs when your backend prefers them: result.path.map((stroke) => stroke.map((p) => [p.x, p.y])).

Live designer

SignatureCardDesigner is a standalone component that turns the rendered card itself into the editor — no side panel:

  • Drag-and-drop on the card: drag any part directly — no handles. The card's own buttons are inert previews in the designer (they show their real state but can't fire). Press and move reorders: a badge naming the part follows the cursor, the source dims, and a blue insertion bar marks the landing spot along the toolbar's item axis. Press and HOLD instead and the part dyes with blue first, then the whole toolbar behind it — the dyed level decides what the drag moves (a button, or the toolbar itself, which then lands on one of the eight labeled drop zones around the canvas). Press Esc to cancel. Driven by pointer events, so mouse and touch both work.
  • Show/hide with the corner eye: every toolbar button carries a small eye badge pinned to its top-right corner — faint at rest, full strength on a hovered part, and an eye-off at full strength on a hidden (dimmed) part. One click toggles it; the hit area is larger than the badge looks. There is no toolbar-level toggle: hiding every button (the invisible spacer doesn't count) is equivalent to hiding the toolbar — the card then renders as if hiddenBlocks contained "toolbar". A config saved by an older version with a hidden toolbar shows a restore-only eye-off instead.
  • Reposition the toolbar by dragging it: while the toolbar is dragged, eight labeled drop zones ring the canvas — bands just inside its edges land the floating -inset placements, strips just outside its edges the docked top / bottom / left / right ones (the side placements render the toolbar items vertically). Dropping on the canvas middle or outside the zones cancels.
  • Hide with a deleted look: hiding doesn't remove a widget — in the designer it stays in place, dimmed and grayscale, until restored. Nothing disappears, so nothing can be covered; the exported config still records it in hiddenBlocks / hiddenButtons.
  • The canvas is the anchor: it cannot be dragged or hidden, but it stays a drop target so the toolbar can be arranged around it.
  • Direct resize: drag the blue edge at the bottom of the signature area.
import { SignatureCardDesigner } from "@miracles-cn/react-signature-card";

<SignatureCardDesigner
  cardProps={{ brush: true }}
  onConfigChange={(config) =>
    localStorage.setItem("sig-layout", JSON.stringify(config))}
/>;

The layout (SignatureLayoutConfig, delivered by onConfigChange) is JSON-serializable; pass a saved one back via defaultConfig:

{
  "blocks": ["toolbar", "canvas"],
  "hiddenBlocks": [],
  "buttons": ["undo", "redo", "clear", "spacer", "device"],
  "hiddenButtons": [],
  "toolbarPosition": "top",
  "parts": {
    "toolbar": {
      "buttonSize": 32,
      "texts": {
        "undo": "↶ 撤销",
        "redo": "↷ 重做",
        "clear": "🗑️ 清空重签",
        "deviceLabel": "当前设备:",
        "deviceNames": {
          "mouse": "鼠标",
          "touch": "触屏",
          "pen": "手写板",
          "unknown": "未检测"
        }
      }
    },
    "canvas": {
      "height": 480,
      "grid": true,
      "placeholder": "请在此区域签名(支持鼠标 / 触屏 / 手写板)"
    }
  }
}

toolbarPosition (optional) is one of "top", "bottom", "left", "right", "top-inset", "bottom-inset", "left-inset", "right-inset": the plain sides reserve a row/column next to the canvas, the -inset variants float the toolbar just inside the canvas edge (purely visual — the input surface shrinks around the floating toolbar, so ink never runs beneath it), and left/right render the toolbar items vertically. When it is omitted the blocks order decides (toolbar before the canvas = on top).

parts (optional) customizes each built-in part's props in layout mode — every field falls back to the built-in default when omitted:

  • parts.toolbar mirrors the toolbar component's props (minus children, which the config's buttons / hiddenButtons compose): buttonSize is the buttons' height in pixels (the font size and horizontal padding scale with it from the built-in 32px look), texts customizes the undo / redo / clear labels and the device badge (deviceLabel plus the deviceNames map, merged over the defaults), textStyle styles the toolbar texts (font size / color / family — applied inline, so it takes over from the built-in 14px / #333 look, hover color swap included), and className / style land on the toolbar itself (the style merges over the position-derived one).
  • parts.canvas mirrors the canvas component's props (minus insetToolbar / insetSide, which stay owned by toolbarPosition): height sets the signature area's height, grid toggles or styles the background grid, placeholder customizes the empty-state copy (null hides it), placeholderStyle styles it (font size / color / ... — overrides the built-in 20px / #bfbfbf), and className / style land on the signing area.

The same buttonSize / texts / textStyle are also real props of the SignatureCard.Toolbar component, so classic (non-layout) compositions can scope them to the buttons they compose inside it — each child still wins with its own props.

The "spacer" toolbar item is a flexible blank placeholder: widgets before it stay at the toolbar's start and widgets after it are pushed to its end. Move it around (or add several) to control where the split happens — in vertical (left/right) toolbars the split is along the column.

Notes: the designer is uncontrolled after mount. Dragging is driven by pointer events (no HTML5 drag), so it also works on touch devices.

Applying a designed layout

SignatureCard understands the config directly through its layout prop — see the layout row in the Parts table above.

An empty config falls back to the classic layout, deleted widgets are skipped, and the canvas is always rendered — the toolbar arranges itself around it according to toolbarPosition (or the blocks order when that field is absent).

Brush width dynamics

By default the ink is fixed-width (固定黑色笔迹). Enable brush for the handwriting effect where strokes get bolder when drawn slowly and taper when drawn fast:

<SignatureCard brush />                                   // sensible defaults
<SignatureCard brush={{ minScale: 0.5, maxScale: 2.2 }} /> // wider dynamics

| BrushOptions | Default | Notes | | -------------------- | ------- | --------------------------------------------------- | | minScale | 0.65 | Width scale for the fastest strokes | | maxScale | 1.8 | Width scale for the slowest strokes / full pressure | | usePointerPressure | true | Use real stylus pressure when the device reports it |

About pressure: a finger on a capacitive touchscreen cannot measure force — browsers report a constant PointerEvent.pressure for touch, so for fingers and mice the effect is simulated from drawing speed. Real pressure is used automatically when a pressure-capable stylus (Apple Pencil, Wacom, Surface Pen, …) is the active pointer.

The per-point scale is preserved in result.path as p (1 = penWidth, rendered width = penWidth × p), so custom renderers can reproduce the exact line. SVG approximates each stroke's dynamics as its average width, because SVG cannot vary stroke-width within one path.

Ref API

const ref = useRef<SignatureCardRef>(null);

ref.current?.clear(); // 清空重签
ref.current?.isEmpty(); // true before the first stroke
const png = ref.current?.toDataURL(); // string | null
const jpeg = ref.current?.toDataURL("image/jpeg", 0.8);
const svg = ref.current?.exportToDataURL({ format: "svg" });
const all = ref.current?.getResult(); // SignatureExportResult | null
const ok = ref.current?.undo(); // true if a stroke was removed
const redone = ref.current?.redo(); // true if a stroke was re-applied
ref.current?.canUndo(); // boolean
ref.current?.canRedo(); // boolean

Development

Requires Deno 2+. The npm-side tooling (Storybook) uses an npm-managed node_modules/ — run npm install once.

deno task fmt              # format with deno fmt
deno task lint             # deno lint
deno task check            # strict TypeScript type check
deno task test             # run tests
deno task build            # build the npm package into ./npm
deno task storybook        # Storybook dev server on http://localhost:6006

Local demo

A minimal browser playground lives in demo/:

deno run -A npm:[email protected] demo/main.tsx --bundle --outfile=demo/bundle.js
# then serve the demo/ folder with any static file server

Publishing

Releases are cut by .github/workflows/release.yml through OIDC trusted publishing — no NPM_TOKEN exists anywhere. npm authenticates with a short-lived, package-scoped token minted for that one workflow run, and the npm-publish environment is restricted to main and v* tags.

Bump version in deno.json (the npm build reads name and version from there), commit, then push a matching tag:

git commit -am "chore(release): 0.21.0"
git tag v0.21.0 && git push origin v0.21.0

The workflow verifies the tag against deno.json, runs fmt --check / lint / check / test, checks the generated manifest and the tarball contents, then publishes. A manual run (Actions → Release → Run workflow) is a dry run unless confirm is set to publish @miracles-cn/react-signature-card@<version>.

No provenance attestation is generated: npm does not support provenance from a private source repository. See RELEASE.md for the one-time npm trust github setup, the remaining hardening steps, and local dry-run / break-glass tasks:

deno task publish:npm:dry   # build + inspect exactly what would be packed

License

MIT