@yaebal/preview

render a telegram-style chat from plain objects to an svg string

experimental / wip. the api and rendering may change without notice. not ready for production — pin a version if you depend on the output.

install

terminal
pnpm add @yaebal/preview

usage

renderChat(messages, options) returns an svg string — rich text, every common media type, the lot. zero runtime, no <foreignObject> (so it rasterizes and survives github's svg sanitizer). drop the result into docs, a readme, or a landing page.

preview.ts
import { renderChat } from "@yaebal/preview";
import { md } from "@yaebal/fmt"; // optional — produces { text, entities }
import { writeFile } from "node:fs/promises";

const svg = renderChat(
  [
    { from: "user", text: "/start", time: "23:33", status: "read" },
    { from: "bot", name: "yaebal", ...md`Hello, **unknown** person`, time: "23:33" },
    { from: "bot", name: "yaebal", photo: [], src: "cat.jpg", caption: "a cat" },
    { from: "bot", name: "yaebal", voice: { duration: 7 } },
    { from: "bot", name: "yaebal", buttons: [["Useless button"]] },
  ],
  { theme: "light", width: 400 },
);

await writeFile("chat.svg", svg); // it's just a string

messages

each entry is a ChatMessage. from is the only required field: "user" renders outgoing (right-aligned, with ticks), "bot" renders incoming (left-aligned, with an avatar).

fieldtypedescription
fromSide ("user" | "bot")required. outgoing vs incoming
namestringsender label (incoming); also drives the avatar initial + colour
timestringtimestamp shown in the message meta
statusTickStatus ("sent" | "delivered" | "read")outgoing read receipt (ticks). ignored for incoming
buttonsstring[][]keyboard rows rendered as buttons under the message
textstringmessage text. wrapped automatically
entitiesMessageEntity[]entities for text (bold/italic/code/link/spoiler/…). spread @yaebal/fmt's md/html to get these for free
captionstringcaption for a media message
captionEntitiesMessageEntity[]entities for caption
srcstringreal image/thumb url or data-uri for the picture-like media (a file_id can't render)
spoilerbooleancover the media with a spoiler
photoPhotoSize[]image (or placeholder) + optional caption
stickerStickerstandalone image, or its emoji big
animationAnimationimage + GIF badge
videoVideoimage + play button + duration
voiceVoicewaveform + duration
audioAudioplay disc + title / performer
documentDocumentfile icon + name + size
venueVenuemap tile + pin + title/address
locationLocationmap tile + pin
contactContactavatar + name + phone
pollPollquestion + options with percentage bars

media

all media fields use the real @yaebal/types shapes (the array/objects you'd get off an Update). add spoiler: true to cover picture media.

media.ts
// real @yaebal/types shapes — hand it a ctx.message almost verbatim.
// for picture-like media add `src` (URL/data-URI) to show real pixels;
// a file_id has none, so without `src` you get a clean placeholder.
renderChat([
  { from: "bot", name: "yaebal", photo: [], src: "cat.jpg", spoiler: true },
  { from: "bot", name: "yaebal", video: { width: 640, height: 360, duration: 42 } },
  { from: "bot", name: "yaebal", sticker: { emoji: "🎈" } },
  { from: "bot", name: "yaebal", document: { file_name: "report.pdf", file_size: 81920 } },
  { from: "bot", name: "yaebal", contact: { first_name: "Ann", phone_number: "+1 555" } },
  {
    from: "bot",
    name: "yaebal",
    poll: {
      question: "tabs?",
      options: [
        { text: "yes", voter_count: 7 },
        { text: "no", voter_count: 1 },
      ],
    },
  },
]);

api

exportsignaturedescription
renderChat(messages: ChatMessage[], options?: RenderOptions) => stringrender a telegram-style chat to an svg string
ChatMessageinterfaceone message — see the table above
RenderOptionsinterfacerender-wide options — see below
Side"user" | "bot"message direction
TickStatus"sent" | "delivered" | "read"outgoing read receipt

RenderOptions

optiontypedefaultdescription
theme"dark" | "light""light"the green-wallpaper look, or dark
widthnumber380canvas width in px
avatarstringname initialoverride the avatar glyph for incoming messages