@manta-eu/horns-client 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,20 @@
1
+ import type { LucideIcon } from "lucide-react";
2
+ export type HornsNavItem = {
3
+ to: string;
4
+ label: string;
5
+ icon: LucideIcon;
6
+ /** Exact for a page that is one page, a prefix where the page has children. */
7
+ isCurrent: (pathname: string) => boolean;
8
+ };
9
+ export type HornsNavSection = {
10
+ label: string;
11
+ items: HornsNavItem[];
12
+ };
13
+ /**
14
+ * Where a person can go inside Horns, in one list.
15
+ *
16
+ * `AppShell` draws it as its sidebar, and a host that supplies a `shell` of its own draws it wherever its
17
+ * own chrome puts navigation — `manta/agents` renders it beside the page, the way Data Explorer does. Two
18
+ * renderings of one list, so a page added here cannot go missing from the other.
19
+ */
20
+ export declare function hornsNavSections(): HornsNavSection[];
@@ -0,0 +1,34 @@
1
+ import { Boxes, Columns3, LayoutGrid, ListChecks, Sigma } from "lucide-react";
2
+ import { hornsPath, plansPath, rulesPath, shapesPath, showcasePath } from "./paths";
3
+ const exact = (path) => (pathname) => pathname === path;
4
+ const under = (path) => (pathname) => pathname.startsWith(path);
5
+ /**
6
+ * Where a person can go inside Horns, in one list.
7
+ *
8
+ * `AppShell` draws it as its sidebar, and a host that supplies a `shell` of its own draws it wherever its
9
+ * own chrome puts navigation — `manta/agents` renders it beside the page, the way Data Explorer does. Two
10
+ * renderings of one list, so a page added here cannot go missing from the other.
11
+ */
12
+ export function hornsNavSections() {
13
+ return [
14
+ {
15
+ label: "Horns",
16
+ items: [
17
+ { to: showcasePath(), label: "Showcase", icon: LayoutGrid, isCurrent: exact(showcasePath()) },
18
+ { to: hornsPath(), label: "Horns", icon: Columns3, isCurrent: exact(hornsPath()) },
19
+ ],
20
+ },
21
+ // The tools behind a Horn: for the people who build them, kept apart so it is not the first thing
22
+ // everyone else is offered.
23
+ {
24
+ label: "For builders",
25
+ items: [
26
+ { to: shapesPath(), label: "Data shapes", icon: Boxes, isCurrent: under(shapesPath()) },
27
+ // The other half of what the datastore knows: not what a thing is, but what Manta works out.
28
+ { to: rulesPath(), label: "Rules", icon: Sigma, isCurrent: under(rulesPath()) },
29
+ // What those rules drafted, and where a Horn is started from one.
30
+ { to: plansPath(), label: "Action plans", icon: ListChecks, isCurrent: under(plansPath()) },
31
+ ],
32
+ },
33
+ ];
34
+ }
@@ -1,3 +1,4 @@
1
+ import type { ReactNode } from "react";
1
2
  /**
2
3
  * Horns' routes as `<Route>` children, for a host that splices them into its own `<Routes>`. A nested
3
4
  * `<Routes>` would rebase every path on its parent's match, so a host mounting Horns under a prefix would
@@ -5,8 +6,15 @@
5
6
  *
6
7
  * `catchAll` is for the host that gives Horns the whole URL space, as `mountHorns` does. A host with routes
7
8
  * of its own leaves it off and keeps its own 404.
9
+ *
10
+ * `shell` is the layout the listed pages draw inside, defaulting to Horns' own. A host that has a shell
11
+ * already passes it, and a Horn then reads as one of that host's pages rather than swapping the chrome for
12
+ * Horns'. It is a layout route's element, so it renders an `<Outlet />`; the Studio and a saved Horn ignore
13
+ * it, because each is a whole screen with nothing beside it.
8
14
  */
9
- export declare function hornsRoutes({ catchAll }?: {
15
+ export type HornsRoutesOptions = {
10
16
  catchAll?: boolean;
11
- }): import("react").JSX.Element;
17
+ shell?: ReactNode;
18
+ };
19
+ export declare function hornsRoutes({ catchAll, shell }?: HornsRoutesOptions): import("react").JSX.Element;
12
20
  export declare function AppRoutes(): import("react").JSX.Element;
