@gramforge/bubble
v0.1.0
Published
Telegram-look chat bubble preview components. **Preview-only**: no network I/O, no filesystem access, and no dependency on `@gramforge/transport-fetch`, `@gramforge/transport-grammy`, or `@gramforge/images` — a bubble render can never require a live bot t
Downloads
33
Readme
@gramforge/bubble
Telegram-look chat bubble preview components. Preview-only: no network I/O,
no filesystem access, and no dependency on @gramforge/transport-fetch,
@gramforge/transport-grammy, or @gramforge/images — a bubble render can
never require a live bot token or a network round trip. npm-only (React
dependency, not JSR-published).
Install
pnpm add @gramforge/bubble @gramforge/core @gramforge/render react react-domimport "@gramforge/bubble/theme.css";entitiesToHtml — the entity→HTML preview path
Converts a @gramforge/render toEntities()-shaped { text, entities }
payload (wrapped in this package's BubblePayload) into a Telegram-look HTML
string. Pure and synchronous — text is escaped, tags are generated from a
fixed mapping, and nested/overlapping entities are reconstructed into properly
nested markup.
import { bold, createMessageText, text } from "@gramforge/core";
import { toEntities } from "@gramforge/render";
import { BubblePayloadSchema, entitiesToHtml } from "@gramforge/bubble";
const message = createMessageText([text("hello "), bold(text("world"))]);
if (message.kind === "ok") {
const { text: renderedText, entities } = toEntities(message.value);
const payload = BubblePayloadSchema.parse({ kind: "message", text: renderedText, entities });
entitiesToHtml(payload); // 'hello <b>world</b>'
}BubblePayloadSchema.safeParse enforces the same Bot API length limits
@gramforge/core uses (4096 UTF-16 code units for a message, 1024 for a
caption) — an over-length payload is rejected before it ever reaches
entitiesToHtml, never silently truncated.
<TelegramBubble> — a Telegram-look chat bubble
import { TelegramBubble } from "@gramforge/bubble";
<TelegramBubble
direction="outgoing"
payload={{ kind: "message", text: "hello world", entities: [{ type: "bold", offset: 6, length: 5 }] }}
timestamp={new Date()}
delivery={{ kind: "read" }}
/>;
<TelegramBubble
direction="incoming"
payload={{ kind: "caption", text: "nice shot", entities: [], imageUrl: "https://cdn.example/photo.png" }}
timestamp={new Date()}
/>;direction discriminates the whole prop set: outgoing bubbles carry a
delivery: DeliveryStatus tick ("sending" | "sent" | "delivered" | "read" |
{ kind: "failed"; reason }, matched exhaustively via ts-pattern) that
renders right-aligned; incoming bubbles render left-aligned with no
read-receipt affordance at all — there is no optional/always-undefined
delivery prop on the incoming variant. A "caption" payload renders an
<img src={imageUrl}> plus caption text below it; the component never calls
@gramforge/images or any renderer to produce that image, it only ever
references the URL it was given.
CSS theme
@gramforge/bubble/theme.css ships .tgb-*-prefixed classes and CSS custom
properties (--tgb-color-*) whose values mirror the Tasker UI palette this
project has standardized on — warm-white background (#FFFFFF/#FAFAFA),
single red accent (#DC4C3E), warm-grey text/border scale — sourced from
openspec/changes/showcase-app/design.md decision D6, not re-derived ad hoc.
House rules
zod schemas (BubblePayloadSchema and friends) are the single source of
truth for payload validation; presence is modeled with discriminated unions
(BubblePayload["kind"], TelegramBubbleProps["direction"],
DeliveryStatus["kind"]) rather than .optional()/.nullable(), and every
union is matched with ts-pattern's .exhaustive().
Distribution
npm-only. React is a peer dependency (>=18); this package is not
JSR-published since JSR targets Deno-portable, React-free packages (see
@gramforge/core/@gramforge/render for those).
