⊼

heracl.es

/libelli

Turn spreadsheet rows into print-ready cards, entirely in the browser, as a web app.

libelli

libelli is a free, open-source web app that turns the rows of a spreadsheet into print-ready cards. It runs entirely in the browser: there is no server and no account, nothing is uploaded, and it keeps working offline.

Name

Libelli is Latin, the plural of libellus: “little book”, a diminutive of liber, “book”. The dragonfly in the logo is a nod to Libellula, a genus of dragonflies whose name is thought to come from the same word.

Overview

A user pastes or imports a table, designs one card, and links each area on the card to a column. libelli then produces one card per row. Typical uses are labels, recipe cards, exhibition captions, revision cards, programmes and small booklets.

The card is laid out on a page measured in millimetres. An area can take its words from a column, hold fixed text, or show an image, a drawing or a barcode. Areas can follow one another down the page, grow or shrink to fit their content, and disappear when their column is empty, so a card without a subtitle has no gap where the subtitle would be. Layout can also read the columns and propose a first draft of the card. The editor and the printed page share one layout, so what is on screen is what is printed.

Every control in the app has a tooltip: hover over it, or press and hold it on a touchscreen. This page covers what a tooltip cannot.

Data

A table can be pasted straight from Excel, Google Sheets, Coda or any other spreadsheet, or imported as a CSV or TSV file. Quoted fields and line breaks inside a cell are kept. When pasting into a table that already has columns, no header row is needed: the cells fill the existing columns from left to right.

Cell content is always treated as text. A cell containing <b>x</b> prints those characters, not bold type.

Column names contain no spaces: spaces become hyphens, and other punctuation is removed. This keeps every name usable as a placeholder (below).

Several tables and templates can be kept side by side, and they are independent of each other: one design can print any table, and one table can be printed with any design. A table exports as CSV; a template as JSON.

Placeholders

Text between double percent signs is replaced on each card. Placeholders work in an area’s own text and inside cells, and Markdown around them still applies.

PlaceholderPrints
%%title%%The title column of this card’s row. Case is ignored.
%%title:find:replace%%The column, with every find replaced. The replacement may be empty; a literal colon is written \:.
%%today%%Today’s date. %%today:YYYY-MM-DD%% sets the format, with YYYY, YY, MM, DD, MMMM, MMM, dddd and ddd.
%%lookup:3:price%%The price column of row 3. Row numbers follow their rows when the table is sorted or rearranged.
%%lookup:next:title%%The same column in the next row; previous for the one before.
%%page:current%%, %%page:total%%The card’s page number, and how many pages there are.
%%toc:title%%A table of contents: each other page’s title with its page number.
%%%A tab: what follows is pushed to the right edge, as in Espresso %%% 2.20. The area’s Leader fills the gap with dots, dashes or a line.

A placeholder that matches nothing is underlined in wavy red in the editor. Typing %% in any text field offers the column names. A single % is ordinary text. today, lookup, page and toc are reserved words; a column with one of those names has to be renamed, or reached with lookup.

Markdown

Areas set to Markdown support a small subset: #, ## and ### headings, bulleted lists (one level of nesting), numbered lists, **bold**, *italic*, ~~strikethrough~~, ==highlight==, `code`, [links](https://…), paragraphs separated by a blank line, and --- for a rule. Anything else is shown as written.

[some words]{red} colours just those words. Hex values, rgb(), hsl() and the standard CSS colour names are accepted.

Template CSS

Each template can carry its own stylesheet. Its rules apply only inside the card, and @import and external url() references are removed.

  • An area is reachable by its name: an area called Job Title is #Job-Title. .area matches every area.
  • Areas also carry classes for their content (.content-field, .content-static, .content-image) and mode (.mode-plain, .mode-markdown, .mode-image, .mode-color, .mode-qr), and .columns when set in columns.
  • Pages are reachable as #page-3, #cover, #back-cover, #inside-cover and #inside-back-cover, and as .recto and .verso when facing pages are on: #cover .area { … }.
  • The page settings are available as variables: --page-w, --page-h, --margin-top (and the other three edges), --bleed, --text-font, --text-size, --text-leading and --text-color.

Starter, in the CSS editor, writes out a sheet describing the current template — its page, fonts and every named area — which can be edited directly or handed to an AI model to style.

Printing

Export opens a preview of every card before anything is printed; untick a card to leave it out. From there the run can be printed, or saved as 300 dpi PNG files, bundled into one ZIP when there are several.

For the print to match the screen, set these in the browser’s print dialog:

  1. Paper size — the size of the card, or of the sheet when several cards share one.
  2. Margins — None.
  3. Headers and footers — off.
  4. Background graphics — on, or the paper colour and background images are dropped.

