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

@orbat-mapper/tactical-notes

v0.2.0

Published

Map-agnostic Notes for tactical maps: content model, frame presets and browser-based rasterisation.

Readme

@orbat-mapper/tactical-notes

Map-agnostic Notes: framed boxes of wrapped text that a tactical map pins to the ground. The package turns a note's Markdown content into HTML, lets the browser lay it out, and rasterises the result through an SVG foreignObject (ADR-0067). The map, image export and the tactical map sheet all use that raster, so line breaks agree between them on one machine.

The browser handles bidi, shaping, line breaking and font fallback, so right-to-left and complex scripts work. It has no dependencies and no map engine or tactical-draw dependency. Rendering needs a browser; the content helpers don't.

Installation

pnpm add @orbat-mapper/tactical-notes

Usage

import { createNoteCapability } from "@orbat-mapper/tactical-notes";

// No fonts are bundled. Notes default to Open Sans; omit `fonts` to use system fonts.
const notes = createNoteCapability({
  fonts: {
    "Open Sans": {
      regular: "/fonts/OpenSans-Regular.ttf",
      bold: "/fonts/OpenSans-Bold.ttf",
      italic: "/fonts/OpenSans-Italic.ttf",
      boldItalic: "/fonts/OpenSans-BoldItalic.ttf",
    },
  },
  images, // optional, for Notes with images
});
const td = new TacticalDraw(adapter, { notes });
notes.subscribe(() => td.render(graphics));

A Note looks like this:

const note: NoteModel = {
  id: "orders",
  width: 24, // em
  frame: { preset: "plate" }, // optional
  content: "# Orders\n\nHold the **ridge**.\n\n- Dig in\n- Report",
};

frame and its preset are optional. Without them a Note uses the basic preset (DEFAULT_NOTE_FRAME_PRESET): a rounded white box with no border or header band. Its text is black or white, whichever contrasts with the background, so a background is all a basic Note needs: frame: { tokens: { background: "#558b3a" } } gives white text.

A preset may leave out textColor and accentText; they then come from contrastingText(background) and contrastingText(accent), which return "#ffffff" when white has at least 3:1 contrast against the colour, else "#000000". Only basic does this, so set textColor too when you change another preset's background.

Content

content is a string in a small Markdown subset. Every string is valid.

| Syntax | Result | | --------------------------------------- | --------------------------------------------------- | | # Title, ## Title | Heading. ### and deeper render as level 2. | | Consecutive lines | One paragraph, with a line break at each new line. | | A blank line | Separates blocks. | | - a, * a, + a, 1. a | List, one level deep. Indented items flatten to it. | | **bold**, __bold__ | Bold. | | *italic*, _italic_ | Italic. Bold and italic nest. | | <u>underline</u> | Underlined. Combines with bold and italic. | | \* | The literal character. Works for ASCII punctuation. | | a `code` span in other text | Its plain text. | | ![alt](resource-id) alone on its line | Image filling the content width, aspect ratio kept. |

Unclosed markers are literal text, and so is everything else, such as tables, links, blockquotes, fences and other HTML; inline markers inside them still apply. A break on a block's last line is ignored, and its \ stays literal. Colours come from the frame, not the content.

The headingAlign frame token ("start", "center" or "end") aligns headings, and bodyAlign aligns paragraphs. Lists stay at the start. bodyStyle sets paragraphs and lists "regular", "italic", "light" (weight 300) or "caps" (capitals); headings keep their own style. Light text uses the family's light and lightItalic files when listed, else its regular ones. headingScale, headingLetterSpacing and headingSmallCaps set their size and spacing: a # heading is 1.5 em × headingScale and a ## 1.2 em × headingScale. headingBold: false sets headings in regular weight; heading1Bold and heading2Bold set one level, and heading1Color and heading2Color colour one level outside the header band. paddingX and paddingY set the padding of one axis. These follow headingBold, textColor and padding unless set, and overriding one of those shared tokens resets a preset's more specific ones. A border with style: "double" draws two rules, and "dashed" a dashed one. The friendly-solid and enemy-solid presets are atlas tags: white text on the side's colour, tight top and bottom.

