@manta-eu/horns-kit 0.1.3 → 0.2.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.
- package/dist/embed/horn-snapshot-view.d.ts +3 -0
- package/dist/embed/horn-snapshot-view.js +6 -4
- package/dist/embed/plan-placement.d.ts +17 -1
- package/dist/embed/plan-placement.js +11 -1
- package/dist/embed/plan-strip.d.ts +7 -0
- package/dist/embed/plan-strip.js +26 -9
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -1
- package/package.json +2 -2
|
@@ -2,6 +2,7 @@ import { type ReactNode } from "react";
|
|
|
2
2
|
import { type HornSnapshot, type SnapshotView } from "@manta-eu/horns/render";
|
|
3
3
|
import { type ChartTheme, type EntityLink } from "../look/index.ts";
|
|
4
4
|
import type { PlanActions } from "./plan-strip.tsx";
|
|
5
|
+
import { type PlanStripArgs } from "./plan-placement.tsx";
|
|
5
6
|
import { type ExplainedFact } from "./view-powers.tsx";
|
|
6
7
|
export type HornViewMode = "page" | "embed" | "capture";
|
|
7
8
|
/**
|
|
@@ -58,6 +59,8 @@ export type HornSnapshotViewProps = {
|
|
|
58
59
|
catalogCard?: (content: Extract<SnapshotView["content"], {
|
|
59
60
|
lane: "component";
|
|
60
61
|
}>, view: SnapshotView, context: WrittenViewContext) => ReactNode;
|
|
62
|
+
/** Draws a plan's changes and its buttons. Same bargain again; without it the kit's own strip draws. */
|
|
63
|
+
planStrip?: (args: PlanStripArgs) => ReactNode;
|
|
61
64
|
};
|
|
62
65
|
/**
|
|
63
66
|
* Where each of a snapshot's IRIs opens, ready to hand a view.
|
|
@@ -69,7 +69,7 @@ function WrittenSlot({ placeholder, draw }) {
|
|
|
69
69
|
}
|
|
70
70
|
function ViewCard(props) {
|
|
71
71
|
const { view, theme, chrome, hornStyle, labels, links, plans, planActions, controls, writtenView } = props;
|
|
72
|
-
const { catalogCard, datastoreId, style } = props;
|
|
72
|
+
const { catalogCard, datastoreId, planStrip, style } = props;
|
|
73
73
|
const context = { labels, links, datastoreId };
|
|
74
74
|
const colors = surfaces[theme];
|
|
75
75
|
const { content } = view;
|
|
@@ -88,6 +88,7 @@ function ViewCard(props) {
|
|
|
88
88
|
links,
|
|
89
89
|
palette: chartPalette(theme, hornStyle),
|
|
90
90
|
...(planActions ? { actions: planActions } : {}),
|
|
91
|
+
...(planStrip ? { strip: planStrip } : {}),
|
|
91
92
|
shown: Object.fromEntries((content.plans ?? []).map((behind) => [behind.plan, behind.entities])),
|
|
92
93
|
}, children: _jsx(WrittenSlot, { placeholder: _jsx("p", { style: { margin: 0, color: colors.muted, fontSize: 14 }, children: "This view is written code, which draws in the browser rather than here." }), draw: () => writtenView?.(content, view, context) }) })) : (_jsx("p", { role: "alert", style: { margin: 0, color: colors.error, fontSize: 14 }, children: content.message }))] }));
|
|
93
94
|
}
|
|
@@ -100,6 +101,7 @@ export function hornSurface(snapshot, theme) {
|
|
|
100
101
|
}
|
|
101
102
|
export function HornSnapshotView(props) {
|
|
102
103
|
const { snapshot, mode = "page", theme = "light", planActions, viewControls, writtenView, catalogCard } = props;
|
|
104
|
+
const { planStrip } = props;
|
|
103
105
|
const { explain, onVariableChange } = props;
|
|
104
106
|
const style = snapshot.style;
|
|
105
107
|
const surface = hornSurface(snapshot, theme);
|
|
@@ -110,10 +112,10 @@ export function HornSnapshotView(props) {
|
|
|
110
112
|
return (_jsx("main", { style: { width: captureWidth, background: colors.card, color: colors.ink, fontFamily: font }, children: snapshot.views.map((view) => (_jsx(ViewCard, { view: view, theme: surface, chrome: chrome, hornStyle: style, labels: snapshot.labels, links: links, datastoreId: snapshot.horn.datastoreId, plans: snapshot.plans,
|
|
111
113
|
// A capture on a server has no browser to draw written source in and passes neither of these, so
|
|
112
114
|
// it still gets the placeholder. A capture taken *by* a browser passes them and gets the view.
|
|
113
|
-
writtenView: writtenView, catalogCard: catalogCard }, view.id))) }));
|
|
115
|
+
writtenView: writtenView, catalogCard: catalogCard, planStrip: planStrip }, view.id))) }));
|
|
114
116
|
}
|
|
115
117
|
if (mode === "embed") {
|
|
116
|
-
return (_jsx("div", { style: { display: "grid", gap: chrome.gap, fontFamily: font, color: colors.ink }, children: snapshot.views.map((view) => (_jsx(ViewCard, { view: view, theme: surface, chrome: chrome, hornStyle: style, labels: snapshot.labels, links: links, datastoreId: snapshot.horn.datastoreId, plans: snapshot.plans, planActions: planActions, writtenView: writtenView, catalogCard: catalogCard, style: { borderRadius: chrome.radius, border: `1px solid ${colors.border}` } }, view.id))) }));
|
|
118
|
+
return (_jsx("div", { style: { display: "grid", gap: chrome.gap, fontFamily: font, color: colors.ink }, children: snapshot.views.map((view) => (_jsx(ViewCard, { view: view, theme: surface, chrome: chrome, hornStyle: style, labels: snapshot.labels, links: links, datastoreId: snapshot.horn.datastoreId, plans: snapshot.plans, planActions: planActions, writtenView: writtenView, catalogCard: catalogCard, planStrip: planStrip, style: { borderRadius: chrome.radius, border: `1px solid ${colors.border}` } }, view.id))) }));
|
|
117
119
|
}
|
|
118
120
|
const byId = new Map(snapshot.views.map((view) => [view.id, view]));
|
|
119
121
|
const order = new Map(snapshot.views.map((view, index) => [view.id, index]));
|
|
@@ -151,7 +153,7 @@ export function HornSnapshotView(props) {
|
|
|
151
153
|
minWidth: `min(100%, ${Math.max(minCard, (span / gridColumns) * minPageWidth)}px)`,
|
|
152
154
|
minHeight: rows * rowHeight + (rows - 1) * chrome.gap,
|
|
153
155
|
boxSizing: "border-box",
|
|
154
|
-
}, children: _jsx(ViewCard, { view: view, theme: surface, chrome: chrome, hornStyle: style, labels: snapshot.labels, links: links, datastoreId: snapshot.horn.datastoreId, plans: snapshot.plans, planActions: planActions, writtenView: writtenView, catalogCard: catalogCard, ...(viewControls ? { controls: viewControls(view) } : {}), style: {
|
|
156
|
+
}, children: _jsx(ViewCard, { view: view, theme: surface, chrome: chrome, hornStyle: style, labels: snapshot.labels, links: links, datastoreId: snapshot.horn.datastoreId, plans: snapshot.plans, planActions: planActions, writtenView: writtenView, catalogCard: catalogCard, planStrip: planStrip, ...(viewControls ? { controls: viewControls(view) } : {}), style: {
|
|
155
157
|
flex: "1 1 auto",
|
|
156
158
|
minWidth: 0,
|
|
157
159
|
borderRadius: chrome.radius,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
1
2
|
import type { PlanStatus, SnapshotPlan } from "@manta-eu/horns/render";
|
|
2
3
|
import type { ChartPalette } from "../look/palette.ts";
|
|
3
4
|
import type { EntityLink } from "../look/types.ts";
|
|
@@ -11,6 +12,21 @@ export type PlanPlacement = {
|
|
|
11
12
|
actions?: PlanActions | undefined;
|
|
12
13
|
/** The things each plan changes that this view draws, by plan IRI. */
|
|
13
14
|
shown?: Readonly<Record<string, readonly string[]>>;
|
|
15
|
+
/**
|
|
16
|
+
* Draws the plan. Same bargain as `writtenView`: the kit knows nothing of `@manta-eu/react`, so a host
|
|
17
|
+
* that wants its own buttons and badges supplies this. Without it the kit's own strip draws, which is what
|
|
18
|
+
* a server render and a capture without a host always see.
|
|
19
|
+
*/
|
|
20
|
+
strip?: (args: PlanStripArgs) => ReactNode;
|
|
21
|
+
};
|
|
22
|
+
/** What a host needs to draw one plan, already resolved against the snapshot. */
|
|
23
|
+
export type PlanStripArgs = {
|
|
24
|
+
plan: SnapshotPlan;
|
|
25
|
+
/** The things the plan changes that this view draws, first in the list. */
|
|
26
|
+
onView: readonly string[];
|
|
27
|
+
labels: Readonly<Record<string, string>>;
|
|
28
|
+
links: Readonly<Record<string, EntityLink>>;
|
|
29
|
+
actions?: PlanActions | undefined;
|
|
14
30
|
};
|
|
15
31
|
export declare const PlanPlacementProvider: import("react").Provider<PlanPlacement | undefined>;
|
|
16
32
|
export type SuggestedChangesProps = {
|
|
@@ -23,7 +39,7 @@ export type SuggestedChangesProps = {
|
|
|
23
39
|
* Says so plainly when there is nothing to draw rather than rendering an empty box: a view naming a plan the
|
|
24
40
|
* Horn does not hold is a mistake worth seeing while somebody is reading the code.
|
|
25
41
|
*/
|
|
26
|
-
export declare function SuggestedChanges({ plan }: SuggestedChangesProps): import("react").JSX.Element | null;
|
|
42
|
+
export declare function SuggestedChanges({ plan }: SuggestedChangesProps): string | number | bigint | boolean | import("react").JSX.Element | Iterable<ReactNode> | Promise<string | number | bigint | boolean | import("react").ReactPortal | import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | Iterable<ReactNode> | null | undefined> | null | undefined;
|
|
27
43
|
/** The plans behind the view being drawn, so a view can name them without being handed callbacks. */
|
|
28
44
|
export declare function usePlansBehindView(): readonly SnapshotPlan[];
|
|
29
45
|
export type { PlanStatus };
|
|
@@ -16,7 +16,17 @@ export function SuggestedChanges({ plan }) {
|
|
|
16
16
|
const found = placement.plans.find((one) => one.iri === plan);
|
|
17
17
|
if (found === undefined)
|
|
18
18
|
return null;
|
|
19
|
-
|
|
19
|
+
const onView = placement.shown?.[plan] ?? [];
|
|
20
|
+
if (placement.strip !== undefined) {
|
|
21
|
+
return placement.strip({
|
|
22
|
+
plan: found,
|
|
23
|
+
onView,
|
|
24
|
+
labels: placement.labels,
|
|
25
|
+
links: placement.links,
|
|
26
|
+
actions: placement.actions,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
return (_jsx(PlanStrip, { plan: found, onView: onView, labels: placement.labels, links: placement.links, palette: placement.palette, actions: placement.actions }));
|
|
20
30
|
}
|
|
21
31
|
/** The plans behind the view being drawn, so a view can name them without being handed callbacks. */
|
|
22
32
|
export function usePlansBehindView() {
|
|
@@ -22,6 +22,13 @@ export type PlanActions = {
|
|
|
22
22
|
export declare function planTitle(plan: SnapshotPlan, labels: Readonly<Record<string, string>>): string;
|
|
23
23
|
/** "Changes 1 campaign and 3 campaign budgets." */
|
|
24
24
|
export declare function planSummary(plan: SnapshotPlan, labels: Readonly<Record<string, string>>): string;
|
|
25
|
+
/**
|
|
26
|
+
* Said when the plan touches more things than it has readable changes for.
|
|
27
|
+
*
|
|
28
|
+
* `kinds` counts what the plan concerns; `changes` holds the ones whose before and after could both be read.
|
|
29
|
+
* "Changes 3 campaigns" over one listed row reads as a bug unless the gap is named.
|
|
30
|
+
*/
|
|
31
|
+
export declare function unreadableChanges(plan: SnapshotPlan): number;
|
|
25
32
|
export declare function planValue(value: PlanChange["from"], change: PlanChange, labels: Readonly<Record<string, string>>): string;
|
|
26
33
|
/**
|
|
27
34
|
* How the plan's last run went, in words. A run from before this page was opened says when it was, because a
|
package/dist/embed/plan-strip.js
CHANGED
|
@@ -47,6 +47,16 @@ export function planSummary(plan, labels) {
|
|
|
47
47
|
}
|
|
48
48
|
return `Changes ${listed(typed.map((kind) => `${kind.count} ${plural(labels[kind.type ?? ""] ?? "", kind.count)}`))}.`;
|
|
49
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* Said when the plan touches more things than it has readable changes for.
|
|
52
|
+
*
|
|
53
|
+
* `kinds` counts what the plan concerns; `changes` holds the ones whose before and after could both be read.
|
|
54
|
+
* "Changes 3 campaigns" over one listed row reads as a bug unless the gap is named.
|
|
55
|
+
*/
|
|
56
|
+
export function unreadableChanges(plan) {
|
|
57
|
+
const things = plan.kinds.reduce((total, kind) => total + kind.count, 0);
|
|
58
|
+
return Math.max(0, things - new Set(plan.changes.map((change) => change.entity)).size);
|
|
59
|
+
}
|
|
50
60
|
// Platforms name states in capitals: "PAUSED" reads "Paused".
|
|
51
61
|
function spoken(text) {
|
|
52
62
|
if (!/^[A-Z][A-Z0-9_]*$/.test(text))
|
|
@@ -68,22 +78,28 @@ export function planValue(value, change, labels) {
|
|
|
68
78
|
* preview from last week says little about the plan as it stands now.
|
|
69
79
|
*/
|
|
70
80
|
export function planStateText(status, earlier = false) {
|
|
71
|
-
const said = planStateWords(status);
|
|
81
|
+
const said = planStateWords(status, earlier);
|
|
72
82
|
const day = status.lastRun?.startedAt.slice(0, 10);
|
|
73
83
|
return earlier && status.state !== "draft" && day !== undefined && day !== ""
|
|
74
|
-
? { ...said, text: `${said.text}
|
|
84
|
+
? { ...said, text: `${said.text} ${day}` }
|
|
75
85
|
: said;
|
|
76
86
|
}
|
|
77
|
-
function planStateWords(status) {
|
|
87
|
+
function planStateWords(status, earlier) {
|
|
78
88
|
const preview = status.lastRun?.preview === true;
|
|
79
89
|
return match(status.state)
|
|
80
|
-
.with("draft", () => ({ text: "Not
|
|
81
|
-
.with("running", () => ({ text: preview ? "
|
|
82
|
-
.with("completed", () =>
|
|
83
|
-
|
|
90
|
+
.with("draft", () => ({ text: "Not checked yet", tone: "quiet" }))
|
|
91
|
+
.with("running", () => ({ text: preview ? "Checking…" : "Making the changes…", tone: "busy" }))
|
|
92
|
+
.with("completed", () =>
|
|
93
|
+
// A check from before this page was opened does not make the plan runnable, so it must not say it is:
|
|
94
|
+
// the run button stays disabled, and "ready to make" beside a disabled button reads as a bug.
|
|
95
|
+
preview
|
|
96
|
+
? {
|
|
97
|
+
text: earlier ? "Checked" : "Checked, ready to make",
|
|
98
|
+
tone: earlier ? "quiet" : "good",
|
|
99
|
+
}
|
|
84
100
|
: { text: "Changes made", tone: "good" })
|
|
85
101
|
.with("failed", () => ({
|
|
86
|
-
text: preview ? "
|
|
102
|
+
text: preview ? "Checking found a problem" : "The changes did not go through",
|
|
87
103
|
tone: "bad",
|
|
88
104
|
}))
|
|
89
105
|
.with("timed-out", () => ({ text: "It took too long and stopped", tone: "bad" }))
|
|
@@ -107,6 +123,7 @@ export function PlanStrip(props) {
|
|
|
107
123
|
const changes = [...plan.changes].sort((a, b) => Number(near.has(b.entity)) - Number(near.has(a.entity)));
|
|
108
124
|
const shown = changes.slice(0, maxShownChanges);
|
|
109
125
|
const hidden = plan.changes.length - shown.length;
|
|
126
|
+
const unreadable = unreadableChanges(plan);
|
|
110
127
|
const toneColor = {
|
|
111
128
|
quiet: palette.muted,
|
|
112
129
|
busy: palette.secondary,
|
|
@@ -142,5 +159,5 @@ export function PlanStrip(props) {
|
|
|
142
159
|
const name = nameOf(change.entity);
|
|
143
160
|
const why = reasonsFor(change.entity);
|
|
144
161
|
return (_jsxs("li", { "data-plan-change": true, children: [link === undefined ? (_jsx("span", { style: { fontWeight: 600 }, children: name })) : (_jsx("a", { href: link.href, target: "_blank", rel: "noopener noreferrer", title: link.label, style: { fontWeight: 600, color: "inherit", textUnderlineOffset: 3 }, children: name })), _jsxs("span", { style: { color: palette.secondary }, children: [` — ${labels[change.property] ?? "A setting"}: `, _jsx("span", { style: { textDecoration: "line-through", textDecorationColor: palette.muted }, children: planValue(change.from, change, labels) }), " → "] }), _jsx("span", { style: { fontWeight: 600 }, children: planValue(change.to, change, labels) }), why.length > 0 ? (_jsx("div", { style: { color: palette.muted }, children: `Why: ${why.map((reason) => reason.text).join("; ")}` })) : null] }, index));
|
|
145
|
-
}) })) : (_jsx("p", { style: { margin: 0, color: palette.muted }, children: "Nothing here differs from how things are now." })), hidden > 0 ? (_jsx("p", { style: { margin: "6px 0 0", color: palette.muted }, children: `And ${hidden} more ${hidden === 1 ? "change" : "changes"}.` })) : null, actions === undefined ? null : (_jsxs("div", { style: { display: "flex", flexWrap: "wrap", gap: 8, marginTop: 10 }, children: [button("
|
|
162
|
+
}) })) : (_jsx("p", { style: { margin: 0, color: palette.muted }, children: "Nothing here differs from how things are now." })), hidden > 0 ? (_jsx("p", { style: { margin: "6px 0 0", color: palette.muted }, children: `And ${hidden} more ${hidden === 1 ? "change" : "changes"}.` })) : null, unreadable > 0 ? (_jsx("p", { style: { margin: "6px 0 0", color: palette.muted }, children: `It also touches ${unreadable} more ${unreadable === 1 ? "thing" : "things"} whose before and after could not be read.` })) : null, actions === undefined ? null : (_jsxs("div", { style: { display: "flex", flexWrap: "wrap", gap: 8, marginTop: 10 }, children: [button("Check it", () => actions.onPreview(plan), !busy, false, "Try the changes without making them"), button("Make these changes", () => actions.onRun(plan), !busy && runnable(live), true, runnable(live) ? "Changes them in the platforms they came from" : "Check it first")] }))] }));
|
|
146
163
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -2,5 +2,6 @@ export { mantaReactCatalog } from "./catalog.ts";
|
|
|
2
2
|
export * from "./look/index.ts";
|
|
3
3
|
export { shapeViewSpecs } from "./shape-views/index.ts";
|
|
4
4
|
export { entityLinks, HornSnapshotView, type HornSnapshotViewProps, type HornViewMode, type WrittenViewContext, } from "./embed/horn-snapshot-view.tsx";
|
|
5
|
-
export { planStateText, planSummary, planTitle, runnable, type PlanActions } from "./embed/plan-strip.tsx";
|
|
5
|
+
export { maxShownChanges, planStateText, planSummary, planTitle, planValue, runnable, unreadableChanges, type PlanActions, } from "./embed/plan-strip.tsx";
|
|
6
|
+
export type { PlanStripArgs } from "./embed/plan-placement.tsx";
|
|
6
7
|
export type { WrittenSurface } from "./written/runtime.ts";
|
package/dist/index.js
CHANGED
|
@@ -2,4 +2,4 @@ export { mantaReactCatalog } from "./catalog.js";
|
|
|
2
2
|
export * from "./look/index.js";
|
|
3
3
|
export { shapeViewSpecs } from "./shape-views/index.js";
|
|
4
4
|
export { entityLinks, HornSnapshotView, } from "./embed/horn-snapshot-view.js";
|
|
5
|
-
export { planStateText, planSummary, planTitle, runnable } from "./embed/plan-strip.js";
|
|
5
|
+
export { maxShownChanges, planStateText, planSummary, planTitle, planValue, runnable, unreadableChanges, } from "./embed/plan-strip.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@manta-eu/horns-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Manta Horns component kit: charts and snapshot views that render on a server and hydrate in any React host, and the components the HornView sandbox runs.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
"dependencies": {
|
|
56
56
|
"sucrase": "^3.35.1",
|
|
57
57
|
"ts-pattern": "^5.9.0",
|
|
58
|
-
"@manta-eu/horns": "0.
|
|
58
|
+
"@manta-eu/horns": "0.2.0"
|
|
59
59
|
},
|
|
60
60
|
"peerDependencies": {
|
|
61
61
|
"react": ">=18",
|