@manta-eu/horns-client 0.1.0 → 0.1.2

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 (73) hide show
  1. package/component-api.d.ts +7 -0
  2. package/dist/app/coverage-run.js +4 -3
  3. package/dist/app/globals.css +2 -0
  4. package/dist/app/home-page.js +6 -4
  5. package/dist/app/horn-page.js +7 -10
  6. package/dist/app/horn-studio.js +63 -10
  7. package/dist/app/horn-view-page.js +13 -9
  8. package/dist/app/horns-page.js +6 -9
  9. package/dist/app/new-horn-page.js +52 -10
  10. package/dist/app/paths.d.ts +15 -10
  11. package/dist/app/paths.js +18 -29
  12. package/dist/app/plans-page.d.ts +1 -0
  13. package/dist/app/plans-page.js +59 -0
  14. package/dist/app/reference-page.d.ts +1 -0
  15. package/dist/app/reference-page.js +55 -0
  16. package/dist/app/routes.js +7 -5
  17. package/dist/app/rule-page.js +18 -11
  18. package/dist/app/rules-page.js +12 -12
  19. package/dist/app/shape-entity-page.js +7 -9
  20. package/dist/app/shape-zoo-page.js +9 -10
  21. package/dist/app/shapes-page.js +13 -15
  22. package/dist/app/showcase-page.js +8 -11
  23. package/dist/components/app-shell.js +8 -7
  24. package/dist/components/catalog-card-view.js +4 -5
  25. package/dist/components/explain-dialog.d.ts +5 -0
  26. package/dist/components/explain-dialog.js +26 -0
  27. package/dist/components/plan-runs.js +2 -2
  28. package/dist/components/read-the-code.d.ts +19 -0
  29. package/dist/components/read-the-code.js +17 -3
  30. package/dist/index.d.ts +2 -3
  31. package/dist/index.js +2 -3
  32. package/dist/lib/horn-review.d.ts +7 -0
  33. package/dist/lib/horn-review.js +15 -0
  34. package/dist/lib/horn-variables.d.ts +8 -0
  35. package/dist/lib/horn-variables.js +39 -0
  36. package/dist/lib/manta/sign-in.d.ts +2 -0
  37. package/dist/lib/manta/sign-in.js +9 -0
  38. package/dist/lib/manta/sparql-select.d.ts +6 -2
  39. package/dist/lib/manta/sparql-select.js +17 -14
  40. package/dist/lib/reference/fake-sparql.d.ts +4 -0
  41. package/dist/lib/reference/fake-sparql.js +216 -0
  42. package/dist/lib/shapes/ldkit-source.d.ts +1 -1
  43. package/dist/lib/shapes/ldkit-source.js +1 -1
  44. package/dist/lib/studio-progress.js +3 -1
  45. package/dist/lib/surface/declarations.d.ts +5 -0
  46. package/dist/lib/surface/declarations.js +77 -0
  47. package/dist/lib/written-surface.js +13 -2
  48. package/dist/reference/supply-chain-360.horn.json +413 -0
  49. package/dist/standalone/app.d.ts +2 -0
  50. package/dist/standalone/app.js +57 -0
  51. package/dist/standalone/client.d.ts +15 -0
  52. package/dist/{client.js → standalone/client.js} +12 -8
  53. package/dist/standalone/gates.d.ts +10 -0
  54. package/dist/standalone/gates.js +15 -0
  55. package/dist/styles.css +1 -1
  56. package/dist/vite/component-api.d.ts +8 -0
  57. package/dist/vite/component-api.js +24 -0
  58. package/dist/vite/index.d.ts +3 -0
  59. package/dist/vite/index.js +3 -0
  60. package/dist/vite/shape-catalog.d.ts +10 -0
  61. package/dist/vite/shape-catalog.js +35 -0
  62. package/dist/vite/typecheck-fs.d.ts +6 -0
  63. package/dist/vite/typecheck-fs.js +68 -0
  64. package/package.json +58 -20
  65. package/shape-catalog.d.ts +9 -0
  66. package/typecheck-fs.d.ts +6 -0
  67. package/dist/app/session-gate.d.ts +0 -6
  68. package/dist/app/session-gate.js +0 -19
  69. package/dist/client.d.ts +0 -10
  70. package/dist/lib/manta/providers.d.ts +0 -10
  71. package/dist/lib/manta/providers.js +0 -48
  72. package/dist/lib/manta/session.d.ts +0 -47
  73. package/dist/lib/manta/session.js +0 -204
@@ -1,25 +1,22 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { isPresent } from "@manta-eu/lib/present";
3
3
  import { useParams, useSearchParams } from "react-router";
4
+ import { useActiveDatastore } from "@manta-eu/react/providers";
4
5
  import { variablesFromSearch } from "@manta-eu/horns/render";
5
6
  import { BackLink, DatastoreMissing, ErrorState, Loading } from "../components/states";
6
7
  import { hornsPath } from "./paths";
7
8
  import { HornStudio } from "./horn-studio";
8
9
  import { useSavedHorn } from "./saved-horn";
9
10
  import { VariableBar } from "./variable-bar";
10
- import { MantaSparqlProvider } from "../lib/manta/providers";
11
- import { useMantaSession } from "../lib/manta/session";
12
11
  // A saved Horn, as the Studio's whole screen: the document comes back from the datastore and the snapshot is
13
12
  // built from it, so what is on screen is what is stored rather than what a client happened to be holding.