To put host data, such as a unit or file name, into content, escape it with escapeNoteText(text). Otherwise 1. Prepare becomes a list and my_file_v2 can turn italic. The result is one line, with whitespace collapsed, and stays plain text in a paragraph, heading, list item or image alt text. noteImageLine(resourceId, alt) builds an image line with its alt text escaped, and throws for a resource id the syntax can't carry: empty, or holding whitespace or parentheses.

const content = [
  `# ${escapeNoteText(unit.name)}`,
  escapeNoteText(unit.remarks),
  noteImageLine(resource.id, file.name),
].join("\n\n");

noteImageResourceIds(note) lists the image resources a Note's content refers to, once each, in reading order. A resource images can't resolve keeps the Note "pending" in a capability, and rejects in renderCanvas. So does one that can't decode, which a capability also reports through onError.

Rendering

createNoteCapability({ fonts, images }) is a note rasteriser for @orbat-mapper/tactical-draw. resolve(note, pxPerEm) returns a PNG data URL raster, or "pending" while the Note's fonts load, an image is unavailable, or its first raster is being drawn. Drawing happens in the background: when it ends the capability bumps generation and calls its subscribe listeners, so the host can render again. Meanwhile a Note with an id keeps its previous raster, so zooming and editing don't flash the placeholder.

Rasters are cached by content and density, so moving or rotating a Note reuses its raster.

measure(note) returns a Note's height in em synchronously, or undefined while its fonts load. It lays the Note out without drawing a raster. TacticalDraw sizes the box from it, so while a width drag rewraps the text the box already has its new height, and the last raster is drawn at its own size from the top-left corner instead of being stretched.

Rasters draw one at a time, returning to the event loop in between so the map stays responsive, and a Note waiting in the queue draws only its latest request. Listeners are called at most about every 50 ms while a queue draws, and once when it ends.

notes.renderCanvas(note, { pxPerEm }) draws a Note at an explicit density, for export, from the capability's fonts and images. It waits for fonts, applies no fade and no size cap, and returns the canvas with the Note's exact heightEm. The map sheet's prepareMapSheetNotes(request, { notes }) uses it, so printed Notes break their lines as the map does. renderNoteCanvas(note, { fonts, images, pxPerEm }) does the same without a capability.

Layout and fonts

Every Note is laid out at one fixed font size and only scaled afterwards, because changing the font size can change where lines break.

fonts lists font files by family. Each is a URL, a URL or an ArrayBuffer of TTF or OTF bytes, fetched once when a Note first needs it. A missing face is synthesised by the browser from the family's others, so regular alone works, and a family that isn't listed falls back to the system's fonts. A CSS generic family, such as serif or system-ui, names a system font and needs no file. For files that aren't at a URL, pass a FontProvider instead: (family, weight, style) => Promise<ArrayBuffer | null>, where null means no file for that face. A file that fails to load keeps its Notes pending and reports through onError.

Presets don't choose a family. defaultFonts: { heading?, body? } sets the families of Notes whose frame doesn't name its own fonts; a role left out keeps DEFAULT_NOTE_FONTS (Open Sans), and an empty name throws. List the families' files in fonts. renderNoteCanvas takes the same option, and resolveFrameTokens(frame, defaultFonts) gives the families a Note will use. A Note without its own fonts takes on the defaults of whichever host opens it, so name families in the frame when a Note must look the same everywhere.

Font bytes are measured in the document and inlined into each raster, because an SVG image can't reach the page's fonts. Without fonts, and for glyphs a font lacks, the system's fonts are used. Those differ between browsers and machines, so a Note relying on them can wrap differently, and be a different height, for another user. Supply fonts for the scripts your users rely on if Notes must match between them. Note heights aren't stored.

Content and frame tokens are treated as untrusted: text is escaped, and a colour that isn't a plain colour value paints nothing. Images must be self-contained data: URLs.

Text layout limits

Lines break where the browser breaks them, and a word wider than the line is split, with no hyphenation. Text in a Note can't be selected in a printed sheet or PDF.