Horns in a React app
Add the Horns pages to your router, or draw Horns inside your own pages, with @manta-eu/horns-client.
What it is
@manta-eu/horns-client is the Horns user interface as a library. It gives a React app three things:
- Pages.
hornsRoutes()returns the Horns routes, such as the list, the Studio and a saved Horn, to splice into your own router.mountHorns()mounts them as a whole page instead, for an app with no router of its own. - Embeds.
HornDrawndraws one Horn you hold, andHornsAboutdraws every Horn about one thing, such as a rule or an action plan, beside your own screen for it. - Build-time inputs.
hornsPlugins(realm)adds the Vite plugins that hand Horns your realm’s shapes and the types written views are checked against.
Everything it requests goes to the Horns server on your app’s own origin.
Import
import { HornDrawn, HornsAbout, hornsRoutes, provideShapeCatalog } from "@manta-eu/horns-client";
import "@manta-eu/horns-client/styles.css";
The package needs these alongside it: react and react-dom 19, react-router 7, @tanstack/react-query 5, nuqs,
next-themes, @sentry/react, posthog-js, @posthog/react, and @manta-eu/react, @manta-eu/ldo and
@manta-eu/rdf-lib. Install one copy of each: a second copy of React or React Query breaks hooks, and a second copy of
LDKit breaks its types. In Vite, list them under resolve.dedupe.
Example
Add the plugins to your Vite config, with the realm you build for:
import { hornsPlugins } from "@manta-eu/horns-client/vite";
export default defineConfig({
plugins: [react(), ...hornsPlugins(realm)],
});
Hand Horns the shape catalog the plugin built, then splice the pages into your datastore routes:
/// <reference types="@manta-eu/horns-client/shape-catalog" />
import { catalog } from "virtual:horns/shape-catalog";
provideShapeCatalog(() => catalog);
export const datastoreRoutes = (
<Route path="/d/:datastoreId" element={<DatastoreLayout />}>
{hornsRoutes({ shell: <YourAppShell /> })}
</Route>
);
shell is a layout route’s element that renders an <Outlet />, so the Horns pages sit inside your own chrome. The
Studio and a saved Horn are whole screens and ignore it.
To draw Horns inside a page you already have:
<HornDrawn datastoreId={datastoreId} horn={horn} cacheKey="open-tasks" />
<HornsAbout
datastoreId={datastoreId}
about={ruleIri}
limit={2}
hornHref={(slug) => `/d/${datastoreId}/horns/${encodeURIComponent(slug)}/view`}
emptyHint="Nothing is drawn about this rule yet."
/>
cacheKey is what a redraw keys on: the Horn’s revision when it has one, or the thing it was built for.
The page must carry the policy from contentSecurityPolicy() in @manta-eu/horns-server. A written view runs in your
page, and connect-src 'self' is what keeps it from calling anywhere else.
Request
HornDrawn posts the Horn to the Horns server and draws the snapshot that comes back:
POST /api/d/your-datastore/horns/snapshot
Content-Type: application/json
{ "horn": { "…": "the Horn" }, "variables": { "status": "open" } }
HornsAbout first asks which Horns are about the IRI:
GET /api/d/your-datastore/horns/by/about/list?iri=<the IRI>&limit=2
Both carry the viewer’s session cookie for your origin.
Response
The snapshot route answers { "snapshot": … }, which the component hands to
HornSnapshotView. The list route answers with the Horns to draw, and total, how many saved Horns
are about the IRI:
{
"horns": [{ "horn": { "…": "the Horn" }, "kept": true, "slug": "open-tasks", "revision": "…", "contentHash": "…" }],
"total": 1
}
When nothing saved is about the IRI, the server builds a Horn on the spot from the rule or plan itself. It comes back
with "kept": false, no slug and no revision, and total is 0.