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

ltn-print-labels

v0.1.15

Published

Standalone vanilla JS module for a printable label grid of student names: last-name disambiguation of identical first names, whole-group repetition to fill A4 pages, portrait/landscape, WYSIWYG full-page preview. No backend.

Readme

ltn-print-labels

Standalone, framework-free module that turns a class roster into a printable label grid of student names — the kind you cut out and stick on trays, cubbies, notebooks. Extracted from letableaunoir's server-side « Étiquettes » export so the same layout logic drives both the live preview and the PDF.

  • First-name disambiguation. Two Léa in the class become Léa M. / Léa C. — and if that is still ambiguous, Léa Ma. / Léa Mi., using the shortest last-name prefix that separates the whole group. Identical last names fall back to Léa (1) / Léa (2).
  • Whole-group repetition. The class is repeated an integer number of times to fill an A4 page — never a partial group.
  • Joined labels. 1px borders double as cut lines.
  • Portrait / landscape, with wider labels allowed in landscape.
  • WYSIWYG preview. createLabelPreview renders a full A4 page scaled down to fit its container (computed in real pixels, no CSS transform — cut lines stay a crisp 1px), horizontally centred.
  • No backend, no network, no runtime dependencies.

Install

npm install ltn-print-labels

Usage

import { computeLabelLayout, createLabelPreview } from "ltn-print-labels";
import "ltn-print-labels/style.css"; // preview only

const roster = [
  { firstname: "Léa", lastname: "Martin", level: "CE1" },
  { firstname: "Léa", lastname: "Claude", level: "CE1" },
  { firstname: "Tom", lastname: "Petit", level: "CE2" },
];

const options = {
  orient: "P", // "P" | "L"
  cols: 4, // labels PER ROW (1–7 portrait, 1–9 landscape) — see LABEL_COLS_BOUNDS
  fontMm: undefined, // MAX font size (mm); omit → auto: ~85 % of labels share it
  fields: "first", // "first" | "last" | "both"
  showLevel: true, // show the level as a light line above the name
};

// `cols` is the honest control: what you ask for is what you get, and the
// label width follows (usable width / cols). The older `labelMm` option still
// works and is unchanged, but a width in mm is only a TARGET, snapped to the
// nearest whole column count — in portrait 90, 110 and 120 mm all yield 2
// columns of 98 mm, and one label per row is unreachable. Prefer `cols`;
// `labelMm` is used only when `cols` is absent.

// `fontMm` is the second control: the MAX font size. Every label uses it and
// only overflowing names shrink (per cell). Omit it and it is derived so that
// ~85 % of the labels keep the same size — the ~15 % longest shrink. The
// layout returns `fontMmAuto` / `fontMmMin` / `fontMmMax` to drive a slider;
// a supplied value is clamped to that range.

// Live preview inside a dialog. `maxPreviewWidth` / `maxPreviewHeight` (px,
// preview only — ignored by computeLabelLayout) fix the target size so the
// sheet is deterministic on every re-render and the whole page stays visible.
const dims = { maxPreviewWidth: 780, maxPreviewHeight: 520 };
const preview = createLabelPreview("#preview", roster, { ...options, ...dims });
preview.update(roster, { ...options, orient: "L", ...dims });
preview.destroy();

// Layout model — hand this to your PDF renderer.
const layout = computeLabelLayout(roster, options);
// { orient, cols, groupes, labelWmm, labelHmm,
//   fontMm, fontMmAuto, fontMmMin, fontMmMax, levelFontMm,
//   levelRowMm, pageWmm, pageHmm,
//   rows: [{ cells: [{ name, level, empty }, ...] }, ...] }
// `levelRowMm`: fixed-height band reserved at the top of every cell for the
// level badge (pinned top-left, same place on every label, independent of
// any student's name or level text — see `showLevel`).
// Each cell also carries its own `fontMm`: the nominal size for names that
// fit, reduced only for the ones that would overflow the label. Words are
// never broken and the label width never varies, so every column stays
// identical — including in a server-side PDF renderer such as mPDF.

API

| Export | Description | | ---------------------------------------------------------- | -------------------------------------------------------------------- | | computeLabelLayout(students, options) | Pure layout model (throws if the roster is empty). | | disambiguateFirstNames(students) | One label string per student, first names only, collisions resolved. | | resolveLabelText(students, fields) | "first" / "last" / "both". | | createLabelPreview(container, students, options) | { update(students?, options?), destroy() }. | | LABEL_COLS_BOUNDS | { P: [1, 7], L: [1, 9] } — labels per row, for a discrete slider. | | LABEL_MM_BOUNDS | { P: [30, 120], L: [30, 260] } — bounds of the legacy labelMm. | | fitFontMm(text, labelWmm, nominalMm) | Font size that makes text fit, never breaking a word. | | autoNominalFontMm(texts, labelWmm, ceilingMm, quantile?) | Max font size at which 1 − quantile (default 85 %) of texts fit. |

students: Array<{ firstname?: string, lastname?: string, level?: string }>.

License

ISC © Jean-François Rondeau