Skip to content

Releases: usetrmnl/trmnl-framework

Framework 3.3.1

Choose a tag to compare

@github-actions github-actions released this 01 Sep 10:07
ce43e4d

Framework v3.3.1

Released on 2026-08-31.

A maps release.

Framework Changes

  • Water reads clearly against land now, on every panel.
  • Woodland and bare rock paint from their own slots, map-forest and map-rock, so natural landscapes have more depth.
  • Generic landuse now carries a texture on 1-bit screens instead of dropping to bare white, so a built-up area reads as ground there too.
  • route() and dot() draw bolder, so a route or a marker holds up over a busy map.
  • Map fills land on the device pixel grid on screens whose scale is not a whole number, TRMNL X among them. They could beat against the grid, paint a neighbouring tone's texture, or come out flat with no texture at all; a horizontal band could cross the map painting the wrong texture below it; and boundary lines could go missing. Maps on those screens will look different from 3.3.0, and correct.
  • The dark theme mirrors the light one.
  • Framework development mode serves your own plugins.js again, so a JavaScript change is visible through a host app's renders.

Upgrade Notes

  • If your theme sets map-green, add map-forest. If it sets map-sand, add map-rock. Both are in the theme boilerplate.
  • The black-and-yellow and white-and-red themes keep the water they had.
  • Routes and markers draw thicker. Pass width or radius for the old sizes.

Documentation Changes

  • The Map examples moved to Atlanta, Berlin, Tiergarten and Central Park.
  • The Map styles example sits at a zoom where the three presets actually differ.
  • Fixed the floating card example's layout and the Strava example's stats.

Framework 3.3.0

Choose a tag to compare

@github-actions github-actions released this 27 Aug 13:49
dcff181

Framework v3.3.0

Released on 2026-08-27.

Framework 3.3 brings maps to plugins. Everything in it is opt-in and rides on the 3.2 foundation: themes, the TRMNLPaint paint API, and the adaptive charts and icons.

New Features

Adaptive maps

Maps join charts on TRMNLPaint. TRMNLMaps ships inside the runtime and builds a MapLibre GL JS style out of framework paint over OpenStreetMap vector tiles, so a map follows the device, bit depth, dark mode, and themes instead of carrying a fixed raster style. What it draws is one still frame, with no interaction and no animation.

Every layer paints from a new map slot, so land, water, parks, fields, buildings, roads, rail, paths, boundaries and labels take dither tiles on 1-bit, solids on 4-bit, hues on a color panel, and a theme's own colors. Roads, rails, routes and markers come out as crisp fills on the pixel grid rather than anti-aliased lines. Place names are framework labels, set in the screen's own fonts with a text stroke, so they hold up over a busy 1-bit map.

Four presets ship: streets, minimal, outline and blank. Plot your own data with route(), dot(), fit() and decodePolyline(), as the Strava activity example does.

Tile sources and keys

A map that names no tile host fetches OpenStreetMap's public Shortbread tiles itself, on the free tier, so it costs its host nothing to run. A plugin points at a source of its own with options({ tiles: { url, key } }), where url is a {z}/{x}/{y} template and {key} in it is filled from key. A host can hand a source to a single plugin instance instead, as window.__TRMNL_MAPS__ = { tiles: { url, key } }, which is how a plugin author's key or a user's key reaches a map without appearing in the markup.

The engine serves TRMNL's own source as well, the 'trmnl' preset at /framework/tiles/{z}/{x}/{y}.mvt. It passes tiles through from Framework.tile_source_url with the right type, encoding and cache headers, and keeps nothing in the gem, on disk or in a database. The docs run on it.

Map slots for themes

Sixteen map slots join the theme slot set: map-land, map-water, map-water-line, map-green, map-farmland, map-sand, map-area, map-site, map-building, map-transit, map-road, map-road-minor, map-path, map-rail, map-boundary and map-label. Each one is a bg-slot, so a theme repaints a map with the same mixin it uses for every other component. The three shipped themes cover all sixteen.

TRMNLPaint slot resolver and MapLibre adapter

Two functions arrive on TRMNLPaint, and a third gains a pair of numbers:

  • TRMNLPaint.slot(name, { kind }) returns what a component slot paints with on the current screen, for a background, text, or line slot.
  • TRMNLPaint.toMapLibre(fill) shapes a Fill for MapLibre GL JS: a solid becomes a color, a dither tile becomes a registered pattern image that lands one tile pixel on one device pixel.
  • TRMNLPaint.scale() now reports the device pixel ratio and the dither ratio too.

Runtime readiness for maps

The terminalize pass now ends by waiting for every attached map to go idle, a bounded TRMNLMaps.settle(), so TRMNL_PLUGINS_READY fires only once the tiles, the widened lines and the labels have drawn. A host that rescales the screen after the pass, the way a screenshot service sets its capture pixel ratio last, calls TRMNLMaps.refresh(): it rebuilds every watched map against the paint the screen resolves now and settles it, with no second pass.

Extended theming contract

