@manta-eu/horns-kit 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/embed/horn-snapshot-view.d.ts +13 -1
- package/dist/embed/horn-snapshot-view.js +50 -32
- package/dist/embed/plan-placement.d.ts +29 -0
- package/dist/embed/plan-placement.js +24 -0
- package/dist/embed/plan-strip.d.ts +1 -1
- package/dist/embed/plan-strip.js +9 -4
- package/dist/embed/view-powers.d.ts +76 -0
- package/dist/embed/view-powers.js +78 -0
- package/dist/written/runtime.js +35 -2
- package/package.json +10 -2
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
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
|
-
import {
|
|
4
|
+
import type { PlanActions } from "./plan-strip.tsx";
|
|
5
|
+
import { type ExplainedFact } from "./view-powers.tsx";
|
|
5
6
|
export type HornViewMode = "page" | "embed" | "capture";
|
|
6
7
|
/**
|
|
7
8
|
* What a written view gets beyond its own rows: the snapshot's names and where each IRI opens. A view is
|
|
@@ -24,6 +25,17 @@ export type HornSnapshotViewProps = {
|
|
|
24
25
|
theme?: ChartTheme;
|
|
25
26
|
/** What the viewer may do with the action plans behind a view. Without it plans are shown, never run. */
|
|
26
27
|
planActions?: PlanActions;
|
|
28
|
+
/**
|
|
29
|
+
* Opens the explanation behind a value a view marked with `WhyThis`. Drawing one needs the design system,
|
|
30
|
+
* which the kit has not got; without this the marks are left off and the values draw alone, which is what
|
|
31
|
+
* a server render and a capture always see.
|
|
32
|
+
*/
|
|
33
|
+
explain?: (fact: ExplainedFact) => void;
|
|
34
|
+
/**
|
|
35
|
+
* Narrows the Horn when a view asks. Every binding taking that variable as a parameter is re-run by the
|
|
36
|
+
* host, which is how a tile filters a list it knows nothing about. Without it a view draws no controls.
|
|
37
|
+
*/
|
|
38
|
+
onVariableChange?: (id: string, value: unknown) => void;
|
|
27
39
|
/**
|
|
28
40
|
* Controls a host hangs on each view's own card — renaming it, taking it out. The kit only makes room: the
|
|
29
41
|
* markup is the host's, so a capture, which passes none, draws exactly what it drew before.
|
|
@@ -3,7 +3,8 @@ import { useEffect, useState } from "react";
|
|
|
3
3
|
import { gridColumns } from "@manta-eu/horns";
|
|
4
4
|
import { captureWidth, entityLinkPath } from "@manta-eu/horns/render";
|
|
5
5
|
import { chartPalette, hornChrome } from "../look/index.js";
|
|
6
|
-
import {
|
|
6
|
+
import { PlanPlacementProvider } from "./plan-placement.js";
|
|
7
|
+
import { ExplainPlacementProvider, HornFiltersProvider } from "./view-powers.js";
|
|
7
8
|
/**
|
|
8
9
|
* Where each of a snapshot's IRIs opens, ready to hand a view.
|
|
9
10
|
*
|
|
@@ -34,11 +35,20 @@ const surfaces = {
|
|
|
34
35
|
function asOf(iso) {
|
|
35
36
|
return `Data as of ${iso.slice(0, 10)} ${iso.slice(11, 16)} UTC`;
|
|
36
37
|
}
|
|
38
|
+
/**
|
|
39
|
+
* What the Horn was drawn for, in the header. A value that is an IRI is shown by the name the snapshot
|
|
40
|
+
* already holds for it: the IRIs are opaque, and one printed here says nothing to the person reading it.
|
|
41
|
+
*/
|
|
37
42
|
function filtersOf(snapshot) {
|
|
38
43
|
const set = snapshot.variables.filter((variable) => variable.value !== null);
|
|
39
44
|
if (set.length === 0)
|
|
40
45
|
return undefined;
|
|
41
|
-
|
|
46
|
+
const said = (value) => {
|
|
47
|
+
if (typeof value !== "string")
|
|
48
|
+
return JSON.stringify(value);
|
|
49
|
+
return snapshot.labels[value] ?? value;
|
|
50
|
+
};
|
|
51
|
+
return set.map((variable) => `${variable.label}: ${said(variable.value)}`).join(" · ");
|
|
42
52
|
}
|
|
43
53
|
/** What share of a row `span` columns is, as a percentage with a third's trailing noise left off. */
|
|
44
54
|
function share(span) {
|
|
@@ -69,12 +79,17 @@ function ViewCard(props) {
|
|
|
69
79
|
justifyContent: "space-between",
|
|
70
80
|
gap: 8,
|
|
71
81
|
margin: `0 0 ${chrome.padding - 4}px`,
|
|
72
|
-
}, children: [_jsx("h2", { style: { margin: 0, fontSize: 15, lineHeight: "20px", color: colors.ink }, children: view.title }), controls === undefined ? null : _jsx("div", { "data-view-controls": true, children: controls })] }), content.lane === "component" ? (_jsx(WrittenSlot, { placeholder: _jsx("p", { style: { margin: 0, color: colors.muted, fontSize: 14 }, children: "This view is a catalog card, which draws in the browser rather than here." }), draw: () => catalogCard?.(content, view, context) })) : content.lane === "sandbox" ? (
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
82
|
+
}, children: [_jsx("h2", { style: { margin: 0, fontSize: 15, lineHeight: "20px", color: colors.ink }, children: view.title }), controls === undefined ? null : _jsx("div", { "data-view-controls": true, children: controls })] }), content.lane === "component" ? (_jsx(WrittenSlot, { placeholder: _jsx("p", { style: { margin: 0, color: colors.muted, fontSize: 14 }, children: "This view is a catalog card, which draws in the browser rather than here." }), draw: () => catalogCard?.(content, view, context) })) : content.lane === "sandbox" ? (
|
|
83
|
+
// The plans behind this view, offered to the source rather than stapled under it. A view that draws
|
|
84
|
+
// none of them shows none: where the buttons go is what the view decides.
|
|
85
|
+
_jsx(PlanPlacementProvider, { value: {
|
|
86
|
+
plans,
|
|
87
|
+
labels,
|
|
88
|
+
links,
|
|
89
|
+
palette: chartPalette(theme, hornStyle),
|
|
90
|
+
...(planActions ? { actions: planActions } : {}),
|
|
91
|
+
shown: Object.fromEntries((content.plans ?? []).map((behind) => [behind.plan, behind.entities])),
|
|
92
|
+
}, 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 }))] }));
|
|
78
93
|
}
|
|
79
94
|
/**
|
|
80
95
|
* The theme a Horn draws in: its own, and the host's only when it declares none. Exported because a host that
|
|
@@ -84,14 +99,15 @@ export function hornSurface(snapshot, theme) {
|
|
|
84
99
|
return snapshot.style.theme ?? theme;
|
|
85
100
|
}
|
|
86
101
|
export function HornSnapshotView(props) {
|
|
87
|
-
const { snapshot, mode = "page", theme = "light", planActions, viewControls, writtenView, catalogCard
|
|
102
|
+
const { snapshot, mode = "page", theme = "light", planActions, viewControls, writtenView, catalogCard } = props;
|
|
103
|
+
const { explain, onVariableChange } = props;
|
|
88
104
|
const style = snapshot.style;
|
|
89
105
|
const surface = hornSurface(snapshot, theme);
|
|
90
106
|
const colors = surfaces[surface];
|
|
91
107
|
const chrome = hornChrome(style);
|
|
92
108
|
const links = entityLinks(snapshot);
|
|
93
109
|
if (mode === "capture") {
|
|
94
|
-
return (_jsx("main", { style: { width: captureWidth, background: colors.card, 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,
|
|
110
|
+
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,
|
|
95
111
|
// A capture on a server has no browser to draw written source in and passes neither of these, so
|
|
96
112
|
// it still gets the placeholder. A capture taken *by* a browser passes them and gets the view.
|
|
97
113
|
writtenView: writtenView, catalogCard: catalogCard }, view.id))) }));
|
|
@@ -117,26 +133,28 @@ export function HornSnapshotView(props) {
|
|
|
117
133
|
// A view the layout does not place still gets drawn: the document is what is shown, not the placement.
|
|
118
134
|
...snapshot.views.filter((view) => !laidOut.has(view.id)).map((view) => ({ view, span: gridColumns, rows: 4 })),
|
|
119
135
|
];
|
|
120
|
-
return (
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
136
|
+
return (
|
|
137
|
+
// One placement for the page: a mark asks, and the host opens the single explanation it owns.
|
|
138
|
+
_jsx(ExplainPlacementProvider, { value: { palette: chartPalette(surface, style), ...(explain ? { open: explain } : {}) }, children: _jsx(HornFiltersProvider, { value: { variables: snapshot.variables, ...(onVariableChange ? { onChange: onVariableChange } : {}) }, children: _jsx("main", { style: { minHeight: "100vh", background: colors.canvas, color: colors.ink, fontFamily: font }, children: _jsxs("div", { style: {
|
|
139
|
+
maxWidth: maxPageWidth,
|
|
140
|
+
margin: "0 auto",
|
|
141
|
+
padding: pagePadding,
|
|
142
|
+
boxSizing: "border-box",
|
|
143
|
+
display: "flex",
|
|
144
|
+
flexDirection: "column",
|
|
145
|
+
gap: 16,
|
|
146
|
+
}, children: [_jsxs("header", { children: [_jsx("h1", { style: { margin: 0, fontSize: 24, lineHeight: "32px" }, children: snapshot.horn.title }), _jsx("p", { style: { margin: "4px 0 0", fontSize: 13, color: colors.muted }, children: `${asOf(snapshot.generatedAt)}${filters ? ` · ${filters}` : ""}` })] }), _jsx("div", { style: { display: "flex", flexWrap: "wrap", alignItems: "stretch", gap: chrome.gap }, children: cards.map(({ view, span, rows }) => (_jsx("div", { style: {
|
|
147
|
+
display: "flex",
|
|
148
|
+
flexGrow: 1,
|
|
149
|
+
flexShrink: 1,
|
|
150
|
+
flexBasis: `calc(${share(span)}% - ${chrome.gap}px)`,
|
|
151
|
+
minWidth: `min(100%, ${Math.max(minCard, (span / gridColumns) * minPageWidth)}px)`,
|
|
152
|
+
minHeight: rows * rowHeight + (rows - 1) * chrome.gap,
|
|
153
|
+
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: {
|
|
155
|
+
flex: "1 1 auto",
|
|
156
|
+
minWidth: 0,
|
|
157
|
+
borderRadius: chrome.radius,
|
|
158
|
+
boxShadow: "0 1px 2px rgba(17, 24, 39, 0.06)",
|
|
159
|
+
} }) }, view.id))) })] }) }) }) }));
|
|
142
160
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { PlanStatus, SnapshotPlan } from "@manta-eu/horns/render";
|
|
2
|
+
import type { ChartPalette } from "../look/palette.ts";
|
|
3
|
+
import type { EntityLink } from "../look/types.ts";
|
|
4
|
+
import { type PlanActions } from "./plan-strip.tsx";
|
|
5
|
+
export type PlanPlacement = {
|
|
6
|
+
plans: readonly SnapshotPlan[];
|
|
7
|
+
labels: Readonly<Record<string, string>>;
|
|
8
|
+
links: Readonly<Record<string, EntityLink>>;
|
|
9
|
+
palette: ChartPalette;
|
|
10
|
+
/** Absent where nothing can be run — a capture, an embed without a viewer. The plan still draws. */
|
|
11
|
+
actions?: PlanActions | undefined;
|
|
12
|
+
/** The things each plan changes that this view draws, by plan IRI. */
|
|
13
|
+
shown?: Readonly<Record<string, readonly string[]>>;
|
|
14
|
+
};
|
|
15
|
+
export declare const PlanPlacementProvider: import("react").Provider<PlanPlacement | undefined>;
|
|
16
|
+
export type SuggestedChangesProps = {
|
|
17
|
+
/** The plan's IRI, as the view's rows carry it. */
|
|
18
|
+
plan: string;
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* One plan's changes and what may be done about them, drawn where the view puts it.
|
|
22
|
+
*
|
|
23
|
+
* Says so plainly when there is nothing to draw rather than rendering an empty box: a view naming a plan the
|
|
24
|
+
* Horn does not hold is a mistake worth seeing while somebody is reading the code.
|
|
25
|
+
*/
|
|
26
|
+
export declare function SuggestedChanges({ plan }: SuggestedChangesProps): import("react").JSX.Element | null;
|
|
27
|
+
/** The plans behind the view being drawn, so a view can name them without being handed callbacks. */
|
|
28
|
+
export declare function usePlansBehindView(): readonly SnapshotPlan[];
|
|
29
|
+
export type { PlanStatus };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { createContext, useContext } from "react";
|
|
3
|
+
import { PlanStrip } from "./plan-strip.js";
|
|
4
|
+
const PlanPlacementContext = createContext(undefined);
|
|
5
|
+
export const PlanPlacementProvider = PlanPlacementContext.Provider;
|
|
6
|
+
/**
|
|
7
|
+
* One plan's changes and what may be done about them, drawn where the view puts it.
|
|
8
|
+
*
|
|
9
|
+
* Says so plainly when there is nothing to draw rather than rendering an empty box: a view naming a plan the
|
|
10
|
+
* Horn does not hold is a mistake worth seeing while somebody is reading the code.
|
|
11
|
+
*/
|
|
12
|
+
export function SuggestedChanges({ plan }) {
|
|
13
|
+
const placement = useContext(PlanPlacementContext);
|
|
14
|
+
if (placement === undefined)
|
|
15
|
+
return null;
|
|
16
|
+
const found = placement.plans.find((one) => one.iri === plan);
|
|
17
|
+
if (found === undefined)
|
|
18
|
+
return null;
|
|
19
|
+
return (_jsx(PlanStrip, { plan: found, onView: placement.shown?.[plan] ?? [], labels: placement.labels, links: placement.links, palette: placement.palette, actions: placement.actions }));
|
|
20
|
+
}
|
|
21
|
+
/** The plans behind the view being drawn, so a view can name them without being handed callbacks. */
|
|
22
|
+
export function usePlansBehindView() {
|
|
23
|
+
return useContext(PlanPlacementContext)?.plans ?? [];
|
|
24
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type PlanChange, type PlanStatus, type SnapshotPlan } from "@manta-eu/horns/render";
|
|
2
2
|
import type { ChartPalette } from "../look/palette.ts";
|
|
3
3
|
import type { EntityLink } from "../look/types.ts";
|
|
4
4
|
export declare const maxShownChanges = 4;
|
package/dist/embed/plan-strip.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { match } from "ts-pattern";
|
|
3
|
+
import { planNameFromIri } from "@manta-eu/horns/render";
|
|
3
4
|
import { formatMeasure, missingValue } from "../look/format.js";
|
|
4
5
|
export const maxShownChanges = 4;
|
|
5
6
|
// "Campaign Budget" → "campaign budgets". English only, like every other word the kit prints.
|
|
@@ -25,6 +26,10 @@ function listed(parts) {
|
|
|
25
26
|
export function planTitle(plan, labels) {
|
|
26
27
|
if (plan.name !== undefined)
|
|
27
28
|
return plan.name;
|
|
29
|
+
// What agents calls it, so one plan does not read as two things in two of Manta's own surfaces.
|
|
30
|
+
const named = planNameFromIri(plan.iri);
|
|
31
|
+
if (named !== undefined)
|
|
32
|
+
return named;
|
|
28
33
|
const settings = [
|
|
29
34
|
...new Set(plan.changes.flatMap((change) => {
|
|
30
35
|
const label = labels[change.property];
|
|
@@ -73,12 +78,12 @@ function planStateWords(status) {
|
|
|
73
78
|
const preview = status.lastRun?.preview === true;
|
|
74
79
|
return match(status.state)
|
|
75
80
|
.with("draft", () => ({ text: "Not run yet", tone: "quiet" }))
|
|
76
|
-
.with("running", () => ({ text: preview ? "
|
|
81
|
+
.with("running", () => ({ text: preview ? "Validating…" : "Making the changes…", tone: "busy" }))
|
|
77
82
|
.with("completed", () => preview
|
|
78
|
-
? { text: "
|
|
83
|
+
? { text: "Validated — nothing was changed", tone: "good" }
|
|
79
84
|
: { text: "Changes made", tone: "good" })
|
|
80
85
|
.with("failed", () => ({
|
|
81
|
-
text: preview ? "
|
|
86
|
+
text: preview ? "Validation found a problem" : "The changes did not go through",
|
|
82
87
|
tone: "bad",
|
|
83
88
|
}))
|
|
84
89
|
.with("timed-out", () => ({ text: "It took too long and stopped", tone: "bad" }))
|
|
@@ -137,5 +142,5 @@ export function PlanStrip(props) {
|
|
|
137
142
|
const name = nameOf(change.entity);
|
|
138
143
|
const why = reasonsFor(change.entity);
|
|
139
144
|
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));
|
|
140
|
-
}) })) : (_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("
|
|
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("Validate", () => actions.onPreview(plan), !busy, false, "Check 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" : "Validate it first")] }))] }));
|
|
141
146
|
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import type { HornSnapshot } from "@manta-eu/horns/render";
|
|
3
|
+
import type { ChartPalette } from "../look/palette.ts";
|
|
4
|
+
/** The fact a mark explains, as the host's explainer will ask for it. */
|
|
5
|
+
export type ExplainedFact = {
|
|
6
|
+
subject: string;
|
|
7
|
+
predicate?: string;
|
|
8
|
+
object: string;
|
|
9
|
+
/**
|
|
10
|
+
* The explanation itself, opaque here: its shape is the design system's, and only the host reads it.
|
|
11
|
+
* Without it the host's explainer would have to fetch, which a written view cannot make it do.
|
|
12
|
+
*/
|
|
13
|
+
data?: unknown;
|
|
14
|
+
/** What to call the explanation when it opens. */
|
|
15
|
+
title?: string;
|
|
16
|
+
};
|
|
17
|
+
export type ExplainPlacement = {
|
|
18
|
+
palette: ChartPalette;
|
|
19
|
+
/** Opens the explanation. Absent on a server render and in a capture, where the mark is not drawn at all. */
|
|
20
|
+
open?: ((fact: ExplainedFact) => void) | undefined;
|
|
21
|
+
};
|
|
22
|
+
export declare const ExplainPlacementProvider: import("react").Provider<ExplainPlacement | undefined>;
|
|
23
|
+
export type WhyThisProps = ExplainedFact & {
|
|
24
|
+
/** The value being claimed: a number, a badge, a word. Drawn as it is, with the mark beside it. */
|
|
25
|
+
children: ReactNode;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* A value that can say why it is what it is.
|
|
29
|
+
*
|
|
30
|
+
* Draws the thing it wraps unchanged and puts a small mark beside it, so a reader can tell at a glance which
|
|
31
|
+
* numbers on a page were worked out rather than recorded. Where nothing can open an explanation — a server
|
|
32
|
+
* render, a captured picture — the mark is left off and the value draws alone.
|
|
33
|
+
*/
|
|
34
|
+
export declare function WhyThis({ children, ...fact }: WhyThisProps): import("react").JSX.Element;
|
|
35
|
+
export type HornFilters = {
|
|
36
|
+
/** The Horn's variables as the snapshot was built with them. */
|
|
37
|
+
variables: HornSnapshot["variables"];
|
|
38
|
+
/**
|
|
39
|
+
* Narrows the Horn to a value, or clears it with `undefined`. Absent where nothing can re-query — a
|
|
40
|
+
* capture, a server render — and then a view draws its marks as plain text rather than as controls.
|
|
41
|
+
*/
|
|
42
|
+
onChange?: ((id: string, value: unknown) => void) | undefined;
|
|
43
|
+
};
|
|
44
|
+
export declare const HornFiltersProvider: import("react").Provider<HornFilters | undefined>;
|
|
45
|
+
export type HornFilter = {
|
|
46
|
+
/** What the Horn is narrowed to now, or undefined. */
|
|
47
|
+
value: unknown;
|
|
48
|
+
/** Whether narrowing is possible at all. A view checks this before drawing something that looks clickable. */
|
|
49
|
+
live: boolean;
|
|
50
|
+
/** Narrows to `value`, or clears it when it is already that. */
|
|
51
|
+
toggle: (value: unknown) => void;
|
|
52
|
+
/** True when `value` is what the Horn is narrowed to. */
|
|
53
|
+
is: (value: unknown) => boolean;
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* One of the Horn's variables, as a filter a view can set.
|
|
57
|
+
*
|
|
58
|
+
* Setting it re-runs every binding that takes it as a parameter, which is how one view narrows another: the
|
|
59
|
+
* tile is not wired to the list, they both answer to the same variable. A view naming a variable the Horn
|
|
60
|
+
* does not declare gets a dead filter rather than an error — the document is what says which exist.
|
|
61
|
+
*/
|
|
62
|
+
export declare function useHornFilter(id: string): HornFilter;
|
|
63
|
+
export type NothingProps = {
|
|
64
|
+
/** What is not here: "No tier 1 suppliers", not "No data". */
|
|
65
|
+
title: string;
|
|
66
|
+
/** What would put something here, or why there is nothing. */
|
|
67
|
+
hint?: string;
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* A view with no rows, said in words.
|
|
71
|
+
*
|
|
72
|
+
* The design system's `EmptyState` takes a required `icon` component, and a written view has no icon module
|
|
73
|
+
* to give it — it renders, then throws. This is the same state drawn the way the frame around it is drawn,
|
|
74
|
+
* out of palette and inline style, so it works wherever the frame does.
|
|
75
|
+
*/
|
|
76
|
+
export declare function Nothing({ title, hint }: NothingProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { createContext, useContext } from "react";
|
|
3
|
+
const ExplainPlacementContext = createContext(undefined);
|
|
4
|
+
export const ExplainPlacementProvider = ExplainPlacementContext.Provider;
|
|
5
|
+
/**
|
|
6
|
+
* A value that can say why it is what it is.
|
|
7
|
+
*
|
|
8
|
+
* Draws the thing it wraps unchanged and puts a small mark beside it, so a reader can tell at a glance which
|
|
9
|
+
* numbers on a page were worked out rather than recorded. Where nothing can open an explanation — a server
|
|
10
|
+
* render, a captured picture — the mark is left off and the value draws alone.
|
|
11
|
+
*/
|
|
12
|
+
export function WhyThis({ children, ...fact }) {
|
|
13
|
+
const placement = useContext(ExplainPlacementContext);
|
|
14
|
+
if (placement?.open === undefined)
|
|
15
|
+
return _jsx(_Fragment, { children: children });
|
|
16
|
+
const { palette, open } = placement;
|
|
17
|
+
return (_jsxs("button", { type: "button", "data-horn-why": fact.predicate ?? fact.subject, title: "Why this value?", onClick: () => open(fact), style: {
|
|
18
|
+
display: "inline-flex",
|
|
19
|
+
alignItems: "center",
|
|
20
|
+
gap: 4,
|
|
21
|
+
padding: 0,
|
|
22
|
+
border: "none",
|
|
23
|
+
background: "none",
|
|
24
|
+
font: "inherit",
|
|
25
|
+
color: "inherit",
|
|
26
|
+
cursor: "pointer",
|
|
27
|
+
}, children: [children, _jsx("svg", { viewBox: "0 0 16 16", width: "12", height: "12", "aria-hidden": "true", style: { flex: "0 0 auto" }, children: _jsx("path", { d: "M8 1.5 L9.3 6.1 L13.5 7.5 L9.3 8.9 L8 13.5 L6.7 8.9 L2.5 7.5 L6.7 6.1 Z", fill: palette.series[0] ?? palette.secondary }) }), _jsx("span", { style: {
|
|
28
|
+
position: "absolute",
|
|
29
|
+
width: 1,
|
|
30
|
+
height: 1,
|
|
31
|
+
overflow: "hidden",
|
|
32
|
+
clip: "rect(0 0 0 0)",
|
|
33
|
+
whiteSpace: "nowrap",
|
|
34
|
+
}, children: "Why this value?" })] }));
|
|
35
|
+
}
|
|
36
|
+
const HornFiltersContext = createContext(undefined);
|
|
37
|
+
export const HornFiltersProvider = HornFiltersContext.Provider;
|
|
38
|
+
/**
|
|
39
|
+
* One of the Horn's variables, as a filter a view can set.
|
|
40
|
+
*
|
|
41
|
+
* Setting it re-runs every binding that takes it as a parameter, which is how one view narrows another: the
|
|
42
|
+
* tile is not wired to the list, they both answer to the same variable. A view naming a variable the Horn
|
|
43
|
+
* does not declare gets a dead filter rather than an error — the document is what says which exist.
|
|
44
|
+
*/
|
|
45
|
+
export function useHornFilter(id) {
|
|
46
|
+
const filters = useContext(HornFiltersContext);
|
|
47
|
+
const declared = filters?.variables.find((variable) => variable.id === id);
|
|
48
|
+
const value = declared?.value ?? undefined;
|
|
49
|
+
// Both have to hold: somewhere to send the change, and a variable that can carry it. A Horn's variable
|
|
50
|
+
// travels in the URL, so one that declares no parameter is settable nowhere and `live` says so.
|
|
51
|
+
const onChange = declared?.settable === true ? filters?.onChange : undefined;
|
|
52
|
+
const is = (candidate) => value !== undefined && value === candidate;
|
|
53
|
+
return {
|
|
54
|
+
value,
|
|
55
|
+
live: onChange !== undefined,
|
|
56
|
+
is,
|
|
57
|
+
toggle: (candidate) => onChange?.(id, is(candidate) ? undefined : candidate),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* A view with no rows, said in words.
|
|
62
|
+
*
|
|
63
|
+
* The design system's `EmptyState` takes a required `icon` component, and a written view has no icon module
|
|
64
|
+
* to give it — it renders, then throws. This is the same state drawn the way the frame around it is drawn,
|
|
65
|
+
* out of palette and inline style, so it works wherever the frame does.
|
|
66
|
+
*/
|
|
67
|
+
export function Nothing({ title, hint }) {
|
|
68
|
+
const placement = useContext(ExplainPlacementContext);
|
|
69
|
+
const palette = placement?.palette;
|
|
70
|
+
return (_jsxs("div", { style: {
|
|
71
|
+
display: "flex",
|
|
72
|
+
flexDirection: "column",
|
|
73
|
+
gap: 4,
|
|
74
|
+
alignItems: "flex-start",
|
|
75
|
+
justifyContent: "center",
|
|
76
|
+
padding: "12px 0",
|
|
77
|
+
}, children: [_jsx("span", { style: { fontSize: 14, fontWeight: 500, color: palette?.ink ?? "currentColor" }, children: title }), hint === undefined ? null : (_jsx("span", { style: { fontSize: 13, color: palette?.muted ?? "currentColor", opacity: palette ? 1 : 0.7 }, children: hint }))] }));
|
|
78
|
+
}
|
package/dist/written/runtime.js
CHANGED
|
@@ -56,12 +56,45 @@ export async function transpileWritten(source) {
|
|
|
56
56
|
function isCallable(value) {
|
|
57
57
|
return typeof value === "function";
|
|
58
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* Names bound to `undefined` around the view, so they shadow the page's own.
|
|
61
|
+
*
|
|
62
|
+
* `tsxViolations` refuses these in the source, but that is a regex over text: a name put together at
|
|
63
|
+
* runtime slips past it. A parameter does not care how the name was spelled. This is not containment — a
|
|
64
|
+
* constructor chain still reaches the realm from inside any function — so it stands with the page's CSP and
|
|
65
|
+
* with a person reading the source, and replaces neither. `eval` is not here because the transpiled module
|
|
66
|
+
* is strict, where it cannot be a parameter name; the source check is all there is for that one.
|
|
67
|
+
*/
|
|
68
|
+
const shadowed = [
|
|
69
|
+
"fetch",
|
|
70
|
+
"XMLHttpRequest",
|
|
71
|
+
"WebSocket",
|
|
72
|
+
"EventSource",
|
|
73
|
+
"navigator",
|
|
74
|
+
"Function",
|
|
75
|
+
"globalThis",
|
|
76
|
+
"window",
|
|
77
|
+
"self",
|
|
78
|
+
"document",
|
|
79
|
+
"parent",
|
|
80
|
+
"top",
|
|
81
|
+
"opener",
|
|
82
|
+
"frames",
|
|
83
|
+
"localStorage",
|
|
84
|
+
"sessionStorage",
|
|
85
|
+
"indexedDB",
|
|
86
|
+
"caches",
|
|
87
|
+
"cookieStore",
|
|
88
|
+
"process",
|
|
89
|
+
"importScripts",
|
|
90
|
+
"postMessage",
|
|
91
|
+
];
|
|
59
92
|
/** `new Function` rather than an import: by now the source is CommonJS, and its only way out is `require`. */
|
|
60
93
|
export function evaluateWritten(code, load) {
|
|
61
94
|
const moduleRecord = { exports: {} };
|
|
62
95
|
try {
|
|
63
|
-
const factory = new Function("require", "exports", "module", "React", code);
|
|
64
|
-
factory(load, moduleRecord.exports, moduleRecord, reactModule);
|
|
96
|
+
const factory = new Function("require", "exports", "module", "React", ...shadowed, code);
|
|
97
|
+
factory(load, moduleRecord.exports, moduleRecord, reactModule, ...shadowed.map(() => undefined));
|
|
65
98
|
}
|
|
66
99
|
catch (cause) {
|
|
67
100
|
if (cause instanceof WrittenViewError)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@manta-eu/horns-kit",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
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": {
|
|
@@ -38,6 +38,14 @@
|
|
|
38
38
|
"./written": {
|
|
39
39
|
"types": "./dist/written/index.d.ts",
|
|
40
40
|
"default": "./dist/written/index.js"
|
|
41
|
+
},
|
|
42
|
+
"./plan": {
|
|
43
|
+
"types": "./dist/embed/plan-placement.d.ts",
|
|
44
|
+
"default": "./dist/embed/plan-placement.js"
|
|
45
|
+
},
|
|
46
|
+
"./view": {
|
|
47
|
+
"types": "./dist/embed/view-powers.d.ts",
|
|
48
|
+
"default": "./dist/embed/view-powers.js"
|
|
41
49
|
}
|
|
42
50
|
},
|
|
43
51
|
"publishConfig": {
|
|
@@ -47,7 +55,7 @@
|
|
|
47
55
|
"dependencies": {
|
|
48
56
|
"sucrase": "^3.35.1",
|
|
49
57
|
"ts-pattern": "^5.9.0",
|
|
50
|
-
"@manta-eu/horns": "0.1.
|
|
58
|
+
"@manta-eu/horns": "0.1.1"
|
|
51
59
|
},
|
|
52
60
|
"peerDependencies": {
|
|
53
61
|
"react": ">=18",
|