TdRichText
A semantic parser for engineering prose. Pass a single plain string; the parser annotates HTTP endpoints, durations, storage sizes, percentiles, percentages, multipliers, complexity notation, comparison operators, ALL-CAPS acronyms, and prose numbers as typographically-polished inline spans against cb-* tokens.
Distinct from its rich-text siblings — see the comparison table below.
ctx.auth.getUserIdentity() and check for null before the SDK call.Installation
npx shadcn@latest add https://craftbits.dev/r/td-rich-text.jsonUsage
import { TdRichText } from "@craft-bits/core";
<TdRichText>
The GET /api/feed handler returns 95% of responses in 200ms with p99 at 450ms.
</TdRichText>The barrel exports the component as TdRichText to keep its name distinct from the markdown-style RichText (@craft-bits/core/edu/rich-text) and the token-array RichTextNodes (@craft-bits/core/text/rich-text). Import via the subpath if you only want this primitive:
import { TdRichText } from "@craft-bits/core/text/td-rich-text";How it differs
| Component | Input shape | Recognises |
|---|---|---|
RichTextNodes | RichTextToken[] | nothing — declarative |
RichTextProse | semantic JSX | nothing — descendant CSS |
RichElements | per-element components | nothing — explicit composition |
RichParagraph | paragraph children | typographic recipe |
RichText (edu) | string | markdown marks |
TdRichText | string | engineering semantics — durations, sizes, percentiles, endpoints, complexity |
The other parsers tokenise markup. This one tokenises meaning: it reads engineering prose the way a careful editor would and lifts the right atoms into polished inline spans.
Recognised atoms
| Atom | Pattern (default) | Renders as |
|---|---|---|
code | backtick spans | mono chip, bordered |
endpoint | GET /api/... | mono chip, data-cb-td-rich-atom="endpoint" |
duration | 200ms, 1.5μs, 40ns | mono chip, data-cb-td-rich-atom="duration" |
size | 36 KB, 1.5 MB | mono chip, data-cb-td-rich-atom="size" |
percentile | p95, p99, p99.9 | mono chip, data-cb-td-rich-atom="percentile" |
percentage | 95%, 0.5% | mono chip, data-cb-td-rich-atom="percentage" |
multiplier | 10×, 5x | mono chip, data-cb-td-rich-atom="multiplier" |
complexity | O(n), O(n log n) | mono italic, accent |
comparison | >= <= != | typographic upgrade |
arrow | → ← ⇒ ⇐ ↔ | accent weighted |
boolean | true, false, null, undefined | serif italic small-caps |
prose-number | standalone digits | mono tabular-nums |
emphasis | ALL-CAPS, 3+ chars | pencil-emphasis squiggle |
keyword | quoted strings, 2+ chars | serif italic with curly quotes |
text | everything else | plain |
Variants
The variant prop controls motion. static (the default) ships zero motion. reveal springs each atom in on mount with a small stagger. interactive adds a scale-on-hover to code chips.
<TdRichText variant="reveal">…</TdRichText>Swap the root element when the surrounding layout expects a block:
<TdRichText as="p">…</TdRichText>Extending the vocabulary
Matchers are exposed via the matchers prop so consumers can layer or replace them. Earlier rules win when ranges overlap.
import {
TdRichText,
TD_RICH_TEXT_DEFAULT_MATCHERS,
type TdRichTextMatcher,
} from "@craft-bits/core";
const EXTRA: TdRichTextMatcher = {
re: /\bRFC \d+\b/g,
type: "emphasis",
};
const MATCHERS = [EXTRA, ...TD_RICH_TEXT_DEFAULT_MATCHERS];
<TdRichText matchers={MATCHERS}>RFC 9110 defines GET semantics.</TdRichText>Pass an empty list to disable parsing entirely (useful for fixture tests).
Props
| Prop | Type | Default | Description |
|---|---|---|---|
children | string | required | Plain-text source. The parser annotates it. |
as | 'span' | 'div' | 'p' | 'span' | Root element. |
variant | 'static' | 'reveal' | 'interactive' | 'static' | Motion mode. |
matchers | readonly TdRichTextMatcher[] | TD_RICH_TEXT_DEFAULT_MATCHERS | Custom matcher list. Earlier rules win. |
className | string | — | Merged onto the root via cn(). |
Accessibility
- Pure inline typography — no interactive controls of its own.
- Decorative arrows render with
aria-hidden="true"so screen readers skip them. variant="reveal"andvariant="interactive"usemotion/react. Reduced-motion is the parent's responsibility — setvariant="static"(the default) whenprefers-reduced-motion: reduce.- ALL-CAPS emphasis runs through
PencilEmphasis, which honoursprefers-reduced-motionitself. - Body text targets WCAG AA contrast against
--cb-bgin both themes.
Credits
- Extracted from:
terminal-dreams(src/components/ui/RichText.tsx). The source was a parser-first engine with 15 engineering-prose matchers (endpoints, durations, percentiles, sizes, multipliers, complexity, …) bound to bespokeApiEndpoint/DurationBadge/SizeBadge/PercentileBadge/PercentageBadge/MultiplierBadge/ComplexityBadge/CustomArrowwidgets, an in-moduleSTATIC_CACHE, and three motion modes. The library version keeps the parser and the three motion modes, generalises the per-atom widgets into a singledata-cb-td-rich-atomstyled chip so consumers can re-skin via CSS, swapsframer-motionformotion/react, replaces the unbounded module-level cache withWeakMap-keyed caches that release with the matcher list, and exposesmatchersso the vocabulary is extensible without forking the component.