# Framework v3.3.0

Released on 2026-08-22.

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`.