@@ -15,19 +15,11 @@ import { ShapeZooPage } from "./shape-zoo-page";
15
15
  import { ShapesPage } from "./shapes-page";
16
16
  import { ShowcasePage } from "./showcase-page";
17
17
  import { ReferencePage } from "./reference-page";
18
- /**
19
- * Horns' routes as `<Route>` children, for a host that splices them into its own `<Routes>`. A nested
20
- * `<Routes>` would rebase every path on its parent's match, so a host mounting Horns under a prefix would
21
- * push `d/:datastoreId/...` a segment too deep; spliced in, the paths are the ones `paths.ts` writes.
22
- *
23
- * `catchAll` is for the host that gives Horns the whole URL space, as `mountHorns` does. A host with routes
24
- * of its own leaves it off and keeps its own 404.
25
- */
26
- export function hornsRoutes({ catchAll = false } = {}) {
18
+ export function hornsRoutes({ catchAll = false, shell } = {}) {
27
19
  return (
28
20
  // Here rather than at the mount: a host splicing these into its own router gets it too, and a Horn page
29
21
  // reads its variables through nuqs on every render — without an adapter that throws rather than degrades.
30
- _jsxs(Route, { element: _jsx(NuqsAdapter, { children: _jsx(Outlet, {}) }), children: [_jsx(Route, { path: "horns/new", element: _jsx(NewHornPage, {}) }), _jsx(Route, { path: "horns/:slug", element: _jsx(HornPage, {}) }), _jsxs(Route, { element: _jsx(AppShell, {}), children: [_jsx(Route, { path: "horns", element: _jsx(HornsPage, {}) }), _jsx(Route, { path: "horns/:slug/view", element: _jsx(HornViewPage, {}) }), _jsx(Route, { path: "horns/showcase", element: _jsx(ShowcasePage, {}) }), _jsx(Route, { path: "horns/plans", element: _jsx(PlansPage, {}) }), _jsx(Route, { path: "horns/rules", element: _jsx(RulesPage, {}) }), _jsx(Route, { path: "horns/rules/:rule", element: _jsx(RulePage, {}) }), _jsx(Route, { path: "horns/shapes", element: _jsx(ShapesPage, {}) }), _jsx(Route, { path: "horns/shapes/:schema", element: _jsx(ShapeZooPage, {}) }), _jsx(Route, { path: "horns/shapes/:schema/entity", element: _jsx(ShapeEntityPage, {}) }), import.meta.env.DEV ? _jsx(Route, { path: "horns/reference", element: _jsx(ReferencePage, {}) }) : null, catchAll ? _jsx(Route, { index: true, element: _jsx(HomePage, {}) }) : null] }), catchAll ? _jsx(Route, { path: "*", element: _jsx(Navigate, { to: "/", replace: true }) }) : null] }));
22
+ _jsxs(Route, { element: _jsx(NuqsAdapter, { children: _jsx(Outlet, {}) }), children: [_jsx(Route, { path: "horns/new", element: _jsx(NewHornPage, {}) }), _jsx(Route, { path: "horns/:slug", element: _jsx(HornPage, {}) }), _jsxs(Route, { element: shell ?? _jsx(AppShell, {}), children: [_jsx(Route, { path: "horns", element: _jsx(HornsPage, {}) }), _jsx(Route, { path: "horns/:slug/view", element: _jsx(HornViewPage, {}) }), _jsx(Route, { path: "horns/showcase", element: _jsx(ShowcasePage, {}) }), _jsx(Route, { path: "horns/plans", element: _jsx(PlansPage, {}) }), _jsx(Route, { path: "horns/rules", element: _jsx(RulesPage, {}) }), _jsx(Route, { path: "horns/rules/:rule", element: _jsx(RulePage, {}) }), _jsx(Route, { path: "horns/shapes", element: _jsx(ShapesPage, {}) }), _jsx(Route, { path: "horns/shapes/:schema", element: _jsx(ShapeZooPage, {}) }), _jsx(Route, { path: "horns/shapes/:schema/entity", element: _jsx(ShapeEntityPage, {}) }), import.meta.env.DEV ? _jsx(Route, { path: "horns/reference", element: _jsx(ReferencePage, {}) }) : null, catchAll ? _jsx(Route, { index: true, element: _jsx(HomePage, {}) }) : null] }), catchAll ? _jsx(Route, { path: "*", element: _jsx(Navigate, { to: "/", replace: true }) }) : null] }));
31
23
  }
