@sea-dev/widget-lit
v0.1.2
Published
Embeddable **document-review widget** for the Sea platform: framework-agnostic Web Components (built with [Lit](https://lit.dev)) that show an uploaded document beside the data our AI extracted from it, links every value to its source, and lets a reviewer
Readme
@sea-dev/widget-lit
Embeddable document-review widget for the Sea platform: framework-agnostic Web Components (built with Lit) that show an uploaded document beside the data our AI extracted from it, links every value to its source, and lets a reviewer correct values or leave feedback. Load it from our CDN with a single <script> tag, or install it from npm — which also provides first-class React bindings under @sea-dev/widget-lit/react.
Everything you need is on this page: quick start, the components, React, styling, wiring your own documents, and going live.
Quick start
1. Get a demo API key. In the Sea app: Settings → API Keys → New API Key. Copy the value (it's shown once).
2. Drop in the widget:
<script type="module" src="https://cdn.sea.dev/widget-lit/latest/index.js"></script>
<div style="position: relative; height: 800px;">
<sea-context
api-key="sk-sea-YOUR-KEY"
base-url="https://api.sea.dev"
deal-id="c3yknp26fhlcewjwmrnjs"
submission-id="iw82zhoga2xwhwhmmk6yh"
>
<div style="position: absolute; inset: 0; display: flex; gap: 4px;">
<sea-deal-nav style="flex: 0 1 18rem;"></sea-deal-nav>
<sea-pdf-viewer style="flex: 1;"></sea-pdf-viewer>
<sea-data-viewer style="flex: 1;"></sea-data-viewer>
</div>
</sea-context>
</div>That's a working three-pane reviewer: the document rail, the source document, and the extracted data. Point deal-id and submission-id at any submission your API key can read; the IDs above are a test deal we set up for your evaluation.
Prefer no setup? Change base-url to demo, set submission-id="sub-1", and drop api-key. The widget then renders a sample document and its extracted data from a dataset baked into the bundle, with no key and no network calls.
Try it live:
- Playground, every component running against live data with no code: https://app.sea.dev/settings/embed
- Pre-created test deals (open one, then upload your own test documents to it):
- https://app.sea.dev/workflows/exkbt4h6rvcfc3zmljgny/4fhwicz2grqa6cw3ns2yu
- https://app.sea.dev/workflows/exkbt4h6rvcfc3zmljgny/cfdnjsbs5pdxkjj8bqpco
The components
| Element | What it does |
|---|---|
| <sea-context> | The provider: holds your key and the active submission-id, fetches the submission once, and shares it with the panes inside. Renders no box of its own. |
| <sea-deal-nav> | The left rail: lists the deal's documents; click one to switch the other panes. Optional if your app already has a document picker. |
| <sea-pdf-viewer> | The source document, with the region behind the selected value highlighted. |
| <sea-data-viewer> | The extracted fields; also where a reviewer edits a value, leaves feedback, and sees correction history. |
<sea-pdf-page>, <sea-image-viewer> and <sea-field-correction-modal> are registered by the same script and used internally.
Main <sea-context> attributes:
api-key,base-url: required.base-url="demo"serves canned data with no network calls.submission-id: the document to show.deal-id: scopes the rail.citation-popover-placement: where the popover docks.gutter(default),rail,below,above,beside,edge.actor-id: stamps who made each correction, optionally withactor-name/actor-email. Attribution only happens whenactor-idis set.track-citations: report each citation click to Sea for reviewer-diligence analytics.
React
If your frontend is React (18 or 19), install the npm package instead of using the script tag and import real React components — camelCase props, typed event callbacks, no refs or customElements plumbing:
npm install @sea-dev/widget-lit// SeaReview.tsx — a complete document-review pane. Drop it into your app and
// render <SeaReview /> anywhere. With no props it runs in demo mode: a sample
// document and its extracted data baked into the bundle — no key, no network.
import {
SeaContext,
SeaDealNav,
SeaPdfViewer,
SeaDataViewer,
} from "@sea-dev/widget-lit/react";
export function SeaReview({
// For live data: baseUrl="https://api.sea.dev", your API key, and a real
// deal / submission id.
baseUrl = "demo",
apiKey = "",
dealId = "deal-1",
submissionId = "sub-1",
}: {
baseUrl?: string;
apiKey?: string;
dealId?: string;
submissionId?: string;
}) {
return (
<div style={{ position: "relative", height: 800 }}>
<SeaContext
apiKey={apiKey}
baseUrl={baseUrl}
dealId={dealId}
submissionId={submissionId}
>
<div style={{ position: "absolute", inset: 0, display: "flex", gap: 4 }}>
<SeaDealNav style={{ flex: "0 1 18rem" }} />
<SeaPdfViewer style={{ flex: 1 }} />
<SeaDataViewer
style={{ flex: 1 }}
onCorrectionSubmitted={(e) =>
console.log("corrected", e.detail.itemId, e.detail.correction)
}
/>
</div>
</SeaContext>
</div>
);
}
// Default export too: replacing a scaffolded src/App.tsx with this file just
// works with the generated `import App from "./App"`.
export default SeaReview;Out of the box the sample renders the demo document, so it proves the integration compiles and renders before any credentials exist. To connect live data, pass baseUrl="https://api.sea.dev", your apiKey, and a real dealId/submissionId.
Every attribute from the table above is available as a camelCase prop, and the wrapper adds typed callbacks for the widget's events: onCitationSelected, onCitationCleared, onValueChanged and onCorrectionSubmitted on SeaDataViewer, onPdfLoaded / onPdfLoadError on SeaPdfViewer. Each receives a CustomEvent whose detail type is exported from @sea-dev/widget-lit/react.
Importing @sea-dev/widget-lit/react registers the custom elements as a side effect — do not also load the CDN script tag, or two copies will race to define the same tags.
PDF rendering
The document pane needs the pdf.js worker file, and everything is served from cdn.sea.dev — the script-tag embed picks it up beside the bundle automatically, and npm builds default to a version-pinned path on the same host. No third-party CDN is involved. If your environment can't reach cdn.sea.dev at all, serve the worker from your own app and pass it in — the widget pins [email protected], and your bundler can emit the file for you:
// Vite
import pdfWorkerUrl from "pdfjs-dist/build/pdf.worker.min.mjs?url";
// Webpack 5: new URL("pdfjs-dist/build/pdf.worker.min.mjs", import.meta.url).href
<SeaPdfViewer workerUrl={pdfWorkerUrl} />Styling
Every component renders inside a shadow DOM that reads its colors, font, spacing, and radius from CSS custom properties. To rebrand the widget, set any of the tokens below at :root (or on any ancestor of the widget element) — custom property values inherit through the shadow boundary:
:root {
/* Brand */
--primary: oklch(0.4 0.2 260);
--primary-foreground: oklch(0.98 0 0);
/* Citation highlights on the document */
--sea-highlight: oklch(0.85 0.15 145);
/* Typography & density */
--sea-font-family: "Inter", sans-serif;
--sea-font-scale: 0.95;
--sea-spacing: 0.22rem;
/* Geometry */
--radius: 0.25rem;
}You should not need to override component CSS, and with shadow DOM you can't: the tokens are the supported styling surface. If a visual you need to change isn't covered by a token, tell your Sea contact — that surface is supposed to be a token. This works identically for the script-tag embed and the React bindings — they are the same elements.
Color tokens
All defaults are OKLch and are inlined as var(--token, default) fallbacks, so the widget renders correctly if the host sets nothing.
| Token | Default | Controls |
|---|---|---|
| --background | oklch(1 0 0) | Widget canvas background |
| --foreground | oklch(0.129 0.042 264.695) | Default text color |
| --card | oklch(1 0 0) | Panel and card surfaces |
| --card-foreground | oklch(0.129 0.042 264.695) | Text on --card surfaces |
| --popover | oklch(1 0 0) | Floating popover background (citation popover) |
| --popover-foreground | oklch(0.129 0.042 264.695) | Text on --popover |
| --primary | oklch(0.259 0.073 284.7) | Primary action color (buttons, active states) |
| --primary-foreground | oklch(0.985 0 0) | Text on --primary fills |
| --secondary | oklch(0.968 0.007 247.896) | Secondary fills |
| --secondary-foreground | oklch(0.208 0.042 265.755) | Text on --secondary fills |
| --muted | oklch(0.968 0.007 247.896) | Subtle panel backgrounds (data-viewer rows, document shells) |
| --muted-foreground | oklch(0.554 0.046 257.417) | Secondary text, icon strokes |
| --accent | oklch(0.968 0.007 247.896) | Hover/selection accent |
| --accent-foreground | oklch(0.208 0.042 265.755) | Text on --accent fills |
| --destructive | oklch(0.577 0.245 27.325) | Error banners, destructive actions |
| --ring | oklch(0.665 0.189 308.1) | Focus ring on inputs |
| --border | oklch(0.929 0.013 255.508) | All borders |
| --input | oklch(0.929 0.013 255.508) | Form input borders |
| --sea-highlight | oklch(90.5% 0.182 98.111) | Citation rectangle fill on PDF pages and images |
The widget reserves the --sea-* namespace for widget-specific tokens. The unprefixed tokens follow the shadcn/ui naming convention, so if your app already themes shadcn components, the same override block works here unchanged.
Geometry, typography and density
| Token | Default | Controls |
|---|---|---|
| --radius | 0.625rem | Base border radius; rounded-md inside the widget resolves to calc(var(--radius) - 2px), rounded-sm to calc(var(--radius) - 4px) |
| --sea-font-family | system-ui stack | Font for all widget text. The widget does not inherit your page font by itself (its internal stylesheet sets a default), so set this to your app's font stack to match |
| --sea-font-family-mono | ui-monospace stack | Monospace text (raw values, citation excerpts) |
| --sea-font-scale | 1 | Multiplier on every text size in the widget; line heights follow proportionally. 0.9 shrinks all type by 10% |
| --sea-spacing | 0.25rem | The base spacing unit. Every padding, margin, and gap inside the widget is a multiple of it, so one token controls overall density — 0.2rem gives a compact embed, 0.3rem an airy one |
Because sizes inside the widget are rem-based, the widget also scales with your document's root font size like the rest of your app.
Dark mode
Set class="dark" or theme="dark" on the widget element or any ancestor:
<div class="dark">
<sea-context …>…</sea-context>
</div>The widget ships a complete dark palette wired to the same tokens. If your document already defines a .dark { --background: …; } palette, that wins — the widget's built-in dark rule is wrapped in :where() (zero specificity), so document-scope tokens stay authoritative.
What about Tailwind utilities directly?
The widget bundles a Tailwind stylesheet inside each shadow root, and its utilities (bg-primary, text-foreground, border-border, …) already resolve to the tokens above. Don't set Tailwind theme variables — set the source tokens (--primary, --foreground, --border) and everything inside picks them up.
Embedding your own documents
Create a submission, upload a document, wait for extraction, then mount the widget on that submission-id. Every call carries Authorization: Bearer <your-key>.
# 1. Create a submission against a form and deal (GET /v2/forms lists your forms)
curl -X POST https://api.sea.dev/v2/submissions \
-H "Authorization: Bearer sk-sea-YOUR-KEY" -H "Content-Type: application/json" \
-d '{"formId": "YOUR_FORM_ID", "dealId": "YOUR_DEAL_ID"}'
# 2. Upload a PDF or image and start extraction (multipart field name: files)
curl -X POST https://api.sea.dev/v2/submissions/$SUBMISSION_ID/extractions \
-H "Authorization: Bearer sk-sea-YOUR-KEY" -F "[email protected]"
# 3. Poll status until "done", then mount the widget on $SUBMISSION_ID
curl https://api.sea.dev/v2/submissions/$SUBMISSION_ID/status \
-H "Authorization: Bearer sk-sea-YOUR-KEY"Extraction is asynchronous, so wait for {"status": "done"} before a reviewer opens the submission.
Reviewing
- Citations: click a value or its highlighted region to link them; a draggable popover shows the field.
- Corrections: click a value to edit it. The editor takes a new value and/or a note and keeps the history. To drive it from your own UI,
POST /v2/submissions/:id/items/:itemId/correctionswithcorrectedValueomitted (feedback only),null(clear the field), or a string (set it). - Finalising: there's no button in the widget; your app calls
PATCH /v2/submissions/:idwith{"status": "final"}(send"draft"to reopen).
Known limits
- PDF and images only. A
.docxextracts fine and its data and citations work, but the document pane can't render it yet: you'll see "Invalid PDF structure". - Evergreen browsers (Chrome, Edge, Firefox, Safari): the widget relies on native custom elements and ES modules.
Going live
The quick start above is for evaluation: a single, browser-visible org key and the latest/ CDN build. For production, we provision an enterprise account with use-case-scoped API keys and managed key rotation, plus a pinned CDN version so a later publish can't change your embedded bytes. Talk to your Sea contact when you're ready to move past the demo.
Development
# Build the CDN bundle (outputs dist/index.js + lazy pdfjs chunks + react.js)
pnpm --filter @sea-dev/widget-lit build
# Type-check
pnpm --filter @sea-dev/widget-lit exec tsc --noEmitThe bundle is published to cdn.sea.dev/widget-lit/<version>/ (immutable) and cdn.sea.dev/widget-lit/latest/ by the publish-widget CI job (scripts/publish-widget-lit.sh) on widget-relevant merges to main. The npm package is published manually with pnpm publish from this directory.
