@agimon-ai/po-web-components
Shared web components and theme tokens for the Po cockpit and its web plugins: shadcn-style primitives on Radix, tuned to the Po palette, with runtime theme configs.
Package details
Install @agimon-ai/po-web-components from npm and Pi will load the resources declared by the package manifest.
$ pi install npm:@agimon-ai/po-web-components- Package
@agimon-ai/po-web-components- Version
0.0.0-alpha.107- Published
- Nov 4, 2026
- Downloads
- 42.7K/mo · 17.4K/wk
- Author
- agiflow-ai
- License
- BUSL-1.1
- Types
- theme
- Size
- 2.2 MB
- Dependencies
- 31 dependencies · 2 peers
Security note
Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.
README
@agimon-ai/po-web-components
Shared web components and theme tokens for the Po cockpit and its web plugins.
The primitives are shadcn/ui components on Radix
(the unified radix-ui package), restyled with Tailwind utilities that name theme tokens only.
Because every colour is a token, a theme config recolours the whole cockpit at runtime.
What it ships
| Export | Components |
|---|---|
. |
Actions and chips: Button, Badge, StatusBadge, Dot, Kbd, NavTab*, Tabs*. Fields: Input, Textarea, Label, Checkbox, Switch, RadioGroup*, Select*. Overlays: Dialog*, Sheet*, Popover*, DropdownMenu*, Tooltip*, Toast*, Command*. Layout and content: Panel*, SectionLabel, Separator, EmptyState, ScrollArea, Collapsible*, Accordion*, OptionList, OptionRow, MessageItem*, MessageLines. Feedback: Spinner, Progress, Skeleton, Avatar*, StreamCursor. Files: CodeEditor, MediaPreview. Review: DiffView, ReviewCommentDraft. Plus the curated icon set, cn, collapseLines, grammarKeyOf, mediaKindOf, the review helpers (buildReviewPrompt, reviewCommentAnchor, diffRangeOf), groupByDirectory, the option-list key helpers, and the tone vocabularies with the maps between them |
./theme |
ThemeConfig, the token lists, BUILTIN_THEMES, applyTheme, parseThemeConfig, themeFromPiTheme, the stored-preference helpers |
./styles.css |
The token stylesheet: the bundled fonts, then styles/theme.css (:root defaults, the Tailwind @theme inline mapping, shadcn semantic aliases, base styles, and the @source for this package's classes) |
./themes/*.json |
The shipped theme configs: po-one-light (default), po-nord-dark |
Using it from the cockpit host
/* src/web/styles/app.css */
@import 'tailwindcss';
@import '@agimon-ai/po-web-components/styles.css';
import { Button, Dialog, DialogContent, StatusBadge } from '@agimon-ai/po-web-components';
import { applyTheme, builtinTheme, readThemePreference } from '@agimon-ai/po-web-components/theme';
applyTheme(builtinTheme(readThemePreference(localStorage) ?? 'po-one-light')!, document.documentElement);
Tones
Three vocabularies, because they answer different questions. A chip tone names a colour
(blue, violet, teal), a status tone names an outcome (running, ok, error, info,
accent, neutral), and a line tone names a role inside a body of text (success, warning,
muted). They live in src/types/tone.ts, and STATUS_TO_CHIP, STATUS_TO_DOT, CHIP_TO_STATUS,
and LINE_TONE_TO_STATUS translate between them.
Reach for a map rather than writing another Record<Tone, string>: a surface that spells its own
state-to-colour table is how the same status ends up yellow in one panel and orange in the next.
Every tone table in this package is checked against these unions at compile time, so a tone added to
one and not the others fails the build.
Tool messages
A tool call's timeline item is a message renderer in the plugin that registers the tool, composed
from MessageItem (the frame, its outcome tone, and the expand state), MessageItemHeader (the
tool name, the summary, the status badge, and the toggle when the item is expandable),
MessageItemBody, MessageItemStatus (the ◐ running / ✗ failed / ✓ done line, or the "N
more line(s)" hint that expands), and MessageLines (toned lines a pure view helper computed) with
collapseLines for the clip. The cockpit's own fallback item is built from the same parts, which
is what keeps host and plugin items alike, and the shell carries the tool-status and
tool-expand test ids every card inherits.
Files
CodeEditor is CodeMirror 6 on the po palette: line numbers, folding, undo, a search panel, and
a grammar chosen from the file's path. It arrives as its own chunk, and each grammar as another, so
a session that never opens a file downloads neither. Give it a height (it fills its container) and
read edits back through onChange; onSelect reports the current range with one-based line
numbers, which is what anchors a review comment to a line.
<CodeEditor value={source} path="src/app.ts" onChange={setSource} className="h-full" />
MediaPreview shows a file the browser can render but not edit: an image, a video, or a PDF from a
URL a server is already serving, and a download link for everything else. mediaKindOf(path) is
the same classification on its own, for a caller deciding between the editor and the preview.
Neither converts a format. A .docx rendered as HTML is a different document, so it is offered as
a download rather than as something that looks editable.
Review
DiffView draws hunks with a numbered gutter and a wash per row. With onSelect set, highlighting
text reports the lines it touches and clicking a line number reports that one line; either way the
selection names its side, so a note on a removed line points at the old file. renderAfterRow
places inline content, such as an open draft, under a row.
ReviewCommentDraft is the box a selection raises: the quoted snippet, its range, and a note.
testIdPrefix names its test ids (<prefix>-draft, -body, -add, -cancel).
buildReviewPrompt(comments, heading?) folds every note into one message, each headed by
reviewCommentAnchor: path:12-14 on the new side, path (removed lines 3-4) on the old.
<DiffView hunks={hunks} testId="review-diff" onSelect={setDraft} />
Using it from a plugin's web/ client
A web plugin's client code may import react, the @tanstack store packages,
@agimon-ai/po-core/web, and this package. Declare it as a workspace:* dependency and
import the same way the host does; the cockpit bundler dedupes it so one copy renders everywhere.
Do not import radix-ui or class-variance-authority directly from plugin code: the po-web
vibe-lint rules route every primitive through this package so a theme change reaches all of them.
Theme tokens
The palette a theme must supply: bg, rail, panel, deep, border, border-soft, hi,
text, dim, faint, blue, green, yellow, red, magenta, violet, cyan, orange,
teal, selected. The derived tokens (tint-<accent>, edge-<accent>, font-mono) are computed
from the palette with color-mix() unless the theme pins them; the shipped dark theme pins only the
rose tint and edge, where the mix lands muddy on its green-grey panel.
Native theme
@agimon-ai/po-web-components/nativeTheme exports NATIVE_THEMES (light and dark),
nativeTheme(config) and PoNativeTheme without importing the web component root. Alongside colours,
spacing, fonts and the type ramp, the theme provides:
letterSpacingin px per type step, including tight xl, 2xl and 3xl titles.lineHeightRelaxedin px per step, using a 1.625 multiplier.borderRadius.DEFAULT(4px) andfull(9999), alongside the named radii.fontWeight: regular400, medium500, semibold600.motion: fade 140ms, pop 160ms, rise 160ms, slide 180ms, with easing[0.16, 1, 0.3, 1].
withAlpha(hex, percent) returns #rrggbbaa for native opacity mixes, for example
withAlpha('#98be65', 40) returns #98be6566. It accepts three- or six-digit hex colours.
The same subpath re-exports CHIP_TONES, DOT_TONES, STATUS_TONES and their types.
Tracked uppercase and shadow-2xl are not ported: labels stay sentence case and native surfaces
supply their own shadows.
Type
Po Bloom bundles two families (@fontsource-variable, OFL): IBM Plex Sans (font-sans) for every
word of UI, headings included, and JetBrains Mono (font-mono) for code, paths, keys, and terminal
output. font-display remains as an alias of the sans family. Hierarchy comes from size, weight, and
ink; the role ladder is in docs/design.md. ./mcp.css carries the
same tokens without the fonts: a Po-generated MCP app gets them inlined by its build, and a
third-party app keeps its own.
Each token is published as --po-<token> on the root element and mapped to Tailwind as
po-<token> (bg-po-panel, text-po-hi, border-po-edge-red), and to shadcn's semantic
names (bg-background, text-muted-foreground, border-input).
Writing a theme
{
"name": "my-theme",
"label": "My theme",
"scheme": "dark",
"tokens": { "bg": "#1e1e2e", "rail": "#181825", "panel": "#1e1e2e", "deep": "#11111b", "...": "..." }
}
parseThemeConfig(json) validates it (null on any problem, so a bad file never takes the page
down) and applyTheme(theme, document.documentElement) writes it. themeFromPiTheme(piThemeJson)
builds a web theme from a third-party Pi theme.
Development
pnpm build # tsdown: one entry per src/exports module, ESM and CJS, browser platform
pnpm test # theme, bridge, and render smoke tests (react-dom/server, no DOM needed)
pnpm typecheck
The po-web vibe-lint preset governs the layout: src/types (the theme contract), src/lib
(cn), src/theme, src/icons, src/components (one PascalCase component per file, props-driven,
no application state), and src/exports (pure re-exports, one file per subpath).