Print Settings can place 2, 4, 6 or 8 cards on one sheet, with bleed and crop marks. Its Order setting can also arrange the pages for folding:

  • 2-up (folio) — a stapled booklet. Print double-sided, flipping on the long edge (if the backs come out upside down, use the other flip setting), fold the stack in half and staple the spine.
  • 8-up (octavo) — the one-sheet mini zine. Print one side, fold in half three times, cut the centre of the middle fold, and fold into a booklet.

A booklet is padded with blank pages to a multiple of four (eight for the mini zine). With Recto / Verso on, left-hand pages mirror the layout of the right-hand ones across the fold.

Keyboard shortcuts

KeysAction
Ctrl/⌘ zUndo
Ctrl/⌘ yRedo
Ctrl/⌘ ⇧ zToggle the last change off and on, to compare
EnterType into the selected area
EscStop typing, leave Select Multiple, deselect, or close what is open
ArrowsNudge the selection 1 mm; with ⇧ 5 mm, with Alt ⇧ 10 mm
Arrows, PageUp / PageDownMove between cards, with nothing selected
⇧ / Ctrl / ⌘ + clickAdd an area to the selection, or remove it
Ctrl/⌘ aSelect every area
Ctrl/⌘ dDuplicate the selected areas
Ctrl/⌘ lLock or unlock the selected areas
Ctrl/⌘ ⇧ lLock or unlock the design
Delete / BackspaceDelete the selected areas
Ctrl/⌘ c / vCopy an area’s words; paste text as a new area
Ctrl/⌘ ⇧ c / vCopy an area’s style; paste it onto the selection
Ctrl/⌘ ⇧ + arrowsChange the alignment
Ctrl/⌘ ⇧ + scrollChange the type size of the area under the pointer
Ctrl/⌘ + scroll, or pinchZoom the page; off the page, the interface’s text size
Ctrl/⌘ + / − / 0Zoom in, out, or fit (⇧ 0 for 100%)
Ctrl/⌘ hShow or hide area outlines
Ctrl/⌘ ; or |Cycle the guides
Ctrl/⌘ ’ or #Show or hide the grid
Ctrl/⌘ sSave the drawing, while drawing
Ctrl/⌘ pExport; press again to print
Ctrl/⌘ ⇧ p or sExport
? or /Help

Holding Undo or Redo beside the page, or right-clicking either, lists every step it could take, so several can be skipped at once.

While typing in a field, undo and Delete act on the text, as usual. On a touchscreen, a pinch on the page zooms it; zoom and pan (under + Area) lets one finger scroll the page and adds an on-screen pad for nudging; and a long press on an area opens its menu.

Storage and offline use

Everything is saved automatically in the browser. Images and fonts are stored by name, so a template stays small and can be shared as a JSON file; whoever opens it is asked for any image or font they do not have. Drawings made in the app are the exception: they are saved inside the table cell, so they travel with the data. In Chromium-based browsers, images can be kept in a folder on disk instead.

Undo history is kept in memory, so it lasts until the page is reloaded; an update waits for the user to accept it for the same reason.

The app keeps a copy of itself and works offline after the first visit. Where the browser supports it, it can be installed as an app. Only one tab edits at a time; a second tab offers to take over.

What leaves this machine

Nothing that is typed, drawn or imported is ever uploaded. There is no backend and no analytics. Three kinds of request do reach the network, each because something was asked for by name:

  • Google Fonts. Choosing a Google font, or opening a template that uses one, loads it from fonts.googleapis.com. A template from someone else can therefore trigger this request when opened. Only a valid font name is accepted. Uploading a font file avoids the request entirely.
  • A linked background image, when a template names one. Only http and https addresses are allowed.
  • The app’s own files, cached so it can work offline.

A template’s CSS cannot add to this list, and neither can a cell: a stored image is written local:name, which a browser never treats as an address.

Development

libelli is a static SvelteKit site with no runtime dependencies; the Markdown renderer, the CSV parser and the barcode encoders are written by hand.

npm install
npm run dev      # http://localhost:5173
npm run verify   # project rules, lint, unit tests and type checks
npm run build    # static output in ./build, for any static host

Versions are numbered 0.minor.patch, and the leading zero never changes:

ChangeBumpsExample
A fixthe patch0.29.6 → 0.29.7
A featurethe minor0.29.6 → 0.30.0

AGENTS.md explains the codebase and its rules, docs/decisions.md the reasoning behind each module, and CHANGELOG.md what each version added.

Credit

Dialectic Acheiropoieton of Heracles Papatheodorou and Claude

Released under the MIT License.