Paint API

TRMNLPaint is the framework's public JavaScript paint API. It reads the live CSS cascade (bit depth, dark mode, theme, limited palette, and tiles all resolved) and hands back a canonical Fill, so token mappings are never duplicated in JavaScript. Charts are just one consumer; any plugin can resolve framework colors from JS for any purpose.

#

How it works

The framework paints every surface through per-mode CSS custom properties on the .screen element, and a theme is CSS that re-points those properties. TRMNLPaint is a reader of that live cascade, never a second definition of the color system.

Each resolver appends a hidden probe element inside the target element, applies the framework's own utility class (bg--<token>, text--<token>, ...), and reads back the browser-resolved computed style. Bit depth, dark mode, inverse subtrees that flip the screen scheme, themes, slot overrides, and limited palettes are all honoured automatically, with zero token mappings duplicated in JavaScript.

Every function is total: a missing .screen or an unknown token returns a Fill with null fields instead of throwing. Plugins render on a screenshot service, and a thrown error would mean a blank device screen.

#

The paint domains

The resolvers are documented by what they paint. Every resolver takes an optional { el }: the id or element whose local cascade supplies the paint, with device settings from its nearest .screen. Omit it on a single-screen plugin and the first screen on the page is used.

Painting Borders

Border and divider rails as BorderFills, for custom rails and Highcharts axes.

  • border(), divider()
  • applyBorder()
  • toHighchartsAxis(), applyHighchartsAxisPaint()
#

Scale values

Resolve scale-aware JavaScript dimensions through TRMNLPaint. It reads the same custom properties as the CSS framework, so device density, Scale, and Text Scale stay in one source of truth.

  • scale({ el }): returns { name, device, modifier, ui, content, textName, textModifier, textUi } for the target screen.
  • px(value, { el, kind }): scales one number or an array of numbers. It uses content scale by default; pass kind: "ui" for framework geometry or kind: "text" for framework typography.

Use px() for numeric chart options, canvas dimensions, and other library configuration. Percentages, data values, zero-width rails, and physical one-pixel strokes should remain unchanged.

var scale = TRMNLPaint.scale({ el: "my-chart" });
var height = TRMNLPaint.px(260, { el: "my-chart" });
var spacing = TRMNLPaint.px([10, 10, 5, 10], { el: "my-chart" });

// Framework-owned component geometry also includes device density.
var componentInset = TRMNLPaint.px(6, { el: "my-chart", kind: "ui" });

// Framework typography includes device density, Scale, and Text Scale.
var fontSize = TRMNLPaint.px(16, { el: "my-chart", kind: "text" });
#

The Fill type

Every resolver returns a canonical Fill, a plain object describing paint independently of any charting library. A Fill with a url and size is a dither tile pattern; a Fill with only a color is a solid. Library-specific shaping lives in adapters, never in the Fill.

Fill = {
  color: string | null,  // resolved "rgb(...)" flat / field color
  image: string | null,  // full resolved background-image; null when "none"
  url:   string | null,  // first url(...) from image; null for gradients / solids
  size:  number | null,  // tile size in px (falls back to --dither-bg-size)
}

// url && size  => a dither tile pattern
// color only   => a solid
#

Painting and reactivity

  • apply(node, fill): paints a node's background from a Fill, compositing the field color under the tile image with the same two-layer CSS the rest of the screen uses.
  • watch(el, onChange, { immediate }): runs onChange now (unless immediate: false) and again whenever the mode, dark or theme classes change on the screen or on a wrapper above it. Returns a stop() function.
  • screen(el): the nearest .screen for a target element, or the first screen on the page.

applyBorder() and applyType() write border and typography longhands the same way; they are documented with their resolvers on Painting Borders and Painting Typography .

#

Extending a theme from JavaScript

Themes never ship JavaScript. A theme that needs to expose extra values to plugin code (a brand accent, a custom ramp) publishes additional public --* custom properties on the screen, and JS reads them back with cssVar(). No framework release and no JS bundle per theme: just CSS that both worlds read.

cssVar(name, { el }) returns the trimmed computed value of any public custom property on the screen. It is the documented escape hatch for exactly these theme-published values.

Read public var families only. The CSS minifier renames private variables (--_*, --framework-internal-*, --tile-*, --bline-*, most of --border-* and --tn-*) in the released plugins.min.css; the readable plugins.css keeps the source names. Never read those families from JS. cssVar("--border-step-40-h-color") answers in the dev build and returns an empty string against every release. See CSS Variables for the families that survive.

// A theme publishes a public var on .screen--theme-my-brand:
//   --my-brand-accent: var(--red-50);
// Read it back from JS, resolved for the active mode/theme:
var accent = TRMNLPaint.cssVar("--my-brand-accent", { el: "my-chart" });