@manta-eu/horns-client 0.1.0

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.
Files changed (101) hide show
  1. package/LICENSE +21 -0
  2. package/dist/app/coverage-run.d.ts +5 -0
  3. package/dist/app/coverage-run.js +221 -0
  4. package/dist/app/home-page.d.ts +1 -0
  5. package/dist/app/home-page.js +11 -0
  6. package/dist/app/horn-page.d.ts +1 -0
  7. package/dist/app/horn-page.js +38 -0
  8. package/dist/app/horn-studio.d.ts +30 -0
  9. package/dist/app/horn-studio.js +428 -0
  10. package/dist/app/horn-view-page.d.ts +1 -0
  11. package/dist/app/horn-view-page.js +96 -0
  12. package/dist/app/horns-page.d.ts +1 -0
  13. package/dist/app/horns-page.js +43 -0
  14. package/dist/app/new-horn-page.d.ts +1 -0
  15. package/dist/app/new-horn-page.js +34 -0
  16. package/dist/app/paths.d.ts +13 -0
  17. package/dist/app/paths.js +32 -0
  18. package/dist/app/routes.d.ts +12 -0
  19. package/dist/app/routes.js +32 -0
  20. package/dist/app/rule-page.d.ts +1 -0
  21. package/dist/app/rule-page.js +84 -0
  22. package/dist/app/rules-page.d.ts +1 -0
  23. package/dist/app/rules-page.js +74 -0
  24. package/dist/app/saved-horn.d.ts +7 -0
  25. package/dist/app/saved-horn.js +26 -0
  26. package/dist/app/session-gate.d.ts +6 -0
  27. package/dist/app/session-gate.js +19 -0
  28. package/dist/app/shape-entity-page.d.ts +1 -0
  29. package/dist/app/shape-entity-page.js +68 -0
  30. package/dist/app/shape-view-studio.d.ts +14 -0
  31. package/dist/app/shape-view-studio.js +163 -0
  32. package/dist/app/shape-zoo-page.d.ts +1 -0
  33. package/dist/app/shape-zoo-page.js +58 -0
  34. package/dist/app/shapes-page.d.ts +1 -0
  35. package/dist/app/shapes-page.js +75 -0
  36. package/dist/app/showcase-page.d.ts +2 -0
  37. package/dist/app/showcase-page.js +99 -0
  38. package/dist/app/variable-bar.d.ts +6 -0
  39. package/dist/app/variable-bar.js +42 -0
  40. package/dist/client.d.ts +10 -0
  41. package/dist/client.js +22 -0
  42. package/dist/components/app-shell.d.ts +1 -0
  43. package/dist/components/app-shell.js +18 -0
  44. package/dist/components/catalog-card-body.d.ts +8 -0
  45. package/dist/components/catalog-card-body.js +29 -0
  46. package/dist/components/catalog-card-view.d.ts +2 -0
  47. package/dist/components/catalog-card-view.js +13 -0
  48. package/dist/components/chat-column.d.ts +22 -0
  49. package/dist/components/chat-column.js +20 -0
  50. package/dist/components/page-header.d.ts +15 -0
  51. package/dist/components/page-header.js +11 -0
  52. package/dist/components/plan-runs.d.ts +28 -0
  53. package/dist/components/plan-runs.js +90 -0
  54. package/dist/components/rasterize.d.ts +34 -0
  55. package/dist/components/rasterize.js +115 -0
  56. package/dist/components/read-the-code.d.ts +14 -0
  57. package/dist/components/read-the-code.js +43 -0
  58. package/dist/components/snapshot-view.d.ts +12 -0
  59. package/dist/components/snapshot-view.js +25 -0
  60. package/dist/components/states.d.ts +23 -0
  61. package/dist/components/states.js +22 -0
  62. package/dist/components/studio-chat.d.ts +47 -0
  63. package/dist/components/studio-chat.js +63 -0
  64. package/dist/components/written-horn-view.d.ts +15 -0
  65. package/dist/components/written-horn-view.js +25 -0
  66. package/dist/index.d.ts +11 -0
  67. package/dist/index.js +16 -0
  68. package/dist/lib/horn-review.d.ts +27 -0
  69. package/dist/lib/horn-review.js +59 -0
  70. package/dist/lib/manta/__tests__/sdk-realm.d.ts +2 -0
  71. package/dist/lib/manta/__tests__/sdk-realm.js +17 -0
  72. package/dist/lib/manta/ldo.d.ts +6 -0
  73. package/dist/lib/manta/ldo.js +14 -0
  74. package/dist/lib/manta/providers.d.ts +10 -0
  75. package/dist/lib/manta/providers.js +48 -0
  76. package/dist/lib/manta/session.d.ts +47 -0
  77. package/dist/lib/manta/session.js +204 -0
  78. package/dist/lib/manta/sparql-select.d.ts +2 -0
  79. package/dist/lib/manta/sparql-select.js +19 -0
  80. package/dist/lib/plan-runs.d.ts +14 -0
  81. package/dist/lib/plan-runs.js +26 -0
  82. package/dist/lib/problem.d.ts +8 -0
  83. package/dist/lib/problem.js +16 -0
  84. package/dist/lib/shapes/catalog.d.ts +14 -0
  85. package/dist/lib/shapes/catalog.js +25 -0
  86. package/dist/lib/shapes/ldkit-source.d.ts +14 -0
  87. package/dist/lib/shapes/ldkit-source.js +26 -0
  88. package/dist/lib/studio-progress.d.ts +7 -0
  89. package/dist/lib/studio-progress.js +36 -0
  90. package/dist/lib/telemetry/sentry.d.ts +1 -0
  91. package/dist/lib/telemetry/sentry.js +17 -0
  92. package/dist/lib/theme.d.ts +2 -0
  93. package/dist/lib/theme.js +7 -0
  94. package/dist/lib/time-ago.d.ts +5 -0
  95. package/dist/lib/time-ago.js +35 -0
  96. package/dist/lib/written-surface.d.ts +9 -0
  97. package/dist/lib/written-surface.js +10 -0
  98. package/dist/render/horn-client.d.ts +1 -0
  99. package/dist/render/horn-client.js +10 -0
  100. package/dist/styles.css +2 -0
  101. package/package.json +77 -0