14
13
  export function HornPage() {
15
- const { datastoreId, slug } = useParams();
16
- const { datastores } = useMantaSession();
17
- const datastore = datastores.find((candidate) => candidate.id === datastoreId);
18
- if (!isPresent(datastore)) {
14
+ const { datastoreId } = useActiveDatastore();
15
+ const { slug } = useParams();
16
+ if (!isPresent(datastoreId)) {
19
17
  return (_jsx(Centered, { children: _jsx(DatastoreMissing, {}) }));
20
18
  }
21
- // The provider supplies the query client, so the reader has to sit inside it.
22
- return (_jsx(MantaSparqlProvider, { datastoreId: datastore.id, children: _jsx(SavedHornPage, { slug: slug ?? "", datastoreId: datastore.id }) }));
19
+ return _jsx(SavedHornPage, { slug: slug ?? "", datastoreId: datastoreId });
23
20
  }
24
21
  /** Until there is a Horn to put in the Studio, the screen is just this. */
25
22
  function Centered({ children }) {
@@ -29,10 +26,10 @@ function SavedHornPage({ datastoreId, slug }) {
29
26
  const [search] = useSearchParams();
30
27
  const horn = useSavedHorn(datastoreId, slug);
31
28
  if (horn.isError) {
32
- return (_jsxs(Centered, { children: [_jsx(BackLink, { to: hornsPath(datastoreId), label: "All Horns" }), _jsx(ErrorState, { title: "That Horn could not be opened", hint: horn.error instanceof Error ? horn.error.message : "It may have been removed." })] }));
29
+ return (_jsxs(Centered, { children: [_jsx(BackLink, { to: hornsPath(), label: "All Horns" }), _jsx(ErrorState, { title: "That Horn could not be opened", hint: horn.error instanceof Error ? horn.error.message : "It may have been removed." })] }));
33
30
  }
34
31
  if (!isPresent(horn.data)) {
35
32
  return (_jsx(Centered, { children: _jsx(Loading, { label: "Opening the Horn\u2026" }) }));
36
33
  }
37
- return (_jsx(HornStudio, { datastoreId: datastoreId, slug: slug, stored: horn.data.horn, revision: horn.data.revision, backTo: hornsPath(datastoreId), variables: variablesFromSearch(horn.data.horn, search.toString()), toolbar: _jsx(VariableBar, { horn: horn.data.horn }) }));
34
+ return (_jsx(HornStudio, { datastoreId: datastoreId, slug: slug, stored: horn.data.horn, revision: horn.data.revision, backTo: hornsPath(), variables: variablesFromSearch(horn.data.horn, search.toString()), toolbar: _jsx(VariableBar, { horn: horn.data.horn }) }));
38
35
  }
@@ -11,10 +11,10 @@ import { describeHornChange, minViewWidth, visualEditOps } from "@manta-eu/horns
11
11
  import { HornSnapshotView } from "@manta-eu/horns-kit";
12
12
  import { writtenHornView } from "../components/written-horn-view";
13
13
  import { catalogCardView } from "../components/catalog-card-view";
14
- import { ReadTheCode, writtenReviewStates } from "../components/read-the-code";
14
+ import { ReadTheCode, ReadTheCodeDialog, writtenReviewStates } from "../components/read-the-code";
15
15
  import { PlanRunDialog, usePlanRuns } from "../components/plan-runs";
16
16
  import { sendHornScreenshot } from "../components/rasterize";
17
- import { reviewHorn } from "../lib/horn-review";
17
+ import { nameOfView, reviewHorn } from "../lib/horn-review";
18
18
  import { StudioChat } from "../components/studio-chat";
19
19
  import { ErrorState, Loading } from "../components/states";
20
20
  import { isRecord } from "@manta-eu/horns";
@@ -42,12 +42,13 @@ function failureOf(body, status) {
42
42
  }
43
43
  /** How much a card grows or shrinks by: a third of the twelve columns, so the page keeps its rhythm. */
44
44
  const widthStep = 4;
45
+ // Trimmed after the cut, not before it: a slug ending in a dash is not one the schema accepts.
45
46
  const kebab = (name) => name
46
47
  .trim()
47
48
  .toLowerCase()
48
49
  .replace(/[^a-z0-9]+/g, "-")
49
- .replace(/^-+|-+$/g, "")
50
- .slice(0, 60);
50
+ .slice(0, 60)
51
+ .replace(/^-+|-+$/g, "");
51
52
  /**
52
53
  * The Studio as a whole screen, the way Lovable lays out a project: the conversation down the left, the Horn in
53
54
  * a frame on the right, and saving in the bar across the top.
@@ -87,6 +88,10 @@ export function HornStudio(props) {
87
88
  // Hashing is async, so the states are held rather than derived: a card shows what was last worked out for
88
89
  // the draft it is drawing.
89
90
  const [reviewStates, setReviewStates] = useState({});
91
+ // Save does not refuse a Horn whose code nobody read; it offers the reading, then goes on saving.
92
+ const [toRead, setToRead] = useState([]);
93
+ const [savingAfterReading, setSavingAfterReading] = useState(false);
94
+ const [readingTotal, setReadingTotal] = useState(0);
90
95
  useEffect(() => {
91
96
  let live = true;
92
97
  void writtenReviewStates(shown.horn).then((states) => {
@@ -182,7 +187,8 @@ export function HornStudio(props) {
182
187
  setProgress([...thinking]);
183
188
  review = await reviewHorn({ datastoreId, horn: reply.horn, asked, variables });
184
189
  for (const problem of review.problems) {
185
- thinking.push({ title: `“${problem.viewId}” would not draw`, detail: problem.message });
190
+ const named = nameOfView(reply.horn, problem.viewId);
191
+ thinking.push({ title: `“${named}” would not draw`, detail: problem.message });
186
192
  }
187
193
  if (isPresent(review.advice)) {
188
194
  const count = review.advice.length;
@@ -198,7 +204,7 @@ export function HornStudio(props) {
198
204
  }
199
205
  const said = [
200
206
  // A view that would not draw is not an opinion; it goes first, and it is a fact the Studio can act on.
201
- ...review.problems.map((problem) => `“${problem.viewId}” does not draw: ${problem.message}`),
207
+ ...review.problems.map((problem) => `“${nameOfView(reply.horn, problem.viewId)}” does not draw: ${problem.message}`),
202
208
  ...review.advice,
203
209
  ];
204
210
  setEntries([
@@ -350,14 +356,37 @@ export function HornStudio(props) {
350
356
  setSaveFailure("Give the Horn a title before saving it: its address is made from the title.");
351
357
  return;
352
358
  }
359
+ // Asked of the Horn being saved rather than the one on screen, which may be an earlier version. The
360
+ // save route refuses unread code, so meeting it here turns a dead end into the reading it asks for.
361
+ const states = await writtenReviewStates(saving.horn);
362
+ const unread = saving.horn.views
363
+ .map((view) => view.id)
364
+ .filter((id) => states[id] === "unread" || states[id] === "stale");
365
+ if (unread.length > 0) {
366
+ setSaveFailure(undefined);
367
+ setToRead(unread);
368
+ setReadingTotal(unread.length);
369
+ setSavingAfterReading(true);
370
+ return;
371
+ }
353
372
  setSaveState("saving");
354
373
  setSaveFailure(undefined);
355
374
  try {
356
- const response = await fetch(`/api/d/${encodeURIComponent(datastoreId)}/horns/${encodeURIComponent(name)}`, {
375
+ const put = (at) => fetch(`/api/d/${encodeURIComponent(datastoreId)}/horns/${encodeURIComponent(at)}`, {
357
376
  method: "PUT",
358
377
  headers: { "content-type": "application/json" },
359
378
  body: JSON.stringify({ horn: saving.horn, ...(isPresent(baseRevision) ? { baseRevision } : {}) }),
360
379
  });
380
+ // A Horn that has never been saved takes its address from its title, and two Horns can be given the
381
+ // same title — every Horn built for a plan nobody named is "Suggested changes". The name is taken, not
382
+ // the Horn: saving under the next one free is what a person means, and a 409 here can mean nothing
383
+ // else, because revising a Horn is what carries a `baseRevision`.
384
+ let at = name;
385
+ let response = await put(at);
386
+ for (let next = 2; response.status === 409 && !isPresent(baseRevision) && next <= 20; next += 1) {
387
+ at = `${name}-${next}`;
388
+ response = await put(at);
389
+ }
361
390
  const body = await response.json().catch(() => undefined);
362
391
  if (!response.ok) {
363
392
  setSaveState("failed");
@@ -365,13 +394,13 @@ export function HornStudio(props) {
365
394
  return;
366
395
  }
367
396
  setBaseRevision(isRecord(body) && typeof body["revision"] === "string" ? body["revision"] : baseRevision);
368
- setSlug(name);
397
+ setSlug(at);
369
398
  setSavedId(saving.id);
370
399
  setSaveState("idle");
371
400
  // The showcase leads with a picture of the Horn, and this browser has it drawn already. A saved Horn is
372
401
  // saved whether or not the picture was taken, so this cannot fail the save and is never waited on.
373
402
  if (isPresent(snapshot.data)) {
374
- void sendHornScreenshot({ datastoreId, slug: name, snapshot: snapshot.data });
403
+ void sendHornScreenshot({ datastoreId, slug: at, snapshot: snapshot.data });
375
404
  }
376
405
  }
377
406
  catch (caught) {
@@ -379,6 +408,13 @@ export function HornStudio(props) {
379
408
  setSaveFailure(caught instanceof Error ? caught.message : String(caught));
380
409
  }
381
410
  }
411
+ useEffect(() => {
412
+ if (!savingAfterReading || toRead.length > 0)
413
+ return;
414
+ setSavingAfterReading(false);
415
+ // The queue emptying is the event; `save` is redefined every render and is not one.
416
+ void save();
417
+ }, [savingAfterReading, toRead]);
382
418
  // Where things stand, as a sentence: a badge says less, and an address like "/spend-watch" says nothing.
383
419
  const status = saveState === "saving"
384
420
  ? "Saving…"
@@ -391,7 +427,24 @@ export function HornStudio(props) {
391
427
  : "Not saved yet";
392
428
  const showChat = !mobile || pane === "chat";
393
429
  const showPreview = !mobile || pane === "preview";
394
- return (_jsxs("div", { "data-horn-studio": true, className: "flex h-svh w-full flex-col bg-sidebar", children: [_jsxs("header", { className: "flex h-14 shrink-0 items-center gap-3 px-3", children: [mobile ? (_jsx(Button, { variant: "ghost", size: "sm", iconOnly: true, icon: ArrowLeft, "aria-label": "All Horns", onClick: () => navigate(backTo) })) : (_jsx(Button, { variant: "ghost", size: "sm", icon: ArrowLeft, showLeftIcon: true, onClick: () => navigate(backTo), children: "All Horns" })), _jsxs("div", { className: "flex min-w-0 flex-col", children: [_jsx("span", { className: "truncate text-sm font-medium text-default-foreground", children: latest.horn.title }), _jsx("span", { "data-save-status": true, className: "truncate text-xs text-muted-foreground", children: status })] }), mobile ? (_jsx("div", { className: "flex items-center gap-1 rounded-full bg-muted p-1", role: "group", "aria-label": "Show", children: ["chat", "preview"].map((each) => (_jsx(Button, { variant: pane === each ? "secondary" : "ghost", size: "xs", "aria-pressed": pane === each, onClick: () => setPane(each), children: each === "chat" ? "Chat" : "Preview" }, each))) })) : null, _jsxs("div", { className: "ml-auto flex items-center gap-2", children: [unsaved ? (_jsx(Button, { variant: "secondary", size: "sm", disabled: saveState === "saving" || pending, onClick: discard, children: mobile ? "Discard" : "Discard changes" })) : null, _jsx(Button, { variant: "primary", size: "sm", disabled: !unsaved || saveState === "saving" || pending, onClick: () => void save(), children: saveState === "saving" ? "Saving…" : mobile ? "Save" : "Save changes" })] })] }), _jsxs("div", { className: "flex min-h-0 flex-1", children: [showChat ? (_jsxs("aside", { ref: panelRef, "aria-label": "Studio chat", className: "flex min-h-0 shrink-0 flex-col", style: mobile ? { width: "100%" } : { width: panelWidth }, children: [isPresent(saveFailure) ? (_jsx("div", { className: "px-4 pt-2", children: _jsx(ErrorState, { title: "That save did not go through", hint: saveFailure }) })) : null, _jsx(StudioChat, { entries: entries, pending: pending, progress: progress, previewing: shown.id, welcome: welcome ?? "What would you like to change?", reassurance: reassurance ?? "Say it in your own words. Nothing is saved until you choose Save changes.", starters: starters ?? restyleStarters, placeholder: placeholder ?? "Describe a change, in your own words…", onSend: (text) => void send(text), onPreview: (id) => {
430
+ const reading = (() => {
431
+ const [first] = toRead;
432
+ if (first === undefined)
433
+ return undefined;
434
+ const view = latest.horn.views.find((candidate) => candidate.id === first);
435
+ if (view === undefined || "ref" in view || view.render.kind !== "tsx")
436
+ return undefined;
437
+ return { id: first, title: view.title, source: view.render.source, state: reviewStates[first] ?? "unread" };
438
+ })();
439
+ return (_jsxs("div", { "data-horn-studio": true, className: "flex h-svh w-full flex-col bg-sidebar", children: [reading === undefined ? null : (_jsx(ReadTheCodeDialog, { viewId: reading.id, title: reading.title, source: reading.source, state: reading.state, open: true, standing: { at: readingTotal - toRead.length + 1, of: readingTotal }, onClose: () => {
440
+ // "Not yet" stops the whole save, rather than moving on to the next one unread.
441
+ setToRead([]);
442
+ setSavingAfterReading(false);
443
+ }, onKeep: (change) => {
444
+ // Only once the keep is in the draft: the save that follows the last one reads the draft, and
445
+ // a draft still missing it looks exactly like code nobody read.
446
+ void edit(change, `Read and kept the code for "${reading.title}".`).then(() => setToRead((was) => was.slice(1)));
447
+ } })), _jsxs("header", { className: "flex h-14 shrink-0 items-center gap-3 px-3", children: [mobile ? (_jsx(Button, { variant: "ghost", size: "sm", iconOnly: true, icon: ArrowLeft, "aria-label": "All Horns", onClick: () => navigate(backTo) })) : (_jsx(Button, { variant: "ghost", size: "sm", icon: ArrowLeft, showLeftIcon: true, onClick: () => navigate(backTo), children: "All Horns" })), _jsxs("div", { className: "flex min-w-0 flex-col", children: [_jsx("span", { className: "truncate text-sm font-medium text-default-foreground", children: latest.horn.title }), _jsx("span", { "data-save-status": true, className: "truncate text-xs text-muted-foreground", children: status })] }), mobile ? (_jsx("div", { className: "flex items-center gap-1 rounded-full bg-muted p-1", role: "group", "aria-label": "Show", children: ["chat", "preview"].map((each) => (_jsx(Button, { variant: pane === each ? "secondary" : "ghost", size: "xs", "aria-pressed": pane === each, onClick: () => setPane(each), children: each === "chat" ? "Chat" : "Preview" }, each))) })) : null, _jsxs("div", { className: "ml-auto flex items-center gap-2", children: [unsaved ? (_jsx(Button, { variant: "secondary", size: "sm", disabled: saveState === "saving" || pending, onClick: discard, children: mobile ? "Discard" : "Discard changes" })) : null, _jsx(Button, { variant: "primary", size: "sm", disabled: !unsaved || saveState === "saving" || pending, onClick: () => void save(), children: saveState === "saving" ? "Saving…" : mobile ? "Save" : "Save changes" })] })] }), _jsxs("div", { className: "flex min-h-0 flex-1", children: [showChat ? (_jsxs("aside", { ref: panelRef, "aria-label": "Studio chat", className: "flex min-h-0 shrink-0 flex-col", style: mobile ? { width: "100%" } : { width: panelWidth }, children: [isPresent(saveFailure) ? (_jsx("div", { className: "px-4 pt-2", children: _jsx(ErrorState, { title: "That save did not go through", hint: saveFailure }) })) : null, _jsx(StudioChat, { entries: entries, pending: pending, progress: progress, previewing: shown.id, welcome: welcome ?? "What would you like to change?", reassurance: reassurance ?? "Say it in your own words. Nothing is saved until you choose Save changes.", starters: starters ?? restyleStarters, placeholder: placeholder ?? "Describe a change, in your own words…", onSend: (text) => void send(text), onPreview: (id) => {
395
448
  setViewing(id === latest.id ? undefined : id);
396
449
  if (mobile)
397
450
  setPane("preview");
@@ -3,6 +3,7 @@ import { useState } from "react";
3
3
  import { useQuery } from "@tanstack/react-query";
4
4
  import { isPresent } from "@manta-eu/lib/present";
5
5
  import { Link, useParams, useSearchParams } from "react-router";
6
+ import { useActiveDatastore } from "@manta-eu/react/providers";
6
7
  import { Camera, Link2, Pencil } from "lucide-react";
7
8
  import { Button, Input } from "@manta-eu/react";
8
9
  import { hornSnapshotSchema, variablesFromSearch } from "@manta-eu/horns/render";
@@ -11,24 +12,23 @@ import { writtenHornView } from "../components/written-horn-view";
11
12
  import { catalogCardView } from "../components/catalog-card-view";
12
13
  import { PlanRunDialog, usePlanRuns } from "../components/plan-runs";
13
14
  import { sendHornScreenshot } from "../components/rasterize";
15
+ import { useExplainDialog } from "../components/explain-dialog";
16
+ import { useHornVariables } from "../lib/horn-variables";
14
17
  import { DatastoreMissing, ErrorState, Loading } from "../components/states";
15
18
  import { hornPath, hornsPath } from "./paths";
16
19
  import { useSavedHorn } from "./saved-horn";
17
20
  import { VariableBar } from "./variable-bar";
18
21
  import { isRecord } from "@manta-eu/horns";
19
- import { MantaSparqlProvider } from "../lib/manta/providers";
20
- import { useMantaSession } from "../lib/manta/session";
21
22
  import { appTheme } from "../lib/theme";
22
23
  // A saved Horn as the thing itself: what a shared link opens. No chat, nothing to save — the URL is the state,
23
24
  // so the link somebody is sent draws exactly the Horn they were looking at, filters and all. The plans behind a
24
25
  // view are still offered here, because a person who can open the Horn is a person who can act on it.
25
26
  export function HornViewPage() {
26
- const { datastoreId, slug } = useParams();
27
- const { datastores } = useMantaSession();
28
- const datastore = datastores.find((candidate) => candidate.id === datastoreId);
29
- if (!isPresent(datastore))
27
+ const { datastoreId } = useActiveDatastore();
28
+ const { slug } = useParams();
29
+ if (!isPresent(datastoreId))
30
30
  return _jsx(DatastoreMissing, {});
31
- return (_jsx(MantaSparqlProvider, { datastoreId: datastore.id, children: _jsx(Viewer, { datastoreId: datastore.id, slug: slug ?? "" }) }));
31
+ return _jsx(Viewer, { datastoreId: datastoreId, slug: slug ?? "" });
32
32
  }
33
33
  function ShareLink() {
34
34
  const [shown, setShown] = useState();
@@ -60,10 +60,14 @@ function UpdatePicture({ datastoreId, slug, snapshot }) {
60
60
  return (_jsxs(_Fragment, { children: [problem === undefined ? null : (_jsx("span", { role: "alert", className: "text-xs text-muted-foreground", children: problem })), _jsxs(Button, { variant: "secondary", size: "sm", disabled: state === "working", onClick: () => void take(), children: [_jsx(Camera, { size: 14, "aria-hidden": "true" }), state === "working" ? "Taking…" : state === "kept" ? "Picture updated" : "Update picture"] })] }));
61
61
  }
62
62
  function Viewer({ datastoreId, slug }) {
63
+ const explain = useExplainDialog();
63
64
  const [search] = useSearchParams();
64
65
  const saved = useSavedHorn(datastoreId, slug);
65
66
  const horn = saved.data?.horn;
66
67
  const variables = horn ? variablesFromSearch(horn, search.toString()) : {};
68
+ // Writing goes through nuqs, which owns the URL; reading stays with `variablesFromSearch`, which is what
69
+ // types a value against the variable that declared it.
70
+ const urlVariables = useHornVariables(horn);
67
71
  const snapshot = useQuery({
68
72
  enabled: isPresent(horn),
69
73
  queryKey: ["horn-view", datastoreId, slug, saved.data?.revision ?? "", variables],
@@ -90,7 +94,7 @@ function Viewer({ datastoreId, slug }) {
90
94
  }
91
95
  if (!isPresent(horn))
92
96
  return _jsx(Loading, { label: "Opening the Horn\u2026", rows: 3 });
93
- return (_jsxs("div", { "data-horn-viewer": true, className: "flex w-full flex-col gap-4", children: [_jsxs("div", { className: "flex flex-wrap items-center justify-between gap-3", children: [_jsxs("div", { className: "flex min-w-0 items-center gap-3", children: [_jsx(Link, { to: hornsPath(datastoreId), className: "text-sm text-muted-foreground hover:text-default-foreground", children: "All Horns" }), _jsx(VariableBar, { horn: horn })] }), _jsxs("div", { className: "flex items-center gap-2", children: [planRuns.problem === undefined ? null : (_jsx("span", { role: "alert", className: "text-xs text-muted-foreground", children: planRuns.problem })), _jsx(UpdatePicture, { datastoreId: datastoreId, slug: slug, snapshot: snapshot.data }), _jsx(ShareLink, {}), _jsx(Button, { variant: "ghost", size: "sm", asChild: true, children: _jsxs(Link, { to: hornPath(datastoreId, slug), children: [_jsx(Pencil, { size: 14, "aria-hidden": "true" }), "Change it"] }) })] })] }), snapshot.isError ? (_jsx(ErrorState, { title: "The Horn could not be drawn", hint: snapshot.error.message })) : !isPresent(snapshot.data) ? (_jsx(Loading, { label: "Drawing the Horn\u2026", rows: 4 })) : (
97
+ return (_jsxs("div", { "data-horn-viewer": true, className: "flex w-full flex-col gap-4", children: [_jsxs("div", { className: "flex flex-wrap items-center justify-between gap-3", children: [_jsxs("div", { className: "flex min-w-0 items-center gap-3", children: [_jsx(Link, { to: hornsPath(), className: "text-sm text-muted-foreground hover:text-default-foreground", children: "All Horns" }), _jsx(VariableBar, { horn: horn })] }), _jsxs("div", { className: "flex items-center gap-2", children: [planRuns.problem === undefined ? null : (_jsx("span", { role: "alert", className: "text-xs text-muted-foreground", children: planRuns.problem })), _jsx(UpdatePicture, { datastoreId: datastoreId, slug: slug, snapshot: snapshot.data }), _jsx(ShareLink, {}), _jsx(Button, { variant: "ghost", size: "sm", asChild: true, children: _jsxs(Link, { to: hornPath(slug), children: [_jsx(Pencil, { size: 14, "aria-hidden": "true" }), "Change it"] }) })] })] }), snapshot.isError ? (_jsx(ErrorState, { title: "The Horn could not be drawn", hint: snapshot.error.message })) : !isPresent(snapshot.data) ? (_jsx(Loading, { label: "Drawing the Horn\u2026", rows: 4 })) : (
94
98
  // The Horn draws its own page, title and canvas.
95
- _jsxs("div", { "data-horn-preview": true, className: "w-full overflow-hidden rounded-xl", children: [_jsx(HornSnapshotView, { snapshot: snapshot.data, mode: "page", theme: appTheme(), planActions: planRuns.actions, writtenView: writtenHornView, catalogCard: catalogCardView }), _jsx(PlanRunDialog, { runs: planRuns, labels: snapshot.data.labels })] }))] }));
99
+ _jsxs("div", { "data-horn-preview": true, className: "w-full overflow-hidden rounded-xl", children: [_jsx(HornSnapshotView, { snapshot: snapshot.data, mode: "page", theme: appTheme(), planActions: planRuns.actions, writtenView: writtenHornView, catalogCard: catalogCardView, explain: explain.open, onVariableChange: urlVariables.set }), _jsx(PlanRunDialog, { runs: planRuns, labels: snapshot.data.labels }), explain.dialog] }))] }));
96
100
  }
@@ -1,6 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { isPresent } from "@manta-eu/lib/present";
3
- import { Link, useParams } from "react-router";
3
+ import { Link } from "react-router";
4
+ import { useActiveDatastore } from "@manta-eu/react/providers";
4
5
  import { ChevronRight, Columns3, Plus } from "lucide-react";
5
6
  import { Button } from "@manta-eu/react";
6
7
  import { PageHeader } from "../components/page-header";
@@ -8,15 +9,11 @@ import { DatastoreMissing, EmptyState, ErrorState, Loading } from "../components
8
9
  import { hornViewPath, newHornPath } from "./paths";
9
10
  import { useQuery } from "@tanstack/react-query";
10
11
  import { isRecord } from "@manta-eu/horns";
11
- import { MantaSparqlProvider } from "../lib/manta/providers";
12
- import { useMantaSession } from "../lib/manta/session";
13
12
  export function HornsPage() {
14
- const { datastoreId } = useParams();
15
- const { datastores } = useMantaSession();
16
- const datastore = datastores.find((candidate) => candidate.id === datastoreId);
17
- if (!isPresent(datastore))
13
+ const { datastoreId } = useActiveDatastore();
14
+ if (!isPresent(datastoreId))
18
15
  return _jsx(DatastoreMissing, {});
19
- return (_jsx(MantaSparqlProvider, { datastoreId: datastore.id, children: _jsx(HornsIn, { datastoreId: datastore.id }) }));
16
+ return _jsx(HornsIn, { datastoreId: datastoreId });
20
17
  }
21
18
  function HornsIn({ datastoreId }) {
22
19
  const horns = useQuery({
@@ -39,5 +36,5 @@ function HornsIn({ datastoreId }) {
39
36
  : []);
40
37
  },
41
38
  });
42
- return (_jsxs("div", { className: "flex w-full flex-col gap-6", children: [_jsx(PageHeader, { title: "Horns", description: "Dashboards and apps made from your data. Open one to look at it or change it, or describe a new one.", actions: _jsx(Button, { variant: "primary", asChild: true, children: _jsxs(Link, { to: newHornPath(datastoreId), children: [_jsx(Plus, { size: 16, "aria-hidden": "true" }), "New Horn"] }) }) }), horns.isPending ? (_jsx(Loading, { label: "Reading this datastore's Horns\u2026" })) : horns.isError ? (_jsx(ErrorState, { title: "Your Horns could not be loaded", hint: "Try again in a moment. If it keeps happening, the data service may be down." })) : !isPresent(horns.data) ? (_jsx(EmptyState, { title: "No Horns 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-horns": true, className: "grid gap-3 sm:grid-cols-2 md:grid-cols-3", children: horns.data.map((horn) => (_jsx("li", { children: _jsxs(Link, { to: hornViewPath(datastoreId, horn.slug), className: "flex items-center gap-3 rounded-lg border border-default-border bg-card px-4 py-3 transition-colors hover:bg-card/70", children: [_jsx("span", { className: "grid size-9 shrink-0 place-items-center rounded-md bg-muted text-muted-foreground", children: _jsx(Columns3, { size: 16 }) }), _jsxs("span", { className: "flex min-w-0 flex-1 flex-col", children: [_jsx("span", { className: "truncate text-sm font-medium text-default-foreground", children: horn.title }), _jsx("span", { className: "truncate text-xs text-muted-foreground", children: horn.number === 1 ? "Saved once" : `Saved ${horn.number} times` })] }), _jsx(ChevronRight, { size: 16, "aria-hidden": "true", className: "shrink-0 text-muted-foreground" })] }) }, horn.slug))) }))] }));
39
+ return (_jsxs("div", { className: "flex w-full flex-col gap-6", children: [_jsx(PageHeader, { title: "Horns", description: "Dashboards and apps made from your data. Open one to look at it or change it, or describe a new one.", actions: _jsx(Button, { variant: "primary", asChild: true, children: _jsxs(Link, { to: newHornPath(), children: [_jsx(Plus, { size: 16, "aria-hidden": "true" }), "New Horn"] }) }) }), horns.isPending ? (_jsx(Loading, { label: "Reading this datastore's Horns\u2026" })) : horns.isError ? (_jsx(ErrorState, { title: "Your Horns could not be loaded", hint: "Try again in a moment. If it keeps happening, the data service may be down." })) : !isPresent(horns.data) ? (_jsx(EmptyState, { title: "No Horns 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(), children: [_jsx(Plus, { size: 14, "aria-hidden": "true" }), "Make your first Horn"] }) }) })) : (_jsx("ul", { "data-horns": true, className: "grid gap-3 sm:grid-cols-2 md:grid-cols-3", children: horns.data.map((horn) => (_jsx("li", { children: _jsxs(Link, { to: hornViewPath(horn.slug), className: "flex items-center gap-3 rounded-lg border border-default-border bg-card px-4 py-3 transition-colors hover:bg-card/70", children: [_jsx("span", { className: "grid size-9 shrink-0 place-items-center rounded-md bg-muted text-muted-foreground", children: _jsx(Columns3, { size: 16 }) }), _jsxs("span", { className: "flex min-w-0 flex-1 flex-col", children: [_jsx("span", { className: "truncate text-sm font-medium text-default-foreground", children: horn.title }), _jsx("span", { className: "truncate text-xs text-muted-foreground", children: horn.number === 1 ? "Saved once" : `Saved ${horn.number} times` })] }), _jsx(ChevronRight, { size: 16, "aria-hidden": "true", className: "shrink-0 text-muted-foreground" })] }) }, horn.slug))) }))] }));
43
40
  }
@@ -1,13 +1,16 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { useState } from "react";
3
+ import { useQuery } from "@tanstack/react-query";
4
+ import { useSearchParams } from "react-router";
5
+ import { z } from "zod";
3
6
  import { isPresent } from "@manta-eu/lib/present";
4
- import { useParams } from "react-router";
7
+ import { useActiveDatastore } from "@manta-eu/react/providers";
8
+ import { hornSchema } from "@manta-eu/horns";
5
9
  import { standaloneHorn } from "@manta-eu/horns/shapes";
6
- import { DatastoreMissing } from "../components/states";
10
+ import { DatastoreMissing, ErrorState, Loading } from "../components/states";
7
11
  import { hornsPath } from "./paths";
8
12
  import { HornStudio } from "./horn-studio";
9
- import { MantaSparqlProvider } from "../lib/manta/providers";
10
- import { useMantaSession } from "../lib/manta/session";
13
+ import { problemFrom } from "../lib/problem";
11
14
  import { shapesRealm } from "../lib/shapes/catalog";
12
15
  // The way in: a Horn from nothing. It opens straight into the Studio with an empty document, the way a new
13
16
  // Lovable project opens on its composer: the first thing typed is what it should show, the model names it as
@@ -18,17 +21,56 @@ const starterPrompts = [
18
21
  "What happened in search last week that I should look at?",
19
22
  ];
20
23
  export function NewHornPage() {
21
- const { datastoreId } = useParams();
22
- const { datastores } = useMantaSession();
23
- const datastore = datastores.find((candidate) => candidate.id === datastoreId);
24
- if (!isPresent(datastore)) {
24
+ const { datastoreId } = useActiveDatastore();
25
+ const [search] = useSearchParams();
26
+ const about = search.get("about");
27
+ if (!isPresent(datastoreId)) {
25
28
  return (_jsx("main", { className: "flex h-svh w-full items-center justify-center bg-sidebar p-6", children: _jsx(DatastoreMissing, {}) }));
26
29
  }
27
- return (_jsx(MantaSparqlProvider, { datastoreId: datastore.id, children: _jsx(NewHorn, { datastoreId: datastore.id }) }));
30
+ return isPresent(about) ? (_jsx(NewHornAbout, { datastoreId: datastoreId, about: about })) : (_jsx(NewHorn, { datastoreId: datastoreId }));
31
+ }
32
+ /**
33
+ * What the lookup answers with: the Horn, and whether it is one somebody kept. A kept Horn brings its slug
34
+ * and the revision it is at, which is what saving an edit of it needs — without them a save would write a
35
+ * second Horn about the same thing, or be refused for revising a Horn it never read.
36
+ */
37
+ const aboutAnswer = z.object({
38
+ horn: hornSchema,
39
+ kept: z.boolean(),
40
+ slug: z.string().min(1).optional(),
41
+ revision: z.string().min(1).optional(),
42
+ });
43
+ /**
44
+ * A Horn started from the thing it is about, rather than from nothing.
45
+ *
46
+ * The worker already builds one for a rule or a plan that nobody has kept — that is what the route answers
47
+ * with — so this opens the Studio on it. Still a draft: it becomes the Horn for that thing when somebody
48
+ * saves it, and `about` is what makes the next lookup find it. One somebody already kept opens as itself,
49
+ * so that saving revises it rather than making another.
50
+ */
51
+ function NewHornAbout({ datastoreId, about }) {
52
+ const seed = useQuery({
53
+ queryKey: ["horn-about", datastoreId, about],
54
+ queryFn: async ({ signal }) => {
55
+ const at = `/api/d/${encodeURIComponent(datastoreId)}/horns/by/about?iri=${encodeURIComponent(about)}`;
56
+ const response = await fetch(at, { signal });
57
+ if (!response.ok)
58
+ throw new Error(await problemFrom(response, `The answer was ${response.status}.`));
59
+ return aboutAnswer.parse(await response.json());
60
+ },
61
+ });
62
+ if (seed.isPending) {
63
+ return (_jsx("main", { className: "flex h-svh w-full items-center justify-center bg-sidebar p-6", children: _jsx(Loading, { label: "Building a Horn for this\u2026" }) }));
64
+ }
65
+ if (seed.isError) {
66
+ return (_jsx("main", { className: "flex h-svh w-full items-center justify-center bg-sidebar p-6", children: _jsx(ErrorState, { title: "Nothing could be built for this", hint: seed.error.message }) }));
67
+ }
68
+ const { horn, kept, slug, revision } = seed.data;
69
+ return (_jsx(HornStudio, { datastoreId: datastoreId, stored: horn, ...(isPresent(slug) ? { slug } : {}), ...(isPresent(revision) ? { revision } : {}), backTo: hornsPath(), welcome: "Here is what this shows. What would you change?", reassurance: kept ? "Saving keeps your changes to this Horn." : "Nothing is saved until you save it.", placeholder: "Describe what you want to see\u2026" }));
28
70
  }
29
71
  function NewHorn({ datastoreId }) {
30
72
  // Made once: the route owns the IRI and the revision on save, and this is a document that has never been
31
73
  // stored. A new render must not make a new one underneath the draft.
32
74
  const [horn] = useState(() => standaloneHorn({ id: "untitled", title: "Untitled Horn", views: [], realm: shapesRealm(), datastoreId }));
33
- return (_jsx(HornStudio, { datastoreId: datastoreId, stored: horn, backTo: hornsPath(datastoreId), welcome: "What should this Horn show?", reassurance: "Say it in your own words, and I will build it from this datastore's data.", placeholder: "Describe what you want to see\u2026", starters: starterPrompts }));
75
+ return (_jsx(HornStudio, { datastoreId: datastoreId, stored: horn, backTo: hornsPath(), welcome: "What should this Horn show?", reassurance: "Say it in your own words, and I will build it from this datastore's data.", placeholder: "Describe what you want to see\u2026", starters: starterPrompts }));
34
76
  }
@@ -1,13 +1,18 @@
1
- export declare function hornsPath(datastoreId: string): string;
2
- export declare function newHornPath(datastoreId: string): string;
3
- export declare function hornPath(datastoreId: string, slug: string): string;
1
+ export declare const hornsPath: () => string;
2
+ export declare const newHornPath: () => string;
3
+ /** A new Horn started from the thing it is about: a rule, an action plan. The IRI rides as a parameter. */
4
+ export declare const newHornAboutPath: (iri: string) => string;
5
+ export declare const hornPath: (slug: string) => string;
4
6
  /** A saved Horn as the thing itself: what a shared link opens, with the variables it carries. */
5
- export declare function hornViewPath(datastoreId: string, slug: string): string;
6
- export declare function shapesPath(datastoreId: string): string;
7
- export declare function shapeZooPath(datastoreId: string, schema: string): string;
8
- export declare function shapeEntityPath(datastoreId: string, schema: string, entity: string): string;
7
+ export declare const hornViewPath: (slug: string) => string;
9
8
  /** A datastore's Horns as cards: what each one answers, who it is for, what it reads. */
10
- export declare const showcasePath: (datastoreId: string) => string;
9
+ export declare const showcasePath: () => string;
11
10
  /** What Manta works out about a datastore: every rule, and whether it can be looked at. */
12
- export declare function rulesPath(datastoreId: string): string;
13
- export declare function rulePath(datastoreId: string, ruleId: string): string;
11
+ export declare const rulesPath: () => string;
12
+ export declare const plansPath: () => string;
13
+ export declare const rulePath: (ruleId: string) => string;
14
+ /** The reference Horn drawn against a fixture. Dev only; see docs/standard.md. */
15
+ export declare const referencePath: () => string;
16
+ export declare const shapesPath: () => string;
17
+ export declare const shapeZooPath: (schema: string) => string;
18
+ export declare const shapeEntityPath: (schema: string, entity: string) => string;
package/dist/app/paths.js CHANGED
@@ -1,32 +1,21 @@
1
- export function hornsPath(datastoreId) {
2
- return `/d/${encodeURIComponent(datastoreId)}/horns`;
3
- }
4
- export function newHornPath(datastoreId) {
5
- return `${hornsPath(datastoreId)}/new`;
6
- }
7
- export function hornPath(datastoreId, slug) {
8
- return `${hornsPath(datastoreId)}/${encodeURIComponent(slug)}`;
9
- }
1
+ // Relative to the datastore: `ActiveDatastoreProvider` scopes the router's basename to `/d/<datastoreId>`,
2
+ // so a helper that wrote the prefix itself would double it.
3
+ export const hornsPath = () => "/horns";
4
+ export const newHornPath = () => "/horns/new";
5
+ /** A new Horn started from the thing it is about: a rule, an action plan. The IRI rides as a parameter. */
6
+ export const newHornAboutPath = (iri) => `/horns/new?about=${encodeURIComponent(iri)}`;
7
+ export const hornPath = (slug) => `/horns/${encodeURIComponent(slug)}`;
10
8
  /** A saved Horn as the thing itself: what a shared link opens, with the variables it carries. */
11
- export function hornViewPath(datastoreId, slug) {
12
- return `${hornPath(datastoreId, slug)}/view`;
13
- }
14
- export function shapesPath(datastoreId) {
15
- return `/d/${encodeURIComponent(datastoreId)}/shapes`;
16
- }
17
- export function shapeZooPath(datastoreId, schema) {
18
- return `${shapesPath(datastoreId)}/${encodeURIComponent(schema)}`;
19
- }
20
- // An entity IRI carries slashes and colons, so the entity travels as a query parameter rather than a segment.
21
- export function shapeEntityPath(datastoreId, schema, entity) {
22
- return `${shapeZooPath(datastoreId, schema)}/entity?iri=${encodeURIComponent(entity)}`;
23
- }
9
+ export const hornViewPath = (slug) => `${hornPath(slug)}/view`;
24
10
  /** A datastore's Horns as cards: what each one answers, who it is for, what it reads. */
25
- export const showcasePath = (datastoreId) => `/d/${encodeURIComponent(datastoreId)}/showcase`;
11
+ export const showcasePath = () => "/horns/showcase";
26
12
  /** What Manta works out about a datastore: every rule, and whether it can be looked at. */
27
- export function rulesPath(datastoreId) {
28
- return `/d/${encodeURIComponent(datastoreId)}/rules`;
29
- }
30
- export function rulePath(datastoreId, ruleId) {
31
- return `${rulesPath(datastoreId)}/${encodeURIComponent(ruleId)}`;
32
- }
13
+ export const rulesPath = () => "/horns/rules";
14
+ export const plansPath = () => "/horns/plans";
15
+ export const rulePath = (ruleId) => `${rulesPath()}/${encodeURIComponent(ruleId)}`;
16
+ /** The reference Horn drawn against a fixture. Dev only; see docs/standard.md. */
17
+ export const referencePath = () => "/horns/reference";
18
+ export const shapesPath = () => "/horns/shapes";
19
+ export const shapeZooPath = (schema) => `${shapesPath()}/${encodeURIComponent(schema)}`;
20
+ // An entity IRI carries slashes and colons, so the entity travels as a query parameter rather than a segment.
21
+ export const shapeEntityPath = (schema, entity) => `${shapeZooPath(schema)}/entity?iri=${encodeURIComponent(entity)}`;
@@ -0,0 +1 @@
1
+ export declare function PlansPage(): import("react").JSX.Element;
@@ -0,0 +1,59 @@
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 } from "react-router";
5
+ import { useActiveDatastore } from "@manta-eu/react/providers";
6
+ import { ChevronRight } from "lucide-react";
7
+ import { Badge, Skeleton } from "@manta-eu/react";
8
+ import { planNameFromIri, plansQuery, readPlans, planStatusQuery, readPlanStatus, } from "@manta-eu/horns/render";
9
+ import { newHornAboutPath } from "./paths";
10
+ import { Card, CardTitle, PageHeader } from "../components/page-header";
11
+ import { DatastoreMissing, EmptyState, ErrorState } from "../components/states";
12
+ import { useSparqlSelect } from "../lib/manta/sparql-select";
13
+ import { shapesRealm } from "../lib/shapes/catalog";
14
+ // The work Manta has drafted, and the way to make a Horn out of one. A plan is a named graph, so it has no
15
+ // page of its own here: the Horn is the page, and this is where you start one.
16
+ const stateWords = {
17
+ draft: { text: "Not run yet", variant: "info" },
18
+ running: { text: "Running", variant: "info" },
19
+ completed: { text: "Done", variant: "success" },
20
+ failed: { text: "Did not go through", variant: "warning" },
21
+ "timed-out": { text: "Took too long", variant: "warning" },
22
+ };
23
+ // Not the SDK's `PlanRow` (`@manta-eu/react/plan-row`): its `PlanRowPlan` wants `actionCount`,
24
+ // `completedActionCount`, `subjects` and a `GQLIntegration`, which come from agents' GraphQL — and the
25
+ // browser gets no GraphQL route (AGENTS.md §Writes). This list reads the plans graph over SPARQL, so it
26
+ // holds the IRI, the name and the state and nothing that would fill those fields honestly.
27
+ function ListedPlan({ plan }) {
28
+ const said = stateWords[plan.status.state];
29
+ return (_jsxs("li", { "data-plan": plan.iri, className: "flex flex-wrap items-center justify-between gap-3 px-4 py-2.5", children: [_jsx("span", { className: "flex min-w-0 flex-col", children: _jsx("span", { className: "text-default-foreground truncate text-sm font-medium", children: plan.status.name ?? planNameFromIri(plan.iri) ?? "Untitled plan" }) }), _jsxs("span", { className: "flex items-center gap-3", children: [_jsx(Badge, { variant: said.variant, children: said.text }), _jsxs(Link, { className: "text-muted-foreground hover:text-default-foreground flex items-center gap-1 text-sm", to: newHornAboutPath(plan.iri), children: ["Make a Horn from this", _jsx(ChevronRight, { size: 14, "aria-hidden": "true" })] })] })] }));
30
+ }
31
+ function Plans({ datastoreId }) {
32
+ const select = useSparqlSelect();
33
+ const realm = shapesRealm();
34
+ const listed = useQuery({
35
+ queryKey: ["plans", datastoreId],
36
+ queryFn: async ({ signal }) => {
37
+ const iris = readPlans(await select(plansQuery(realm), signal));
38
+ if (iris.length === 0)
39
+ return [];
40
+ // Names and states come from the plans graph, not from the IRIs, which say nothing.
41
+ const statuses = readPlanStatus(await select(planStatusQuery(realm, iris), signal));
42
+ return iris.map((iri) => ({ iri, status: statuses.get(iri) ?? { state: "draft" } }));
43
+ },
44
+ });
45
+ if (listed.isPending)
46
+ return _jsx(Skeleton, { className: "h-64 w-full" });
47
+ if (listed.isError)
48
+ return _jsx(ErrorState, { title: "The plans could not be read.", hint: listed.error.message });
49
+ if (!isPresent(listed.data)) {
50
+ return (_jsx(EmptyState, { title: "No action plans yet", hint: "Manta drafts these when a rule finds work to do. When one appears, you can make a Horn from it here." }));
51
+ }
52
+ return (_jsxs(Card, { children: [_jsx(CardTitle, { children: "What Manta has drafted" }), _jsx("ul", { className: "divide-default-border divide-y", children: listed.data.map((plan) => (_jsx(ListedPlan, { plan: plan }, plan.iri))) })] }));
53
+ }
54
+ export function PlansPage() {
55
+ const { datastoreId } = useActiveDatastore();
56
+ if (!isPresent(datastoreId))
57
+ return _jsx(DatastoreMissing, {});
58
+ return (_jsxs("div", { className: "flex flex-col gap-6", children: [_jsx(PageHeader, { title: "Action plans", description: "The changes Manta has worked out and not made. Open one as a Horn to see what it would do." }), _jsx(Plans, { datastoreId: datastoreId })] }));
59
+ }
@@ -0,0 +1 @@
1
+ export declare function ReferencePage(): import("react").JSX.Element;
@@ -0,0 +1,55 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useMemo } from "react";
3
+ import { keepPreviousData, useQuery } from "@tanstack/react-query";
4
+ import { hornSchema } from "@manta-eu/horns";
5
+ import { buildHornSnapshot } from "@manta-eu/horns/render";
6
+ import { HornSnapshotView } from "@manta-eu/horns-kit";
7
+ import { writtenHornView } from "../components/written-horn-view";
8
+ import { catalogCardView } from "../components/catalog-card-view";
9
+ import { useExplainDialog } from "../components/explain-dialog";
10
+ import { ErrorState, Loading } from "../components/states";
11
+ import { PageHeader } from "../components/page-header";
12
+ import { useHornVariables } from "../lib/horn-variables";
13
+ import { fakeSparqlSelect, referenceEntity } from "../lib/reference/fake-sparql";
14
+ import { appTheme } from "../lib/theme";
15
+ // The reference Horn from docs/standard.md, drawn against a datastore that does not exist. Dev only, and the
16
+ // document and its fixture are imported lazily so neither reaches a deployed bundle.
17
+ //
18
+ // It is the whole real path — the binding compiles, the label pass runs, the written source is transpiled and
19
+ // bound to the design system — with `select` swapped for a stub. That is what makes it worth looking at: what
20
+ // is on screen is what a datastore holding this data would show.
21
+ /** Parsed once: the document is a static import and re-parsing it per render would rebuild every view. */
22
+ function useReferenceHorn() {
23
+ return useMemo(async () => {
24
+ const document = (await import("../reference/supply-chain-360.horn.json")).default;
25
+ return hornSchema.parse(document);
26
+ }, []);
27
+ }
28
+ export function ReferencePage() {
29
+ const explain = useExplainDialog();
30
+ const pending = useReferenceHorn();
31
+ const loaded = useQuery({ queryKey: ["reference-horn-document"], queryFn: () => pending });
32
+ // What the page is narrowed to, in the URL. A view sets one and every binding taking it as a parameter is
33
+ // asked again, which is the whole of how one card filters another.
34
+ const filters = useHornVariables(loaded.data);
35
+ const drawn = useQuery({
36
+ enabled: loaded.data !== undefined,
37
+ // The page it drew stays up while the next one is asked for, so narrowing does not blink.
38
+ placeholderData: keepPreviousData,
39
+ queryKey: ["reference-horn", filters.values],
40
+ queryFn: async ({ signal }) => {
41
+ const horn = loaded.data;
42
+ if (horn === undefined)
43
+ throw new Error("The reference Horn has not loaded.");
44
+ return await buildHornSnapshot({
45
+ horn,
46
+ select: fakeSparqlSelect,
47
+ variables: { entity: referenceEntity, ...filters.values },
48
+ // Plans come from a datastore, and there is none.
49
+ plans: false,
50
+ signal,
51
+ });
52
+ },
53
+ });
54
+ return (_jsxs("div", { className: "flex flex-col gap-6", children: [_jsx(PageHeader, { title: "Supply Chain 360", description: "The reference Horn from docs/standard.md, drawn against a fixture. No datastore is read." }), drawn.isPending && drawn.data === undefined ? (_jsx(Loading, { label: "Drawing the reference Horn\u2026", rows: 4 })) : drawn.isError ? (_jsx(ErrorState, { title: "The reference Horn could not be drawn", hint: drawn.error.message })) : (_jsxs("div", { "data-horn-preview": true, className: "w-full overflow-hidden rounded-xl", children: [_jsx(HornSnapshotView, { snapshot: drawn.data, mode: "page", theme: appTheme(), writtenView: writtenHornView, catalogCard: catalogCardView, explain: explain.open, onVariableChange: filters.set }), explain.dialog] }))] }));
55
+ }