Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,83 @@ called out explicitly under **Changed** or **Removed**.
Generating a template from a spec file is still not wired; see `kizen docs
show email-templates`.

- **`kizen messages templates create --spec-file <f>` builds a complete email
template — `craft_json` and the compiled, Outlook-safe `content` HTML —
from one declarative spec.** `update <tmpl> --spec-file <f>` rewrites an
existing template the same way, as an alternative to its existing
field-level `--craft-json-file`/`--content-file` PATCH path. Both fields
come from one pass over one node tree, so a spec can never describe one
without the other — no flag and no spec key accepts a raw `craft_json` or
`content` value.

A spec's rows pick one of 4 column layouts by name (`1 Column`, `2
Columns`, `2 Columns (1/3 and 2/3)`, `2 Columns (2/3 and 1/3)`) and cells
hold `text`/`image`/`button`/`divider` blocks — both closed sets, so an
unsupported layout or block kind is a clear error, never a silent partial
template. An `image` block names a local PNG/JPEG file; it's uploaded
(`source="public_image"`, publicly readable so recipients can load it) and
its real pixel dimensions are read from the file's own header bytes — no
new dependency. `--dry-run` resolves images offline instead of uploading,
so it never writes. `messages templates craft-config` previews the
`{craft_json, content}` pair offline, with `--out-html` to drop the
compiled body somewhere a browser (or Outlook) can open it.

A real test send opened in Outlook is still the only way to confirm actual
rendering — nothing offline can substitute for that.

- **Email template specs can now set the layout knobs a designed newsletter
needs — `Section`/`Row` width and padding, `Divider` thickness, `Button`
corner radius/padding/alignment — instead of every template landing at
this emitter's fixed defaults.** `sections[].max_width`/`container_width`/
`padding` and `sections[].rows[].width`/`container_width`/`padding` set
`Section`/`Row` props directly; `padding` is four independent
`{top, right, bottom, left}` strings, matching the wire format's four
independent `containerPadding*` keys rather than a lossy CSS-style
shorthand. `button` blocks gain `border_radius`/`padding_left`/
`padding_right`/`alignment`; `divider` blocks gain `size`. Every new field
defaults to this emitter's exact pre-existing hardcoded value, so a spec
that sets none of them produces the same output as before this change.
The compiled `content` HTML's row widths now track these same values too
(previously frozen at a hardcoded 880px regardless of what the spec set —
a real `craft_json`/`content` divergence, the exact failure this whole
surface exists to prevent), `content` now carries `Section`/`Row`
padding at all (previously absent entirely, on every template — text
always rendered flush against the canvas edge regardless of what
`craft_json` said), `content`'s `Button` markup now carries `align`
(previously every button rendered left-aligned regardless of the spec's
`alignment`), and a centered `Image` (`position: "center"`, the only
value this surface sets) now actually renders centered in `content`
instead of flush left.

### Fixed

- **Compiled email `content` no longer diverges from what Kizen's own
builder produces for the same layout.** Every recipient's email now
carries a real `font-family` for body text (`Root.props.fontFamily`,
via the same `kizen-text-styles` wrapper class/`<style>` rules Kizen's
own compiler uses) — previously `content` carried no `font-family` at
all, so any template without hand-inlined font styles rendered in the
client's serif fallback on every send. Also fixed: the missing
`.moz-text-html` rule (Gecko-based clients like Thunderbird key
column-stacking behaviour off it), the missing MJML reset block, a
hardcoded `480px` mobile breakpoint that now reads
`Root.props.mobileBreak`, a dropped `<body>` background colour, and
`Section.container_width` now reaching `content` via an outer
background-table wrapper (previously `craft_json`-only, deferred from
the layout-knobs change above). `Image` blocks gain a genuine
full-bleed auto-sizing mode — omitting `width` in an `image` spec block
now sizes the image to its parent Section's `containerWidth` (capped at
its own natural width by a per-image CSS rule) instead of silently
defaulting to a fixed 150px — and their compiled markup now matches
Kizen's own attribute/style set exactly (`data-natural-width`/
`data-natural-height`, confirmed unused anywhere in this repo, are
gone). Finally, a float-formatting artifact that printed
`880.0px`-style widths in the compiled CSS (or any other row whose
computed width happened to land on a whole number) now prints `880px`.
Every fix was checked against Kizen's real compiled `content` for the
same layout, not inferred from `craft_json` alone — see `kizen docs
show email-templates`.

- **`kizen upgrade --check` can now find a release tag from a `uv tool
install`/`pipx`/direct-VCS install, not just an editable checkout.**
Previously any non-checkout install shape skipped straight to the
Expand Down
88 changes: 78 additions & 10 deletions scripts/cli-tree-baseline.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2264,11 +2264,17 @@
│ --help Show this message and exit. │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
╭─ Commands ───────────────────────────────────────────────────────────────────────────────────────╮
│ list List email templates (pass one's name or UUID as `--template`). │
│ get Show one email template, including how well its two content fields agree. │
│ clone Copy an email template, both content fields included. │
│ update PATCH one email template's fields. │
│ delete Delete an email template. │
│ list List email templates (pass one's name or UUID as `--template`). │
│ get Show one email template, including how well its two content fields agree. │
│ create Create an email template from a spec file — `craft_json` and `content` │
│ built together from one pass over the spec's `sections`, so the two can │
│ never be authored out of sync. │
│ clone Copy an email template, both content fields included. │
│ update PATCH one email template's fields. │
│ delete Delete an email template. │
│ craft-config Preview the `{craft_json, content}` pair a spec would produce — │
│ offline, no live calls, runs the exact emitter `create`/`update │
│ --spec-file` use. │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯

Usage: messages templates clone [OPTIONS] {source}
Expand All @@ -2293,6 +2299,54 @@
Wire format (the two coupled content fields): see `kizen docs show email-templates`


Usage: messages templates craft-config [OPTIONS]

Preview the `{craft_json, content}` pair a spec would produce — offline, no live calls, runs the
exact emitter `create`/`update --spec-file` use.

Unlike `dashboards dashlet-config`, this command's output is **not**
meant to be pasted into a create/update spec: any Image block is
resolved without uploading (no live calls here), so its `fileId`/`src`
are obvious placeholder tokens, not real ones. Use `--out-html` to drop
the compiled body somewhere a browser (or Outlook) can open it.

╭─ Options ────────────────────────────────────────────────────────────────────────────────────────╮
│ --spec-file <str> Path to a JSON email-template spec. Omit to list the available block │
│ kinds and column layouts instead. │
│ --out-html <str> Write the compiled `content` HTML to this file. │
│ --help Show this message and exit. │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯

Spec shape: see `kizen docs show email-templates`


Usage: messages templates create [OPTIONS]

Create an email template from a spec file — `craft_json` and `content` built together from one
pass over the spec's `sections`, so the two can never be authored out of sync.

No flag and no spec key accepts a raw `craft_json` or `content` value —
that hand-author-both-and-hope-they-agree foot-gun is exactly what this
command exists to close. `sender_type` is always `"business"`, the only
value ever observed live. Any Image block's local file is uploaded for
real before the plan is built (there's no way to know its real
`fileId`/`src`/`naturalWidth`/`naturalHeight` otherwise) — except under
`--dry-run`, which resolves images offline instead (placeholder
`fileId`/`src`, real dimensions read from the file's own header bytes)
so a dry run never writes.

╭─ Options ────────────────────────────────────────────────────────────────────────────────────────╮
│ --spec-file <str> Path to a JSON email-template spec. Default: read from stdin. │
│ --dry-run Show the plan without applying. │
│ --yes -y Skip the y/N confirmation prompt. │
│ --json Emit JSON (plan with --dry-run, results otherwise). │
│ --help Show this message and exit. │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯

Spec shape: see `kizen docs show email-templates`. Generate one with `messages templates
craft-config`.


Usage: messages templates delete [OPTIONS] {template}

Delete an email template.
Expand Down Expand Up @@ -2345,11 +2399,15 @@

PATCH one email template's fields.

The server stores both content fields verbatim and compiles neither
from the other (confirmed live 2026-08-25), so sending `craft_json`
without `content` — or the reverse — leaves the builder showing one
email while recipients receive another. Pass both together, and check
the result with `messages templates get`.
Two ways to update: `--spec-file` rebuilds `craft_json`/`content` from a
spec, both fields together, the same way `create` does. The raw path
(`--craft-json-file`/`--content-file`/`--name`/`--subject`) PATCHes
fields verbatim instead — the server stores both content fields
independently and compiles neither from the other (confirmed live
2026-08-25), so sending `craft_json` without `content` on the raw path —
or the reverse — leaves the builder showing one email while recipients
receive another. Check the result either way with `messages templates
get`.

╭─ Arguments ──────────────────────────────────────────────────────────────────────────────────────╮
│ * template <str> Template name or UUID. [required] │
Expand All @@ -2359,6 +2417,10 @@
│ --subject <str> Set the subject line. │
│ --craft-json-file <str> Path to a JSON file to send as `craft_json`. │
│ --content-file <str> Path to an HTML file to send as `content`. │
│ --spec-file <str> Path to a JSON email-template spec — rebuilds │
│ `craft_json`/`content` together the same way `create` does. │
│ Mutually exclusive with │
│ --name/--subject/--craft-json-file/--content-file. │
│ --dry-run Show the plan without applying. │
│ --yes -y Skip the y/N confirmation prompt. │
│ --json Emit JSON (plan with --dry-run, results otherwise). │
Expand Down Expand Up @@ -4369,6 +4431,12 @@
===== messages templates clone =====


===== messages templates craft-config =====


===== messages templates create =====


===== messages templates delete =====


Expand Down
Loading
Loading