@@ -0,0 +1,58 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useQuery } from "@tanstack/react-query";
3
+ import { isPresent } from "@manta-eu/lib/present";
4
+ import { Link, useParams } from "react-router";
5
+ import { ChevronRight } from "lucide-react";
6
+ import { Badge } from "@manta-eu/react";
7
+ import { shapeViewSpecs } from "@manta-eu/horns-kit";
8
+ import { auditShapeZoo, buildShapeZoo, shapeViewProblems, summarizeZooFindings, zooProblems, } from "@manta-eu/horns/shapes";
9
+ import { shapeEntityPath, shapesPath } from "./paths";
10
+ import { ShapeViewStudio } from "./shape-view-studio";
11
+ import { SnapshotViewBody } from "../components/snapshot-view";
12
+ import { PageHeader } from "../components/page-header";
13
+ import { DatastoreMissing, EmptyState, ErrorState, Loading } from "../components/states";
14
+ import { MantaSparqlProvider } from "../lib/manta/providers";
15
+ import { useMantaSession } from "../lib/manta/session";
16
+ import { browserSparqlSelect } from "../lib/manta/sparql-select";
17
+ import { ldkitShapeCatalog, shapesRealm } from "../lib/shapes/catalog";
18
+ function Problems({ heading, problems }) {
19
+ if (!isPresent(problems))
20
+ return null;
21
+ return (_jsxs("div", { role: "alert", "data-problems": true, className: "rounded-lg border border-error bg-error/10 px-4 py-3 text-sm", children: [_jsx("p", { className: "font-medium text-default-foreground", children: heading }), _jsx("ul", { className: "mt-2 flex list-disc flex-col gap-1 pl-5 text-muted-foreground", children: problems.map((problem) => (_jsx("li", { children: problem }, problem))) })] }));
22
+ }
23
+ function Zoo(props) {
24
+ const { datastoreId, spec, version } = props;
25
+ const realm = shapesRealm();
26
+ const zoo = useQuery({
27
+ // Keyed on the version: a changed view is a different zoo, and returning the cached one would show the
28
+ // card the person just moved away from.
29
+ queryKey: ["shape-zoo", datastoreId, spec.schema, version],
30
+ queryFn: ({ signal }) => buildShapeZoo({ spec, realm, datastoreId, select: browserSparqlSelect(datastoreId), signal }),
31
+ });
32
+ if (zoo.isPending)
33
+ return _jsx(Loading, { label: "Sampling instances\u2026", rows: 4 });
34
+ if (zoo.isError)
35
+ return _jsx(ErrorState, { title: "The zoo could not be built.", hint: zoo.error.message });
36
+ const { sampled, distinct, variants, fillRates, snapshot } = zoo.data;
37
+ const view = snapshot.views[0];
38
+ const content = view?.content;
39
+ if (sampled === 0) {
40
+ return (_jsx(EmptyState, { title: `This datastore holds no ${spec.schema.replace(/Schema$/, "")} instances yet.`, hint: "A zoo needs real data to show the variants a view has to survive." }));
41
+ }
42
+ return (_jsxs("div", { className: "flex flex-col gap-4", children: [_jsx("p", { className: "text-sm text-muted-foreground", "data-zoo-summary": true, children: `Sampled ${sampled} instances · ${distinct} distinct variants · showing ${variants.length}` }), _jsx(Problems, { heading: "What the zoo shows is wrong with this view", problems: [...zooProblems(zoo.data, spec), ...summarizeZooFindings(auditShapeZoo(zoo.data))] }), _jsxs("details", { className: "group rounded-lg border border-default-border bg-card", children: [_jsxs("summary", { className: "flex cursor-pointer list-none items-center gap-3 px-4 py-3", children: [_jsx(ChevronRight, { size: 16, "aria-hidden": "true", className: "shrink-0 text-muted-foreground transition-transform group-open:rotate-90" }), _jsx("span", { className: "font-medium text-default-foreground", children: "How often each field is filled" })] }), _jsx("ul", { className: "divide-y divide-default-border border-t border-default-border", children: spec.fields.map((field) => (_jsxs("li", { className: "flex items-center justify-between gap-4 px-4 py-1.5 text-sm", children: [_jsx("span", { className: "font-mono text-default-foreground", children: field.name }), _jsx("span", { className: "text-muted-foreground", children: `${Math.round((fillRates[field.name] ?? 0) * 100)}%` })] }, field.name))) })] }), content?.lane === "error" ? _jsx(ErrorState, { title: "The view failed.", hint: content.message }) : null, _jsx("div", { className: "grid gap-4 [grid-template-columns:repeat(auto-fit,minmax(22rem,1fr))]", children: variants.map((variant, index) => (_jsxs("section", { "data-zoo-variant": variant.entity, className: "flex flex-col gap-2 rounded-lg border border-default-border bg-card p-4", children: [_jsxs("header", { className: "flex flex-col gap-1", children: [_jsx("h2", { className: "text-sm font-medium", children: _jsx(Link, { className: "text-default-foreground hover:underline", to: shapeEntityPath(datastoreId, spec.schema, variant.entity), children: `Variant ${index + 1}` }) }), _jsxs("p", { className: "text-xs text-muted-foreground", children: [`${variant.populated.length} of ${spec.fields.length} fields · ${variant.lookalikes} ${variant.lookalikes === 1 ? "instance" : "instances"} like this`, isPresent(variant.missing) ? ` · no ${variant.missing.join(", ")}` : ""] })] }), view && content?.lane === "sandbox" ? (_jsx(SnapshotViewBody, { snapshot: snapshot, view: {
43
+ ...view,
44
+ title: `${spec.schema} variant ${index + 1}`,
45
+ content: { ...content, rows: content.rows.filter((row) => row["this"] === variant.entity) },
46
+ } })) : null] }, variant.entity))) })] }));
47
+ }
48
+ export function ShapeZooPage() {
49
+ const { datastoreId, schema } = useParams();
50
+ const { datastores } = useMantaSession();
51
+ const datastore = datastores.find((candidate) => candidate.id === datastoreId);
52
+ if (!isPresent(datastore) || !isPresent(schema))
53
+ return _jsx(DatastoreMissing, {});
54
+ const realm = shapesRealm();
55
+ const shape = ldkitShapeCatalog().shapes.find((candidate) => candidate.schema === schema);
56
+ const spec = shapeViewSpecs.find((candidate) => candidate.schema === schema);
57
+ return (_jsx(MantaSparqlProvider, { datastoreId: datastore.id, children: _jsxs("div", { className: "flex w-full flex-col gap-6", children: [_jsx(PageHeader, { title: `${schema} zoo`, description: "Real instances of this shape, one per variant of the fields it fills. Every card must survive all of them.", meta: spec ? (_jsx(Badge, { variant: spec.status === "curated" ? "success" : "info", children: `${spec.classCurie} · ${spec.status}` })) : undefined, actions: _jsx(Link, { className: "text-sm text-muted-foreground hover:text-default-foreground", to: shapesPath(datastore.id), children: "Back to shapes" }) }), !shape ? (_jsx(ErrorState, { title: `${schema} is not a shape the SDK knows.` })) : !spec ? (_jsx(EmptyState, { title: `${shape.schema} has no HornView yet.`, hint: "Run the Studio over the coverage worklist to draft one." })) : (_jsx(ShapeViewStudio, { datastoreId: datastore.id, stored: spec, children: (current, version) => (_jsxs(_Fragment, { children: [_jsx(Problems, { heading: "This view does not fit its shape", problems: shapeViewProblems(current, shape, realm) }), _jsx(Zoo, { datastoreId: datastore.id, spec: current, version: version })] })) }))] }) }));
58
+ }
@@ -0,0 +1 @@
1
+ export declare function ShapesPage(): import("react").JSX.Element;
@@ -0,0 +1,75 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useQuery } from "@tanstack/react-query";
3
+ import { isPresent } from "@manta-eu/lib/present";
4
+ import { Link, useParams } from "react-router";
5
+ import { ChevronRight } from "lucide-react";
6
+ import { Badge, Progress, Skeleton } from "@manta-eu/react";
7
+ import { compactIri } from "@manta-eu/horns";
8
+ import { shapeViewSpecs } from "@manta-eu/horns-kit";
9
+ import { coverageWorklist, instanceCountsQuery, observedClassesQuery, readInstanceCounts, readObservedClasses, shapeCoverage, } from "@manta-eu/horns/shapes";
10
+ import { CoverageRun } from "./coverage-run";
11
+ import { shapeZooPath } from "./paths";
12
+ import { Card, CardTitle, PageHeader } from "../components/page-header";
13
+ import { DatastoreMissing, ErrorState } from "../components/states";
14
+ import { MantaSparqlProvider } from "../lib/manta/providers";
15
+ import { useMantaSession } from "../lib/manta/session";
16
+ import { browserSparqlSelect } from "../lib/manta/sparql-select";
17
+ import { ldkitShapeCatalog, shapesRealm } from "../lib/shapes/catalog";
18
+ function percentOf(part, whole) {
19
+ return whole === 0 ? 0 : Math.round((part / whole) * 1000) / 10;
20
+ }
21
+ // Past this the list stops being a queue and becomes the namespace list again.
22
+ const maxReviewLinks = 24;
23
+ function StatusBadge({ status }) {
24
+ if (status === "curated")
25
+ return _jsx(Badge, { variant: "success", children: "Curated" });
26
+ if (status === "drafted")
27
+ return _jsx(Badge, { variant: "info", children: "Drafted" });
28
+ return _jsx(Badge, { variant: "secondary", children: "No view" });
29
+ }
30
+ function ShapeRow({ entry, realm, datastoreId, }) {
31
+ const shape = entry.shape;
32
+ return (_jsxs("li", { "data-shape": shape.schema, "data-status": entry.status, className: "flex flex-wrap items-center justify-between gap-3 px-4 py-2.5", children: [_jsxs("span", { className: "flex min-w-0 flex-col", children: [_jsx("span", { className: "truncate font-mono text-sm text-default-foreground", children: shape.schema }), _jsxs("span", { className: "truncate text-xs text-muted-foreground", children: [`${compactIri(realm, shape.classIri)} · ${shape.properties.length} properties`, entry.observed ? " · in this datastore" : ""] })] }), _jsxs("span", { className: "flex shrink-0 items-center gap-3", children: [_jsx(StatusBadge, { status: entry.status }), entry.status === "missing" ? null : (_jsx(Link, { className: "text-sm text-muted-foreground underline hover:text-default-foreground", to: shapeZooPath(datastoreId, shape.schema), children: entry.status === "curated" ? "Zoo" : "Zoo (draft)" }))] })] }));
33
+ }
34
+ function ShapeCoverageDashboard({ datastoreId }) {
35
+ const realm = shapesRealm();
36
+ const catalog = ldkitShapeCatalog();
37
+ const observed = useQuery({
38
+ queryKey: ["observed-classes", datastoreId],
39
+ queryFn: async ({ signal }) => readObservedClasses(await browserSparqlSelect(datastoreId)(observedClassesQuery(realm), signal)),
40
+ });
41
+ // The queue is ordered by how much data a class has, so the views worth building come first. Counted in one
42
+ // query, bounded by the classes this datastore actually holds.
43
+ const counts = useQuery({
44
+ queryKey: ["instance-counts", datastoreId, observed.dataUpdatedAt],
45
+ enabled: isPresent(observed.data),
46
+ queryFn: async ({ signal }) => {
47
+ const held = catalog.shapes.filter((shape) => observed.data?.has(shape.classIri)).map((shape) => shape.classIri);
48
+ if (!isPresent(held))
49
+ return new Map();
50
+ return readInstanceCounts(await browserSparqlSelect(datastoreId)(instanceCountsQuery(held), signal));
51
+ },
52
+ });
53
+ const coverage = shapeCoverage({
54
+ shapes: catalog.shapes,
55
+ views: shapeViewSpecs,
56
+ ...(observed.data ? { observedClasses: observed.data } : {}),
57
+ ...(counts.data ? { instanceCounts: counts.data } : {}),
58
+ });
59
+ const worklist = coverageWorklist(coverage, 8);
60
+ const drafted = coverage.namespaces.flatMap((group) => group.shapes.filter((entry) => entry.status === "drafted"));
61
+ const percent = percentOf(coverage.covered, coverage.total);
62
+ return (_jsxs("div", { className: "flex flex-col gap-4", children: [_jsxs(Card, { children: [_jsxs("section", { "aria-label": "Coverage summary", className: "flex flex-col gap-3", children: [_jsxs("div", { className: "flex flex-wrap items-baseline gap-3", children: [_jsx("span", { className: "text-4xl font-semibold text-default-foreground", children: `${coverage.covered} / ${coverage.total}` }), _jsx("span", { className: "text-sm text-muted-foreground", children: "shapes have a HornView" }), _jsx("span", { className: "ml-auto text-2xl font-semibold text-default-foreground", children: `${percent}%` })] }), _jsx(Progress, { value: percent, "aria-label": "Shapes covered", "aria-valuenow": percent, "aria-valuetext": `${percent}% of shapes have a HornView` }), _jsxs("p", { className: "text-sm text-muted-foreground", children: [`${coverage.curated} curated after review in a zoo`, coverage.observed
63
+ ? ` · ${coverage.observed.covered} / ${coverage.observed.total} of the shapes this datastore holds`
64
+ : ""] }), observed.isPending ? _jsx(Skeleton, { className: "h-4 w-64" }) : null] }), observed.isError ? (_jsx("div", { className: "pt-3", children: _jsx(ErrorState, { title: "Could not read which shapes this datastore holds.", hint: observed.error.message }) })) : null] }), _jsxs("div", { className: "grid gap-4 md:grid-cols-2", children: [isPresent(drafted) ? (_jsx(Card, { children: _jsxs("section", { "aria-labelledby": "review", className: "flex flex-col gap-3", children: [_jsx(CardTitle, { aside: `${drafted.length} to review`, children: _jsx("span", { id: "review", children: "Drafted, waiting on a person" }) }), _jsx("p", { className: "text-sm text-muted-foreground", children: "Each of these was designed by the Studio and has not been looked at. Open its zoo: what the audit reports there is what to fix, and a view that holds up is promoted to curated." }), _jsx("ul", { "data-review": true, className: "flex flex-wrap gap-2", children: drafted.slice(0, maxReviewLinks).map((entry) => (_jsx("li", { children: _jsx(Link, { "data-review-shape": entry.shape.schema, className: "rounded-md border border-default-border px-2 py-1 font-mono text-xs text-muted-foreground hover:bg-card/70 hover:text-default-foreground", to: shapeZooPath(datastoreId, entry.shape.schema), children: entry.shape.schema }) }, entry.shape.schema))) }), drafted.length > maxReviewLinks ? (_jsx("p", { className: "text-xs text-muted-foreground", children: `…${drafted.length - maxReviewLinks} more, in the list below.` })) : null] }) })) : null, isPresent(worklist) ? (_jsx(Card, { children: _jsxs("section", { "aria-labelledby": "worklist", className: "flex flex-col gap-3", children: [_jsx(CardTitle, { aside: `${worklist.length} next`, children: _jsx("span", { id: "worklist", children: "Next for the Studio" }) }), _jsx("ol", { className: "flex flex-col divide-y divide-default-border text-sm", children: worklist.map((entry) => (_jsxs("li", { className: "flex items-center justify-between gap-4 py-2", children: [_jsx("span", { className: "truncate font-mono text-default-foreground", children: entry.shape.schema }), _jsxs("span", { className: "shrink-0 text-xs text-muted-foreground", children: [entry.status === "drafted" ? "review the draft in its zoo" : "needs a view", entry.instances === undefined
65
+ ? ""
66
+ : ` · ${entry.instances.toLocaleString("en")} instance${entry.instances === 1 ? "" : "s"}`] })] }, entry.shape.schema))) })] }) })) : null] }), _jsx(CoverageRun, { datastoreId: datastoreId }), _jsx("section", { "aria-label": "Shapes by namespace", className: "flex flex-col gap-2", children: coverage.namespaces.map((group) => (_jsxs("details", { "data-namespace": group.namespace, className: "group rounded-lg border border-default-border bg-card", children: [_jsxs("summary", { className: "flex cursor-pointer list-none items-center gap-3 px-4 py-3", children: [_jsx(ChevronRight, { size: 16, "aria-hidden": "true", className: "shrink-0 text-muted-foreground transition-transform group-open:rotate-90" }), _jsx("span", { className: "font-medium text-default-foreground", children: group.namespace }), _jsx("span", { className: "text-sm text-muted-foreground", children: `${group.covered} / ${group.total}` }), _jsx("span", { className: "ml-auto text-sm text-muted-foreground", children: `${percentOf(group.covered, group.total)}%` })] }), _jsx("ul", { className: "divide-y divide-default-border border-t border-default-border", children: group.shapes.map((entry) => (_jsx(ShapeRow, { entry: entry, realm: realm, datastoreId: datastoreId }, entry.shape.schema))) })] }, group.namespace))) })] }));
67
+ }
68
+ export function ShapesPage() {
69
+ const { datastoreId } = useParams();
70
+ const { datastores } = useMantaSession();
71
+ const datastore = datastores.find((candidate) => candidate.id === datastoreId);
72
+ if (!isPresent(datastore))
73
+ return _jsx(DatastoreMissing, {});
74
+ return (_jsx(MantaSparqlProvider, { datastoreId: datastore.id, children: _jsxs("div", { className: "flex w-full flex-col gap-6", children: [_jsx(PageHeader, { title: "Shape coverage", description: "Every SHACL-derived LDKit shape should have a HornView. A covered shape opens a zoo of the variants its real instances in this datastore take." }), _jsx(ShapeCoverageDashboard, { datastoreId: datastore.id })] }) }));
75
+ }
@@ -0,0 +1,2 @@
1
+ export declare const maxShowcaseCards = 48;
2
+ export declare function ShowcasePage(): import("react").JSX.Element;
@@ -0,0 +1,99 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState } from "react";
3
+ import { useQueries, useQuery } from "@tanstack/react-query";
4
+ import { isPresent } from "@manta-eu/lib/present";
5
+ import { Link, useParams } from "react-router";
6
+ import { Columns3, Plus } from "lucide-react";
7
+ import { Button, cn } from "@manta-eu/react";
8
+ import { hornSchema, hornSources, isRealm } from "@manta-eu/horns";
9
+ import { PageHeader } from "../components/page-header";
10
+ import { DatastoreMissing, EmptyState, ErrorState, Loading } from "../components/states";
11
+ import { hornViewPath, newHornPath } from "./paths";
12
+ import { isRecord } from "@manta-eu/horns";
13
+ import { MantaSparqlProvider } from "../lib/manta/providers";
14
+ import { useMantaSession } from "../lib/manta/session";
15
+ import { timeAgo } from "../lib/time-ago";
16
+ // Every Horn this datastore holds, as a card showing what it looked like when someone last saved it, what it
17
+ // answers, who it is for and what it reads — so a person finds one by recognising it rather than by
18
+ // remembering its name.
19
+ export const maxShowcaseCards = 48;
20
+ const everyone = "Everything";
21
+ function listedOf(entry) {
22
+ const { slug, title, screenshot, capturedAt } = entry;
23
+ if (typeof slug !== "string")
24
+ return [];
25
+ return [
26
+ {
27
+ slug,
28
+ title: typeof title === "string" ? title : slug,
29
+ ...(typeof screenshot === "string" ? { screenshot } : {}),
30
+ ...(typeof capturedAt === "string" ? { capturedAt } : {}),
31
+ },
32
+ ];
33
+ }
34
+ function detailsOf(horn) {
35
+ return {
36
+ slug: horn.slug,
37
+ ...(horn.summary === undefined ? {} : { summary: horn.summary }),
38
+ ...(horn.audience === undefined ? {} : { audience: horn.audience }),
39
+ sources: isRealm(horn.realm) ? hornSources(horn, horn.realm) : [],
40
+ };
41
+ }
42
+ /** The picture, read back through the redirect that turns a file's IRI into something a browser can load. */
43
+ function screenshotSrc(datastoreId, fileIri) {
44
+ return `/d/${encodeURIComponent(datastoreId)}/redirect?iri=${encodeURIComponent(fileIri)}`;
45
+ }
46
+ export function ShowcasePage() {
47
+ const { datastoreId } = useParams();
48
+ const { datastores } = useMantaSession();
49
+ const datastore = datastores.find((candidate) => candidate.id === datastoreId);
50
+ if (!isPresent(datastore))
51
+ return _jsx(DatastoreMissing, {});
52
+ return (_jsx(MantaSparqlProvider, { datastoreId: datastore.id, children: _jsx(Showcase, { datastoreId: datastore.id }) }));
53
+ }
54
+ function Showcase({ datastoreId }) {
55
+ const [showing, setShowing] = useState(everyone);
56
+ const listed = useQuery({
57
+ queryKey: ["horns", datastoreId],
58
+ queryFn: async ({ signal }) => {
59
+ const response = await fetch(`/api/d/${encodeURIComponent(datastoreId)}/horns`, { signal });
60
+ if (!response.ok)
61
+ throw new Error(`The datastore answered ${response.status}.`);
62
+ const body = await response.json();
63
+ const entries = isRecord(body) && Array.isArray(body["horns"]) ? body["horns"] : [];
64
+ return entries.flatMap((entry) => (isRecord(entry) ? listedOf(entry) : [])).slice(0, maxShowcaseCards);
65
+ },
66
+ });
67
+ // Each card says what its Horn reads, which only the document knows; they are read side by side, and the
68
+ // cards are drawn before they arrive rather than after all of them have.
69
+ const documents = useQueries({
70
+ queries: (listed.data ?? []).map((entry) => ({
71
+ queryKey: ["horn", datastoreId, entry.slug],
72
+ staleTime: Infinity,
73
+ queryFn: async ({ signal }) => {
74
+ const response = await fetch(`/api/d/${encodeURIComponent(datastoreId)}/horns/${encodeURIComponent(entry.slug)}`, { signal });
75
+ if (!response.ok)
76
+ throw new Error(`That Horn could not be read (${response.status}).`);
77
+ const body = await response.json();
78
+ return detailsOf(hornSchema.parse(isRecord(body) ? body["horn"] : undefined));
79
+ },
80
+ })),
81
+ });
82
+ const details = new Map(documents.flatMap((query) => (query.data ? [[query.data.slug, query.data]] : [])));
83
+ const cards = (listed.data ?? []).map((entry) => {
84
+ const found = details.get(entry.slug);
85
+ return { ...entry, ...(found ? { details: found } : {}) };
86
+ });
87
+ const audiences = [
88
+ everyone,
89
+ ...new Set([...details.values()].flatMap((one) => (one.audience ? [one.audience] : []))),
90
+ ];
91
+ // A Horn whose document has not arrived is not yet known to be of another audience, so it stays shown.
92
+ const shown = showing === everyone ? cards : cards.filter((card) => card.details?.audience === showing);
93
+ return (_jsxs("div", { className: "flex w-full flex-col gap-6", children: [_jsx(PageHeader, { title: "Showcase", description: "What this datastore can already answer. Open one to look at it, or describe a new one in your own words.", actions: _jsx(Button, { variant: "primary", asChild: true, children: _jsxs(Link, { to: newHornPath(datastoreId), children: [_jsx(Plus, { size: 16, "aria-hidden": "true" }), "New Horn"] }) }) }), audiences.length > 1 ? (_jsx("div", { "data-audiences": true, className: "flex flex-wrap gap-2", children: audiences.map((audience) => (_jsx("button", { type: "button", "aria-pressed": audience === showing, onClick: () => setShowing(audience), className: cn("rounded-full border px-3 py-1 text-xs transition-colors", audience === showing
94
+ ? "border-default-border bg-muted font-medium text-default-foreground"
95
+ : "border-muted-border text-muted-foreground hover:text-default-foreground"), children: audience }, audience))) })) : null, listed.isPending ? (_jsx(Loading, { label: "Reading this datastore's Horns\u2026", rows: 3 })) : listed.isError ? (_jsx(ErrorState, { title: "The showcase could not be loaded", hint: "Try again in a moment. If it keeps happening, the data service may be down." })) : !isPresent(shown) ? (_jsx(EmptyState, { title: "Nothing here yet", hint: "Describe what you want to see, in your own words, and it is built for you from this datastore's data.", icon: _jsx(Columns3, { size: 20 }), action: _jsx(Button, { variant: "primary", size: "sm", asChild: true, children: _jsxs(Link, { to: newHornPath(datastoreId), children: [_jsx(Plus, { size: 14, "aria-hidden": "true" }), "Make your first Horn"] }) }) })) : (_jsx("ul", { "data-showcase": true, className: "grid gap-4 sm:grid-cols-2 lg:grid-cols-3", children: shown.map((card) => {
96
+ const age = card.capturedAt === undefined ? undefined : timeAgo(card.capturedAt);
97
+ return (_jsx("li", { children: _jsxs(Link, { "data-showcase-card": card.slug, to: hornViewPath(datastoreId, card.slug), className: "flex h-full flex-col gap-3 rounded-xl border border-default-border bg-card p-4 transition-colors hover:bg-card/70", children: [card.screenshot === undefined ? null : (_jsx("span", { "data-showcase-shot": true, className: "block overflow-hidden rounded-lg border border-muted-border bg-muted", style: { aspectRatio: "16 / 10" }, children: _jsx("img", { src: screenshotSrc(datastoreId, card.screenshot), alt: "", loading: "lazy", className: "h-full w-full object-cover object-top" }) })), _jsxs("span", { className: "flex items-start justify-between gap-3", children: [_jsx("span", { className: "text-sm font-medium text-default-foreground", children: card.title }), card.details?.audience ? (_jsx("span", { "data-audience": card.details.audience, className: "shrink-0 rounded-full bg-muted px-2 py-0.5 text-[11px] text-muted-foreground", children: card.details.audience })) : null] }), card.details?.summary ? (_jsx("span", { className: "text-xs leading-relaxed text-muted-foreground", children: card.details.summary })) : null, _jsxs("span", { className: "mt-auto flex flex-col gap-1.5 pt-1", children: [isPresent(card.details?.sources) ? (_jsxs("span", { className: "flex flex-wrap items-center gap-1.5", children: [_jsx("span", { className: "text-[11px] text-muted-foreground", children: "Reads" }), card.details.sources.map((source) => (_jsx("span", { className: "rounded-md border border-muted-border px-1.5 py-0.5 text-[11px] text-default-foreground", children: source }, source)))] })) : null, age === undefined ? null : (_jsx("span", { "data-showcase-age": true, className: "text-[11px] text-muted-foreground", children: `as of ${age}` }))] })] }) }, card.slug));
98
+ }) }))] }));
99
+ }
@@ -0,0 +1,6 @@
1
+ import type { Horn } from "@manta-eu/horns";
2
+ type VariableBarProps = {
3
+ horn: Horn;
4
+ };
5
+ export declare function VariableBar(props: VariableBarProps): import("react").JSX.Element | undefined;
6
+ export {};
@@ -0,0 +1,42 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { isPresent } from "@manta-eu/lib/present";
3
+ import { Input } from "@manta-eu/react";
4
+ import { variablesFromSearch } from "@manta-eu/horns/render";
5
+ import { useSearchParams } from "react-router";
6
+ import { isRecord } from "@manta-eu/horns";
7
+ /** The two ends of a range, which is what a `dateRange` value is. */
8
+ function isRange(value) {
9
+ return isRecord(value) && typeof value["start"] === "string" && typeof value["end"] === "string";
10
+ }
11
+ /** The span a `dateRange` puts in one parameter, which is the text the input shows. */
12
+ function textOf(value) {
13
+ if (!isPresent(value))
14
+ return "";
15
+ if (isRange(value))
16
+ return `${value.start}..${value.end}`;
17
+ return String(value);
18
+ }
19
+ export function VariableBar(props) {
20
+ const { horn } = props;
21
+ const [search, setSearch] = useSearchParams();
22
+ const shared = horn.variables.filter((variable) => isPresent(variable.urlParam));
23
+ if (!isPresent(shared))
24
+ return undefined;
25
+ // What the URL sets, which is what the snapshot is built with; a variable it does not name keeps its default.
26
+ const set = variablesFromSearch(horn, search.toString());
27
+ const write = (variable, value) => {
28
+ const next = new URLSearchParams(search);
29
+ const param = variable.urlParam ?? "";
30
+ const trimmed = value.trim();
31
+ if (!isPresent(trimmed))
32
+ next.delete(param);
33
+ else
34
+ next.set(param, trimmed);
35
+ setSearch(next, { replace: true });
36
+ };
37
+ return (_jsx("form", { "data-variable-bar": true, className: "flex min-w-0 items-center gap-3", onSubmit: (event) => event.preventDefault(), children: shared.map((variable) => {
38
+ const current = textOf(set[variable.id] ?? variable.default);
39
+ const values = variable.options && "values" in variable.options ? variable.options.values : undefined;
40
+ return (_jsxs("label", { className: "flex items-center gap-2 text-xs whitespace-nowrap text-muted-foreground", children: [variable.label, isPresent(values) ? (_jsxs("select", { "aria-label": variable.label, className: "h-8 rounded-md border border-default-border bg-card px-2 text-sm text-default-foreground", value: current, onChange: (event) => write(variable, event.target.value), children: [_jsx("option", { value: "", children: "All" }), values.map((value) => (_jsx("option", { value: String(value), children: String(value) }, String(value))))] })) : (_jsx(Input, { className: "h-8 w-44", "aria-label": variable.label, value: current, placeholder: variable.type === "dateRange" ? "2026-01-01..2026-02-01" : undefined, onChange: (event) => write(variable, event.target.value) }))] }, variable.id));
41
+ }) }));
42
+ }
@@ -0,0 +1,10 @@
1
+ import { type ShapeCatalogSource } from "./lib/shapes/catalog";
2
+ import "./app/globals.css";
3
+ export type MountHornsOptions = {
4
+ /** The realm's shapes. Horns ships none: they describe a deployment's data model, not the library's. */
5
+ shapes: ShapeCatalogSource;
6
+ /** Defaults to `#app`, which is what this package's own document declares. */
7
+ container?: HTMLElement;
8
+ };
9
+ /** Mount Horns as a whole page. A host with a router of its own splices `hornsRoutes()` in instead. */
10
+ export declare function mountHorns({ shapes, container }: MountHornsOptions): void;
package/dist/client.js ADDED
@@ -0,0 +1,22 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { StrictMode } from "react";
3
+ import { createRoot } from "react-dom/client";
4
+ import { BrowserRouter } from "react-router";
5
+ import { ErrorBoundary } from "@sentry/react";
6
+ import { AppRoutes } from "./app/routes";
7
+ import { MantaSessionProvider } from "./lib/manta/session";
8
+ import { provideShapeCatalog } from "./lib/shapes/catalog";
9
+ import { initSentry } from "./lib/telemetry/sentry";
10
+ import "./app/globals.css";
11
+ function AppCrashed() {
12
+ return (_jsx("main", { className: "grid min-h-screen place-items-center p-8 text-center", children: _jsxs("div", { children: [_jsx("h1", { className: "text-xl font-semibold", children: "Something went wrong" }), _jsx("p", { className: "mt-2 text-sm opacity-70", children: "Reload the page to try again." })] }) }));
13
+ }
14
+ /** Mount Horns as a whole page. A host with a router of its own splices `hornsRoutes()` in instead. */
15
+ export function mountHorns({ shapes, container }) {
16
+ provideShapeCatalog(shapes);
17
+ initSentry();
18
+ const root = container ?? document.getElementById("app");
19
+ if (!root)
20
+ throw new Error("mountHorns found no container: pass one, or give the document an #app element.");
21
+ createRoot(root).render(_jsx(StrictMode, { children: _jsx(ErrorBoundary, { fallback: _jsx(AppCrashed, {}), children: _jsx(BrowserRouter, { children: _jsx(MantaSessionProvider, { children: _jsx(AppRoutes, {}) }) }) }) }));
22
+ }
@@ -0,0 +1 @@
1
+ export declare function AppShell(): import("react").JSX.Element;
@@ -0,0 +1,18 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { isPresent } from "@manta-eu/lib/present";
3
+ import { Link, Outlet, useLocation, useParams } from "react-router";
4
+ import { Boxes, Columns3, Database, LayoutGrid, Sigma } from "lucide-react";
5
+ import { MainContent, MantaLogo, NavUser, Sidebar, SidebarContent, SidebarFooter, SidebarGroup, SidebarGroupContent, SidebarGroupLabel, SidebarHeader, SidebarMenu, SidebarMenuButton, SidebarMenuItem, SidebarProvider, SidebarTrigger, } from "@manta-eu/react";
6
+ import { hornsPath, rulesPath, shapesPath, showcasePath } from "../app/paths";
7
+ import { useMantaSession } from "../lib/manta/session";
8
+ function NavLink({ to, icon, label, active }) {
9
+ return (_jsx(SidebarMenuItem, { children: _jsx(SidebarMenuButton, { asChild: true, isActive: active, tooltip: label, children: _jsxs(Link, { to: to, "aria-current": active ? "page" : undefined, children: [icon, _jsx("span", { children: label })] }) }) }));
10
+ }
11
+ export function AppShell() {
12
+ const { datastoreId } = useParams();
13
+ const { pathname } = useLocation();
14
+ const { user, datastore, datastores } = useMantaSession();
15
+ const current = datastores.find((candidate) => candidate.id === datastoreId) ?? datastore;
16
+ const inDatastore = isPresent(datastoreId) && isPresent(current);
17
+ 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(datastoreId), icon: _jsx(LayoutGrid, { "aria-hidden": "true" }), label: "Showcase", active: pathname === showcasePath(datastoreId) }), _jsx(NavLink, { to: hornsPath(datastoreId), icon: _jsx(Columns3, { "aria-hidden": "true" }), label: "Horns", active: pathname === hornsPath(datastoreId) })] }) })] }), _jsxs(SidebarGroup, { children: [_jsx(SidebarGroupLabel, { children: "For builders" }), _jsx(SidebarGroupContent, { children: _jsxs(SidebarMenu, { children: [_jsx(NavLink, { to: shapesPath(datastoreId), icon: _jsx(Boxes, { "aria-hidden": "true" }), label: "Data shapes", active: pathname.startsWith(shapesPath(datastoreId)) }), _jsx(NavLink, { to: rulesPath(datastoreId), icon: _jsx(Sigma, { "aria-hidden": "true" }), label: "Rules", active: pathname.startsWith(rulesPath(datastoreId)) })] }) })] })] })) : 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
+ }
@@ -0,0 +1,8 @@
1
+ import type { SnapshotView } from "@manta-eu/horns/render";
2
+ type CatalogCardBodyProps = {
3
+ content: Extract<SnapshotView["content"], {
4
+ lane: "component";
5
+ }>;
6
+ };
7
+ export declare function CatalogCardBody({ content }: CatalogCardBodyProps): import("react").JSX.Element;
8
+ export {};
@@ -0,0 +1,29 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Loader } from "@manta-eu/react";
3
+ import { isPresent } from "@manta-eu/lib/present";
4
+ import { createNodeRouter, useEntityNodes } from "@manta-eu/react/ldo-ui";
5
+ // The card fetches its own entity against the SPARQL context this app mounts, so the snapshot carries IRIs
6
+ // rather than rows — and a card draws in a browser only, as a written view does.
7
+ // `wrap`/`fallback` are for the graph canvas; only `getEntityView` is wanted here.
8
+ const router = createNodeRouter({
9
+ wrap: () => () => null,
10
+ fallback: () => null,
11
+ });
12
+ export function CatalogCardBody({ content }) {
13
+ const { nodesByIri, isLoading } = useEntityNodes([...content.iris]);
14
+ if (isLoading)
15
+ return _jsx(Loader, {});
16
+ if (!isPresent(content.iris)) {
17
+ return _jsx("p", { className: "text-muted-foreground m-0 text-sm", children: "Nothing to show here yet." });
18
+ }
19
+ return (_jsxs("div", { className: "flex flex-col gap-2", children: [content.iris.map((iri) => {
20
+ const node = nodesByIri.get(iri);
21
+ if (node === undefined)
22
+ return null;
23
+ // By the entity's own type, which is how the design system itself chooses. The snapshot's `component`
24
+ // records what the Studio pinned; where the two could disagree, the package's own routing wins,
25
+ // because it is what knows which card can draw a given type today.
26
+ const View = router.getEntityView(node.data.type);
27
+ return View === undefined ? null : _jsx(View, { data: node.data }, iri);
28
+ }), content.truncated ? (_jsxs("p", { className: "text-muted-foreground m-0 text-xs", children: ["More than this fit; showing the first ", content.iris.length, "."] })) : null] }));
29
+ }
@@ -0,0 +1,2 @@
1
+ import type { HornSnapshotViewProps } from "@manta-eu/horns-kit";
2
+ export declare const catalogCardView: NonNullable<HornSnapshotViewProps["catalogCard"]>;
@@ -0,0 +1,13 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { lazy, Suspense } from "react";
3
+ import { Loader } from "@manta-eu/react";
4
+ import { MantaSparqlProvider } from "../lib/manta/providers";
5
+ // Behind `lazy`: the body pulls in the graph routing and every typed entity card.
6
+ const CatalogCardBody = lazy(async () => {
7
+ const body = await import("./catalog-card-body");
8
+ return { default: body.CatalogCardBody };
9
+ });
10
+ // The provider comes with it, because a card fetches its own entity and the page it draws on may have no
11
+ // providers of its own — a server-rendered Horn document hydrates embeds and nothing else, so a shared link
12
+ // with a card on it spun for ever without this.
13
+ export const catalogCardView = (content, _view, context) => (_jsx(MantaSparqlProvider, { datastoreId: context.datastoreId, children: _jsx(Suspense, { fallback: _jsx(Loader, {}), children: _jsx(CatalogCardBody, { content: content }) }) }));
@@ -0,0 +1,22 @@
1
+ import type { ArrowUp } from "lucide-react";
2
+ export type ChatTurn = {
3
+ role: "user" | "assistant";
4
+ content: string;
5
+ };
6
+ type ChatColumnProps = {
7
+ title: string;
8
+ subtitle: string;
9
+ starters: readonly {
10
+ id: string;
11
+ title: string;
12
+ }[];
13
+ turns: readonly ChatTurn[];
14
+ /** What the turn is doing, as it happens. Empty when nothing is running. */
15
+ progress: readonly string[];
16
+ pending: boolean;
17
+ placeholder: string;
18
+ submitIcon: typeof ArrowUp;
19
+ onSend: (text: string) => void;
20
+ };
21
+ export declare function ChatColumn(props: ChatColumnProps): import("react").JSX.Element;
22
+ export {};
@@ -0,0 +1,20 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useEffect, useRef } from "react";
3
+ import { isPresent } from "@manta-eu/lib/present";
4
+ import { Sparkles } from "lucide-react";
5
+ import { ChatConversationStarters, ChatInput, ChatInputSubmit, ChatInputTextarea, ChatMessage, ChatPanel, ChatReasoning, ChatWelcome, chatPanelWidth, } from "@manta-eu/react";
6
+ export function ChatColumn(props) {
7
+ const { title, subtitle, starters, turns, progress, pending, placeholder, submitIcon, onSend } = props;
8
+ const scroller = useRef(null);
9
+ // A conversation's newest line is the one being read, so the panel follows it down.
10
+ useEffect(() => {
11
+ const element = scroller.current;
12
+ if (isPresent(element))
13
+ element.scrollTop = element.scrollHeight;
14
+ }, [turns, progress]);
15
+ return (_jsxs("div", { className: "sticky top-4 flex min-w-0 flex-col gap-3",
16
+ // The column is as wide as the layout gives it and no wider than the panel's own width. The height is
17
+ // the one thing the utilities cannot say — no compiled max-height is a viewport — and without it the
18
+ // panel hugs its content, so the composer walks down the page as the conversation grows.
19
+ style: { maxWidth: chatPanelWidth, height: "calc(100svh - 11rem)" }, children: [_jsxs("div", { className: "flex items-center gap-2 text-sm font-medium text-default-foreground", children: [_jsx(Sparkles, { size: 16, "aria-hidden": "true" }), title] }), _jsx(ChatPanel, { children: _jsxs("div", { ref: scroller, "data-chat-history": true, className: "flex flex-col gap-3 p-4", children: [!isPresent(turns) ? (_jsxs(_Fragment, { children: [_jsx(ChatWelcome, { channel: "MANTA", title: subtitle, subtitle: "" }), _jsx(ChatConversationStarters, { starters: starters.map((starter) => ({ id: starter.id, title: starter.title })), onStarterClick: (starter) => onSend(starter.title) })] })) : (turns.map((turn, index) => (_jsx(ChatMessage, { role: turn.role, children: turn.content }, index)))), isPresent(progress) ? (_jsx(ChatReasoning, { phase: "pending", text: "Working", items: progress.map((line) => ({ icon: null, title: line })) })) : null] }) }), _jsxs(ChatInput, { onSubmit: (value) => onSend(value), children: [_jsx(ChatInputTextarea, { placeholder: placeholder, "aria-label": placeholder }), _jsx(ChatInputSubmit, { icon: submitIcon, "aria-label": "Send", isLoading: pending, disabled: pending })] })] }));
20
+ }
@@ -0,0 +1,15 @@
1
+ import type { ReactNode } from "react";
2
+ export declare function PageHeader({ title, description, meta, actions, }: {
3
+ title: string;
4
+ description?: string;
5
+ meta?: ReactNode;
6
+ actions?: ReactNode;
7
+ }): import("react").JSX.Element;
8
+ export declare function Card({ children, className }: {
9
+ children: ReactNode;
10
+ className?: string;
11
+ }): import("react").JSX.Element;
12
+ export declare function CardTitle({ children, aside }: {
13
+ children: ReactNode;
14
+ aside?: ReactNode;
15
+ }): import("react").JSX.Element;
@@ -0,0 +1,11 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { isPresent } from "@manta-eu/lib/present";
3
+ export function PageHeader({ title, description, meta, actions, }) {
4
+ return (_jsxs("header", { "data-page-header": true, className: "flex flex-wrap items-start justify-between gap-3", children: [_jsxs("div", { className: "flex min-w-0 flex-col gap-1", children: [_jsxs("div", { className: "flex flex-wrap items-center gap-3", children: [_jsx("h1", { className: "text-2xl font-semibold text-default-foreground", children: title }), meta] }), isPresent(description) ? _jsx("p", { className: "max-w-2xl text-sm text-muted-foreground", children: description }) : null] }), isPresent(actions) ? _jsx("div", { className: "flex shrink-0 items-center gap-2", children: actions }) : null] }));
5
+ }
6
+ export function Card({ children, className }) {
7
+ return (_jsx("section", { className: `rounded-lg border border-default-border bg-card p-5 ${className ?? ""}`, children: children }));
8
+ }
9
+ export function CardTitle({ children, aside }) {
10
+ return (_jsxs("div", { className: "flex flex-wrap items-baseline justify-between gap-2", children: [_jsx("h2", { className: "text-base font-medium text-default-foreground", children: children }), isPresent(aside) ? _jsx("span", { className: "text-sm text-muted-foreground", children: aside }) : null] }));
11
+ }
@@ -0,0 +1,28 @@
1
+ import type { Realm } from "@manta-eu/horns";
2
+ import { type SnapshotPlan } from "@manta-eu/horns/render";
3
+ import { type PlanActions } from "@manta-eu/horns-kit";
4
+ export type PlanRuns = {
5
+ actions: PlanActions;
6
+ /** The plan waiting for the viewer to say yes to a real run. */
7
+ confirming: SnapshotPlan | undefined;
8
+ confirm: () => void;
9
+ cancel: () => void;
10
+ problem: string | undefined;
11
+ dismiss: () => void;
12
+ };
13
+ /**
14
+ * Previews and runs of the action plans a Horn shows. A preview starts straight away: it changes nothing. A real
15
+ * run waits for a yes in `PlanRunDialog`. Either way the graph is then asked every few seconds how the run went,
16
+ * and a real run that finished redraws the Horn, because the things it changed now look different.
17
+ */
18
+ export declare function usePlanRuns(args: {
19
+ datastoreId: string;
20
+ realm: Realm;
21
+ onChanged: () => void;
22
+ }): PlanRuns;
23
+ type PlanRunDialogProps = {
24
+ runs: PlanRuns;
25
+ labels: Readonly<Record<string, string>>;
26
+ };
27
+ export declare function PlanRunDialog(props: PlanRunDialogProps): import("react").JSX.Element;
28
+ export {};