A theme reaches further into a screen than it could in 3.2. The title bar's inks split into slots of their own (title, instance, icon, and a stroke separate from the rest), item cards gain a fill, a paired ink, padding and a border art, and dividers gain a slot, so a theme restyles those surfaces instead of leaving them on the screen's defaults. Item cards can paint from generated per-rail ordered-dither ramps, tiled at their own width so the pixels the dither decided survive.

Structure moves too, through unitless factors over the unthemed geometry, where 1 is always the unthemed screen: layout-factors() takes whitespace, corners, title bar height and progress, font-weight-shift() moves every vector-face role by one signed amount, and text-modifiers() sets case and tracking per text role. Size and line height are deliberately not theme axes, since those belong to the device and to the reader's own text scale. Tracking takes whole positive pixels, because the bitmap bundles are drawn on the pixel grid and a fractional advance puts their glyphs off it. Every slot is listed on the Theme Slots page.

Position utilities

relative and absolute put one element over another instead of beside it, so a full-bleed panel can take the whole layout and carry a compact card in one corner of it. The offsets inset--{size}, top--{size}, right--{size}, bottom--{size} and left--{size} run on the spacing scale, and z--0 to z--3 decides which of two overlapping elements draws on top. All of them take the size and orientation variants the rest of the box model takes. An element out of flow sits outside the terminalize budgets and carries its own size and clamps, a rule the new Position page states along with the fill and the edge an overlay needs to stay readable on 1-bit.

Outline for floating cards

outline now rounds the element it draws on to the same 8px curve its dots trace, so a card with a background of its own ends on that curve rather than squaring off around the outline, and the dotted and the solid treatment round alike. outline--muted draws the edge in a mid gray that still prints on a 1-bit screen, for a card that should sit quietly over the content behind it.

Fixes and Improvements

  • Theme hues stay off limited palettes. The stroke chain was the one paint chain on a limited palette that never went through the panel's accent table, so a theme bound to violet drew a violet outline on a panel whose inks are black, white, red and yellow. Border lines, dividers, border-token line art, title-bar text and text-stroke--* all read that chain, and the preview rails mapped it to the original hue in the same way.
  • Plugin markup can repaint the stroke contrast again. --framework-stroke-contrast became unreachable in 3.2.0 once the semantic channel led the chain, so a recipe that repainted it on an inverted chip stroked white on white and lost the glyph. Subtree overrides reach it again.
  • Item text follows an ancestor's text utility. An item's content named the primary text channel, so a text--* utility on an ancestor stopped reaching bare text inside a card. It now falls through to inherit when the item ink slot is unset.
  • Themed title-bar instances render on device. The 2-bit and 4-bit rails read their own secondary tone for the bar's instance without consulting the bar ink, so a theme stating a white instance came out near-black on the device.
  • Map marks keep their ink. Dashed lines read past a tile SVG's missing fill attribute and took its white under-color, so railways, ferries, paths and boundaries drew white on white on 1-bit and 2-bit renders, and dark mode failed the mirror image of it. Only the processed-bundle test variant sees this: the live build keeps the attribute, which is why the suite stayed green while devices were not.

Upgrade Notes

Framework 3.3 is backward compatible with 3.0, 3.1 and 3.2: documented classes and attributes keep their meaning, and every 3.3 feature is opt-in.

  • MapLibre GL JS is a plugin dependency, not a framework one. A map plugin loads https://trmnl.com/js/maplibre-gl/5.24.0/maplibre-gl.js and its stylesheet with classic tags, the way chart plugins load Highcharts. The framework vendors the same build and serves it next to the runtime for the docs. 5.24.0 is the last release with a UMD build; moving to 6.x is a separate decision.
  • Map tiles ride the free tier by default. The public default and the 'trmnl' preset's default upstream are OpenStreetMap's community endpoint, whose usage policy allows light use and forbids a fleet. A map plugin that goes out to many devices brings its own source and key, or a host points Framework.tile_source_url (or TRMNL_FRAMEWORK_TILE_SOURCE_URL) at its own Shortbread tile source and injects a source per plugin instance.
  • Screenshot renderers rescale after the pass. A renderer that lays the page out at 1x and applies the capture pixel ratio afterwards calls TRMNLMaps.refresh() and awaits it before it freezes timers and captures, so the canvas is built for the pixels the capture has.

Documentation Changes

  • Added the Map page (Streets, Map styles, Markers and routes, Grayscale and color, Strava activity) under Components and the Painting Maps page under Paint, with the readiness and refresh contract on the Framework Runtime page.
  • Added the Position page under Arrangement, with the classes, the offset scale, the stacking levels, an overlay demo, and what an element out of flow has to handle for itself. The Map page gains a Floating card pattern, carrying the fill, edge and shadow a card over a map needs.
  • Documented the extended theming contract on the Theme Slots and Theme Authoring pages, including the item card slots, the spacing slots and the full set of text roles.
  • The current docs track is 3.3; 3.2 URLs redirect to their 3.3 twins. The 3.0 and 3.1 tracks stay frozen.
  • Documented the tile endpoint and its configuration in the engine integration guide, the Open Source page and the README, and the map go-live checklist in docs/MAPS_GO_LIVE.md.