@artificialpoets/content
v0.1.0
Published
Poetic UI — primitives for rendering technical content (code, math, persistent tabs). Usable in any React app. Opt-in: consumers who don't install this package pay zero bytes.
Readme
@artificialpoets/content
Primitives for rendering technical content — syntax-highlighted code, math equations, persistent multi-variant tabs.
Part of the Poetic UI ecosystem. Neutral React components (no brand coupling, no next/* imports) that work in any React-19 app — Next App Router, Remix, Astro, plain SPAs.
Opt-in. Consumers who don't install this package pay zero bytes. Those who do get per-component subpath exports +
sideEffects: falsefor tree-shaking, plus Server-Component pre-rendering that keeps Shiki's WASM and grammars off the client. See Four layers of bundle control below.
Install
bun add @artificialpoets/content
pnpm add @artificialpoets/content
npm install @artificialpoets/content
yarn add @artificialpoets/contentPeer dependencies: react@^19, react-dom@^19.
Runtime deps pulled in automatically: shiki, katex, react-katex, plus the two sibling workspace packages @artificialpoets/components and @artificialpoets/tokens.
Quick start
import {
BlockMath,
CodeBlock,
Example,
InlineMath,
LanguageTabs,
PackageManagerTabs,
PersistentTabs,
} from "@artificialpoets/content";
// Opt-in CSS — import once in your app entry:
import "@artificialpoets/content/styles/code-block";
import "@artificialpoets/content/styles/katex";
export default function QuickStart() {
return (
<section>
<PackageManagerTabs add="@acme/api-client" />
<CodeBlock
lang="ts"
code={`const x: number = 42;\nconsole.log(x);`}
/>
<LanguageTabs defaultValue="ts">
<Example lang="ts" label="TypeScript">
{`const client = new Poet({ apiKey: process.env.POET_KEY! });`}
</Example>
<Example lang="python" label="Python">
{`client = Poet(api_key=os.environ["POET_KEY"])`}
</Example>
</LanguageTabs>
<p>
The Pythagorean identity <InlineMath math="a^2 + b^2 = c^2" /> is the
basis of the Euclidean norm.
</p>
<BlockMath math={String.raw`\int_0^\infty e^{-x^2}\,dx = \frac{\sqrt{\pi}}{2}`} />
</section>
);
}The primitives
<CodeBlock> — Shiki syntax highlighting
Async Server Component. Renders syntax-highlighted HTML at render time and ships zero client JS for the highlighter. Emits dual-theme CSS variables so a .dark ancestor class flips colors instantly with no re-render.
<CodeBlock lang="ts" code="const x: number = 42;" />
// Customize the theme pair:
<CodeBlock
lang="tsx"
themes={{ light: "github-light", dark: "one-dark-pro" }}
code="…"
/>Default grammars (10): ts, tsx, js, jsx, bash, json, css, html, md, python.
Default themes: github-light, github-dark-dimmed.
Need more grammars? Build your own highlighter and pass it in:
import { CodeBlock, createHighlighter } from "@artificialpoets/content";
const highlighter = await createHighlighter({
langs: ["ts", "bash", "php", "ruby", "rust"],
themes: ["github-light", "github-dark-dimmed"],
});
<CodeBlock lang="rust" highlighter={highlighter} code={`fn main() {}`} /><BlockMath> / <InlineMath> — KaTeX
Client-rendered via react-katex. Two sizes (default, compact) via CVA:
<BlockMath math={String.raw`\sum_{n=1}^\infty \frac{1}{n^2} = \frac{\pi^2}{6}`} />
<InlineMath math="e^{i\pi} + 1 = 0" size="compact" />CSS is opt-in. import "@artificialpoets/content/styles/katex" once in your app entry to pull in KaTeX's fonts and spacing. Without it, math renders structurally but looks plain. The bundled CSS also adds color: currentColor so equations inherit your theme's text color automatically.
<PersistentTabs> — generic tab strip with preference sync
Built on <SegmentedTabs> from @artificialpoets/components/navigation plus usePref(). Every <PersistentTabs> instance sharing the same storageKey stays in lockstep on the page (same-tab) and across browser tabs (via the storage event).
<PersistentTabs
storageKey="my-app:theme-preview"
defaultValue="auto"
options={[
{ value: "light", label: "Light" },
{ value: "dark", label: "Dark" },
{ value: "auto", label: "Auto" },
]}
>
{(active) => <p>You picked: {active}</p>}
</PersistentTabs>Under the hood: useSyncExternalStore with a per-key pub/sub. SSR-safe — returns defaultValue during server render, hydrates to the stored value on the client with no visual flash (assuming the server-rendered UI was built with defaultValue).
<PackageManagerTabs> — bun / pnpm / npm / yarn
Pass exactly one verb prop; the component generates the correct command for every package manager:
<PackageManagerTabs install />
<PackageManagerTabs add="react react-dom" />
<PackageManagerTabs remove="lodash" />
<PackageManagerTabs run="build" />
<PackageManagerTabs exec="tsc --watch" />
<PackageManagerTabs create="next-app my-app --typescript" />Every <PackageManagerTabs> on the page syncs under storageKey="poeticui:pref:package-manager". Your visitor picks "bun" once; every install/add/run snippet across your docs reflects that choice from then on.
<LanguageTabs> + <Example> — cross-language code samples
Children-based API. The <Example> marker tells <LanguageTabs> which samples to expose:
<LanguageTabs defaultValue="ts">
<Example lang="ts" label="TypeScript">
{`const client = new Poet({ apiKey: process.env.POET_KEY! });`}
</Example>
<Example lang="python" label="Python">
{`client = Poet(api_key=os.environ["POET_KEY"])`}
</Example>
<Example lang="bash" label="cURL">
{`curl -H "Authorization: Bearer $POET_KEY" https://api.acme.com/v1/me`}
</Example>
</LanguageTabs>Sync key: poeticui:pref:language. Graceful degradation — if the user picked python on another page but this card only has ts + bash, we fall back to the first example instead of crashing.
Four layers of bundle control
The package is designed so consumers can precisely pick what ships to the browser. Each layer composes with the ones above it.
Layer 1 — Package
Don't install @artificialpoets/content and you pay zero bytes. Most line-of-business dashboards and admin UIs fall here. This is the default. Only reach for the package when a concrete feature needs a primitive.
Layer 2 — Import (subpath exports)
Per-component subpaths let you bypass the barrel for zero-risk tree-shaking — even bundlers that conservatively keep barrel imports will get per-export granularity:
// Only pulls CodeBlock + Shiki. Nothing else from the package loads.
import { CodeBlock } from "@artificialpoets/content/code-block";
// Only pulls Math + KaTeX.
import { BlockMath } from "@artificialpoets/content/math";
// Only pulls PersistentTabs — no CodeBlock, no Math.
import { PersistentTabs } from "@artificialpoets/content/persistent-tabs";All subpaths:
@artificialpoets/content— barrel (everything)@artificialpoets/content/code-block@artificialpoets/content/math@artificialpoets/content/persistent-tabs@artificialpoets/content/package-manager-tabs@artificialpoets/content/language-tabs@artificialpoets/content/pref-store@artificialpoets/content/storage-keys@artificialpoets/content/styles/code-block— CSS@artificialpoets/content/styles/katex— CSS
Layer 3 — Route-level (Next App Router / equivalent)
In a framework that code-splits per route, any primitive only loads on the routes that import it. Your marketing landing page imports <CodeBlock> → it's in the marketing chunk. Your dashboard index route doesn't → the dashboard chunk stays lean.
Layer 4 — Dynamic import
Defer the heaviest primitives (Shiki's WASM is ~500KB with default grammars) to first use:
const { CodeBlock } = await import("@artificialpoets/content/code-block");Or with React's <Suspense>:
import { lazy } from "react";
const CodeBlock = lazy(() =>
import("@artificialpoets/content/code-block").then((m) => ({ default: m.CodeBlock })),
);Server Components & the 'use client' boundary
| Primitive | Boundary | Why |
|---|---|---|
| <CodeBlock> | Server (async) | Shiki highlighting is async; pre-rendering keeps the highlighter off the client. |
| <BlockMath> / <InlineMath> | Client | react-katex needs DOM layout. |
| <PersistentTabs> | Client | Needs localStorage + useSyncExternalStore. |
| <PackageManagerTabs> | Server (async) | Pre-renders 4 CodeBlocks server-side, hands them to an internal client shell that toggles visibility. |
| <LanguageTabs> | Server (async) | Same pattern as above — pre-renders each <Example>'s CodeBlock server-side. |
Practical impact: Shiki never reaches the browser for the tab components. The only JS that ships for a <PackageManagerTabs> is the ~2KB shell that toggles which pre-rendered block is visible. KaTeX CSS + JS does reach the browser (it has to — math layout is client-only), but only on pages that import it.
Design tokens & theming
Same rule as @artificialpoets/components: tokens-only. bg-card instead of bg-white, text-foreground instead of text-zinc-900. Every primitive flips with the .dark ancestor class for free, and picks up your brand's primary/secondary colors if you've layered a theme overlay on top of @artificialpoets/tokens.
Live examples
- Content / Overview — full tour of every primitive (Poetic UI Storybook)
- Content / Examples / API Landing Page — realistic end-to-end mock combining every primitive in a developer-facing marketing page
(Run locally: bun run storybook from packages/components/ → http://localhost:6006 → Content.)
Contributing
Open a PR with tests (jest + @testing-library/react) and a story. See packages/components/CONTRIBUTING.md for the authoring spec — the same rules apply here (semantic tokens only, cx() helper, CVA for variants, tests colocated under src/__tests__/).
License
Apache-2.0. See LICENSE at the repo root.
