@credence/react
v0.6.0
Published
Citation UI for Credence: clickable evidence pills, hover cards, and PDF bbox highlights.
Downloads
518
Readme
@credence/react
The surface a human touches. Everything else in this repo is for the engineer
wiring Credence into an agent. This package is for the person on the other end —
the analyst, the auditor, the lawyer whose signature the output has to survive. A
reader sees (1), clicks it, and lands on the exact region of the exact page the
number came from.
That double audience is the whole design brief. An engineer installs it in an afternoon; somebody else has to defend its output in a room. So every component answers the same question the ledger answers — how do you know? — and refuses to render a blank where the honest answer is "nobody looked".
import { ClaimValue, DocumentPage, IntegrityIndicator } from "@credence/react";
<ClaimValue value={410_000} status="verified" citations={citations} />
// → 410000 ✓ verified (1) ...an unsourced value renders "⚠ no source"
<ClaimValue value={undefined} status="unknown" /> // → · unknown (nobody looked)
<ClaimValue value={{ $credence: "absent" }} status="absent" />
// → ∅ absent (someone looked)
<DocumentPage src="/page-4.png" page={4} citations={citations} activeCitationId={active?.id} />
// → the page with the bbox highlighted — or an explicit "source could not be loaded"
<IntegrityIndicator />
// → ✗ Trail NOT verified ...because a missing prop must never render a tickComponents
| Component | What it does |
|---|---|
| Citations / CitationPill | numbered pills with a hover card (source + locator + stance as a glyph and a word); renders ⚠ no source when a value has no evidence; an unstated stance prints nothing |
| ClaimValue | value + epistemic badge + citations + optional attribution, together; ∅ absent for verified absence |
| EpistemicBadge | reported / inferred / verified / disputed, as a glyph and a word |
| DocumentPage / BboxHighlight | page image with bbox highlights overlaid, positioned in percentages |
| MatrixTable | the review matrix; a cell clicks through to the page and bounding box its value came from |
| FindingCard / FindingList | a finding with the act that quieted it — author, reason, time — or a call-out when there is none |
| AttributionLine | who asserted a value, with which model version, and against which tool receipt |
| IntegrityIndicator | whether the case's append-only trail verifies — in five outcomes, never a default tick |
| OversightBanner | that the case is stopped, by whom, why, and what else is blocking it |
| LoadingBlock / ErrorBlock / EmptyBlock | the three states every component above can be in |
Helpers: bboxToPercent, citationAriaLabel, citationLabel, citationIndex,
describeValueState, formatAttribution, formatDismissal, formatIntegrity,
formatLocator, BBOX_SCALE, VALUE_STATE_TOKENS, EVIDENCE_STANCE_TOKENS,
tokenStyle, v, credenceStyles.
AttributionLine, IntegrityIndicator and OversightBanner render what the
ledger's audit spine produces — Claim.assertedBy / Claim.modelVersion plus the
chain entry, verifyChain, and oversight_status. Host apps that have not wired
those simply do not render these three; nothing degrades.
MatrixTable shows the value AND the rule
A cell's state answers "what does the ledger hold for this field?", which can be
impeccable while a rule still objects to it — a verified, evidenced debt of 5M is
ok and a blocking risk at once. So a cell needing attention is marked ⚑. A
table that rendered only the value would tell a reader everything is fine while the
Case Index said otherwise.
States
Every component has a loading, an error and an empty state, because a silent blank teaches a reader that "blank" means "fine".
| Component | loading | error | empty | degraded |
|---|---|---|---|---|
| Citations | ⋯ Loading evidence… | ⚠ Could not load… + retry | ⚠ no source | a citation with no locator still renders, without one |
| ClaimValue | LoadingBlock replaces value and citations | ErrorBlock replaces both | — (unknown / absent are values, not emptiness) | no attribution prop ⇒ no attribution row; null ⇒ the gap is shown |
| MatrixTable | ⋯ Loading review matrix… | ErrorBlock + retry | No requirements in this view — never a table of blanks | a cell with no resolvable citations says no evidence in its label |
| DocumentPage | ⋯ Loading source page… | "Source document could not be loaded" — and the highlights are withdrawn | No evidence recorded on this page | citations on the page with no bbox are counted out loud, never guessed at |
| FindingList / FindingCard | ⋯ Loading findings… | ErrorBlock + retry | No findings on this case | a dismissal with no author or reason is called out, not rendered as quiet |
| AttributionLine | ⋯ Loading attribution… | (host's) | Not attributed — no actor recorded | actorKind: null renders as its own third state, not as human or machine |
| IntegrityIndicator | ⋯ Verifying the trail… | "Trail integrity could not be checked. Treat this case as unverified." | absent prop ⇒ ✗ Trail NOT verified | an ok chain with no witness renders ◐, not ✓ |
| OversightBanner | ⋯ Loading oversight status… | (host's) | renders nothing when nothing is held and nothing blocks | a declared autonomy budget nobody counted against prints no fraction at all |
An error is an alert and a loading state is a polite status, because one will
resolve on its own and the other will not.
Theming
Every component ships complete inline styles, so the package works with zero CSS. Colours resolve through CSS custom properties with inline fallbacks, so theming is one declaration on any ancestor:
:root {
--credence-verified-fg: #047857;
--credence-verified-bg: rgba(4, 120, 87, 0.12);
}The optional stylesheet documents every knob in one place and adds focus rings,
prefers-reduced-motion handling and print rules:
import { credenceStyles } from "@credence/react/styles";
<style dangerouslySetInnerHTML={{ __html: credenceStyles }} />It is wrapped in @layer credence. An unlayered author rule beats a layered one
regardless of specificity, so your .credence-badge { … } wins over ours without
!important and without counting selectors. Stable credence-* class names and
data-* state attributes (data-status, data-integrity, data-autonomy,
data-actor-kind, data-held, data-stance) are the styling hooks.
Accessibility
Not a follow-up; asserted in tests.
- One tab stop per matrix.
MatrixTableimplements the WAI-ARIA roving tabindex: arrow keys move between cells,Home/Endjump to the corners. A 12×8 matrix with a tab stop per cell would put 96 stops between a keyboard user and the next control on the page. - A cell with nothing to open is
aria-disabled, notdisabled— it keeps its place in the grid, so the cursor is the only thing that changes. - Escape dismisses the hover card while focus stays on the pill, and the card opens on focus as well as hover.
- Every state carries a glyph and a word, never only a colour. Pills are real
<button>s with full-sentencearia-labels ("Citation 1: balance-2025.pdf — p. 4 · region"); the card is arole="tooltip"linked byaria-describedby. - Politeness matches urgency. A hold and a broken chain are assertive
alerts; loading and a passing chain are politestatuses. - Server-rendering is a test, not a hope. Every exported component is rendered
with
react-dom/serverin a node environment, so nothing touches the DOM at import or render time.
Honesty
The design bar for this package is that a three-state thing never renders as two, and absence is visible.
unknownvsabsent. Nobody looked, versus somebody looked and there is verifiably nothing there. Different glyphs, different words, different borders.⚠ no source. A value with no evidence says so rather than rendering nothing. A failed load says something different again — collapsing the two would make this surface assert what it does not know.- A source page that will not load withdraws its highlights. Boxes floating over a broken image still look like located evidence, and looking like evidence while being none is worse than showing nothing.
- Trail integrity never defaults to a checkmark. No prop means
✗ Trail NOT verified;indeterminateis its own answer, neither a tick nor an accusation; and anokchain checked against nothing but itself renders differently from one checked against a witness. - An uninstrumented autonomy budget is not a comfortable zero.
0/8 iterationsis the most reassuring line that could appear on a banner, and it is exactly what a ceiling nobody counted against produces — sounreportedprints no fraction. - A machine stop is not human oversight. A case an agent halted has not been looked at by anybody.
- Greyscale survives. Audit packs get printed, and roughly one reader in twelve cannot tell the green from the red.
- A citation that does not support is not rendered as one. Stance is a glyph
and a word on the hover card (
supports/contradicts/cited). Unstated prints nothing — not a placeholder, not a default — because a mark on every historical pill would teach readers to skip the column.
Reviewed through a committed gallery, not Storybook
pnpm --filter @credence/react gallery renders every component in every state to
gallery/gallery.golden.html, which is committed and asserted byte-for-byte by
test/gallery.test.ts. Open the file — no dev server, no port, no install — or
read the diff in a pull request, where a regression in the honesty vocabulary shows
up as text rather than as something a human has to remember about a screenshot.
Design decisions
- No
@credence/coredependency, including types. Core pulls Drizzle and PGlite, which have no business in a browser bundle. The types here are local and structurally compatible, so values read from the ledger drop straight in; parity is asserted from both sides, andtest/boundary.test.tsfails on any import of a sibling package — by module specifier over comment-stripped source, becauseimport typeis erased and would never show up in a bundle. - React is the only peer. No CSS framework, no component library, no runtime deps.
- Percentages, not pixels. Highlights stay glued to the content through zoom, resize and responsive layout.
- Refs are handed in, never assembled here. The
credence://scheme is configurable in core, so a ref built in this package would be wrong for any deployment that rebranded it.
Non-goals — this is a component library, not an application
@credence/react renders props. It does not fetch, does not hold session state,
does not know what a tenant is, and never will. Everything below is deliberately
out of scope:
| Not here | Where it belongs |
|---|---|
| Auth, SSO, RBAC — who may see which case | Cloud |
| Data fetching, caching, resolve_ref round-trips, chain verification | the host app; this package takes the results as props |
| Routing, navigation, a case list, a portfolio dashboard | Cloud |
| PDF rendering — turning a PDF into page images | the host (pdf.js, a render service); DocumentPage takes an image src |
| Writes of any kind — dismissing a finding, releasing a hold, recording a decision | the host calls @credence/kit; components emit onSelect* / onOpenRef callbacks and nothing more |
| Retention, redaction, PII masking on locators | Cloud, per docs/ROADMAP.md → Later |
| Real-time updates, websockets, optimistic state | the host |
The line is not stylistic. A component that fetches is a component that needs a client, a base URL, an auth token and an error policy — and a component library with an auth token in it cannot be dropped into someone else's app, which is the entire reason this package exists.
