KQ-1753 · BULLETIN/COMPOSER

Bulletin Composer

An email layout editor: a palette of header, text, image, button and divider blocks feeds a one-column letter drawn in a device frame, and each block opens an edit panel for its content. Toggling Desktop and Phone changes the frame's width in one render and every block FLIPs to its new box on springs.glide — corners round into a phone bezel, pictures go full bleed, the call to action stretches and text slides to its new wrap without being squashed — while a ruler over the letter marks where the phone width will cut. Blocks and palette tiles drag 1:1: an accent drop line opens a gap between the blocks under it on glide and, on release, every block glides from where it is drawn to its new slot with the throw's velocity; the letter is a listbox where Up and Down move, Alt with Up or Down moves the block, Enter edits it and Delete removes it, and missing alt text is flagged on the picture, in its field and in a count that jumps to it.

KQ-1753 · BULLETIN/COMPOSER

October letter

Fernworks · 7 blocks

Up and Down move between blocks, Alt with Up or Down moves the block, Enter edits it, Delete removes it, Escape clears the selection.

desktop

desktop · 7 blocks · 1 to fix

Install

One command — the source lands in your repo. Or copy it from the Code tab.

$ pnpm dlx shadcn@latest add @kinetiq/bulletin-composer

Props

previewWidth
number
default 380
Width the phone preview narrows the letter to, in px; the ruler over the desktop letter marks it.
blockStyle
"seamless" | "framed" | "stacked"
default "seamless"
How blocks sit on the canvas: edge to edge like the sent letter, each in a labelled frame, or as separate cards with grips.
accent
string
default "var(--accent-bright)"
The letter's brand colour (header wash, button, sender mark) and the editor's selection, drop line and ruler. Fills are taken at a fixed lightness so they read in both themes.
blocks / defaultBlocks
BulletinBlock[]
default — / defaultBulletinBlocks
The letter top to bottom: header { kicker, title, align }, text { body }, image { art, alt, caption }, button { label, href, fullWidthOnPhone }, divider { style }.
onChange
(blocks) => void
default —
Fires with every block after an edit, insert, move or removal.
preview / defaultPreview
"desktop" | "phone"
default — / "desktop"
Which device the letter is previewed on.
onPreviewChange
(preview) => void
default —
Fires from the toggle or arrow key that changed the preview.
selected / defaultSelected
string | null
default — / null
The selected block's id, whose edit panel the rail shows.
onSelectedChange
(id) => void
default —
Fires when a click, key, drag or insert selects a block, or the selection clears.
title
string
default "October letter"
The editor's heading and the letter list's accessible name.
subject
string
default "The fern house reopens"
The subject line shown in the frame's inbox bar.
preheader
string
default "Misting at nine, new panes, Thursday spore prints."
The line an inbox shows after the subject.
sender
string
default "Fernworks"
Who the letter is from; also its avatar initial and the footer.
onSendTest
(address, blocks) => void | Promise
default —
Send test was pressed with a valid address. Return a promise to show it sending; a rejection says it failed and keeps the dialog open.
testAddress
string
default "ines@fernworks.example"
The address the test dialog starts with.
onSchedule
(at, blocks) => void | Promise
default —
A send slot was confirmed, in ms since the epoch. Return a promise to show it saving.
now
number | Date
default defaultBulletinNow
Today, for the three schedule slots and the phone's clock — never the wall clock, so server and browser agree.
status
"ready" | "loading" | "error"
default "ready"
Whether the letter has arrived: loading draws skeleton blocks, error offers Try again.
onRetry
() => void
default —
Try again was pressed after the letter failed to load.
height
number
default 540
The editor's height in px; the letter scrolls inside it.
disabled
boolean
default false
Read only: blocks can be browsed and previewed but not edited, moved, sent or scheduled.
className
string
default —
Classes for the editor's outer box.