@odla-ai/ui
v0.20.0
Published
The odla design system: semantic CSS tokens, seven themes, application shells, safe authored Markdown, exact-artifact review cards, bottom work docks, accessible status, friendly principal identity, anchored mentions, canvas helpers, and lightweight Preac
Maintainers
Readme
@odla-ai/ui
⚠️ Early access — pre-1.0. Agents work from bounded runbooks; humans approve credentials, production changes, releases, and merges. APIs and exact package availability can change. Review the documented guarantees and limitations; this software is MIT-licensed and provided without warranty.
The odla design system: one semantic token contract (--ui-*), seven themes,
class-scoped component CSS, canvas chart helpers, and optional Preact
components. CSS-first — every odla surface can consume it, from zero-build
static sites to small Vite + Preact apps, and it covers marketing/brand sites
as well as app chrome.
- Tokens: colors, text tiers, status, fonts, spacing, radius, focus,
chart and chat roles, reading preferences (root size + scale, the
body/medium/strong/heading weight ladder, font smoothing), plus a marketing
tier (fluid display type scale, section rhythm, decor hooks). Defaults ship
at zero specificity (
:where()), so themes and app overrides always win. - Reading preferences: five axes the reader owns, one attribute each on
<html>— appearance (data-theme), palette (data-ui-palette), text size (data-ui-text), text weight (data-ui-weight), motion (data-ui-motion). Themonoandcontrastpalettes layer over any theme instead of replacing it, winning on specificity rather than load order.contrastsharpens rather than replaces — it references only roles the theme owns, and each theme deepens its own accents to AAA inui.css, hue held, measured on every test run.PrefsMenuis the popover for apps,prefsMenuHtml()+bindPrefsMenu()the same control for static sites,PREFS_NO_FLASHthe<head>snippet that applies a stored choice before first paint, andprefersReducedMotion()the hook script-driven motion needs. Layout measures must multiply--ui-font-size-rootrather than userem, or the text-size axis widens the page too — zoom, not a text setting. - Scopes:
[data-ui-accent="<name>"]swaps the accent family to a theme-shipped named variant inside any subtree (salt ships six Silver & Salt page palettes); the.ui-invertclass (per-theme generatedinvert.css) renders a section in the theme's dark palette regardless of the global mode. Importthemes/<name>/scope.cssand useThemeScopewhen an application subtree needs an explicit light/dark mode and local brand tokens without changing the document root. - Themes:
odla-brand(the odla Studio default),paper,flightline(field manual meets flight recorder), plusjuniper,salt,chalk, andclay(the blog themes —@odla-ai/blogresolves those four from this package). Each theme =styles.css(its own palette) +ui.css(mapping onto the contract) +theme.json. - Components (CSS): buttons/segmented/pills, form fields with validation
states, cards/panels/badges, data tables, container-native app chrome with a
navigation rail + sidebar, accessible in-content tabs, a persistent
bottom notification edge with Gmail-style docked cards, a sticky or contained
top bar, grouped docs left-nav, friendly principal identity badges,
exact-artifact human review cards, toasts/banners/dropzones, chat surfaces (message bubbles, tool-call cards,
thinking panels, composer — shaped by
@odla-ai/ai's streaming contract), chart containers, and calendar surfaces (month grid, week/day time grid, agenda list, toolbar). - Marketing (CSS,
mk-family): section bands + content wells, the eyebrow/display-title/lead ladder, hero decor (spotlight, hairlines, watermark, grain, glow), stat cells, striped proof rows, portrait quote card, page sources/footnotes, step flows and pipelines, FAQ accordion, veil-blur marketing nav (bar and two-tier rail), sitemap footer, CTA band, and the split-circle brand mark. - JS helpers (buildless ESM): the reading-preference model
(
PREF_AXES,readPrefs/setPref,PREFS_NO_FLASH,prefsMenuHtml,bindPrefsMenu), the older mode-only light/dark toggle it supersedes, a--ui-*palette reader for canvas, and chart primitives (fitCanvas,niceTicks,bandFill,drawTip, …). /components: form controls, includingInlineActionFieldfor a compact label/input/action row with a reserved accessible status region;PrefsMenu;TopBar/TopBarLink;AppShell,NavigationRail,Sidebar; accessibleTabs;Dock/DockCardandEdgeNotificationBar;StatusIndicator;PrincipalBadge;MentionEditor;ReviewCardfor server-confirmed artifact decisions; bounded, HTML-inertMarkdownfor authored prose;DocsNav/DocsNavGroup/DocsNavLink;SectionNav(in-page table of contents with scroll-spy highlighting + smooth scroll, built on theuseScrollSpyhook, also exported);CalendarMonth/CalendarWeek/CalendarDay/CalendarAgenda+CalendarToolbar(timezone-aware views whoseeventsaccept@odla-ai/calendarBooking[]as-is — no dependency between the packages); marketing components (MarketingNav/MarketingNavLink/MarketingNavMenu,Hero,StatBand,StepIndicator/StepPipeline,Accordion/AccordionItem,MarketingFooter);FilterChips;DataTable(sortable/filterable, app-rendered cells that survive reorder via requiredrowKey); and data viz (Choroplethover app-owned SVG geometry with theme heat-ramp fills,CycleDiagram) — authored and shipped against Preact. The only compatibility helper in the component build is Preact's ownforwardRef; hooks and JSX use nativepreact/hooksandpreact/jsx-runtime.
Tabs keeps every panel mounted by default so ordinary in-page forms retain
local state. URL-backed application workspaces should use mount="active" to
prevent hidden panels from fetching or running effects. MasterDetail accepts
closeHref so mobile return navigation can be a native anchor while retaining
focus restoration.
Ask the runbooks first. odla's operational procedures live in a database, not in this file:
npx @odla-ai/cli runbook ask "<question>"returns the current steps, and unlike anything written here it cannot be out of date. Use it before searching the web or working from memory. This README and the JSDoc in the shipped.d.tsare the version-matched API reference; a runbook is the procedure. Most tasks need an answer from both.
Install
npm install @odla-ai/ui preactThe /components and /gallery entries require Preact 10.29.7 or newer in the
10.x line. CSS and buildless JS entry points have no framework runtime.
60-second start (Vite + Preact)
// main.tsx
import "@odla-ai/ui/fonts/plex.css";
import "@odla-ai/ui/themes/paper/styles.css";
import "@odla-ai/ui/themes/paper/ui.css";
import "@odla-ai/ui/index.css";import { Button, Field, Input } from "@odla-ai/ui/components";
<div className="card">
<h4>New note</h4>
<Field label="Title" htmlFor="t">
<Input id="t" placeholder="…" />
</Field>
<div className="row">
<Button>Save</Button>
<Button variant="secondary">Cancel</Button>
</div>
</div>InlineActionField owns the stable .inline-action-field-* class contract,
keeps its status output mounted to avoid layout jumps, and stacks from its own
container width. Use code for monospace identifiers and pass native input
behavior such as autoComplete="one-time-code" through inputProps.
Static sites copy odla-ui.css (pre-flattened, no imports) plus a theme's
styles.css + ui.css and use the same classes in plain HTML.
Safe authored Markdown
Markdown renders the compact prose dialect used by app-owned content fields:
headings, ordered and unordered nested lists, blockquotes, bold, italic,
strikethrough, inline code, and links. It creates Preact nodes directly; it
never uses an HTML string or dangerouslySetInnerHTML, so raw HTML remains
visible text. Relative links and credential-free absolute HTTPS links are
navigable. Active, credential-bearing, plain-HTTP, protocol-relative, malformed,
or oversized destinations keep their readable label and lose the anchor.
import {
Markdown,
MAX_MARKDOWN_LENGTH,
MAX_MARKDOWN_NESTING_DEPTH,
} from "@odla-ai/ui/components";
<Markdown source={membershipTier.authoredBody} />;The safety bounds are fixed: MAX_MARKDOWN_LENGTH is 65,536 UTF-16 code units and
MAX_MARKDOWN_NESTING_DEPTH is 8, with a private output-node budget as a third
guard. Source beyond the input/output budget renders only a bounded form plus
an explicit notice; formatting deeper than the nesting bound is shown as text.
Validate the same length at the authoring boundary when truncation would change
the meaning of public copy. Images, fenced code, tables, and raw HTML are
deliberately outside this component's dialect.
Bottom-edge work and mentions
Dock belongs in AppShell's dock slot and may contain several independent
DockCards. A card header minimizes/restores it; onDismiss closes only that
card. Use size="lg" plus bodyOverflow="contained" for a complete child work
surface. Set active on the viewer's current card so it receives focus and
scrolls into view in an overflowing narrow dock. StatusIndicator is the shared
accessible dot-and-label state for live connections and background work. The
dock's empty span passes pointer input through to the underlying work surface,
so opening one card never prevents selecting another item to pin.
import {
AppShell,
Dock,
DockCard,
MentionEditor,
StatusIndicator,
} from "@odla-ai/ui/components";
<AppShell
dock={
<Dock aria-label="Open discussions">
<DockCard
title="Release notes"
meta="3 replies"
size="lg"
bodyOverflow="contained"
active
>
<Thread />
</DockCard>
</Dock>
}
>
<StatusIndicator active tone="good">Live updates</StatusIndicator>
<MentionEditor {...composer} menuPlacement="above" />
</AppShell>MentionEditor defaults to menuPlacement="below". Bottom-edge or sticky
composers should use "above" so candidate rows stay beside the text and
inside the viewport.
Friendly principal identity
Use PrincipalBadge wherever humans, agents, and services share an activity
stream, approval surface, or discussion. The component is deliberately
data-free: the host resolves an authoritative profile and passes a friendly
displayName, optional validated handle, and—only for an agent—the friendly
name of its accountable human in managedBy.
import { PrincipalBadge } from "@odla-ai/ui/components";
<PrincipalBadge kind="human" displayName="Cory" handle="cory" />
<PrincipalBadge
kind="agent"
displayName="Odla"
handle="cory/odla"
managedBy="Cory"
/>
<PrincipalBadge kind="service" displayName="Platform" handle="platform" />Kind is always printed as text and reinforced with an H/A/S mark and a different shape, so color is never the only distinction. Do not pass raw principal or manager IDs as display content. There is intentionally no ID prop or ID fallback: if an authoritative friendly name is unavailable, resolve it before rendering.
ChatThread message items expose a presentation-only byline slot for the
same trusted badge. Resolve its data outside the design system; the thread
component never guesses identity from an author id.
<ChatThread items={[{
kind: "message",
id: message.id,
role: "assistant",
byline: (
<PrincipalBadge
kind="agent"
displayName="Brand partner"
handle="cory/brand"
managedBy="Cory"
/>
),
body: message.body,
}]} />Human review of agent changes
Use ReviewCard for a proposed brand facet, CRM rule, content revision, or
code/configuration change. Its children are the exact preview the person is
judging; its title and meta can carry revision and trusted PrincipalBadge
context. The component does not grant authority. Its callbacks ask the host to
decide, and the host must revalidate the current human and exact resource,
persist an immutable receipt, then return the server-confirmed decision.
import { PrincipalBadge, ReviewCard } from "@odla-ai/ui/components";
<ReviewCard
title="Spring campaign palette"
meta={<PrincipalBadge kind="agent" displayName="Brand partner" handle="brand" />}
approveLabel="Accept palette"
busy={saving}
decision={serverDecision}
onApprove={() => approveExactProposal(proposal)}
onReject={() => rejectExactProposal(proposal)}
>
<ExactPalettePreview proposal={proposal} />
</ReviewCard>Never set decision="approved" from optimistic local state. A card can make
the workflow legible; only the product's authenticated server boundary can
make the decision real. The Design gallery demonstrates the pending and
terminal states.
Chapter/CRM application shells using the Salt visual grammar can use one load-order-safe import instead of assembling the three layers themselves:
import "@odla-ai/ui/themes/salt/app.css";Themes
| Theme | Look | Fonts |
| --- | --- | --- |
| odla-brand | Growth-green product brand (the odla Studio default) | IBM Plex Sans/Mono (fonts/plex.css) |
| paper | Warm paper dashboard | IBM Plex Sans/Mono (fonts/plex.css) |
| flightline | Technical field manual by day, flight recorder at night | IBM Plex Sans/Mono (fonts/plex.css) |
| juniper | Quiet long-form reading, violet accents | Lora + Gill Sans stack |
| salt | Warm editorial cream/moss/rust; six named accent variants (rust/teal/ink/plum/lime/moss), grain + & watermark decor | Cormorant Garamond + Satoshi |
| chalk | Chalkboard on graph paper | Cormorant + Spectral + JetBrains Mono |
| clay | Warm light editorial, terracotta | System stacks only (zero font requests) |
Dark mode: every theme defines dark tokens on [data-theme="dark"] and the
prefers-color-scheme fallback (kept byte-identical — tested). Toggle with
toggleTheme() from the JS helpers. For dark islands inside a light
page, import themes/<name>/invert.css (generated) and put .ui-invert on
the section.
For an isolated application surface, additionally import
themes/<name>/scope.css and wrap the surface:
import { ThemeScope } from "@odla-ai/ui/components";
<ThemeScope theme="paper" colorScheme="light" tokens={{ accent: "#35654d" }}>
<AdminApp />
</ThemeScope>The full contract — token tables, class inventory, recipes, invariants — lives in this installed README and the exported TypeScript declarations/JSDoc. The rendered public reference is at https://odla.ai/docs/packages/ui.
Development
npm test -w @odla-ai/ui # builds dist, then node --test
npm run gen:css -w @odla-ai/ui # refresh odla-ui.css after editing css/Tests enforce: theme dark-block identity across all seven themes, full
--ui-* coverage per theme, accent-variant completeness (every
[data-ui-accent] block declares the full swap set), invert/accent scope
byte-identity with the root defaults, class-scoped selectors (no bare
elements) in component sheets, generated-CSS freshness (odla-ui.css,
tokens.css, invert.css), gallery completeness for every /components
export, and component render/interaction behavior.
