@echothink-ui/events 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,55 @@
1
+ # @echothink-ui/events
2
+
3
+ The backend-event contract for `@echothink-ui` components.
4
+
5
+ `@echothink-ui` components expose plain React callbacks (`onClick`,
6
+ `EthAction.onSelect`, form `onSubmit`, table `rowActions`) with no path to a
7
+ backend process. This package provides the serializable binding shape and the
8
+ host-injected dispatch that carry those events to a backend (ultimately
9
+ `runProcess` → gateway), while staying structurally compatible with the
10
+ echothink-fugue registry `EventBinding` / action-graph contract.
11
+
12
+ ## Pieces
13
+
14
+ - `EthEventBinding` / `EthAction` — the serializable "when this event fires, run
15
+ these actions" shape a builder authors. Action kinds: `runProcess`,
16
+ `navigate`, `setState`, `openModal`, `closeModal`, `showToast`.
17
+ - `EchoEventProvider` — the host injects a `dispatch(binding, payload, data)`
18
+ implementation here. A no-op default lets components render with no provider.
19
+ - `useEchoEvents()` — returns `{ dispatch }`.
20
+ - `useEthAction(binding)` — returns an `onSelect`/`onClick` handler to wire
21
+ `EthAction.onSelect` → dispatch (or `undefined` when unbound).
22
+ - `useEthEventHandler(binding)` — returns a payload-carrying handler for events
23
+ that produce live data (form values, selected row, chosen rowAction).
24
+ - `toRegistryEventBinding(binding)` — the edge adapter that renames the authoring
25
+ shape (`kind`/`payload`/`target`) to the registry action-graph shape
26
+ (`action`/`externalInput`/`route`/`modalId`/`key`).
27
+
28
+ ## Usage
29
+
30
+ ```tsx
31
+ import {
32
+ EchoEventProvider,
33
+ useEthAction,
34
+ toRegistryEventBinding,
35
+ type EthEventBinding,
36
+ } from "@echothink-ui/events";
37
+
38
+ // Host wires dispatch to the registry/renderer action runtime:
39
+ const dispatch = (binding, payload) => {
40
+ const reg = toRegistryEventBinding(binding);
41
+ pageRuntime.dispatch([reg], binding.event, { row: payload });
42
+ };
43
+
44
+ <EchoEventProvider dispatch={dispatch}>
45
+ <MyPage />
46
+ </EchoEventProvider>;
47
+
48
+ // Component wires a rich callback to a backend binding:
49
+ function ArchiveButton({ binding }: { binding?: EthEventBinding }) {
50
+ const onSelect = useEthAction(binding);
51
+ return <Button onClick={onSelect}>Archive</Button>;
52
+ }
53
+ ```
54
+
55
+ See `../echothink-UI-components/docs/backend-events.md` for the full design.
@@ -0,0 +1,48 @@
1
+ /**
2
+ * `bridge` — the field-renaming adapter that turns an {@link EthEventBinding}
3
+ * (the UI-library shape, discriminant `kind`) into the echothink-fugue registry
4
+ * `EventBinding` shape (discriminant `action`) that
5
+ * `runActionGraph`/`runProcess` consume.
6
+ *
7
+ * This lives in the UI library (not the registry) so the UI side OWNS its
8
+ * authoring shape and the host's `dispatch` can be a thin `runActionGraph(
9
+ * toRegistryActions(binding.actions), ctx)`. It is the documented, single point
10
+ * where the two contracts meet — keeping the rest of `@echothink-ui/events`
11
+ * free of any registry coupling.
12
+ *
13
+ * The output objects are plain JSON-compatible records typed `unknown` at the
14
+ * boundary; the registry validates/narrows them with its own
15
+ * `validateActionGraph`. We do not import registry types here.
16
+ */
17
+ import type { EthAction, EthEventBinding } from "./types.js";
18
+ /** A registry-shaped action node (discriminant `action`). */
19
+ export type RegistryActionNode = Record<string, unknown> & {
20
+ action: string;
21
+ };
22
+ /** A registry-shaped event binding (`{ event, actions: ActionNode[] }`). */
23
+ export interface RegistryEventBinding {
24
+ event: string;
25
+ actions: RegistryActionNode[];
26
+ }
27
+ /**
28
+ * Convert one {@link EthAction} to the registry `ActionNode` shape. The mapping:
29
+ * - `kind` → `action`
30
+ * - `runProcess`: `payload` → `externalInput`, `target`/`expectedVersion`/`optimistic` pass through
31
+ * - `navigate`: `target` → `route`, `params` pass through
32
+ * - `setState`: `target` → `key`, `value` passes through
33
+ * - `openModal`/`closeModal`: `target` → `modalId`
34
+ * - `showToast`: `message`/`level` pass through
35
+ */
36
+ export declare function toRegistryAction(action: EthAction): RegistryActionNode;
37
+ /**
38
+ * Convert an {@link EthEventBinding} to the registry `EventBinding` shape. A host
39
+ * that speaks the registry contract uses this to adapt at the dispatch edge:
40
+ *
41
+ * ```ts
42
+ * const dispatch: EchoEventDispatch = (binding, payload) => {
43
+ * const reg = toRegistryEventBinding(binding);
44
+ * runtime.dispatch([reg], binding.event, { row: payload });
45
+ * };
46
+ * ```
47
+ */
48
+ export declare function toRegistryEventBinding(binding: EthEventBinding): RegistryEventBinding;
@@ -0,0 +1,43 @@
1
+ /**
2
+ * `EchoEventContext` — the injectable dispatch surface a component reads to send
3
+ * its authored {@link EthEventBinding} to the backend.
4
+ *
5
+ * Mirrors the registry's `EventDispatchContext` / `PageInteractionProvider`
6
+ * pattern: the UI library owns the CONTEXT + hooks; the HOST (the echothink-fugue
7
+ * renderer, a demo app, or a test) provides the real `dispatch` implementation
8
+ * that walks the action graph → `runProcess` → gateway. A NO-OP default lets
9
+ * components render with no provider, so standalone render/snapshot tests never
10
+ * crash.
11
+ */
12
+ import { type ReactNode } from "react";
13
+ import type { EchoEventDispatch } from "./types.js";
14
+ /**
15
+ * The interaction surface a component reads via {@link useEchoEvents}. The host
16
+ * constructs the real one; absent a provider, {@link NOOP_DISPATCH} is used so
17
+ * components never crash.
18
+ */
19
+ export interface EchoEventContextValue {
20
+ /** Carry a binding (+ live payload) to the backend. */
21
+ dispatch: EchoEventDispatch;
22
+ }
23
+ /**
24
+ * Provide an {@link EchoEventDispatch} (constructed by the host) to the
25
+ * component tree. The host's `dispatch` is the bridge to the backend — it
26
+ * typically adapts each {@link EthEventBinding} to the registry action graph and
27
+ * runs it through `runProcess` → gateway.
28
+ *
29
+ * ```tsx
30
+ * <EchoEventProvider dispatch={(binding, payload) => runtime.dispatch(binding, payload)}>
31
+ * <MyAuthoredPage />
32
+ * </EchoEventProvider>
33
+ * ```
34
+ */
35
+ export declare function EchoEventProvider({ dispatch, children, }: {
36
+ dispatch: EchoEventDispatch;
37
+ children: ReactNode;
38
+ }): ReactNode;
39
+ /**
40
+ * Read the {@link EchoEventContextValue}. Returns the no-op dispatch when no
41
+ * provider is mounted, so components (and their render tests) work standalone.
42
+ */
43
+ export declare function useEchoEvents(): EchoEventContextValue;
@@ -0,0 +1,34 @@
1
+ import type { EthDispatchData, EthEventBinding } from "./types.js";
2
+ /**
3
+ * Build an `onSelect` / `onClick`-shaped handler from an {@link EthEventBinding}.
4
+ *
5
+ * Returns `undefined` when `binding` is absent, so a component can spread it into
6
+ * `EthAction.onSelect` and fall back to its own default when unbound:
7
+ *
8
+ * ```tsx
9
+ * const onSelect = useEthAction(action.binding);
10
+ * <Button onClick={onSelect}>{action.label}</Button>
11
+ * // or, for EthAction:
12
+ * { ...action, onSelect: useEthAction(action.binding) ?? action.onSelect }
13
+ * ```
14
+ *
15
+ * The optional `data` (current row / route params) is threaded into the dispatch
16
+ * so the host can resolve `field:` / `route:` payloads — matching the registry's
17
+ * `DispatchDataContext`.
18
+ */
19
+ export declare function useEthAction(binding: EthEventBinding | undefined, data?: EthDispatchData): (() => void) | undefined;
20
+ /**
21
+ * Build a payload-carrying handler from an {@link EthEventBinding} — for events
22
+ * that produce live data (form `onSubmit` values, the selected row, the chosen
23
+ * `rowAction` id). The returned function forwards whatever it is called with as
24
+ * the dispatch `payload`, so the host can merge it into the binding's actions.
25
+ *
26
+ * Returns a no-op (still callable) when `binding` is absent, so callers don't
27
+ * need to null-check at the call site.
28
+ *
29
+ * ```tsx
30
+ * const onSubmit = useEthEventHandler(binding);
31
+ * <form onSubmit={(e) => { e.preventDefault(); onSubmit(readFormValues(e)); }} />
32
+ * ```
33
+ */
34
+ export declare function useEthEventHandler<P = unknown>(binding: EthEventBinding | undefined, data?: EthDispatchData): (payload?: P) => void;
package/dist/index.cjs ADDED
@@ -0,0 +1,128 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ EchoEventProvider: () => EchoEventProvider,
24
+ toRegistryAction: () => toRegistryAction,
25
+ toRegistryEventBinding: () => toRegistryEventBinding,
26
+ useEchoEvents: () => useEchoEvents,
27
+ useEthAction: () => useEthAction,
28
+ useEthEventHandler: () => useEthEventHandler
29
+ });
30
+ module.exports = __toCommonJS(index_exports);
31
+
32
+ // src/context.tsx
33
+ var import_react = require("react");
34
+ var NOOP_DISPATCH = () => {
35
+ };
36
+ var EchoEventContext = (0, import_react.createContext)({
37
+ dispatch: NOOP_DISPATCH
38
+ });
39
+ function EchoEventProvider({
40
+ dispatch,
41
+ children
42
+ }) {
43
+ return (0, import_react.createElement)(
44
+ EchoEventContext.Provider,
45
+ { value: { dispatch } },
46
+ children
47
+ );
48
+ }
49
+ function useEchoEvents() {
50
+ return (0, import_react.useContext)(EchoEventContext);
51
+ }
52
+
53
+ // src/hooks.tsx
54
+ var import_react2 = require("react");
55
+ function useEthAction(binding, data) {
56
+ const { dispatch } = useEchoEvents();
57
+ return (0, import_react2.useCallback)(() => {
58
+ if (!binding) return;
59
+ void dispatch(binding, void 0, data);
60
+ }, [dispatch, binding, data]);
61
+ }
62
+ function useEthEventHandler(binding, data) {
63
+ const { dispatch } = useEchoEvents();
64
+ return (0, import_react2.useCallback)(
65
+ (payload) => {
66
+ if (!binding) return;
67
+ void dispatch(binding, payload, data);
68
+ },
69
+ [dispatch, binding, data]
70
+ );
71
+ }
72
+
73
+ // src/bridge.ts
74
+ function toRegistryAction(action) {
75
+ switch (action.kind) {
76
+ case "runProcess":
77
+ return {
78
+ action: "runProcess",
79
+ process: action.process,
80
+ ...action.target !== void 0 ? { target: action.target } : {},
81
+ ...action.payload !== void 0 ? { externalInput: action.payload } : {},
82
+ ...action.expectedVersion !== void 0 ? { expectedVersion: action.expectedVersion } : {},
83
+ ...action.optimistic !== void 0 ? { optimistic: action.optimistic } : {}
84
+ };
85
+ case "navigate":
86
+ return {
87
+ action: "navigate",
88
+ route: action.target,
89
+ ...action.params !== void 0 ? { params: action.params } : {}
90
+ };
91
+ case "setState":
92
+ return {
93
+ action: "setState",
94
+ key: action.target,
95
+ ...action.value !== void 0 ? { value: action.value } : {}
96
+ };
97
+ case "openModal":
98
+ return { action: "openModal", modalId: action.target };
99
+ case "closeModal":
100
+ return { action: "closeModal", modalId: action.target };
101
+ case "showToast":
102
+ return {
103
+ action: "showToast",
104
+ message: action.message,
105
+ ...action.level !== void 0 ? { level: action.level } : {}
106
+ };
107
+ default: {
108
+ const unknownAction = action;
109
+ return { ...action, action: unknownAction.kind };
110
+ }
111
+ }
112
+ }
113
+ function toRegistryEventBinding(binding) {
114
+ return {
115
+ event: binding.event,
116
+ actions: (binding.actions ?? []).map(toRegistryAction)
117
+ };
118
+ }
119
+ // Annotate the CommonJS export names for ESM import in node:
120
+ 0 && (module.exports = {
121
+ EchoEventProvider,
122
+ toRegistryAction,
123
+ toRegistryEventBinding,
124
+ useEchoEvents,
125
+ useEthAction,
126
+ useEthEventHandler
127
+ });
128
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/context.tsx","../src/hooks.tsx","../src/bridge.ts"],"sourcesContent":["/**\n * `@echothink-ui/events` — public API.\n *\n * The backend-event contract for `@echothink-ui` components: a serializable\n * {@link EthEventBinding} (structurally compatible with the echothink-fugue\n * registry `EventBinding`), an {@link EchoEventProvider} the host uses to inject\n * a backend `dispatch`, a {@link useEchoEvents} hook, and component-facing\n * {@link useEthAction} / {@link useEthEventHandler} helpers that wire rich\n * semantic callbacks (`EthAction.onSelect`, form `onSubmit`, table `rowActions`)\n * to that dispatch. The {@link toRegistryEventBinding} bridge adapts the\n * authoring shape to the registry action-graph shape at the host edge.\n */\nexport type {\n EthEventName,\n EthActionTarget,\n EthActionKind,\n EthAction,\n EthRunProcessAction,\n EthNavigateAction,\n EthSetStateAction,\n EthOpenModalAction,\n EthCloseModalAction,\n EthShowToastAction,\n EthEventBinding,\n EthDispatchData,\n EchoEventDispatch,\n} from \"./types.js\";\n\nexport {\n EchoEventProvider,\n useEchoEvents,\n type EchoEventContextValue,\n} from \"./context.js\";\n\nexport { useEthAction, useEthEventHandler } from \"./hooks.js\";\n\nexport {\n toRegistryAction,\n toRegistryEventBinding,\n type RegistryActionNode,\n type RegistryEventBinding,\n} from \"./bridge.js\";\n","/**\n * `EchoEventContext` — the injectable dispatch surface a component reads to send\n * its authored {@link EthEventBinding} to the backend.\n *\n * Mirrors the registry's `EventDispatchContext` / `PageInteractionProvider`\n * pattern: the UI library owns the CONTEXT + hooks; the HOST (the echothink-fugue\n * renderer, a demo app, or a test) provides the real `dispatch` implementation\n * that walks the action graph → `runProcess` → gateway. A NO-OP default lets\n * components render with no provider, so standalone render/snapshot tests never\n * crash.\n */\nimport {\n createContext,\n createElement,\n useContext,\n type ReactNode,\n} from \"react\";\nimport type { EchoEventDispatch } from \"./types.js\";\n\n/**\n * The interaction surface a component reads via {@link useEchoEvents}. The host\n * constructs the real one; absent a provider, {@link NOOP_DISPATCH} is used so\n * components never crash.\n */\nexport interface EchoEventContextValue {\n /** Carry a binding (+ live payload) to the backend. */\n dispatch: EchoEventDispatch;\n}\n\n/** A no-op dispatch — used when no provider is mounted. */\nconst NOOP_DISPATCH: EchoEventDispatch = () => {};\n\nconst EchoEventContext = createContext<EchoEventContextValue>({\n dispatch: NOOP_DISPATCH,\n});\n\n/**\n * Provide an {@link EchoEventDispatch} (constructed by the host) to the\n * component tree. The host's `dispatch` is the bridge to the backend — it\n * typically adapts each {@link EthEventBinding} to the registry action graph and\n * runs it through `runProcess` → gateway.\n *\n * ```tsx\n * <EchoEventProvider dispatch={(binding, payload) => runtime.dispatch(binding, payload)}>\n * <MyAuthoredPage />\n * </EchoEventProvider>\n * ```\n */\nexport function EchoEventProvider({\n dispatch,\n children,\n}: {\n dispatch: EchoEventDispatch;\n children: ReactNode;\n}): ReactNode {\n return createElement(\n EchoEventContext.Provider,\n { value: { dispatch } },\n children,\n );\n}\n\n/**\n * Read the {@link EchoEventContextValue}. Returns the no-op dispatch when no\n * provider is mounted, so components (and their render tests) work standalone.\n */\nexport function useEchoEvents(): EchoEventContextValue {\n return useContext(EchoEventContext);\n}\n","/**\n * Component-facing helpers that wire a component's rich semantic callbacks\n * (`EthAction.onSelect`, `onClick`, form `onSubmit`, table `rowActions`) to a\n * backend {@link EthEventBinding} via the injected {@link EchoEventDispatch}.\n *\n * These are the OPT-IN seam: a component that wants backend-aware behavior takes\n * an optional `binding?: EthEventBinding` prop and calls one of these helpers to\n * produce the handler it passes to its native callback prop. A component with no\n * binding behaves exactly as today (the no-op dispatch is inert).\n */\nimport { useCallback } from \"react\";\nimport { useEchoEvents } from \"./context.js\";\nimport type { EthDispatchData, EthEventBinding } from \"./types.js\";\n\n/**\n * Build an `onSelect` / `onClick`-shaped handler from an {@link EthEventBinding}.\n *\n * Returns `undefined` when `binding` is absent, so a component can spread it into\n * `EthAction.onSelect` and fall back to its own default when unbound:\n *\n * ```tsx\n * const onSelect = useEthAction(action.binding);\n * <Button onClick={onSelect}>{action.label}</Button>\n * // or, for EthAction:\n * { ...action, onSelect: useEthAction(action.binding) ?? action.onSelect }\n * ```\n *\n * The optional `data` (current row / route params) is threaded into the dispatch\n * so the host can resolve `field:` / `route:` payloads — matching the registry's\n * `DispatchDataContext`.\n */\nexport function useEthAction(\n binding: EthEventBinding | undefined,\n data?: EthDispatchData,\n): (() => void) | undefined {\n const { dispatch } = useEchoEvents();\n return useCallback(() => {\n if (!binding) return;\n void dispatch(binding, undefined, data);\n }, [dispatch, binding, data]);\n}\n\n/**\n * Build a payload-carrying handler from an {@link EthEventBinding} — for events\n * that produce live data (form `onSubmit` values, the selected row, the chosen\n * `rowAction` id). The returned function forwards whatever it is called with as\n * the dispatch `payload`, so the host can merge it into the binding's actions.\n *\n * Returns a no-op (still callable) when `binding` is absent, so callers don't\n * need to null-check at the call site.\n *\n * ```tsx\n * const onSubmit = useEthEventHandler(binding);\n * <form onSubmit={(e) => { e.preventDefault(); onSubmit(readFormValues(e)); }} />\n * ```\n */\nexport function useEthEventHandler<P = unknown>(\n binding: EthEventBinding | undefined,\n data?: EthDispatchData,\n): (payload?: P) => void {\n const { dispatch } = useEchoEvents();\n return useCallback(\n (payload?: P) => {\n if (!binding) return;\n void dispatch(binding, payload, data);\n },\n [dispatch, binding, data],\n );\n}\n","/**\n * `bridge` — the field-renaming adapter that turns an {@link EthEventBinding}\n * (the UI-library shape, discriminant `kind`) into the echothink-fugue registry\n * `EventBinding` shape (discriminant `action`) that\n * `runActionGraph`/`runProcess` consume.\n *\n * This lives in the UI library (not the registry) so the UI side OWNS its\n * authoring shape and the host's `dispatch` can be a thin `runActionGraph(\n * toRegistryActions(binding.actions), ctx)`. It is the documented, single point\n * where the two contracts meet — keeping the rest of `@echothink-ui/events`\n * free of any registry coupling.\n *\n * The output objects are plain JSON-compatible records typed `unknown` at the\n * boundary; the registry validates/narrows them with its own\n * `validateActionGraph`. We do not import registry types here.\n */\nimport type { EthAction, EthEventBinding } from \"./types.js\";\n\n/** A registry-shaped action node (discriminant `action`). */\nexport type RegistryActionNode = Record<string, unknown> & { action: string };\n\n/** A registry-shaped event binding (`{ event, actions: ActionNode[] }`). */\nexport interface RegistryEventBinding {\n event: string;\n actions: RegistryActionNode[];\n}\n\n/**\n * Convert one {@link EthAction} to the registry `ActionNode` shape. The mapping:\n * - `kind` → `action`\n * - `runProcess`: `payload` → `externalInput`, `target`/`expectedVersion`/`optimistic` pass through\n * - `navigate`: `target` → `route`, `params` pass through\n * - `setState`: `target` → `key`, `value` passes through\n * - `openModal`/`closeModal`: `target` → `modalId`\n * - `showToast`: `message`/`level` pass through\n */\nexport function toRegistryAction(action: EthAction): RegistryActionNode {\n switch (action.kind) {\n case \"runProcess\":\n return {\n action: \"runProcess\",\n process: action.process,\n ...(action.target !== undefined ? { target: action.target } : {}),\n ...(action.payload !== undefined ? { externalInput: action.payload } : {}),\n ...(action.expectedVersion !== undefined\n ? { expectedVersion: action.expectedVersion }\n : {}),\n ...(action.optimistic !== undefined ? { optimistic: action.optimistic } : {}),\n };\n case \"navigate\":\n return {\n action: \"navigate\",\n route: action.target,\n ...(action.params !== undefined ? { params: action.params } : {}),\n };\n case \"setState\":\n return {\n action: \"setState\",\n key: action.target,\n ...(action.value !== undefined ? { value: action.value } : {}),\n };\n case \"openModal\":\n return { action: \"openModal\", modalId: action.target };\n case \"closeModal\":\n return { action: \"closeModal\", modalId: action.target };\n case \"showToast\":\n return {\n action: \"showToast\",\n message: action.message,\n ...(action.level !== undefined ? { level: action.level } : {}),\n };\n default: {\n // Forward-compatible: pass an unknown kind through as its own action.\n const unknownAction = action as { kind: string };\n return { ...(action as Record<string, unknown>), action: unknownAction.kind };\n }\n }\n}\n\n/**\n * Convert an {@link EthEventBinding} to the registry `EventBinding` shape. A host\n * that speaks the registry contract uses this to adapt at the dispatch edge:\n *\n * ```ts\n * const dispatch: EchoEventDispatch = (binding, payload) => {\n * const reg = toRegistryEventBinding(binding);\n * runtime.dispatch([reg], binding.event, { row: payload });\n * };\n * ```\n */\nexport function toRegistryEventBinding(binding: EthEventBinding): RegistryEventBinding {\n return {\n event: binding.event,\n actions: (binding.actions ?? []).map(toRegistryAction),\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACWA,mBAKO;AAcP,IAAM,gBAAmC,MAAM;AAAC;AAEhD,IAAM,uBAAmB,4BAAqC;AAAA,EAC5D,UAAU;AACZ,CAAC;AAcM,SAAS,kBAAkB;AAAA,EAChC;AAAA,EACA;AACF,GAGc;AACZ,aAAO;AAAA,IACL,iBAAiB;AAAA,IACjB,EAAE,OAAO,EAAE,SAAS,EAAE;AAAA,IACtB;AAAA,EACF;AACF;AAMO,SAAS,gBAAuC;AACrD,aAAO,yBAAW,gBAAgB;AACpC;;;AC1DA,IAAAA,gBAA4B;AAqBrB,SAAS,aACd,SACA,MAC0B;AAC1B,QAAM,EAAE,SAAS,IAAI,cAAc;AACnC,aAAO,2BAAY,MAAM;AACvB,QAAI,CAAC,QAAS;AACd,SAAK,SAAS,SAAS,QAAW,IAAI;AAAA,EACxC,GAAG,CAAC,UAAU,SAAS,IAAI,CAAC;AAC9B;AAgBO,SAAS,mBACd,SACA,MACuB;AACvB,QAAM,EAAE,SAAS,IAAI,cAAc;AACnC,aAAO;AAAA,IACL,CAAC,YAAgB;AACf,UAAI,CAAC,QAAS;AACd,WAAK,SAAS,SAAS,SAAS,IAAI;AAAA,IACtC;AAAA,IACA,CAAC,UAAU,SAAS,IAAI;AAAA,EAC1B;AACF;;;AChCO,SAAS,iBAAiB,QAAuC;AACtE,UAAQ,OAAO,MAAM;AAAA,IACnB,KAAK;AACH,aAAO;AAAA,QACL,QAAQ;AAAA,QACR,SAAS,OAAO;AAAA,QAChB,GAAI,OAAO,WAAW,SAAY,EAAE,QAAQ,OAAO,OAAO,IAAI,CAAC;AAAA,QAC/D,GAAI,OAAO,YAAY,SAAY,EAAE,eAAe,OAAO,QAAQ,IAAI,CAAC;AAAA,QACxE,GAAI,OAAO,oBAAoB,SAC3B,EAAE,iBAAiB,OAAO,gBAAgB,IAC1C,CAAC;AAAA,QACL,GAAI,OAAO,eAAe,SAAY,EAAE,YAAY,OAAO,WAAW,IAAI,CAAC;AAAA,MAC7E;AAAA,IACF,KAAK;AACH,aAAO;AAAA,QACL,QAAQ;AAAA,QACR,OAAO,OAAO;AAAA,QACd,GAAI,OAAO,WAAW,SAAY,EAAE,QAAQ,OAAO,OAAO,IAAI,CAAC;AAAA,MACjE;AAAA,IACF,KAAK;AACH,aAAO;AAAA,QACL,QAAQ;AAAA,QACR,KAAK,OAAO;AAAA,QACZ,GAAI,OAAO,UAAU,SAAY,EAAE,OAAO,OAAO,MAAM,IAAI,CAAC;AAAA,MAC9D;AAAA,IACF,KAAK;AACH,aAAO,EAAE,QAAQ,aAAa,SAAS,OAAO,OAAO;AAAA,IACvD,KAAK;AACH,aAAO,EAAE,QAAQ,cAAc,SAAS,OAAO,OAAO;AAAA,IACxD,KAAK;AACH,aAAO;AAAA,QACL,QAAQ;AAAA,QACR,SAAS,OAAO;AAAA,QAChB,GAAI,OAAO,UAAU,SAAY,EAAE,OAAO,OAAO,MAAM,IAAI,CAAC;AAAA,MAC9D;AAAA,IACF,SAAS;AAEP,YAAM,gBAAgB;AACtB,aAAO,EAAE,GAAI,QAAoC,QAAQ,cAAc,KAAK;AAAA,IAC9E;AAAA,EACF;AACF;AAaO,SAAS,uBAAuB,SAAgD;AACrF,SAAO;AAAA,IACL,OAAO,QAAQ;AAAA,IACf,UAAU,QAAQ,WAAW,CAAC,GAAG,IAAI,gBAAgB;AAAA,EACvD;AACF;","names":["import_react"]}
@@ -0,0 +1,16 @@
1
+ /**
2
+ * `@echothink-ui/events` — public API.
3
+ *
4
+ * The backend-event contract for `@echothink-ui` components: a serializable
5
+ * {@link EthEventBinding} (structurally compatible with the echothink-fugue
6
+ * registry `EventBinding`), an {@link EchoEventProvider} the host uses to inject
7
+ * a backend `dispatch`, a {@link useEchoEvents} hook, and component-facing
8
+ * {@link useEthAction} / {@link useEthEventHandler} helpers that wire rich
9
+ * semantic callbacks (`EthAction.onSelect`, form `onSubmit`, table `rowActions`)
10
+ * to that dispatch. The {@link toRegistryEventBinding} bridge adapts the
11
+ * authoring shape to the registry action-graph shape at the host edge.
12
+ */
13
+ export type { EthEventName, EthActionTarget, EthActionKind, EthAction, EthRunProcessAction, EthNavigateAction, EthSetStateAction, EthOpenModalAction, EthCloseModalAction, EthShowToastAction, EthEventBinding, EthDispatchData, EchoEventDispatch, } from "./types.js";
14
+ export { EchoEventProvider, useEchoEvents, type EchoEventContextValue, } from "./context.js";
15
+ export { useEthAction, useEthEventHandler } from "./hooks.js";
16
+ export { toRegistryAction, toRegistryEventBinding, type RegistryActionNode, type RegistryEventBinding, } from "./bridge.js";
package/dist/index.js ADDED
@@ -0,0 +1,100 @@
1
+ // src/context.tsx
2
+ import {
3
+ createContext,
4
+ createElement,
5
+ useContext
6
+ } from "react";
7
+ var NOOP_DISPATCH = () => {
8
+ };
9
+ var EchoEventContext = createContext({
10
+ dispatch: NOOP_DISPATCH
11
+ });
12
+ function EchoEventProvider({
13
+ dispatch,
14
+ children
15
+ }) {
16
+ return createElement(
17
+ EchoEventContext.Provider,
18
+ { value: { dispatch } },
19
+ children
20
+ );
21
+ }
22
+ function useEchoEvents() {
23
+ return useContext(EchoEventContext);
24
+ }
25
+
26
+ // src/hooks.tsx
27
+ import { useCallback } from "react";
28
+ function useEthAction(binding, data) {
29
+ const { dispatch } = useEchoEvents();
30
+ return useCallback(() => {
31
+ if (!binding) return;
32
+ void dispatch(binding, void 0, data);
33
+ }, [dispatch, binding, data]);
34
+ }
35
+ function useEthEventHandler(binding, data) {
36
+ const { dispatch } = useEchoEvents();
37
+ return useCallback(
38
+ (payload) => {
39
+ if (!binding) return;
40
+ void dispatch(binding, payload, data);
41
+ },
42
+ [dispatch, binding, data]
43
+ );
44
+ }
45
+
46
+ // src/bridge.ts
47
+ function toRegistryAction(action) {
48
+ switch (action.kind) {
49
+ case "runProcess":
50
+ return {
51
+ action: "runProcess",
52
+ process: action.process,
53
+ ...action.target !== void 0 ? { target: action.target } : {},
54
+ ...action.payload !== void 0 ? { externalInput: action.payload } : {},
55
+ ...action.expectedVersion !== void 0 ? { expectedVersion: action.expectedVersion } : {},
56
+ ...action.optimistic !== void 0 ? { optimistic: action.optimistic } : {}
57
+ };
58
+ case "navigate":
59
+ return {
60
+ action: "navigate",
61
+ route: action.target,
62
+ ...action.params !== void 0 ? { params: action.params } : {}
63
+ };
64
+ case "setState":
65
+ return {
66
+ action: "setState",
67
+ key: action.target,
68
+ ...action.value !== void 0 ? { value: action.value } : {}
69
+ };
70
+ case "openModal":
71
+ return { action: "openModal", modalId: action.target };
72
+ case "closeModal":
73
+ return { action: "closeModal", modalId: action.target };
74
+ case "showToast":
75
+ return {
76
+ action: "showToast",
77
+ message: action.message,
78
+ ...action.level !== void 0 ? { level: action.level } : {}
79
+ };
80
+ default: {
81
+ const unknownAction = action;
82
+ return { ...action, action: unknownAction.kind };
83
+ }
84
+ }
85
+ }
86
+ function toRegistryEventBinding(binding) {
87
+ return {
88
+ event: binding.event,
89
+ actions: (binding.actions ?? []).map(toRegistryAction)
90
+ };
91
+ }
92
+ export {
93
+ EchoEventProvider,
94
+ toRegistryAction,
95
+ toRegistryEventBinding,
96
+ useEchoEvents,
97
+ useEthAction,
98
+ useEthEventHandler
99
+ };
100
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/context.tsx","../src/hooks.tsx","../src/bridge.ts"],"sourcesContent":["/**\n * `EchoEventContext` — the injectable dispatch surface a component reads to send\n * its authored {@link EthEventBinding} to the backend.\n *\n * Mirrors the registry's `EventDispatchContext` / `PageInteractionProvider`\n * pattern: the UI library owns the CONTEXT + hooks; the HOST (the echothink-fugue\n * renderer, a demo app, or a test) provides the real `dispatch` implementation\n * that walks the action graph → `runProcess` → gateway. A NO-OP default lets\n * components render with no provider, so standalone render/snapshot tests never\n * crash.\n */\nimport {\n createContext,\n createElement,\n useContext,\n type ReactNode,\n} from \"react\";\nimport type { EchoEventDispatch } from \"./types.js\";\n\n/**\n * The interaction surface a component reads via {@link useEchoEvents}. The host\n * constructs the real one; absent a provider, {@link NOOP_DISPATCH} is used so\n * components never crash.\n */\nexport interface EchoEventContextValue {\n /** Carry a binding (+ live payload) to the backend. */\n dispatch: EchoEventDispatch;\n}\n\n/** A no-op dispatch — used when no provider is mounted. */\nconst NOOP_DISPATCH: EchoEventDispatch = () => {};\n\nconst EchoEventContext = createContext<EchoEventContextValue>({\n dispatch: NOOP_DISPATCH,\n});\n\n/**\n * Provide an {@link EchoEventDispatch} (constructed by the host) to the\n * component tree. The host's `dispatch` is the bridge to the backend — it\n * typically adapts each {@link EthEventBinding} to the registry action graph and\n * runs it through `runProcess` → gateway.\n *\n * ```tsx\n * <EchoEventProvider dispatch={(binding, payload) => runtime.dispatch(binding, payload)}>\n * <MyAuthoredPage />\n * </EchoEventProvider>\n * ```\n */\nexport function EchoEventProvider({\n dispatch,\n children,\n}: {\n dispatch: EchoEventDispatch;\n children: ReactNode;\n}): ReactNode {\n return createElement(\n EchoEventContext.Provider,\n { value: { dispatch } },\n children,\n );\n}\n\n/**\n * Read the {@link EchoEventContextValue}. Returns the no-op dispatch when no\n * provider is mounted, so components (and their render tests) work standalone.\n */\nexport function useEchoEvents(): EchoEventContextValue {\n return useContext(EchoEventContext);\n}\n","/**\n * Component-facing helpers that wire a component's rich semantic callbacks\n * (`EthAction.onSelect`, `onClick`, form `onSubmit`, table `rowActions`) to a\n * backend {@link EthEventBinding} via the injected {@link EchoEventDispatch}.\n *\n * These are the OPT-IN seam: a component that wants backend-aware behavior takes\n * an optional `binding?: EthEventBinding` prop and calls one of these helpers to\n * produce the handler it passes to its native callback prop. A component with no\n * binding behaves exactly as today (the no-op dispatch is inert).\n */\nimport { useCallback } from \"react\";\nimport { useEchoEvents } from \"./context.js\";\nimport type { EthDispatchData, EthEventBinding } from \"./types.js\";\n\n/**\n * Build an `onSelect` / `onClick`-shaped handler from an {@link EthEventBinding}.\n *\n * Returns `undefined` when `binding` is absent, so a component can spread it into\n * `EthAction.onSelect` and fall back to its own default when unbound:\n *\n * ```tsx\n * const onSelect = useEthAction(action.binding);\n * <Button onClick={onSelect}>{action.label}</Button>\n * // or, for EthAction:\n * { ...action, onSelect: useEthAction(action.binding) ?? action.onSelect }\n * ```\n *\n * The optional `data` (current row / route params) is threaded into the dispatch\n * so the host can resolve `field:` / `route:` payloads — matching the registry's\n * `DispatchDataContext`.\n */\nexport function useEthAction(\n binding: EthEventBinding | undefined,\n data?: EthDispatchData,\n): (() => void) | undefined {\n const { dispatch } = useEchoEvents();\n return useCallback(() => {\n if (!binding) return;\n void dispatch(binding, undefined, data);\n }, [dispatch, binding, data]);\n}\n\n/**\n * Build a payload-carrying handler from an {@link EthEventBinding} — for events\n * that produce live data (form `onSubmit` values, the selected row, the chosen\n * `rowAction` id). The returned function forwards whatever it is called with as\n * the dispatch `payload`, so the host can merge it into the binding's actions.\n *\n * Returns a no-op (still callable) when `binding` is absent, so callers don't\n * need to null-check at the call site.\n *\n * ```tsx\n * const onSubmit = useEthEventHandler(binding);\n * <form onSubmit={(e) => { e.preventDefault(); onSubmit(readFormValues(e)); }} />\n * ```\n */\nexport function useEthEventHandler<P = unknown>(\n binding: EthEventBinding | undefined,\n data?: EthDispatchData,\n): (payload?: P) => void {\n const { dispatch } = useEchoEvents();\n return useCallback(\n (payload?: P) => {\n if (!binding) return;\n void dispatch(binding, payload, data);\n },\n [dispatch, binding, data],\n );\n}\n","/**\n * `bridge` — the field-renaming adapter that turns an {@link EthEventBinding}\n * (the UI-library shape, discriminant `kind`) into the echothink-fugue registry\n * `EventBinding` shape (discriminant `action`) that\n * `runActionGraph`/`runProcess` consume.\n *\n * This lives in the UI library (not the registry) so the UI side OWNS its\n * authoring shape and the host's `dispatch` can be a thin `runActionGraph(\n * toRegistryActions(binding.actions), ctx)`. It is the documented, single point\n * where the two contracts meet — keeping the rest of `@echothink-ui/events`\n * free of any registry coupling.\n *\n * The output objects are plain JSON-compatible records typed `unknown` at the\n * boundary; the registry validates/narrows them with its own\n * `validateActionGraph`. We do not import registry types here.\n */\nimport type { EthAction, EthEventBinding } from \"./types.js\";\n\n/** A registry-shaped action node (discriminant `action`). */\nexport type RegistryActionNode = Record<string, unknown> & { action: string };\n\n/** A registry-shaped event binding (`{ event, actions: ActionNode[] }`). */\nexport interface RegistryEventBinding {\n event: string;\n actions: RegistryActionNode[];\n}\n\n/**\n * Convert one {@link EthAction} to the registry `ActionNode` shape. The mapping:\n * - `kind` → `action`\n * - `runProcess`: `payload` → `externalInput`, `target`/`expectedVersion`/`optimistic` pass through\n * - `navigate`: `target` → `route`, `params` pass through\n * - `setState`: `target` → `key`, `value` passes through\n * - `openModal`/`closeModal`: `target` → `modalId`\n * - `showToast`: `message`/`level` pass through\n */\nexport function toRegistryAction(action: EthAction): RegistryActionNode {\n switch (action.kind) {\n case \"runProcess\":\n return {\n action: \"runProcess\",\n process: action.process,\n ...(action.target !== undefined ? { target: action.target } : {}),\n ...(action.payload !== undefined ? { externalInput: action.payload } : {}),\n ...(action.expectedVersion !== undefined\n ? { expectedVersion: action.expectedVersion }\n : {}),\n ...(action.optimistic !== undefined ? { optimistic: action.optimistic } : {}),\n };\n case \"navigate\":\n return {\n action: \"navigate\",\n route: action.target,\n ...(action.params !== undefined ? { params: action.params } : {}),\n };\n case \"setState\":\n return {\n action: \"setState\",\n key: action.target,\n ...(action.value !== undefined ? { value: action.value } : {}),\n };\n case \"openModal\":\n return { action: \"openModal\", modalId: action.target };\n case \"closeModal\":\n return { action: \"closeModal\", modalId: action.target };\n case \"showToast\":\n return {\n action: \"showToast\",\n message: action.message,\n ...(action.level !== undefined ? { level: action.level } : {}),\n };\n default: {\n // Forward-compatible: pass an unknown kind through as its own action.\n const unknownAction = action as { kind: string };\n return { ...(action as Record<string, unknown>), action: unknownAction.kind };\n }\n }\n}\n\n/**\n * Convert an {@link EthEventBinding} to the registry `EventBinding` shape. A host\n * that speaks the registry contract uses this to adapt at the dispatch edge:\n *\n * ```ts\n * const dispatch: EchoEventDispatch = (binding, payload) => {\n * const reg = toRegistryEventBinding(binding);\n * runtime.dispatch([reg], binding.event, { row: payload });\n * };\n * ```\n */\nexport function toRegistryEventBinding(binding: EthEventBinding): RegistryEventBinding {\n return {\n event: binding.event,\n actions: (binding.actions ?? []).map(toRegistryAction),\n };\n}\n"],"mappings":";AAWA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OAEK;AAcP,IAAM,gBAAmC,MAAM;AAAC;AAEhD,IAAM,mBAAmB,cAAqC;AAAA,EAC5D,UAAU;AACZ,CAAC;AAcM,SAAS,kBAAkB;AAAA,EAChC;AAAA,EACA;AACF,GAGc;AACZ,SAAO;AAAA,IACL,iBAAiB;AAAA,IACjB,EAAE,OAAO,EAAE,SAAS,EAAE;AAAA,IACtB;AAAA,EACF;AACF;AAMO,SAAS,gBAAuC;AACrD,SAAO,WAAW,gBAAgB;AACpC;;;AC1DA,SAAS,mBAAmB;AAqBrB,SAAS,aACd,SACA,MAC0B;AAC1B,QAAM,EAAE,SAAS,IAAI,cAAc;AACnC,SAAO,YAAY,MAAM;AACvB,QAAI,CAAC,QAAS;AACd,SAAK,SAAS,SAAS,QAAW,IAAI;AAAA,EACxC,GAAG,CAAC,UAAU,SAAS,IAAI,CAAC;AAC9B;AAgBO,SAAS,mBACd,SACA,MACuB;AACvB,QAAM,EAAE,SAAS,IAAI,cAAc;AACnC,SAAO;AAAA,IACL,CAAC,YAAgB;AACf,UAAI,CAAC,QAAS;AACd,WAAK,SAAS,SAAS,SAAS,IAAI;AAAA,IACtC;AAAA,IACA,CAAC,UAAU,SAAS,IAAI;AAAA,EAC1B;AACF;;;AChCO,SAAS,iBAAiB,QAAuC;AACtE,UAAQ,OAAO,MAAM;AAAA,IACnB,KAAK;AACH,aAAO;AAAA,QACL,QAAQ;AAAA,QACR,SAAS,OAAO;AAAA,QAChB,GAAI,OAAO,WAAW,SAAY,EAAE,QAAQ,OAAO,OAAO,IAAI,CAAC;AAAA,QAC/D,GAAI,OAAO,YAAY,SAAY,EAAE,eAAe,OAAO,QAAQ,IAAI,CAAC;AAAA,QACxE,GAAI,OAAO,oBAAoB,SAC3B,EAAE,iBAAiB,OAAO,gBAAgB,IAC1C,CAAC;AAAA,QACL,GAAI,OAAO,eAAe,SAAY,EAAE,YAAY,OAAO,WAAW,IAAI,CAAC;AAAA,MAC7E;AAAA,IACF,KAAK;AACH,aAAO;AAAA,QACL,QAAQ;AAAA,QACR,OAAO,OAAO;AAAA,QACd,GAAI,OAAO,WAAW,SAAY,EAAE,QAAQ,OAAO,OAAO,IAAI,CAAC;AAAA,MACjE;AAAA,IACF,KAAK;AACH,aAAO;AAAA,QACL,QAAQ;AAAA,QACR,KAAK,OAAO;AAAA,QACZ,GAAI,OAAO,UAAU,SAAY,EAAE,OAAO,OAAO,MAAM,IAAI,CAAC;AAAA,MAC9D;AAAA,IACF,KAAK;AACH,aAAO,EAAE,QAAQ,aAAa,SAAS,OAAO,OAAO;AAAA,IACvD,KAAK;AACH,aAAO,EAAE,QAAQ,cAAc,SAAS,OAAO,OAAO;AAAA,IACxD,KAAK;AACH,aAAO;AAAA,QACL,QAAQ;AAAA,QACR,SAAS,OAAO;AAAA,QAChB,GAAI,OAAO,UAAU,SAAY,EAAE,OAAO,OAAO,MAAM,IAAI,CAAC;AAAA,MAC9D;AAAA,IACF,SAAS;AAEP,YAAM,gBAAgB;AACtB,aAAO,EAAE,GAAI,QAAoC,QAAQ,cAAc,KAAK;AAAA,IAC9E;AAAA,EACF;AACF;AAaO,SAAS,uBAAuB,SAAgD;AACrF,SAAO;AAAA,IACL,OAAO,QAAQ;AAAA,IACf,UAAU,QAAQ,WAAW,CAAC,GAAG,IAAI,gBAAgB;AAAA,EACvD;AACF;","names":[]}
@@ -0,0 +1,125 @@
1
+ /**
2
+ * `@echothink-ui/events` — the BACKEND-EVENT CONTRACT for `@echothink-ui`
3
+ * components.
4
+ *
5
+ * `@echothink-ui` components expose plain React callbacks (`onClick`,
6
+ * `onSelect`, `EthAction.onSelect`, form `onSubmit`, table `rowActions`) with no
7
+ * path to a backend process/action. This module defines the serializable
8
+ * data shape — {@link EthEventBinding} + {@link EthAction} — that lets a
9
+ * builder-authored page declare "when this UI event fires, run these backend
10
+ * actions", plus the {@link EchoEventDispatch} contract a HOST injects to
11
+ * actually carry those actions to a backend (ultimately `runProcess` → gateway).
12
+ *
13
+ * STRUCTURAL COMPATIBILITY (the load-bearing constraint): these types are kept
14
+ * field-for-field compatible with the echothink-fugue registry's `EventBinding`
15
+ * / `ActionNode` contract (see `echothink-component-registry/.../events.tsx` and
16
+ * `echothink-app-renderer/.../logic/graph.ts`). A plain `EthEventBinding[]`
17
+ * array produced here flows through that registry's `eventsAware` HOC →
18
+ * `PageInteractionRuntime.dispatch` → `runActionGraph` → `runProcess` WITHOUT
19
+ * conversion. We deliberately DO NOT import the registry types (that would
20
+ * invert the dependency direction — the registry/renderer depend on the UI
21
+ * library, never the reverse); we mirror the shapes locally instead.
22
+ */
23
+ /**
24
+ * The known UI event names. Kept open (`string & {}`) so custom / semantic
25
+ * events (e.g. `"onRowAction:archive"`) flow through, mirroring the registry's
26
+ * `EventName`.
27
+ */
28
+ export type EthEventName = "onClick" | "onSelect" | "onSubmit" | "onChange" | "onLoad" | (string & {});
29
+ /** Optimistic-process target reference (the entity the process acts on). */
30
+ export interface EthActionTarget {
31
+ entityType?: string;
32
+ entityId?: string;
33
+ }
34
+ /**
35
+ * The action discriminant — the verbs the host runtime knows how to perform.
36
+ * Kept identical to the registry `ActionNode["action"]` vocabulary (the subset a
37
+ * UI component realistically authors); unknown kinds are tolerated by the
38
+ * runtime, so this stays forward-compatible.
39
+ */
40
+ export type EthActionKind = "runProcess" | "navigate" | "setState" | "openModal" | "closeModal" | "showToast";
41
+ /** Invoke a unit process through the host runtime (ultimately the gateway). */
42
+ export interface EthRunProcessAction {
43
+ kind: "runProcess";
44
+ /** The unit-process name to invoke. */
45
+ process: string;
46
+ target?: EthActionTarget;
47
+ /** Inline payload for the process. May contain `field:` / `route:` refs. */
48
+ payload?: Record<string, unknown>;
49
+ expectedVersion?: number;
50
+ /** Hint that the UI applied an optimistic change before this ran. */
51
+ optimistic?: boolean;
52
+ }
53
+ /** Client-side navigation to a route, with optional params. */
54
+ export interface EthNavigateAction {
55
+ kind: "navigate";
56
+ /** Target route / href. */
57
+ target: string;
58
+ params?: Record<string, string>;
59
+ }
60
+ /** Set a page-scoped state key to a value. */
61
+ export interface EthSetStateAction {
62
+ kind: "setState";
63
+ /** State key to set. */
64
+ target: string;
65
+ value?: unknown;
66
+ }
67
+ /** Open a modal by id. */
68
+ export interface EthOpenModalAction {
69
+ kind: "openModal";
70
+ /** Modal id to open. */
71
+ target: string;
72
+ }
73
+ /** Close a modal by id. */
74
+ export interface EthCloseModalAction {
75
+ kind: "closeModal";
76
+ /** Modal id to close. */
77
+ target: string;
78
+ }
79
+ /** Surface a transient toast notification. */
80
+ export interface EthShowToastAction {
81
+ kind: "showToast";
82
+ /** Toast message (the `target` mirrors `message` for field uniformity). */
83
+ message: string;
84
+ level?: "info" | "success" | "error";
85
+ }
86
+ /** The action union an {@link EthEventBinding} carries (discriminant: `kind`). */
87
+ export type EthAction = EthRunProcessAction | EthNavigateAction | EthSetStateAction | EthOpenModalAction | EthCloseModalAction | EthShowToastAction;
88
+ /**
89
+ * An event → action(s) binding attached to a component (the value a builder
90
+ * authors). When the named `event` fires the host runs `actions` in order.
91
+ *
92
+ * Structurally compatible with the registry `EventBinding` — see the conversion
93
+ * note in {@link toRegistryEventBinding}: `event` is the same field; the
94
+ * registry calls its action list `actions` with an `action` discriminant, while
95
+ * we use `kind` for ergonomics. {@link toRegistryEventBinding} renames the two
96
+ * fields (`kind`→`action`, `payload`→`externalInput`, `target`→`route`/`modalId`/`key`)
97
+ * so a host that speaks the registry contract can adapt at the edge.
98
+ */
99
+ export interface EthEventBinding {
100
+ /** The UI event this binding reacts to. */
101
+ event: EthEventName;
102
+ /** The ordered backend/client actions to run when `event` fires. */
103
+ actions: EthAction[];
104
+ }
105
+ /**
106
+ * Optional dispatch-time data context, structurally compatible with the
107
+ * registry's `DispatchDataContext` / `EventDispatchData` — `row` is the current
108
+ * data-binding row (for `field:` resolution), `route` the route params.
109
+ */
110
+ export interface EthDispatchData {
111
+ row?: Record<string, unknown>;
112
+ route?: Record<string, string>;
113
+ }
114
+ /**
115
+ * The capability a HOST injects (via {@link EchoEventProvider}) to carry a
116
+ * component's authored binding to the backend. `payload` is the live runtime
117
+ * data captured at the event (e.g. the form values, the selected row, the
118
+ * chosen action id). The host is responsible for merging `payload` into the
119
+ * binding's actions and running them (typically by adapting to the registry
120
+ * action graph → `runProcess` → gateway).
121
+ *
122
+ * Returns the host's promise so callers MAY await completion (e.g. to clear an
123
+ * optimistic flag); a fire-and-forget host returns `void`.
124
+ */
125
+ export type EchoEventDispatch = (binding: EthEventBinding, payload?: unknown, data?: EthDispatchData) => void | Promise<void>;
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@echothink-ui/events",
3
+ "version": "0.1.0",
4
+ "private": false,
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js",
13
+ "require": "./dist/index.cjs"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist",
18
+ "src",
19
+ "README.md"
20
+ ],
21
+ "peerDependencies": {
22
+ "react": ">=18.3.0",
23
+ "react-dom": ">=18.3.0"
24
+ },
25
+ "devDependencies": {
26
+ "@types/node": "^24.9.1",
27
+ "@types/react": "^19.2.2",
28
+ "@types/react-dom": "^19.2.2"
29
+ },
30
+ "publishConfig": {
31
+ "access": "public"
32
+ },
33
+ "scripts": {
34
+ "build": "tsup src/index.ts --format esm,cjs --sourcemap --clean --external react --external react-dom && tsc -p tsconfig.json --declaration --emitDeclarationOnly --noEmit false --outDir dist",
35
+ "typecheck": "tsc -p tsconfig.json --noEmit",
36
+ "test": "vitest run --root . --config ../echothink-UI-components/vitest.config.ts --passWithNoTests",
37
+ "lint": "eslint src"
38
+ }
39
+ }
@@ -0,0 +1,159 @@
1
+ import { describe, it, expect, vi } from "vitest";
2
+ import * as React from "react";
3
+ import { act } from "react";
4
+ import { createRoot } from "react-dom/client";
5
+
6
+ // Opt into React's act() testing environment so state updates flush synchronously
7
+ // and React does not warn about an unconfigured act environment.
8
+ (globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
9
+
10
+ import {
11
+ EchoEventProvider,
12
+ useEthAction,
13
+ useEthEventHandler,
14
+ toRegistryEventBinding,
15
+ type EchoEventDispatch,
16
+ type EthEventBinding,
17
+ } from "../index.js";
18
+
19
+ /* A binding a builder would author for a Button → backend process. */
20
+ const archiveBinding: EthEventBinding = {
21
+ event: "onSelect",
22
+ actions: [
23
+ { kind: "runProcess", process: "ArchiveOrder", payload: { reason: "stale" } },
24
+ { kind: "showToast", message: "Archived", level: "success" },
25
+ ],
26
+ };
27
+
28
+ /** Mount a component into a real DOM container (jsdom) and return cleanup. */
29
+ function mount(ui: React.ReactElement) {
30
+ const container = document.createElement("div");
31
+ document.body.appendChild(container);
32
+ const root = createRoot(container);
33
+ act(() => {
34
+ root.render(ui);
35
+ });
36
+ return {
37
+ container,
38
+ cleanup() {
39
+ act(() => root.unmount());
40
+ container.remove();
41
+ },
42
+ };
43
+ }
44
+
45
+ /* A tiny component that wires EthAction.onSelect → useEthAction(binding). */
46
+ function ArchiveButton({ binding }: { binding?: EthEventBinding }) {
47
+ const onSelect = useEthAction(binding);
48
+ return (
49
+ <button type="button" data-testid="archive" onClick={onSelect}>
50
+ Archive
51
+ </button>
52
+ );
53
+ }
54
+
55
+ /* A component that forwards live payload (form values) through the handler. */
56
+ function SubmitForm({ binding }: { binding?: EthEventBinding }) {
57
+ const onSubmit = useEthEventHandler<Record<string, unknown>>(binding);
58
+ return (
59
+ <button
60
+ type="button"
61
+ data-testid="submit"
62
+ onClick={() => onSubmit({ name: "Ada" })}
63
+ >
64
+ Submit
65
+ </button>
66
+ );
67
+ }
68
+
69
+ describe("useEthAction → EchoEventProvider.dispatch", () => {
70
+ it("dispatches the exact binding when EthAction.onSelect fires", () => {
71
+ const dispatch = vi.fn<EchoEventDispatch>();
72
+ const { container, cleanup } = mount(
73
+ <EchoEventProvider dispatch={dispatch}>
74
+ <ArchiveButton binding={archiveBinding} />
75
+ </EchoEventProvider>,
76
+ );
77
+
78
+ const button = container.querySelector<HTMLButtonElement>('[data-testid="archive"]')!;
79
+ act(() => button.click());
80
+
81
+ expect(dispatch).toHaveBeenCalledTimes(1);
82
+ expect(dispatch).toHaveBeenCalledWith(archiveBinding, undefined, undefined);
83
+ cleanup();
84
+ });
85
+
86
+ it("is inert (no dispatch) when no binding is authored", () => {
87
+ const dispatch = vi.fn<EchoEventDispatch>();
88
+ const { container, cleanup } = mount(
89
+ <EchoEventProvider dispatch={dispatch}>
90
+ <ArchiveButton />
91
+ </EchoEventProvider>,
92
+ );
93
+ const button = container.querySelector<HTMLButtonElement>('[data-testid="archive"]')!;
94
+ act(() => button.click());
95
+ expect(dispatch).not.toHaveBeenCalled();
96
+ cleanup();
97
+ });
98
+
99
+ it("does not crash with no provider mounted (no-op dispatch)", () => {
100
+ const { container, cleanup } = mount(<ArchiveButton binding={archiveBinding} />);
101
+ const button = container.querySelector<HTMLButtonElement>('[data-testid="archive"]')!;
102
+ expect(() => act(() => button.click())).not.toThrow();
103
+ cleanup();
104
+ });
105
+ });
106
+
107
+ describe("useEthEventHandler — live payload", () => {
108
+ it("forwards the live payload to dispatch", () => {
109
+ const dispatch = vi.fn<EchoEventDispatch>();
110
+ const binding: EthEventBinding = {
111
+ event: "onSubmit",
112
+ actions: [{ kind: "runProcess", process: "CreateUser" }],
113
+ };
114
+ const { container, cleanup } = mount(
115
+ <EchoEventProvider dispatch={dispatch}>
116
+ <SubmitForm binding={binding} />
117
+ </EchoEventProvider>,
118
+ );
119
+ const button = container.querySelector<HTMLButtonElement>('[data-testid="submit"]')!;
120
+ act(() => button.click());
121
+ expect(dispatch).toHaveBeenCalledWith(binding, { name: "Ada" }, undefined);
122
+ cleanup();
123
+ });
124
+ });
125
+
126
+ describe("toRegistryEventBinding — registry structural compatibility", () => {
127
+ it("renames kind→action and payload→externalInput for runProcess", () => {
128
+ const reg = toRegistryEventBinding(archiveBinding);
129
+ expect(reg.event).toBe("onSelect");
130
+ expect(reg.actions[0]).toEqual({
131
+ action: "runProcess",
132
+ process: "ArchiveOrder",
133
+ externalInput: { reason: "stale" },
134
+ });
135
+ expect(reg.actions[1]).toEqual({
136
+ action: "showToast",
137
+ message: "Archived",
138
+ level: "success",
139
+ });
140
+ });
141
+
142
+ it("maps navigate/setState/openModal targets to registry field names", () => {
143
+ const reg = toRegistryEventBinding({
144
+ event: "onClick",
145
+ actions: [
146
+ { kind: "navigate", target: "/orders", params: { tab: "open" } },
147
+ { kind: "setState", target: "selected", value: 3 },
148
+ { kind: "openModal", target: "confirm" },
149
+ ],
150
+ });
151
+ expect(reg.actions[0]).toEqual({
152
+ action: "navigate",
153
+ route: "/orders",
154
+ params: { tab: "open" },
155
+ });
156
+ expect(reg.actions[1]).toEqual({ action: "setState", key: "selected", value: 3 });
157
+ expect(reg.actions[2]).toEqual({ action: "openModal", modalId: "confirm" });
158
+ });
159
+ });
package/src/bridge.ts ADDED
@@ -0,0 +1,96 @@
1
+ /**
2
+ * `bridge` — the field-renaming adapter that turns an {@link EthEventBinding}
3
+ * (the UI-library shape, discriminant `kind`) into the echothink-fugue registry
4
+ * `EventBinding` shape (discriminant `action`) that
5
+ * `runActionGraph`/`runProcess` consume.
6
+ *
7
+ * This lives in the UI library (not the registry) so the UI side OWNS its
8
+ * authoring shape and the host's `dispatch` can be a thin `runActionGraph(
9
+ * toRegistryActions(binding.actions), ctx)`. It is the documented, single point
10
+ * where the two contracts meet — keeping the rest of `@echothink-ui/events`
11
+ * free of any registry coupling.
12
+ *
13
+ * The output objects are plain JSON-compatible records typed `unknown` at the
14
+ * boundary; the registry validates/narrows them with its own
15
+ * `validateActionGraph`. We do not import registry types here.
16
+ */
17
+ import type { EthAction, EthEventBinding } from "./types.js";
18
+
19
+ /** A registry-shaped action node (discriminant `action`). */
20
+ export type RegistryActionNode = Record<string, unknown> & { action: string };
21
+
22
+ /** A registry-shaped event binding (`{ event, actions: ActionNode[] }`). */
23
+ export interface RegistryEventBinding {
24
+ event: string;
25
+ actions: RegistryActionNode[];
26
+ }
27
+
28
+ /**
29
+ * Convert one {@link EthAction} to the registry `ActionNode` shape. The mapping:
30
+ * - `kind` → `action`
31
+ * - `runProcess`: `payload` → `externalInput`, `target`/`expectedVersion`/`optimistic` pass through
32
+ * - `navigate`: `target` → `route`, `params` pass through
33
+ * - `setState`: `target` → `key`, `value` passes through
34
+ * - `openModal`/`closeModal`: `target` → `modalId`
35
+ * - `showToast`: `message`/`level` pass through
36
+ */
37
+ export function toRegistryAction(action: EthAction): RegistryActionNode {
38
+ switch (action.kind) {
39
+ case "runProcess":
40
+ return {
41
+ action: "runProcess",
42
+ process: action.process,
43
+ ...(action.target !== undefined ? { target: action.target } : {}),
44
+ ...(action.payload !== undefined ? { externalInput: action.payload } : {}),
45
+ ...(action.expectedVersion !== undefined
46
+ ? { expectedVersion: action.expectedVersion }
47
+ : {}),
48
+ ...(action.optimistic !== undefined ? { optimistic: action.optimistic } : {}),
49
+ };
50
+ case "navigate":
51
+ return {
52
+ action: "navigate",
53
+ route: action.target,
54
+ ...(action.params !== undefined ? { params: action.params } : {}),
55
+ };
56
+ case "setState":
57
+ return {
58
+ action: "setState",
59
+ key: action.target,
60
+ ...(action.value !== undefined ? { value: action.value } : {}),
61
+ };
62
+ case "openModal":
63
+ return { action: "openModal", modalId: action.target };
64
+ case "closeModal":
65
+ return { action: "closeModal", modalId: action.target };
66
+ case "showToast":
67
+ return {
68
+ action: "showToast",
69
+ message: action.message,
70
+ ...(action.level !== undefined ? { level: action.level } : {}),
71
+ };
72
+ default: {
73
+ // Forward-compatible: pass an unknown kind through as its own action.
74
+ const unknownAction = action as { kind: string };
75
+ return { ...(action as Record<string, unknown>), action: unknownAction.kind };
76
+ }
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Convert an {@link EthEventBinding} to the registry `EventBinding` shape. A host
82
+ * that speaks the registry contract uses this to adapt at the dispatch edge:
83
+ *
84
+ * ```ts
85
+ * const dispatch: EchoEventDispatch = (binding, payload) => {
86
+ * const reg = toRegistryEventBinding(binding);
87
+ * runtime.dispatch([reg], binding.event, { row: payload });
88
+ * };
89
+ * ```
90
+ */
91
+ export function toRegistryEventBinding(binding: EthEventBinding): RegistryEventBinding {
92
+ return {
93
+ event: binding.event,
94
+ actions: (binding.actions ?? []).map(toRegistryAction),
95
+ };
96
+ }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * `EchoEventContext` — the injectable dispatch surface a component reads to send
3
+ * its authored {@link EthEventBinding} to the backend.
4
+ *
5
+ * Mirrors the registry's `EventDispatchContext` / `PageInteractionProvider`
6
+ * pattern: the UI library owns the CONTEXT + hooks; the HOST (the echothink-fugue
7
+ * renderer, a demo app, or a test) provides the real `dispatch` implementation
8
+ * that walks the action graph → `runProcess` → gateway. A NO-OP default lets
9
+ * components render with no provider, so standalone render/snapshot tests never
10
+ * crash.
11
+ */
12
+ import {
13
+ createContext,
14
+ createElement,
15
+ useContext,
16
+ type ReactNode,
17
+ } from "react";
18
+ import type { EchoEventDispatch } from "./types.js";
19
+
20
+ /**
21
+ * The interaction surface a component reads via {@link useEchoEvents}. The host
22
+ * constructs the real one; absent a provider, {@link NOOP_DISPATCH} is used so
23
+ * components never crash.
24
+ */
25
+ export interface EchoEventContextValue {
26
+ /** Carry a binding (+ live payload) to the backend. */
27
+ dispatch: EchoEventDispatch;
28
+ }
29
+
30
+ /** A no-op dispatch — used when no provider is mounted. */
31
+ const NOOP_DISPATCH: EchoEventDispatch = () => {};
32
+
33
+ const EchoEventContext = createContext<EchoEventContextValue>({
34
+ dispatch: NOOP_DISPATCH,
35
+ });
36
+
37
+ /**
38
+ * Provide an {@link EchoEventDispatch} (constructed by the host) to the
39
+ * component tree. The host's `dispatch` is the bridge to the backend — it
40
+ * typically adapts each {@link EthEventBinding} to the registry action graph and
41
+ * runs it through `runProcess` → gateway.
42
+ *
43
+ * ```tsx
44
+ * <EchoEventProvider dispatch={(binding, payload) => runtime.dispatch(binding, payload)}>
45
+ * <MyAuthoredPage />
46
+ * </EchoEventProvider>
47
+ * ```
48
+ */
49
+ export function EchoEventProvider({
50
+ dispatch,
51
+ children,
52
+ }: {
53
+ dispatch: EchoEventDispatch;
54
+ children: ReactNode;
55
+ }): ReactNode {
56
+ return createElement(
57
+ EchoEventContext.Provider,
58
+ { value: { dispatch } },
59
+ children,
60
+ );
61
+ }
62
+
63
+ /**
64
+ * Read the {@link EchoEventContextValue}. Returns the no-op dispatch when no
65
+ * provider is mounted, so components (and their render tests) work standalone.
66
+ */
67
+ export function useEchoEvents(): EchoEventContextValue {
68
+ return useContext(EchoEventContext);
69
+ }
package/src/hooks.tsx ADDED
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Component-facing helpers that wire a component's rich semantic callbacks
3
+ * (`EthAction.onSelect`, `onClick`, form `onSubmit`, table `rowActions`) to a
4
+ * backend {@link EthEventBinding} via the injected {@link EchoEventDispatch}.
5
+ *
6
+ * These are the OPT-IN seam: a component that wants backend-aware behavior takes
7
+ * an optional `binding?: EthEventBinding` prop and calls one of these helpers to
8
+ * produce the handler it passes to its native callback prop. A component with no
9
+ * binding behaves exactly as today (the no-op dispatch is inert).
10
+ */
11
+ import { useCallback } from "react";
12
+ import { useEchoEvents } from "./context.js";
13
+ import type { EthDispatchData, EthEventBinding } from "./types.js";
14
+
15
+ /**
16
+ * Build an `onSelect` / `onClick`-shaped handler from an {@link EthEventBinding}.
17
+ *
18
+ * Returns `undefined` when `binding` is absent, so a component can spread it into
19
+ * `EthAction.onSelect` and fall back to its own default when unbound:
20
+ *
21
+ * ```tsx
22
+ * const onSelect = useEthAction(action.binding);
23
+ * <Button onClick={onSelect}>{action.label}</Button>
24
+ * // or, for EthAction:
25
+ * { ...action, onSelect: useEthAction(action.binding) ?? action.onSelect }
26
+ * ```
27
+ *
28
+ * The optional `data` (current row / route params) is threaded into the dispatch
29
+ * so the host can resolve `field:` / `route:` payloads — matching the registry's
30
+ * `DispatchDataContext`.
31
+ */
32
+ export function useEthAction(
33
+ binding: EthEventBinding | undefined,
34
+ data?: EthDispatchData,
35
+ ): (() => void) | undefined {
36
+ const { dispatch } = useEchoEvents();
37
+ return useCallback(() => {
38
+ if (!binding) return;
39
+ void dispatch(binding, undefined, data);
40
+ }, [dispatch, binding, data]);
41
+ }
42
+
43
+ /**
44
+ * Build a payload-carrying handler from an {@link EthEventBinding} — for events
45
+ * that produce live data (form `onSubmit` values, the selected row, the chosen
46
+ * `rowAction` id). The returned function forwards whatever it is called with as
47
+ * the dispatch `payload`, so the host can merge it into the binding's actions.
48
+ *
49
+ * Returns a no-op (still callable) when `binding` is absent, so callers don't
50
+ * need to null-check at the call site.
51
+ *
52
+ * ```tsx
53
+ * const onSubmit = useEthEventHandler(binding);
54
+ * <form onSubmit={(e) => { e.preventDefault(); onSubmit(readFormValues(e)); }} />
55
+ * ```
56
+ */
57
+ export function useEthEventHandler<P = unknown>(
58
+ binding: EthEventBinding | undefined,
59
+ data?: EthDispatchData,
60
+ ): (payload?: P) => void {
61
+ const { dispatch } = useEchoEvents();
62
+ return useCallback(
63
+ (payload?: P) => {
64
+ if (!binding) return;
65
+ void dispatch(binding, payload, data);
66
+ },
67
+ [dispatch, binding, data],
68
+ );
69
+ }
package/src/index.ts ADDED
@@ -0,0 +1,42 @@
1
+ /**
2
+ * `@echothink-ui/events` — public API.
3
+ *
4
+ * The backend-event contract for `@echothink-ui` components: a serializable
5
+ * {@link EthEventBinding} (structurally compatible with the echothink-fugue
6
+ * registry `EventBinding`), an {@link EchoEventProvider} the host uses to inject
7
+ * a backend `dispatch`, a {@link useEchoEvents} hook, and component-facing
8
+ * {@link useEthAction} / {@link useEthEventHandler} helpers that wire rich
9
+ * semantic callbacks (`EthAction.onSelect`, form `onSubmit`, table `rowActions`)
10
+ * to that dispatch. The {@link toRegistryEventBinding} bridge adapts the
11
+ * authoring shape to the registry action-graph shape at the host edge.
12
+ */
13
+ export type {
14
+ EthEventName,
15
+ EthActionTarget,
16
+ EthActionKind,
17
+ EthAction,
18
+ EthRunProcessAction,
19
+ EthNavigateAction,
20
+ EthSetStateAction,
21
+ EthOpenModalAction,
22
+ EthCloseModalAction,
23
+ EthShowToastAction,
24
+ EthEventBinding,
25
+ EthDispatchData,
26
+ EchoEventDispatch,
27
+ } from "./types.js";
28
+
29
+ export {
30
+ EchoEventProvider,
31
+ useEchoEvents,
32
+ type EchoEventContextValue,
33
+ } from "./context.js";
34
+
35
+ export { useEthAction, useEthEventHandler } from "./hooks.js";
36
+
37
+ export {
38
+ toRegistryAction,
39
+ toRegistryEventBinding,
40
+ type RegistryActionNode,
41
+ type RegistryEventBinding,
42
+ } from "./bridge.js";
package/src/types.ts ADDED
@@ -0,0 +1,177 @@
1
+ /**
2
+ * `@echothink-ui/events` — the BACKEND-EVENT CONTRACT for `@echothink-ui`
3
+ * components.
4
+ *
5
+ * `@echothink-ui` components expose plain React callbacks (`onClick`,
6
+ * `onSelect`, `EthAction.onSelect`, form `onSubmit`, table `rowActions`) with no
7
+ * path to a backend process/action. This module defines the serializable
8
+ * data shape — {@link EthEventBinding} + {@link EthAction} — that lets a
9
+ * builder-authored page declare "when this UI event fires, run these backend
10
+ * actions", plus the {@link EchoEventDispatch} contract a HOST injects to
11
+ * actually carry those actions to a backend (ultimately `runProcess` → gateway).
12
+ *
13
+ * STRUCTURAL COMPATIBILITY (the load-bearing constraint): these types are kept
14
+ * field-for-field compatible with the echothink-fugue registry's `EventBinding`
15
+ * / `ActionNode` contract (see `echothink-component-registry/.../events.tsx` and
16
+ * `echothink-app-renderer/.../logic/graph.ts`). A plain `EthEventBinding[]`
17
+ * array produced here flows through that registry's `eventsAware` HOC →
18
+ * `PageInteractionRuntime.dispatch` → `runActionGraph` → `runProcess` WITHOUT
19
+ * conversion. We deliberately DO NOT import the registry types (that would
20
+ * invert the dependency direction — the registry/renderer depend on the UI
21
+ * library, never the reverse); we mirror the shapes locally instead.
22
+ */
23
+
24
+ /* -------------------------------------------------------------------------- */
25
+ /* Event names (mirror the registry `EventBinding["event"]` union) */
26
+ /* -------------------------------------------------------------------------- */
27
+
28
+ /**
29
+ * The known UI event names. Kept open (`string & {}`) so custom / semantic
30
+ * events (e.g. `"onRowAction:archive"`) flow through, mirroring the registry's
31
+ * `EventName`.
32
+ */
33
+ export type EthEventName =
34
+ | "onClick"
35
+ | "onSelect"
36
+ | "onSubmit"
37
+ | "onChange"
38
+ | "onLoad"
39
+ // eslint-disable-next-line @typescript-eslint/ban-types
40
+ | (string & {});
41
+
42
+ /* -------------------------------------------------------------------------- */
43
+ /* Action targets / payload */
44
+ /* -------------------------------------------------------------------------- */
45
+
46
+ /** Optimistic-process target reference (the entity the process acts on). */
47
+ export interface EthActionTarget {
48
+ entityType?: string;
49
+ entityId?: string;
50
+ }
51
+
52
+ /**
53
+ * The action discriminant — the verbs the host runtime knows how to perform.
54
+ * Kept identical to the registry `ActionNode["action"]` vocabulary (the subset a
55
+ * UI component realistically authors); unknown kinds are tolerated by the
56
+ * runtime, so this stays forward-compatible.
57
+ */
58
+ export type EthActionKind =
59
+ | "runProcess"
60
+ | "navigate"
61
+ | "setState"
62
+ | "openModal"
63
+ | "closeModal"
64
+ | "showToast";
65
+
66
+ /** Invoke a unit process through the host runtime (ultimately the gateway). */
67
+ export interface EthRunProcessAction {
68
+ kind: "runProcess";
69
+ /** The unit-process name to invoke. */
70
+ process: string;
71
+ target?: EthActionTarget;
72
+ /** Inline payload for the process. May contain `field:` / `route:` refs. */
73
+ payload?: Record<string, unknown>;
74
+ expectedVersion?: number;
75
+ /** Hint that the UI applied an optimistic change before this ran. */
76
+ optimistic?: boolean;
77
+ }
78
+
79
+ /** Client-side navigation to a route, with optional params. */
80
+ export interface EthNavigateAction {
81
+ kind: "navigate";
82
+ /** Target route / href. */
83
+ target: string;
84
+ params?: Record<string, string>;
85
+ }
86
+
87
+ /** Set a page-scoped state key to a value. */
88
+ export interface EthSetStateAction {
89
+ kind: "setState";
90
+ /** State key to set. */
91
+ target: string;
92
+ value?: unknown;
93
+ }
94
+
95
+ /** Open a modal by id. */
96
+ export interface EthOpenModalAction {
97
+ kind: "openModal";
98
+ /** Modal id to open. */
99
+ target: string;
100
+ }
101
+
102
+ /** Close a modal by id. */
103
+ export interface EthCloseModalAction {
104
+ kind: "closeModal";
105
+ /** Modal id to close. */
106
+ target: string;
107
+ }
108
+
109
+ /** Surface a transient toast notification. */
110
+ export interface EthShowToastAction {
111
+ kind: "showToast";
112
+ /** Toast message (the `target` mirrors `message` for field uniformity). */
113
+ message: string;
114
+ level?: "info" | "success" | "error";
115
+ }
116
+
117
+ /** The action union an {@link EthEventBinding} carries (discriminant: `kind`). */
118
+ export type EthAction =
119
+ | EthRunProcessAction
120
+ | EthNavigateAction
121
+ | EthSetStateAction
122
+ | EthOpenModalAction
123
+ | EthCloseModalAction
124
+ | EthShowToastAction;
125
+
126
+ /* -------------------------------------------------------------------------- */
127
+ /* The binding */
128
+ /* -------------------------------------------------------------------------- */
129
+
130
+ /**
131
+ * An event → action(s) binding attached to a component (the value a builder
132
+ * authors). When the named `event` fires the host runs `actions` in order.
133
+ *
134
+ * Structurally compatible with the registry `EventBinding` — see the conversion
135
+ * note in {@link toRegistryEventBinding}: `event` is the same field; the
136
+ * registry calls its action list `actions` with an `action` discriminant, while
137
+ * we use `kind` for ergonomics. {@link toRegistryEventBinding} renames the two
138
+ * fields (`kind`→`action`, `payload`→`externalInput`, `target`→`route`/`modalId`/`key`)
139
+ * so a host that speaks the registry contract can adapt at the edge.
140
+ */
141
+ export interface EthEventBinding {
142
+ /** The UI event this binding reacts to. */
143
+ event: EthEventName;
144
+ /** The ordered backend/client actions to run when `event` fires. */
145
+ actions: EthAction[];
146
+ }
147
+
148
+ /* -------------------------------------------------------------------------- */
149
+ /* Dispatch contract */
150
+ /* -------------------------------------------------------------------------- */
151
+
152
+ /**
153
+ * Optional dispatch-time data context, structurally compatible with the
154
+ * registry's `DispatchDataContext` / `EventDispatchData` — `row` is the current
155
+ * data-binding row (for `field:` resolution), `route` the route params.
156
+ */
157
+ export interface EthDispatchData {
158
+ row?: Record<string, unknown>;
159
+ route?: Record<string, string>;
160
+ }
161
+
162
+ /**
163
+ * The capability a HOST injects (via {@link EchoEventProvider}) to carry a
164
+ * component's authored binding to the backend. `payload` is the live runtime
165
+ * data captured at the event (e.g. the form values, the selected row, the
166
+ * chosen action id). The host is responsible for merging `payload` into the
167
+ * binding's actions and running them (typically by adapting to the registry
168
+ * action graph → `runProcess` → gateway).
169
+ *
170
+ * Returns the host's promise so callers MAY await completion (e.g. to clear an
171
+ * optimistic flag); a fire-and-forget host returns `void`.
172
+ */
173
+ export type EchoEventDispatch = (
174
+ binding: EthEventBinding,
175
+ payload?: unknown,
176
+ data?: EthDispatchData,
177
+ ) => void | Promise<void>;