All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
Unreleased
0.1.2 - 2026-07-24
Added
- Page layout. Documents can now carry a page setup — paper size
(
:letter,:legal,:a4,:a5,:tabloid,:executiveor a custom%{width:, height:}), orientation, margins, running headers and footers, and page numbering — on thedocnode'sattrs.page.Tiptapex.Pagereads, merges, validates and stores it; lengths are millimetres and accept CSS units ("1in","2cm","72pt","96px"). - Live pagination in the editor. When a document has a page setup, the
editor renders a stack of measured sheets instead of a continuous surface,
with the running header and footer drawn in every page's margins. Nothing
is written to the document — the breaks are ProseMirror decorations, so
undo/redo, collaboration and the persisted JSON are untouched. New JS
exports:
Pagination,computeBreaks,paginationKey. - Headers and footers with three slots each (
left,center,right) and the tokens{page},{pages},{date},{time},{title}. Page numbering (numbering: %{enabled:, region:, align:, format:}) drops the number into the slot you name, so left/centre/right numbering is a one-liner. - Logos in headers and footers. A slot is either a string (text) or
%{text: ..., image: %{src:, height:, alt:}}, with:heightin millimetres. The page dialog gives every slot a logo field with an upload button that posts to the editor's ownupload_url, so logos run through the host'sTiptapex.Uploadhandler and its validation.srcis held to the same allow-list as document URLs, plusdata:URIs for images — which is what Chrome needs, since it resolves no relative URLs in a running header. New:Tiptapex.Page.images?/1,Tiptapex.Export.PDF.running_html/3andTiptapex.Export.PDF.with_pdf_generator/3(wkhtmltopdf cannot draw an image from--header-left, so a region with a logo needs--header-html; this writes those files and cleans them up). The JS upload contract is now also exposed on its own asuploadFile/2. - Formatting in header/footer text. Slots accept a small allow-listed
HTML subset (
<b>,<span style="…">,<h1>,<a>,<img>, …) via the newTiptapex.Renderer.Markup, which tokenises the string, validates every tag and attribute against a closed list and rebuilds the output — nothing from the field is emitted verbatim, so an unknown tag or anonclickbecomes visible text rather than markup.stylereuses the document CSS grammar and URLs the document allow-lists; the editor renders the same subset withcreateElement, neverinnerHTML. New:Tiptapex.Renderer.URL.safe_image_url/1(shared by logos and markup) and the JSrenderMarkup/hasMarkup/safeUrl/safeImageUrl/safeCssValueexports. - Forced page breaks — a new
pageBreaknode, the toolbar's page-break button,Cmd/Ctrl+Shift+Enter, andeditor.commands.setPageBreak(). The server renderer emits<div data-page-break>with the CSS break inline. Tiptapex.Export.PDF— print-ready HTML plus ready-made options for the two common engines, with no new dependency:chromic_pdf/2returns{source, opts}forChromicPDF.print_to_pdf/2,pdf_generator/2returns{html, opts}forPdfGenerator.generate/2, andto_html/2covers everything else. Header/footer slots are translated to each engine's native running elements (<span class="pageNumber">/[page]), so{pages}and per-page numbering are always correct. The exported HTML inlines the package stylesheet and mirrors the paged editor's margin model, so editor and PDF break in the same places.- New toolbar group
page(in the default set): a page-setup dialog covering whether the document is paginated at all, size, orientation, margins, header/footer slots and logos, numbering and the document title, plus the page-break button. Page layout is turned on and off by the dialog's first checkbox — Paginate this document — so the control is always visible and the state is always reversible. The dialog's strings go through the existinglabelsmap. pageattribute ontiptapex_editor/1andtiptapex_viewer/1—niluses the document's own setup,falseforces a continuous editor, and a map/trueoverrides the document. The viewer paginates a document with a page setup when hydrated, and gives the server-rendered fallback the paper's width and margins.Tiptapex.Components.set_page/3— push a page setup into a mounted editor without touching its content. This is also how you keep collaborators in step: Yjs syncs the document's content, not thedocnode's attributes.{pages}is now available incount_template.mix js.test— pure-logic JS checks (page normalisation, which must agree withTiptapex.Page, and the pagination maths), wired into CI.
Fixed
- The exported PDF now has the margins it was told to have. Chrome lets a
CSS
@page { margin }override theprintToPDFmargin parameters rather than adding to them, so emittingmargin: 0on the ChromicPDF path printed the body edge to edge while the running header still sat inside the margin box. Both now carry the same values. - The print stylesheet now outranks the packaged one.
.ttx-prose > *loses to.ttx-prose h1on specificity, so heading top margins survived into the PDF and it paginated out of step with the editor. The print rules are scoped under.ttx-printand mirror the paged editor's heading padding. - wkhtmltopdf output is no longer ~25% too small. It lays out at 75 DPI
and smart-shrinks the result;
pdf_generator/2now passes--dpi 96 --disable-smart-shrinking, which brings its page count in line with Chrome's and with the editor. - The wkhtmltopdf running header is no longer indented twice. It already
lays the header HTML out inside the page margins, so
running_html/3repeating them as padding pushed the logo and date inwards. - Exported HTML sets a sans-serif stack. Neither
.ttx-prosenor.ttx-contentdeclares a font family — in an app the host stylesheet supplies one, but a standalone PDF has no host and fell back to the engine's default serif. - The editable HTML source view no longer drops document-level attributes.
The source carries the content only, so the
docnode's attributes (the page setup) are now carried across explicitly instead of being lost on the round trip.
0.1.1 - 2026-07-24
Added
- Editable HTML source view in the editor: a new
htmltoolbar group (enabled by default,</>button) toggles between the WYSIWYG surface and a textarea showing the document's pretty-printed HTML. Edits are parsed back into the Tiptap document (debounced), so change events and hidden-input sync keep working while in source mode. In collaborative editors the parsed document is written through the shared Y.Doc so source edits propagate to peers. Disable withextensions={%{html_view: false}}. JS exports:attachHtmlView/3,formatHtml/1,htmlToJSON/2. - Optional CodeMirror 6 surface for the HTML source view: build the hook
with
makeEditorHook({ htmlEditor: CodeMirrorHtmlEditor })(newtiptapex/html-editorentry point, optionalcodemirror/@codemirror/*peers) to upgrade the source textarea to a real code editor with HTML/CSS syntax highlighting, line numbers, and autocompletion. Any object implementing the same surface contract can be plugged in instead.
0.1.0 - 2026-07-22
Added
Tiptapex.Components.tiptapex_editor/1andtiptapex_viewer/1HEEx components withdata-ttx-*-driven configuration, two sync modes (:push_eventand{:hidden_input, field}), per-id namespacedset-contentevents, and an:actionsfooter slot.- JS hooks (
TiptapexEditor,TiptapexViewer) shipped as raw ESM inside the Hex package with@tiptap/*as npm peer dependencies; configurable toolbar (group registry, i18n labels, custom buttons), floating table menu, resizable image/video extensions, font-size/line-height/ background-color/trailing-node/selection-preserve extensions, attachment drag & drop. Tiptapex.Renderer— server-side Tiptap JSON → safe HTML with full node/mark coverage, URL and CSS sanitization, YouTube-only iframes, extensible node/mark registries,to_plain_text/1andheadings/1.Tiptapex.Uploadbehaviour,Tiptapex.Upload.Controllermacro, andTiptapex.Upload.Validatorwith size limits and magic-byte content-type verification;Tiptapex.Upload.LocalDiskconvenience handler.Tiptapex.Collab.Channelmacro implementing the Yjs-over-Phoenix- Channels relay protocol, withauthorize/3and optionalload_state/2/persist_update/3hooks;CollabPluginJS entry point (tiptapex/collaboration) kept separate so yjs stays out of non-collab bundles.Tiptapex.Schema.DocumentEcto type (optionalectodependency).- Themeable stylesheet (
priv/static/tiptapex.css) scoped underttx-*classes with--ttx-*custom properties falling back to daisyUI tokens. - Single-file dev/demo server (
iex -S mix dev).