32
24
  export function AppRoutes() {
33
25
  return _jsx(Routes, { children: hornsRoutes({ catchAll: true }) });
@@ -1,9 +1,9 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { isPresent } from "@manta-eu/lib/present";
3
3
  import { Link, Outlet, useLocation } from "react-router";
4
- import { Boxes, Columns3, Database, LayoutGrid, ListChecks, Sigma } from "lucide-react";
4
+ import { Database } from "lucide-react";
5
5
  import { MainContent, MantaLogo, NavUser, Sidebar, SidebarContent, SidebarFooter, SidebarGroup, SidebarGroupContent, SidebarGroupLabel, SidebarHeader, SidebarMenu, SidebarMenuButton, SidebarMenuItem, SidebarProvider, SidebarTrigger, } from "@manta-eu/react";
6
- import { hornsPath, plansPath, rulesPath, shapesPath, showcasePath } from "../app/paths";
6
+ import { hornsNavSections } from "../app/nav";
7
7
  import { useActiveDatastore } from "@manta-eu/react/providers";
8
8
  import { useAuthContext } from "@manta-eu/react/providers/AuthContext";
9
9
  function NavLink({ to, icon, label, active }) {
@@ -15,5 +15,5 @@ export function AppShell() {
15
15
  const { user, datastore, datastores = [] } = useAuthContext();
16
16
  const current = datastores.find((candidate) => candidate.id === datastoreId) ?? datastore;
17
17
  const inDatastore = isPresent(datastoreId) && isPresent(current);
18
- return (_jsxs(SidebarProvider, { children: [_jsxs(Sidebar, { collapsible: "icon", children: [_jsx(SidebarHeader, { children: _jsxs(Link, { to: "/", className: "flex items-center gap-2 px-2 py-1.5", "aria-label": "Manta Horns", children: [_jsx(MantaLogo, { size: 24 }), _jsx("span", { className: "font-semibold group-data-[collapsible=icon]:hidden", children: "Horns" })] }) }), _jsx(SidebarContent, { children: _jsxs("nav", { "aria-label": "Datastore", className: "flex flex-col gap-2", children: [isPresent(datastoreId) && inDatastore ? (_jsxs(_Fragment, { children: [_jsxs(SidebarGroup, { children: [_jsx(SidebarGroupLabel, { children: current.name }), _jsx(SidebarGroupContent, { children: _jsxs(SidebarMenu, { children: [_jsx(NavLink, { to: showcasePath(), icon: _jsx(LayoutGrid, { "aria-hidden": "true" }), label: "Showcase", active: pathname === showcasePath() }), _jsx(NavLink, { to: hornsPath(), icon: _jsx(Columns3, { "aria-hidden": "true" }), label: "Horns", active: pathname === hornsPath() })] }) })] }), _jsxs(SidebarGroup, { children: [_jsx(SidebarGroupLabel, { children: "For builders" }), _jsx(SidebarGroupContent, { children: _jsxs(SidebarMenu, { children: [_jsx(NavLink, { to: shapesPath(), icon: _jsx(Boxes, { "aria-hidden": "true" }), label: "Data shapes", active: pathname.startsWith(shapesPath()) }), _jsx(NavLink, { to: rulesPath(), icon: _jsx(Sigma, { "aria-hidden": "true" }), label: "Rules", active: pathname.startsWith(rulesPath()) }), _jsx(NavLink, { to: plansPath(), icon: _jsx(ListChecks, { "aria-hidden": "true" }), label: "Action plans", active: pathname.startsWith(plansPath()) })] }) })] })] })) : null, _jsxs(SidebarGroup, { children: [_jsx(SidebarGroupLabel, { children: "All datastores" }), _jsx(SidebarGroupContent, { children: _jsx(SidebarMenu, { children: _jsx(NavLink, { to: "/", icon: _jsx(Database, { "aria-hidden": "true" }), label: `Datastores (${datastores.length})`, active: pathname === "/" }) }) })] })] }) }), _jsx(SidebarFooter, { children: _jsx(NavUser, { name: user?.name ?? user?.email ?? "Signed in", currentDatastore: { name: current?.name ?? "No datastore" }, onNavigateToAccount: () => window.location.assign("/"), onLogout: () => window.location.assign("/logout") }) })] }), _jsx(MainContent, { className: "pb-8", children: _jsxs("div", { className: "flex w-full flex-col gap-4 px-4 sm:px-6", children: [_jsx(SidebarTrigger, { "aria-label": "Toggle Sidebar" }), _jsx(Outlet, {})] }) })] }));
18
+ return (_jsxs(SidebarProvider, { children: [_jsxs(Sidebar, { collapsible: "icon", children: [_jsx(SidebarHeader, { children: _jsxs(Link, { to: "/", className: "flex items-center gap-2 px-2 py-1.5", "aria-label": "Manta Horns", children: [_jsx(MantaLogo, { size: 24 }), _jsx("span", { className: "font-semibold group-data-[collapsible=icon]:hidden", children: "Horns" })] }) }), _jsx(SidebarContent, { children: _jsxs("nav", { "aria-label": "Datastore", className: "flex flex-col gap-2", children: [isPresent(datastoreId) && inDatastore ? (_jsx(_Fragment, { children: hornsNavSections().map((section, index) => (_jsxs(SidebarGroup, { children: [_jsx(SidebarGroupLabel, { children: index === 0 ? current.name : section.label }), _jsx(SidebarGroupContent, { children: _jsx(SidebarMenu, { children: section.items.map(({ to, label, icon: Icon, isCurrent }) => (_jsx(NavLink, { to: to, icon: _jsx(Icon, { "aria-hidden": "true" }), label: label, active: isCurrent(pathname) }, to))) }) })] }, section.label))) })) : null, _jsxs(SidebarGroup, { children: [_jsx(SidebarGroupLabel, { children: "All datastores" }), _jsx(SidebarGroupContent, { children: _jsx(SidebarMenu, { children: _jsx(NavLink, { to: "/", icon: _jsx(Database, { "aria-hidden": "true" }), label: `Datastores (${datastores.length})`, active: pathname === "/" }) }) })] })] }) }), _jsx(SidebarFooter, { children: _jsx(NavUser, { name: user?.name ?? user?.email ?? "Signed in", currentDatastore: { name: current?.name ?? "No datastore" }, onNavigateToAccount: () => window.location.assign("/"), onLogout: () => window.location.assign("/logout") }) })] }), _jsx(MainContent, { className: "pb-8", children: _jsxs("div", { className: "flex w-full flex-col gap-4 px-4 sm:px-6", children: [_jsx(SidebarTrigger, { "aria-label": "Toggle Sidebar" }), _jsx(Outlet, {})] }) })] }));
19
19
  }
@@ -1,3 +1,4 @@
1
+ import { type Horn } from "@manta-eu/horns";
1
2
  import { type HornSnapshotViewProps } from "@manta-eu/horns-kit";
2
3
  /**
3
4
  * Where the worker answers for a thing. The IRI goes in a query parameter and is encoded: a plan's carries
@@ -6,6 +7,29 @@ import { type HornSnapshotViewProps } from "@manta-eu/horns-kit";
6
7
  export declare function hornAboutPath(datastoreId: string, about: string): string;
7
8
  /** Where a Horn is turned into rows to draw. */
8
9
  export declare function hornSnapshotPath(datastoreId: string): string;
10
+ /** Where every Horn about a thing is listed. */
11
+ export declare function hornsAboutPath(datastoreId: string, about: string, limit?: number): string;
12
+ /** What the drawing needs, apart from which Horn it is. */
13
+ export type HornDrawProps = {
14
+ mode?: HornSnapshotViewProps["mode"];
15
+ planActions?: HornSnapshotViewProps["planActions"];
16
+ explain?: HornSnapshotViewProps["explain"];
17
+ onVariableChange?: HornSnapshotViewProps["onVariableChange"];
18
+ };
19
+ /**
20
+ * One Horn, turned into rows by the worker and drawn.
21
+ *
22
+ * Written views and catalog cards are bound here because they need `@manta-eu/react`, which the kit has
23
+ * not got. Everything else a host wants to hang on it is a prop.
24
+ */
25
+ export type HornDrawnProps = HornDrawProps & {
26
+ datastoreId: string;
27
+ horn: Horn;
28
+ /** What a redraw keys on: the revision where there is one, or the thing the Horn was built for. */
29
+ cacheKey: string;
30
+ variables?: Record<string, unknown>;
31
+ };
32
+ export declare function HornDrawn(props: HornDrawnProps): import("react").JSX.Element;
9
33
  export type HornAboutProps = {
10
34
  datastoreId: string;
11
35
  /** The thing to show: an action plan, a rule. Opaque — it is only ever passed on. */
@@ -28,6 +28,35 @@ export function hornAboutPath(datastoreId, about) {
28
28
  export function hornSnapshotPath(datastoreId) {
29
29
  return `/api/d/${encodeURIComponent(datastoreId)}/horns/snapshot`;
30
30
  }
31
+ /** Where every Horn about a thing is listed. */
32
+ export function hornsAboutPath(datastoreId, about, limit) {
33
+ const capped = limit === undefined ? "" : `&limit=${limit}`;
34
+ return `/api/d/${encodeURIComponent(datastoreId)}/horns/by/about/list?iri=${encodeURIComponent(about)}${capped}`;
35
+ }
36
+ export function HornDrawn(props) {
37
+ const { datastoreId, horn, cacheKey, variables = {}, mode = "embed", planActions, explain, onVariableChange } = props;
38
+ const snapshot = useQuery({
39
+ enabled: isPresent(horn),
40
+ queryKey: ["horn-snapshot", datastoreId, cacheKey, variables],
41
+ queryFn: async ({ signal }) => {
42
+ const response = await fetch(hornSnapshotPath(datastoreId), {
43
+ method: "POST",
44
+ headers: { "content-type": "application/json" },
45
+ body: JSON.stringify({ horn, variables }),
46
+ signal,
47
+ });
48
+ if (!response.ok)
49
+ throw new Error(await problemFrom(response, "The Horn could not be drawn."));
50
+ const body = await response.json();
51
+ return hornSnapshotSchema.parse(isRecord(body) ? body["snapshot"] : undefined);
52
+ },
53
+ });
54
+ if (snapshot.isError)
55
+ return _jsx(ErrorState, { title: "This could not be drawn", hint: snapshot.error.message });
56
+ if (!isPresent(snapshot.data))
57
+ return _jsx(Loading, { label: "Drawing this\u2026", rows: 3 });
58
+ return (_jsx(HornSnapshotView, { snapshot: snapshot.data, mode: mode, theme: appTheme(), writtenView: writtenHornView, planStrip: PlanChanges, catalogCard: catalogCardView, ...(isPresent(planActions) ? { planActions } : {}), ...(isPresent(explain) ? { explain } : {}), ...(isPresent(onVariableChange) ? { onVariableChange } : {}) }));
59
+ }
31
60
  /**
32
61
  * The Horn for a thing, drawn wherever a host puts it.
33
62
  *
@@ -49,28 +78,10 @@ export function HornAbout(props) {
49
78
  return aboutAnswer.parse(await response.json());
50
79
  },
51
80
  });
52
- const horn = seed.data?.horn;
53
- const snapshot = useQuery({
54
- enabled: isPresent(horn),
55
- queryKey: ["horn-about-snapshot", datastoreId, about, seed.data?.revision ?? "", variables],
56
- queryFn: async ({ signal }) => {
57
- const response = await fetch(hornSnapshotPath(datastoreId), {
58
- method: "POST",
59
- headers: { "content-type": "application/json" },
60
- body: JSON.stringify({ horn, variables }),
61
- signal,
62
- });
63
- if (!response.ok)
64
- throw new Error(await problemFrom(response, "The Horn could not be drawn."));
65
- const body = await response.json();
66
- return hornSnapshotSchema.parse(isRecord(body) ? body["snapshot"] : undefined);
67
- },
68
- });
69
81
  if (seed.isError)
70
82
  return _jsx(ErrorState, { title: "Nothing could be shown for this", hint: seed.error.message });
71
- if (snapshot.isError)
72
- return _jsx(ErrorState, { title: "This could not be drawn", hint: snapshot.error.message });
73
- if (!isPresent(snapshot.data))
83
+ const horn = seed.data?.horn;
84
+ if (!isPresent(horn))
74
85
  return _jsx(Loading, { label: "Drawing this\u2026", rows: 3 });
75
- return (_jsx(HornSnapshotView, { snapshot: snapshot.data, mode: mode, theme: appTheme(), writtenView: writtenHornView, planStrip: PlanChanges, catalogCard: catalogCardView, ...(isPresent(planActions) ? { planActions } : {}), ...(isPresent(explain) ? { explain } : {}), ...(isPresent(onVariableChange) ? { onVariableChange } : {}) }));
86
+ return (_jsx(HornDrawn, { datastoreId: datastoreId, horn: horn, cacheKey: seed.data?.revision ?? about, variables: variables, mode: mode, ...(isPresent(planActions) ? { planActions } : {}), ...(isPresent(explain) ? { explain } : {}), ...(isPresent(onVariableChange) ? { onVariableChange } : {}) }));
76
87
  }
@@ -0,0 +1,29 @@
1
+ import { type HornDrawProps } from "./horn-about";
2
+ export type HornsAboutProps = HornDrawProps & {
3
+ datastoreId: string;
4
+ /** The thing these are about: a rule, a plan, a campaign, a person. Opaque — only ever passed on. */
5
+ about: string;
6
+ /** How many to draw. The worker caps it; leaving it off takes the worker's default. */
7
+ limit?: number;
8
+ variables?: Record<string, unknown>;
9
+ /**
10
+ * Where a Horn opens in full, where the host has somewhere to send a person. A plain href, not a route:
11
+ * the Horns UI may be another document, or another origin entirely.
12
+ */
13
+ hornHref?: (slug: string) => string;
14
+ /** What to say when nothing is about this thing yet. */
15
+ emptyHint?: string;
16
+ };
17
+ /**
18
+ * Every Horn about one thing, drawn.
19
+ *
20
+ * This is the whole of embedding Horns: a host that has an IRI — a rule, an action plan, a campaign, a
21
+ * person — puts this beside it and gets what Manta already knows about that thing, as pictures rather than
22
+ * links. It needs no saved Horn, no route of the host's and no Studio: the worker answers with the kept
23
+ * ones, or builds one from the rule or plan itself.
24
+ *
25
+ * The one thing a host must bring is the policy. A written view runs in the page's own realm, so the
26
+ * document has to carry `connect-src 'self'` and the rest of `contentSecurityPolicy()`; without it the
27
+ * views still draw, but nothing fences the code they came from.
28
+ */
29
+ export declare function HornsAbout(props: HornsAboutProps): import("react").JSX.Element;
@@ -0,0 +1,50 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useQuery } from "@tanstack/react-query";
3
+ import { z } from "zod";
4
+ import { isPresent } from "@manta-eu/lib/present";
5
+ import { hornSchema } from "@manta-eu/horns";
6
+ import { EmptyState, ErrorState, Loading } from "../components/states";
7
+ import { problemFrom } from "../lib/problem";
8
+ import { HornDrawn, hornsAboutPath } from "./horn-about";
9
+ const listAnswer = z.object({
10
+ horns: z.array(z.object({
11
+ horn: hornSchema,
12
+ kept: z.boolean(),
13
+ slug: z.string().min(1).optional(),
14
+ revision: z.string().min(1).optional(),
15
+ })),
16
+ total: z.number().int().min(0),
17
+ });
18
+ /**
19
+ * Every Horn about one thing, drawn.
20
+ *
21
+ * This is the whole of embedding Horns: a host that has an IRI — a rule, an action plan, a campaign, a
22
+ * person — puts this beside it and gets what Manta already knows about that thing, as pictures rather than
23
+ * links. It needs no saved Horn, no route of the host's and no Studio: the worker answers with the kept
24
+ * ones, or builds one from the rule or plan itself.
25
+ *
26
+ * The one thing a host must bring is the policy. A written view runs in the page's own realm, so the
27
+ * document has to carry `connect-src 'self'` and the rest of `contentSecurityPolicy()`; without it the
28
+ * views still draw, but nothing fences the code they came from.
29
+ */
30
+ export function HornsAbout(props) {
31
+ const { datastoreId, about, limit, variables = {}, hornHref, emptyHint, ...draw } = props;
32
+ const listed = useQuery({
33
+ queryKey: ["horns-about", datastoreId, about, limit ?? "default"],
34
+ queryFn: async ({ signal }) => {
35
+ const response = await fetch(hornsAboutPath(datastoreId, about, limit), { signal });
36
+ if (!response.ok)
37
+ throw new Error(await problemFrom(response, `The answer was ${response.status}.`));
38
+ return listAnswer.parse(await response.json());
39
+ },
40
+ });
41
+ if (listed.isError)
42
+ return _jsx(ErrorState, { title: "Nothing could be shown for this", hint: listed.error.message });
43
+ if (!isPresent(listed.data))
44
+ return _jsx(Loading, { label: "Looking for what is known about this\u2026", rows: 2 });
45
+ const { horns, total } = listed.data;
46
+ if (horns.length === 0) {
47
+ return (_jsx(EmptyState, { title: "Nothing is about this yet", hint: emptyHint ?? "Once somebody makes a Horn about it, it will be drawn here." }));
48
+ }
49
+ return (_jsxs("div", { "data-horns-about": true, className: "flex w-full flex-col gap-8", children: [horns.map(({ horn, slug, revision }) => (_jsxs("section", { "data-horn": slug, className: "flex flex-col gap-3", children: [_jsxs("div", { className: "flex flex-col gap-1", children: [_jsx("h2", { className: "text-lg font-semibold text-default-foreground", children: isPresent(slug) && isPresent(hornHref) ? (_jsx("a", { href: hornHref(slug), className: "hover:underline", children: horn.title })) : (horn.title) }), isPresent(horn.summary) ? _jsx("p", { className: "text-muted-foreground text-sm", children: horn.summary }) : null] }), _jsx(HornDrawn, { datastoreId: datastoreId, horn: horn, cacheKey: revision ?? slug ?? about, variables: variables, ...draw })] }, slug ?? revision ?? horn.title))), total > horns.length ? (_jsxs("p", { className: "text-sm text-muted-foreground", children: [total - horns.length, " more ", total - horns.length === 1 ? "Horn is" : "Horns are", " about this."] })) : null] }));
50
+ }
package/dist/index.d.ts CHANGED
@@ -1,6 +1,9 @@
1
- export { AppRoutes, hornsRoutes } from "./app/routes.tsx";
1
+ export { AppRoutes, hornsRoutes, type HornsRoutesOptions } from "./app/routes.tsx";
2
+ export { hornsNavSections, type HornsNavItem, type HornsNavSection } from "./app/nav.ts";
2
3
  export { HornsApp } from "./standalone/app.tsx";
3
- export { HornAbout, type HornAboutProps } from "./embed/horn-about.tsx";
4
+ export { HornAbout, HornDrawn, hornAboutPath, hornsAboutPath, hornSnapshotPath } from "./embed/horn-about.tsx";
5
+ export type { HornAboutProps, HornDrawProps, HornDrawnProps } from "./embed/horn-about.tsx";
6
+ export { HornsAbout, type HornsAboutProps } from "./embed/horns-about.tsx";
4
7
  export { AppShell } from "./components/app-shell.tsx";
5
8
  export { signInHref } from "./lib/manta/sign-in.ts";
6
9
  export { useSparqlSelect } from "./lib/manta/sparql-select.ts";
package/dist/index.js CHANGED
@@ -7,12 +7,16 @@
7
7
  // What the browser needs beyond this is the stylesheet: `@manta-eu/horns-client/styles.css`, built here
8
8
  // rather than compiled by the host, so a host with no Tailwind still gets the app looking like Manta.
9
9
  export { AppRoutes, hornsRoutes } from "./app/routes.js";
10
+ // What a host with a shell of its own draws as navigation, so a page cannot be in one list and not the other.
11
+ export { hornsNavSections } from "./app/nav.js";
10
12
  // The session gate, and the only thing that provides the AuthContext every Horns page reads. A host
11
13
  // splicing `hornsRoutes()` into a router of its own mounts this above them, or provides that context itself.
12
14
  export { HornsApp } from "./standalone/app.js";
13
15
  // A thing's Horn, drawn inside a host's own screen: it asks the worker for the kept Horn or one built
14
16
  // from the plan or rule itself, so a host needs no saved Horn and no route of its own.
15
- export { HornAbout } from "./embed/horn-about.js";
17
+ export { HornAbout, HornDrawn, hornAboutPath, hornsAboutPath, hornSnapshotPath } from "./embed/horn-about.js";
18
+ // Every Horn about one thing: what a host with an IRI embeds.
19
+ export { HornsAbout } from "./embed/horns-about.js";
16
20
  export { AppShell } from "./components/app-shell.js";
17
21
  export { signInHref } from "./lib/manta/sign-in.js";
18
22
  export { useSparqlSelect } from "./lib/manta/sparql-select.js";