Blog
Meta

Component Gallery: Every Building Block in One Post

Tashif Ahmad Khan August 8, 2026 8 min read 57 views 0 likes

A <Panel> rendered clean on the blog and came out as a bare, unstyled <div> on the portfolio. Same Markdown file, same post, two different renders, because one site was a version behind on the shared renderer package. That’s the actual bug that made this page necessary: one post that exercises every component in the registry, tag spelling and directive spelling both, so a drift like that shows up here, in public, before a reader ever sees it.

That list above is the registry in one screen: layout, structure, data, marginalia, media, tabs, and two spellings for every one of them. Everything under a heading below has its own live demo.

why components at all#

Plain Markdown gets you prose, lists, tables, code. Fine for a tutorial. Not enough for a plan, a comparison, or a postmortem, the kind of document that wants structure the reader can see instead of infer from paragraph breaks.

So the renderer adds a closed set of components on top of that. Not MDX. Nothing gets imported, nothing gets evaluated, on purpose. A tag either resolves against the registry or it just sits there as ordinary HTML. That constraint is the whole point: it’s what lets a post travel over the API as a plain string and still render the same way on three different sites.

Two spellings, one result

Everything here can be written as a tag (<Steps>) or as a directive (:::steps). They parse to the same tokens. Tags nest more legibly; directives survive being read on GitHub, where an unknown tag would simply vanish.

layout#

columns#

The wide column takes the argument, and the narrow one takes the aside. That break point is about 34rem of container width, not viewport, since posts render inside resizable windows. Below it the columns stack and a rule appears between them.

Anything works in here: lists, code, even another component.

Note to self

Equal columns: <Cols cols={2|3|4}>. Unequal tracks: ratio values such as 1:1, 2:1, 1:2, 1:1:1, 2:1:1, 1:1:1:1. Unknown ratios fall back to equal columns and fail the publish check.

Three equal columns, the layout behind the feature cards:

Input Modality
  • PDF, images (JPG, PNG)
  • Single image ≤ 10MB, PDF ≤ 50MB
  • Maximum support: 100 pages
Output Modality

Text / Image Links / MD Documents

Supported Language

Chinese, English, French, Spanish, Russian, German, Japanese, Korean, etc.

panels#

Setup

A panel is the generic card. It takes an optional title, a Lucide-style icon, a strip of masking tape, a tone, and a tilt in degrees.

The tilt is inert unless the host site opts in by setting --md-rotate: 1, so the same post reads as a zine on one site and as a clean article on another.

Heads up

Toned panels tint their border and background from the site’s own palette rather than a hardcoded hue. Icons use the same stroke geometry as Lucide’s react-icons set, so write icon="zap" the way you would import Zap.

Icons work standalone too, not just inside a panel title:

Tape works solo too, with nothing under it, pure decoration:

ink band#

House rules

An inverted section, for a break in the page. Everything inside follows the inverted colour rather than the page’s, including links and code.

strips#

  • Each list item becomes its own card
  • With a small alternating nudge, where tilt is enabled
  • Good for a set of short, unordered points

document structure#

steps#

  1. Install

    Run bun install and copy .env.example to .env.

  2. Configure

    Set GITHUB_TOKEN and EDITOR_SESSION_SECRET. The secret must be at least 32 characters.

    bash
    openssl rand -base64 32
    
  3. Publish

    Numbering comes from a CSS counter, so inserting a step here would not mean renumbering the ones below by hand.

phases#

doneFoundation

Registry, tag syntax, validation.

nowVocabulary

The components in this post.

nextConsumers

Blog theme, API manifest, portfolio, editor palette.

checklist#

Before publish

  • Directives validate
  • Images attached
  • Cover image set

collapsible detail#

Why not just use MDX?

MDX compiles to JavaScript with arbitrary imports. The API serves Markdown text, the portfolio fetches it at build time, and the editor’s reading pane renders a string. None of those can evaluate a module. A closed registry keeps the authoring ergonomics and drops the runtime.

data#

headline numbers#

34 components
290 tests
1 renderer

scored rubric#

Renderer drift between sites 2/10

One canonical package, mirrored by a script with a --check mode that fails on drift.

Python outline duplicating TS logic 5/10

Contained by keeping the Python side regex-level and advisory, with a shared fixture and a version constant asserted in both suites.

Stale mirror on a consuming site 7/10

The renderer field lets a consumer notice and warn rather than print a raw tag into the page.

bars#

Where the lines went

components.ts 720
markdown.css 820
validate.ts 330
jsx.ts 300
  • Parser
  • Styles
  • Checks

marginalia#

Inline components sit inside a sentence: a highlighted phrase, a NEW stamp, or a note in the margin where the handwriting face is enabled.

Standing alone, the same component becomes a block instead, since a span cannot hold paragraphs. The element follows the position.

media#

diagrams that markdown would mangle#

The body of an Ascii block is captured verbatim. The alignment, the pipes, the underscores, all of it survives untouched, because none of it passes through the Markdown parser the way a normal paragraph would.

figures#

A mermaid diagram inside an explicit figureGenerated at render time

embeds#

Only an allowlisted provider with a pattern-checked id ever turns into a frame. Anything else stays a plain link. This content crosses an API boundary and renders on two other sites I don’t control frame by frame, so an arbitrary embed URL isn’t a risk worth taking.

tabs#

bun

bash
bun add @tashif/markdown

npm

bash
npm install @tashif/markdown

pnpm

bash
pnpm add @tashif/markdown

Kill the JavaScript and every panel just stays visible, stacked under its own heading. It reads like a document that happens to have subheadings, not like a widget that failed to load. That distinction is deliberate: a tab strip that goes blank without JS is a worse failure than no tabs at all.

the directive spelling#

Everything demonstrated above also works written with colons instead of angle brackets. That’s the syntax already-published posts use, and it’s the one that stays readable if someone opens the raw file on GitHub instead of the rendered site:

Still supported

:::tip and <Tip> produce byte-identical HTML. The two-col name is kept as an alias for Cols so nothing published before this change had to be edited.

Directive

text
:::steps
:::step First
do it
:::
:::

Tag

text
<Steps>
<Step title="First">
do it
</Step>
</Steps>

callouts#

Note

The six callouts predate the registry and are unchanged.

Careful

A callout takes a title as a bare value or as {title="..."}.

Important

All six exist: note, tip, important, warning, caution and danger.

Caution

Each maps to its own colour token, so a site can tune the palette without the renderer knowing about it.

Danger

The tag spelling and the directive spelling produce identical markup.

Danger

The GitHub alert spelling still rewrites into the same markup, so a post reads correctly on GitHub as well as on both sites.

Bottom line: this page is the documentation and the regression test in one file. Every component the renderer understands shows up here, in both spellings, across three sites that only ever receive this post as a plain Markdown string over an API. If one of them breaks, it breaks here first, in the open, not quietly on a reader’s screen somewhere I’m not looking.

That’s the whole gallery. Next time a <Panel> misbehaves on one of the sites, this is the first page I open.

Discussion

0

Leave a comment