Every release is listed here with the packages it changed — all packages share one version number.
OctNov
20 releases in 24 days
0.59.1
Latest
Fix@templatical/editor@templatical/types
Add PreviewSurface ("preview" | "test-email") and the required PreviewResolveContext.surface, so a resolver knows whether the editor's preview mode or the test-email dialog is calling. Read surface, not the presence of recipient, to tell them apart: the dialog leaves recipient out while its recipient field is empty. A resolver the editor calls needs no change; code that builds a PreviewResolveContext itself, such as a test that calls its own resolver, adds surface: "preview" or surface: "test-email".
Add PreviewResolveTrigger ("open" | "refresh" | "recipient" | "locale"), the required PreviewResolveContext.trigger, and the refreshPreview() instance method. A Refresh preview button is available in preview mode and the test-email dialog when resolvePreview is configured. A resolver the editor calls needs no change; code that builds a PreviewResolveContext itself, such as a test that calls its own resolver, adds trigger: "open". A failed re-resolve now shows the unresolved template instead of stale resolved content, opening the test-email dialog makes one resolver call with its seeded recipient, and a test-email recipient change always waits 500 ms, including before the first answer lands.
Feature@templatical/editor
Add header.actions for host-owned controls in the editor header, with darkModeToggle, previewToggle and viewportToggle opt-outs. The editor reports compact and resolved-theme state on the host element.
Fix@templatical/core@templatical/editor
Fix undo history after replacing an editor document. Undo after loading another template could write the previous template's content into it on save, and undo after setContent() could restore part of the old document. Loading a template, creating one with new content, or calling the public setContent() now clears undo and redo; creating a template from the current content keeps history. In collaborative editing, reloading the same template no longer sends the loaded document as an edit or drops presence and locks, and creating a template from the current content keeps undo and redo available in the new template.
Fix@templatical/import-beefree
Read the Menu, Table and Video modules in the shape BeeFree exports (IPluginModuleMenu, IPluginModuleTable and IPluginModuleVideo in BeeFree's bee.ts). The importer read keys BeeFree never writes, so a real menu or table imported with no content and a video lost its thumbnail and width.
custom-block render and custom-block validate pass their own liquidjs to renderToMjml, which no longer imports it. CLI output is unchanged.
Fix@templatical/import-beefree
Skip a BeeFree spacer whose height is an explicit 0px, reported as skipped, instead of importing it as a 24px spacer. A spacer with a missing, unreadable or negative height now imports at BeeFree's 60px default rather than 24px. In an Icons module, an explicit 0pxitemsSpacing no longer falls back to the legacy itemSpacing key.
Fix@templatical/import-beefree
Preserve social icon scheme prefixes (mailto:, tel:, sms:) and keep custom social icons as images with their original file and link during Beefree imports.
A locale operation (updateBlockLocale, updateSettingsLocale) writes only to the block's or settings' own l10n layer for that language. A layer inherited through the object's prototype is no longer reused and changed in place; the edit gets a fresh own layer instead.
cloneBlock serializes a block tree once and remints IDs throughout the copy.
Fix@templatical/ai-kit
The shipped eval set's domain-mismatch case checks link text with >[^<>]*(?:www\.|://) instead of >(?:[^<]*)(?:www\.|://). It matches the same text, and checking a turn whose output holds a long run of > no longer takes quadratic time.
Fix@templatical/import-beefree
BeeFree multi-column rows now convert outer column edges and matching top or bottom sides into section borders; painted column backgrounds, inner borders, and overlapping row borders still warn.
Custom-block Liquid renders now have budgets: createCustomBlockLiquid sets memoryLimit to CUSTOM_BLOCK_LIQUID_MEMORY_LIMIT (1e7) and renderLimit to CUSTOM_BLOCK_LIQUID_RENDER_LIMIT (2000 ms), both exported from @templatical/types, so a template such as a loop over (1..1000000000) fails with a Liquid limit error in the editor, renderToMjml and custom-block validate instead of exhausting memory. Data-source preview recipes render with the same limits.
Fix@templatical/import-beefree
Preserve explicit zero and shorthand padding on BeeFree buttons, with longhands taking precedence. Let an entirely bold label override any button style weight; import missing or unreadable weights as regular otherwise, and note lost partial bold formatting.
resolveLocale copies each block once, so resolving a deeply nested template takes time in proportion to its size. Masking merge tags with a custom syntax runs each regex once per match instead of again on every tag.
link.localhost-or-staging matches links.nonProductionHosts globs in linear time, so a very long URL host no longer stalls the linter, and compatibility rules group CSS declarations and clients without copying arrays. The Easy Email Pro importer decodes HTML entities by looking at most six characters ahead of each &.
The Unlayer, BeeFree and Topol importers check for a repeated warning in constant time, so a design with thousands of languages no longer slows the import quadratically. The Chamaileon importer looks up variable references in a table built once per document. The renderer escapes control characters in a block's type and id before it logs the missing-renderer warning. templatical live answers 400 to a request target it can't parse, where it used to exit.
Fix@templatical/core
Localized operations preserve other language layers when an existing block moves into a newly added wrapper. Mixed section and wrapper parent objects resolve to the section column, matching the operation engine.
The canvas draws a button at the same height as the export (#1144). The renderer now writes line-height="120%" explicitly; compiled HTML is unchanged.
Fix@templatical/ai-kit
translateFields sends the model short field aliases
Fix@templatical/editor
Fix the header switching to its compact layout while the save status shows. "Unsaved" and "Saved" appear on every edit and save; in an editor just wide enough for the full header, their words compacted the whole header until the status cleared, about 5 seconds with templates.autoSave. The badge keeps a fixed slot (its icon) in the header's fit measurement and shows its words only where the full header has room for them. Without room it shows the icon alone, a different one per state (dot, check, alert), and keeps the words as its tooltip and for screen readers. In the compact header the badge is always the icon alone.
Fix@templatical/ai-kit
An editing session checks locale layers in time proportional to the template size. Each locale is resolved only for the blocks that carry its layer, so a template with thousands of locales no longer makes every batch resolve the whole template once per locale.
Fix@templatical/core@templatical/editor
Preview surfaces are read-only: the canvas in preview mode and the test-email and saved-block previews no longer start inline edits or write table cells, so resolved preview content can't overwrite the template.
convertTopolTemplate() accepts designs whose sections sit under mj-body and converts them like mj-container designs, including the container's background-color. When a design has neither, the empty-design warning names both tags.
displayConditions.onRequest lets a host open its own condition builder. The callback receives a copy of the block and, when one is set, the current condition including opaque data. null cancels. conditions is optional. With onRequest set, conditions and allowCustom are ignored. DisplayCondition.data round-trips through the template.
Feature@templatical/editor@templatical/types
Spread syntax presets with matching value and logic patterns use the built-in linear merge-tag scanner across localization and editor parsing. Different pattern sources or flags other than g and y keep custom regex matching. @templatical/types exports builtInPresetOf(syntax), which returns the built-in preset name whose patterns a syntax uses.
A section wrapper or a layout wrapper can span the recipient's window. Set fullWidth: true on section.wrapper or on a layout wrapper block and the renderer emits MJML's full-width="full-width" on that mj-wrapper: the band's background color and background image run edge to edge, and the section or the wrapper's children stay at settings.width. Omit fullWidth, or set it false, and the band stays the email width, exactly as before. A corner radius is not rendered while full width is on; the stored radius remains and comes back when full width is off.
A real-design audit of the BeeFree importer found it converts every design but loses fidelity and, in one case, breaks Twig at send time. This release closes the gaps it found.
Built-in merge-tag scanning, merge-tag span handling, and CSS rule parsing complete in linear time on malformed template input. Pixel value parsers reject long invalid whitespace suffixes without super-linear backtracking.
SocialPlatform includes xing. Social icon blocks can use a XING icon, drawn from the same glyph as the other platforms, in the editor and in every rendered style and tone.
Fix@templatical/quality
Lint auto-fixes now rewrite single-quoted attributes without adding attributes. Double-quoted values are copied as written, including entities, and a rewritten anchor tag is lowercase. Link and alignment fixes skip markup inside comments, link fixes use the same attribute decoding and HTML whitespace rules as detection, and alignment fixes split declarations on decoded CSS characters, so ; separates declarations, and keep the character references in the declarations they keep.
Fix@templatical/import-beefree
A 7/12 or 5/12 BeeFree column row imports as an even two-column section. When a snapped column is narrower than its grid share, the shortfall comes back from the padding that faces the next column, so a one-line footer stays one line.
Template values in MJML attributes are escaped before rendering. Body background colours containing markup use a transparent fallback so the compiled HTML does not carry that markup. CSS-valued attributes (background colours, border radius, background size, position and repeat) also drop ;, {, } and line breaks, so a value stays one CSS declaration.
Title blocks now unwrap paragraphs with quoted attributes correctly in the editor and rendered email. The editor sanitizes title HTML after unwrapping to close a script-injection path in the canvas. findOpenTagEnd now treats a quote inside an unquoted attribute value as an ordinary character, as HTML does.
Fix@templatical/editor
In preview mode the canvas hides lint issue badges, like the rest of the block chrome.
convertBeeFreeTemplate() imports images at the width BeeFree shows them, keeps spacing dividers invisible, and converts the Icons module, addons and empty modules.
The Unlayer and HTML importers import a divider as the line it draws, so one that draws nothing no longer becomes a black line.
Fix@templatical/renderer
Compiling renderToMjml() output with mjml no longer throws TypeError: Cannot read properties of undefined (reading 'indexOf') when a paragraph or title holds a character reference in a style attribute.
The editor sanitizes live rich-text HTML, custom-block canvas HTML, and inline SVG icons with DOMPurify. Custom-block export HTML is unchanged. URL attribute checks use a shared scheme policy from @templatical/types; the link dialog and @templatical/quality use the same scheme parsing. Unsupported DOM environments display escaped text.
Add default-on compatibility lint for authored pass-through HTML and CSS, using a pinned caniemail snapshot, plus an explicit compiled-HTML Gmail size check. Agents and headless callers receive a concrete suggestion and reference on every issue, with affected clients and an excerpt when available. The editor shows suggestions and source links, and offers an on-demand Check size action when its render provider can compile without saving. templatical validate --html reports htmlBytes and a clipping warning.
Feature@templatical/editor
@templatical/quality is an optional dependency of the editor and installs by default. Leaving it out is supported: the editor works without it and hides the Issues tab.
Feature@templatical/quality
lintTemplate now runs the deliverability category by default. Agents can use lintDeliverability, DELIVERABILITY_RULES, localized messages, DEFAULT_SHORTENER_HOSTS, and DEFAULT_DELIVERABILITY_THRESHOLDS directly. The category flags deliverability.link-text-domain-mismatch (warning), deliverability.url-shortener (warning), deliverability.ip-address-url (warning), and deliverability.low-text (info). Each rule reports a JSON-detectable content cue; none auto-fixes or scores a template.
Feature@templatical/editor@templatical/types
Custom merge-tag syntaxes get autocomplete through opener and closer
Feature@templatical/editor
init({ ai }): bring your own AI backend. Each feature is labelled as AI, in every editor locale:
Feature@templatical/types
AI as a bring-your-own contract (BYO contract):
Feature@templatical/ai-kit
New package @templatical/ai-kit, MIT and with no LLM SDK dependency: the editor's AI over your own backend.
Feature@templatical/core
The collaboration controller applies an AI turn's operations as this session's batches, marked origin: 'ai', one undo step per turn (applyOperations). invertOperations now comes from @templatical/types; core re-exports it.
Size an image by its height with the aspect ratio locked. When the image file's size is known, the image settings show a link toggle on the Height row, on by default: typing a height sets the width from the file's ratio, and a height that would make the image wider than its column sets it to Full width, with a note of the height it lands at. Under the lock the stored height stays unset, because a stored height squashes an image on any phone narrower than its width (MJML shrinks the width and keeps the height).
Titles stay bold while you edit them. A title showed at weight 700 on the canvas and in the email, but its edit surface showed it at 400, so the text turned lighter, and could rewrap, the moment editing started. The canvas, the edit surface and the exported heading now all use 700, which @templatical/types exports as TITLE_FONT_WEIGHT. The renderer writes it onto the heading as font-weight:700, so emails look as before in clients that already bold headings, and a page that resets heading styles (Tailwind's preflight, for one) no longer shows the email's titles at regular weight. With shadowDom: false on such a page, the canvas title was regular too; it is now bold.
An image set narrower than its column keeps its width on phones (#861). The renderer set fluid-on-mobile on every mj-image through <mj-attributes>, so a 120px logo grew to the whole column on a phone while the editor's mobile preview showed it at 120px, and an image with a set height was stretched out of shape (120×40 became 355×40 on a 375px phone). The attribute is now set per image, only on one that fills its column on desktop: width: "full", or a pixel width at or above the room MJML gives it, which is the column less the side padding and border of the wrapper band, the section and the image. Images wider than the phone still shrink to fit, and video thumbnails follow the same rule.
Fix@templatical/media-library
The standalone media library (init() and MediaLibraryModal) now renders as designed on host pages without a CSS reset. A reset such as Tailwind's preflight hid all of this. Inside the editor, the media library already rendered correctly.
Imported images come in at the width the source email shows them, and every importer decides width: "full" by one rule. A missing width, 100%, or a px width that reaches the image's room (its column less its own side padding, and border where the importer reads one) is "full"; a narrower px width stays px; a percentage under 100% becomes px of the room and is reported approximated, since a px width keeps its size on a phone.