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

@reekon-tools/boldr-cad

v1.1.0

Published

BOLDR's 2D layout editor (React Native, iOS/Android/web) and how its drawings are kept in BOLDR.

Downloads

374

Readme

@reekon-tools/boldr-cad

BOLDR's 2D layout editor (BOLDR CAD) as a React Native component, plus the model, solver and exporters behind it. Walls, doors and windows, spaces, dimensions and constraints, wall elevations and markup, drawn with Skia on iOS and Android and with react-native-web in a browser. Drawings are kept in BOLDR through a small provider interface, so the package has no Firebase dependency of its own.

Install

npm install @reekon-tools/boldr-cad

Peer dependencies:

| Package | Range | Needed for | | ----------------------------- | --------- | ------------------------------------------- | | react | >=19 | always | | react-native | >=0.74 | always (react-native-web in a browser) | | @shopify/react-native-skia | >=2.5 | the editor on iOS and Android | | react-native-gesture-handler| >=2.31 | the editor on iOS and Android | | react-native-reanimated | >=4 | the editor on iOS and Android (pan and zoom run on the UI thread; its Babel plugin must be in the app's config) | | react-native-svg | >=15 | the editor's icons on iOS and Android |

Only ./core (see below) runs without the four native packages.

How it resolves

The package has two entry points:

  • @reekon-tools/boldr-cad is the editor: CadEditor, the data layer and the device hooks.
  • @reekon-tools/boldr-cad/core is the drawing model without React: types, migrateProject, project files, unit formatting and the PDF/DXF exporters. For servers and tools.

React Native hosts read the TypeScript source. The package exports a react-native condition that points at src/, so Metro compiles it like app code and picks the .native platform files itself. Nothing is prebuilt for native, which also means a local checkout linked into an app is live with no build step.

For TypeScript in a React Native app, two compiler options are needed. Expo's base tsconfig already sets the first:

{
  "compilerOptions": {
    "customConditions": ["react-native"],
    "moduleSuffixes": [".ios", ".android", ".native", ""]
  }
}

Web and Node take the built dist/ through the default and types conditions. In a Vite app, alias react-native to react-native-web and let .web.js win, because the platform files in dist/ have no bare X.js:

// vite.config.ts
resolve: {
  alias: [{ find: /^react-native$/, replacement: 'react-native-web' }],
  extensions: ['.web.js', '.web.tsx', '.web.ts', '.mjs', '.js', '.mts', '.ts', '.jsx', '.tsx', '.json'],
},

dist/ is emitted with extensionless relative imports, so plain Node cannot import('@reekon-tools/boldr-cad/core') directly. Run it through a bundler (esbuild, tsx, vite-node) as BOLDR's own services do.

Using the editor

import { CadEditor, openCadDrawing, NotACadDrawing, type CadSync } from '@reekon-tools/boldr-cad';

function DrawingScreen({ provider }: { provider: CadDataProvider }) {
  const [sync, setSync] = useState<CadSync | null>(null);

  useEffect(() => {
    let cancelled = false;
    openCadDrawing(provider, { onError: e => console.error(e) }).then(
      s => (cancelled ? s.stop() : setSync(s)),
      e => { if (e instanceof NotACadDrawing) console.log(e.reason); }, // 'missing' | 'legacy'
    );
    return () => { cancelled = true; };
  }, [provider]);

  if (!sync) return null;
  return <CadEditor sync={sync} onBack={goBack} theme="dark" devTools={false} />;
}

Mount one CadEditor per screen. Mounting starts an editing session, unmounting saves and stops the sync it was given. Without sync the drawing lives in the browser's own storage, which is what the dev harness uses.

CadEditor props:

| Prop | Type | Purpose | | ------------- | ---------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- | | sync | CadSync | The drawing, from openCadDrawing. Saved there as it changes. | | onBack | () => void | The top bar's back button. Without it, Back opens the project's pages. | | headerRight | ReactNode \| (buttonSize: number) => ReactNode | The host's own buttons in the top bar, such as a Bluetooth status icon. As a function it gets the bar's round-button size (42 pt on a phone, 46 otherwise). | | theme | 'light' \| 'dark' | Canvas colors. The chrome is dark either way. Without it, the person's Theme setting decides. | | units | { format: (inches: number) => string; metric: boolean; unit?: Unit; fractions?: boolean; denom?: number } | Lengths the host's way: how to write one, the unit a bare number is typed in (unit: ftin, in, ft, mm, cm or m; metric alone means mm), whether they're typed with fractions, and the fraction precision. Settings' Units then offers Account beside the editor's own. | | fonts | SkiaFonts | Typefaces for text on the plan by CSS weight, with a fallback. Device only. | | pickImage | () => Promise<{ dataUrl: string; w: number; h: number } \| null> | A photo or plan to trace. In a browser a file picker is the default. | | saveFile | (name: string, mime: string, data: string) => void | How exports (PDF, DXF, project file) reach the person, e.g. a share sheet. In a browser they download. | | readOnly | boolean | Pan and zoom only, nothing saved. A tap on a link's tile asks, then opens it. | | devTools | boolean | Reset to the demo, open and save project files, in Settings. Default true; turn off in production. | | links | CadLinks | The job's files to link on the drawing, and its lengths for lines (see below). Without it there's nothing to link. |

Fonts on a device, with react-native-skia:

const regular = useTypeface(require('./assets/Inter-Regular.ttf'));
const bold = useTypeface(require('./assets/Inter-Bold.ttf'));
const fonts: SkiaFonts | null = regular && bold ? { weights: { 400: regular, 700: bold }, fallback: regular } : null;

Wait for the typefaces before mounting the editor.

Keeping a drawing in BOLDR

openCadDrawing(provider, options) loads a drawing through a CadDataProvider and keeps it in step while the editor is open: only the pages that changed are written, each stamped with the session's id; traced images are uploaded and stored by reference; pages another device changed are taken in when they aren't being edited locally.

interface CadDataProvider {
  subscribeFile(cb: (file: { name: string; meta: CadFileMeta | null } | null, meta?: CadSnapshotMeta) => void): () => void;
  subscribePages(cb: (pages: Record<string, CadPageDoc>, meta?: CadSnapshotMeta) => void): () => void;
  writeMeta(meta: CadFileMeta): Promise<void>;
  writePage(id: string, page: CadPageDoc): Promise<void>;
  deletePage(id: string): Promise<void>;
  uploadImage(pageId: string, dataUrl: string): Promise<string>; // returns a URL or a `stored:<path>` reference
  imageUrl?(ref: string): Promise<string>;                        // turns a stored reference into a URL
  uploadThumbnail?(dataUrl: string): Promise<void>;
  measurements?: CadMeasurements;
}

A drawing is one file document with fileData: CadFileMeta (format: 'boldr-cad', version: 1, pageOrder, shared settings) and one page document per page (name, the sketch, the underlay, and the rev of the session that wrote it last). Storage paths are relative to the drawing's own folder, so copying or moving a drawing with its folder keeps it working.

CadSnapshotMeta says whether a snapshot is the local cache's word (fromCache: true) or the server's. Firestore answers from the cache first when it's offline, and for a drawing never opened on that device the cache has no pages: the editor then waits for the server rather than starting a blank first page over the real one. Pass snapshot.metadata.fromCache from both listeners and subscribe with includeMetadataChanges: true, so the server's confirmation of an empty collection still arrives.

Options: initialize: true says the file was just created here, so its first page is made without waiting for the server (and starts a new drawing when the file has no drawing record yet); onError hears about failed writes and uploads; signal (an AbortSignal) closes the drawing while it's still opening, rejecting with Error('closed'). The promise rejects with NotACadDrawing whose reason is 'missing' (no such file) or 'legacy' (a file from the retired layout tool).

The returned CadSync has stop(), idle() (resolves once every write so far has finished), saveThumbnail(pngDataUrl) and imageUrl(ref).

Measurements. A dimension drawn in the editor is the drawing's alone: no measurement is made for it. The person saves one to the drawing's group from its bar (Save to group, through links.saveMeasurement), and it's linked to that measurement from then on. Drawings made by earlier versions kept every driving dimension as a measurement of the drawing's file; when the provider supplies measurements, those still follow their dimensions both ways (create is no longer called). setPlaced(ids, linked) hears every measurement the drawing shows: its own, and the job's it's linked to, each with its group (and a form's column). measurementOf(constraint) gives a constraint's value as BOLDR keeps it — lengths in micrometres, angles in degrees — or null for reference and print dimensions and linked ones.

Linking the job's files and measurements

With links, Annotate gets a yellow + (under the rail; a phone's in the trash's corner while nothing's selected) that puts the job's files on the plan as tiles, with leaders, opened from the bar. A measurement comes into a drawing only as the dimension it drives: in Draft, a line's length keypad (select the line, tap +) offers the job's lengths (a page it swipes to on a touch screen, Select from list on a desktop), and the one picked sets the line, linked to it. A dimension is saved to the group from its bar (Save to group). A new length for a linked dimension asks first: Change both, Only this drawing (unlinks), or Cancel.

interface CadLinks {
  pick(req: { want: 'files'; linked: LinkTarget[] }): Promise<LinkTarget[]>; // [] for nothing
  measurements?(cb: (list: LinkTarget[]) => void): () => void; // the job's lengths and angles, in the order to show them
  pickMeasurement?(req: { kind: 'length' | 'angle' }): Promise<LinkTarget | null>; // the host's own list (a side drawer)
  subscribe(targets: LinkTarget[], cb: (info: Record<string, LinkInfo>) => void): () => void;
  open(target: LinkTarget): void;
  update?(target: LinkTarget, value: number, capture?: unknown): void; // µm or degrees, the person having said so
  saveMeasurement?(m: { kind: 'length' | 'angle'; value: number; capture?: unknown }): Promise<LinkTarget | null>;
  canSave?: boolean; // false where a measurement can't be saved (a form's group)
}
  • A LinkTarget is { kind: 'file' | 'measurement', id, groupId, columnId? } plus what it was when it was linked (name, groupName, fileType, mkind, value, text). Resolve it by groupId and id in the job open now, never by a stored path, so copied and moved jobs keep working.
  • pick offers the job's files to pick. BOLDR's pickers show the measurements too, greyed out, and a tap on one says they're added in Draft; anything but a file handed back is dropped.
  • A line's length keypad and a dimension's angle keypad take the job's measurements. On a touch screen the keypad swipes to a page of them; on a desktop it has Select from list, which calls pickMeasurement (the host's own list, those of kind pickable and the rest greyed out), or shows that page when the host has none. measurements calls back with the job's lengths and angles as measurement targets (with mkind, value in micrometres or degrees, and text), again as they change, and not until the whole job's been heard from; the keypad greys out those that aren't its kind. A host keypad (keypad) gets measurementKind and pickMeasurement on such a request: show the job's measurements a swipe away, greying out the other kind, and hand back the one tapped.
  • To draw that page itself, so the list looks as it does everywhere else in the host, pass measurementList to CadEditor: a function of { kind, pick, width, height } that returns the list's element (for example page => <JobList {...page} />). It's rendered as a component of its own from when the page is first shown, and again when what it's given changes. List the job's measurements at that size, grey out the other kind, and call pick with the one tapped. BOLDR does on a tablet, with its phone keypad's list. Without it the page is the editor's own grid, made from measurements. Either way the page is there only when links has measurements or pickMeasurement, and a desktop shows it only without pickMeasurement.
  • subscribe answers by linkKey(target), and again whenever that changes. Say exists: false only once it's known to be gone, never while loading. text is the value written the host's way; thumbUrl a picture to show.
  • Pass one links object for as long as the drawing is open (memoize it).

Laser readings. laserReading(inches, capture) takes the host's own record of a reading (anything JSON: its device, its number). It's kept on the dimension the reading sets and handed back with saveMeasurement, or with update for a linked one, so a measurement saved from a reading records its device. A value typed over it isn't the reading's.

For tests and prototypes, memoryProvider() is an in-memory provider with remote() and remoteMeasurement() to change things as another device would.

Measuring devices

import { setLaserConnected, laserReading } from '@reekon-tools/boldr-cad';

setLaserConnected(true);        // shows the reading tray, enables the keypad's Laser key
laserReading(inches, capture);  // into an open keypad, onto the selected wall, or into the tray

laserReading returns false when no drawing is open to take the value, so the host can handle it instead.

./core

import { sketchToPdf, pdfFileName, sketchToDxf, dxfFileName, fmtLen, parseLen, projectFromFile, migrateProject } from '@reekon-tools/boldr-cad/core';

The model types (Project, Page, Sketch, Line, Constraint, ...), migrateProject for older saves, projectToFile / projectFromFile for project files, the unit helpers (fmtLen, parseLen, fmtArea) and the PDF and DXF exporters, which return the file's contents as a string.

Geometry is stored in inches. Units are display only, and switching them never changes a drawing.

Development

npm ci
npm run dev              # the editor in a browser at http://localhost:5173 (react-native-web, saves locally)
npm test                 # vitest: units, solver, rooms, trim, sync
npm run e2e              # playwright flows against the dev server (npx playwright install first)
npm run typecheck        # the browser build
npm run typecheck:native # the device build (.native files)
npm run build:lib        # dist/ for web and Node consumers
npm run build:single     # one self-contained HTML file for testers, in dist-single/

npm run build is the browser harness and writes to dist/ too, replacing the library output. npm run build:lib always starts from an empty dist/.

To work on the package inside an app without publishing, depend on the checkout with link: (yarn) or file: (npm). Metro then needs the checkout as a watch folder, the package name mapped to it, and the checkout's own node_modules blocked so React and React Native aren't bundled twice. The BOLDR Pro app derives all three from its link: dependencies in metro.config.js.

Publishing

npm publish --access public

prepublishOnly runs the native typecheck, the tests and build:lib first, so the tarball always carries a fresh dist/ next to src/.

License

MIT