Manta Developers
GitHub
Menu

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. HornDrawn draws one Horn you hold, and HornsAbout draws 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.

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.