Skip to content

Elements

The elements are the browser half of the vocabulary: one framework-free custom element per block kind. A host gives each its parsed data and listens for one event, pbs-answer.

import "pi-better-show/elements"; // defines every pbs-* tag (a no-op without a DOM)
import { configure } from "pi-better-show/elements";
el.data = segment.data; // or <pbs-form data-json='{...}'>
el.addEventListener("pbs-answer", (event) => send(encodeAnswer(event.detail)));
el.answered = event.detail.fields; // once delivered: locks the element and shows what was sent

A host gives an element the block’s parsed data and listens for pbs-answer (bubbles, composed, cancelable) whose detail is a core Answer. An element never reads or writes HTML: text reaches the page as text nodes, and links are http(s) only with rel="noopener noreferrer" and target="_blank". Each element is a shadow root with constructed stylesheets (adoptedStyleSheets), so it runs under default-src 'self': no <style> element, no style attribute. examples/ is such a page; run it with bun run demo (loopback, port 4177).

Tag Block data Answer Parts
pbs-callout alert AlertData callout header icon title body
pbs-cards cards CardsData grid card title icon body
pbs-options options OptionsData pick grid option title badge body pros cons actions pick status
pbs-form form FormData form form title question label hint choices choice badge other textarea error actions submit status sent
pbs-review review ReviewData review review title items item item-title pick media image enlarge frame unavailable body comment actions submit status lightbox close sent
pbs-mindmap mindmap MindmapData mindmap tree item node toggle label group
pbs-suggest suggest SuggestData suggest suggest chips chip status
pbs-checklist checklist ChecklistData checklist checklist title items item progress actions submit status sent
pbs-table table TableData table bar filter count scroll header head sort body row cell empty
pbs-steps steps StepsData steps step rail marker content title badge when body
pbs-stats stats StatsData grid stat label value delta note
pbs-diff diff DiffData diff scroll line
pbs-diagram mermaid MermaidData diagram image source note
pbs-tree tree TreeData treeview bar expand-all collapse-all tree item node toggle name count tags tag note more group

TAGS maps a block kind to its tag. There is no whole-document element: the extension renders Markdown on its page server, and other hosts use their own renderer.

Every element has these.

Name Kind Meaning
data / data-json property / attribute The block’s data; bad JSON or a shape the element cannot draw shows an error panel and fires pbs-error.
answered / data-answered property / attribute The submitted [name, value][]: locks the element and shows them. undefined reopens it.
ref, or doc and block attributes property / attributes The block’s address, copied into every answer’s ref.
density attribute and property comfortable (default) or compact.
scheme attribute and property light or dark, to force a scheme; else the element follows the host’s color-scheme.
lang attribute (or an ancestor’s, or the document’s) zh* shows Traditional Chinese, anything else English.
refresh() method Draws again. Call it after the host changes color-scheme or the document lang, which an element cannot observe.

A pick (pbs-options) or a chip (pbs-suggest) sends at once and locks itself once a listener takes the event; a listener that calls preventDefault() refuses it and the element stays open. A form, review, or checklist stays editable after sending (the status line says “Sent.”), so the user can send a changed answer; the host locks it by setting answered. A form’s required questions are checked before sending. Ctrl/Cmd+Enter sends from a text box.

The host looks after what an element must not do itself: where diagram images, review images, and prototypes come from, the words, and whether a diagram can be pointed at.

configure({
strings: { "zh-Hant": { send: "送出" } }, // per locale, per word (see Strings)
diagramUrl: (source, scheme) => `/diagram.svg?...`, // an image URL for pbs-diagram; without it the source is shown
imageUrl: (ref) => `/files/${ref}`, // a review item's `image`; default admits http(s) and raster data URLs only
pageUrl: (ref) => `/pages/${ref}`, // a review item's `page`, shown in a sandboxed frame; without it, its name
diagramMap: async (url) => readTargets(url), // makes a diagram's nodes and edges clickable (below); without it the diagram is an image
});

Serve diagram images from pi-better-show/server (mermaidSvg(source, { scheme, transparent: true })); no diagram markup enters the page. resetConfiguration() restores the defaults.

With diagramMap set, a diagram’s nodes and edges become buttons a reader can comment on.

What the host provides

With diagramMap set, pbs-diagram lays a transparent button over each node, edge (its line and its label box), and subgraph header of the image. diagramMap(url) receives the URL diagramUrl returned and resolves to a DiagramMap: { viewBox, targets: [{ kind: "node" | "edge" | "group", text, shapes: [{ tag, attrs }] }] }, shapes in the image’s own coordinates and carrying geometry attributes only. The host reads and parses the drawing, because elements never parse markup; the extension’s page does it for the SVG mermaidSvg draws (g.node, .edge, g.edge-label, g.subgraph with their data-* labels: flowcharts and state diagrams; other diagram types give no targets and stay images). The targets come in reading order, the order the arrow keys follow; whatever the order, groups are drawn below edges and edges below nodes. A click, or Enter or Space on a focused button, dispatches pbs-diagram-target (bubbles, composed) with detail: { kind, text }, from the button itself, so composedPath()[0] is where a host returns focus. The diagram is one tab stop, on the button last focused (the first at the start); the arrow keys move to the next or previous button, Home and End to the first or last. The buttons sit in a role="group" whose description (aria-describedby, an SVG desc) tells a screen reader that the arrow keys move between them; the DOM keeps the drawing layers, not the reading order. Each button has an aria-label (“Comment on edge: …”) and shows a hover and focus tint drawn over the image, so nothing moves.

Set a --pbs-* property on an element or any ancestor; each has a light-dark() default that follows the host’s color-scheme.

Token Meaning
--pbs-bg --pbs-surface Panel background; tinted surface (table header, code)
--pbs-fg --pbs-muted Text; secondary text
--pbs-line --pbs-control Panel and divider lines; the edge of an input (3:1 against its surface)
--pbs-accent --pbs-on-accent Buttons, links, selection; text on them
--pbs-note --pbs-ok --pbs-warn --pbs-risk Tones
--pbs-radius --pbs-gap Corner radius; space between blocks and inside them
--pbs-font --pbs-mono --pbs-font-size Type
--pbs-font-display Titles and headings (defaults to --pbs-font)

Structure carries part names for ::part() restyling. A panel has the same border on all four sides, or none; a tone shows as a tint, a badge, an icon, and the heading colour, never a coloured or thicker one-sided border. examples/demo.css maps the Roundtable and Straw boss tokens as examples.

Every control is a native control, labelled. pbs-mindmap follows the ARIA tree pattern (arrows move, Right and Left open and close, Enter and Space toggle, Home and End jump); pbs-tree follows it too, and opens folders to depth 2 (deeper folders that hold a tagged node open their path) with “expand all” and “collapse all” buttons; pbs-table sorts from header buttons (aria-sort) and filters from a labelled search box.