sfn-diagram-react 0.4.0 → 0.5.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 CHANGED
@@ -15,7 +15,7 @@ npm install sfn-diagram-react sfn-diagram
15
15
  pnpm add sfn-diagram-react sfn-diagram
16
16
  ```
17
17
 
18
- `react` and `sfn-diagram` are peer dependencies.
18
+ `react` and `sfn-diagram` are peer dependencies. `sfn-diagram >= 1.6.0` is required — earlier versions don't have the options this package forwards (`collapse`, `catchHandling`, `showVariables`, `generateHtml`, and `edgeOverrides`' qualified-id form).
19
19
 
20
20
  ## Usage
21
21
 
@@ -58,20 +58,160 @@ emphasized, and per-state duration and retry counts are annotated.
58
58
  `history` accepts a `GetExecutionHistory` events array, the raw command output, or a
59
59
  JSON string of either. In the browser, pass the JSON string.
60
60
 
61
+ `history` and `before` (diff mode, below) cannot be combined — core has no API for
62
+ overlaying an execution onto a diff. Combining them calls `onError` and renders `null`,
63
+ the same way any other invalid input is handled.
64
+
65
+ **`format="mermaid"` combined with `history` ignores every other diagram option** —
66
+ `catchHandling`, `collapse`, `edgeStyle`, `showVariables`, and the rest. Core's
67
+ Mermaid execution-overlay function (`generateMermaidExecution`) accepts only the ASL
68
+ definition, `history`, `layout`, and `theme`; unlike the plain Mermaid path
69
+ (`generateMermaid`), which honours the full option set. The SVG execution overlay is
70
+ unaffected — it honours every option.
71
+
72
+ ### Interactive viewer (`format="html"`)
73
+
74
+ Set `format="html"` to render the full interactive pan/zoom/search viewer instead of
75
+ static markup. Since embedding a complete HTML *document* is only possible in an
76
+ `<iframe>`, that's what this renders — as a sandboxed `srcDoc`, not a same-origin embed.
77
+ Consequences worth knowing:
78
+
79
+ - Page CSS does not reach the iframe's content — style it via the diagram options
80
+ (`theme`, `customColors` are not exposed by this package yet — file an issue if you
81
+ need them) rather than page-level CSS.
82
+ - The iframe has no intrinsic height; size it yourself (`style={{ height: 600 }}`, or a
83
+ wrapper with a fixed aspect ratio).
84
+ - `onStateClick` does not fire from inside it — clicks inside a sandboxed iframe never
85
+ reach the parent document's event handlers.
86
+ - `history` cannot be combined with `format="html"` — core's `generateHtml` accepts
87
+ neither `history` nor a diff. Combining them calls `onError` and renders `null`.
88
+
89
+ ```tsx
90
+ <SfnDiagram definition={definition} format="html" style={{ height: 600, width: '100%' }} />
91
+ ```
92
+
93
+ Give the iframe a `title` (defaults to `'Step Functions diagram'`) when a page renders
94
+ more than one, so screen readers can tell them apart.
95
+
96
+ ### Diff mode
97
+
98
+ Pass `before` to render a diff instead of a plain diagram: `definition` becomes the
99
+ *after* side and `before` the original side. Added, modified, and removed states are
100
+ coloured (green / yellow / red) instead of the machine's normal styling.
101
+
102
+ ```tsx
103
+ <SfnDiagram before={previousDefinition} definition={definition} />
104
+ ```
105
+
106
+ - `before` accepts an ASL object or a JSON string, same as `definition`.
107
+ - `before` cannot be combined with `history` or `format="html"` — core has no diff API
108
+ for either combination. Combining them calls `onError` and renders `null`.
109
+ - The **SVG diff** (`generateDiff`) honours every diagram option this package forwards
110
+ (`collapse`, `catchHandling`, `showIcons`, and the rest).
111
+ - The **Mermaid diff** (`generateMermaidDiff`) accepts only the two definitions — every
112
+ other prop (`layout`, `theme`, `collapse`, …) is silently ignored in `format="mermaid"`
113
+ diff mode.
114
+
115
+ ### `onStateClick`
116
+
117
+ Fires when a state node is clicked, SVG format only:
118
+
119
+ ```tsx
120
+ <SfnDiagram
121
+ definition={definition}
122
+ onStateClick={({ stateId }) => console.log('clicked', stateId)}
123
+ />
124
+ ```
125
+
126
+ - Not invoked for `format="mermaid"` (no clickable elements) or `format="html"` (the
127
+ sandboxed iframe's clicks never reach the parent document).
128
+ - `stateId` is the graph node id, which equals the state name for every state whose
129
+ name is unique across the machine. A nested state whose name repeats elsewhere in the
130
+ machine gets an id qualified by its scope — read it off the rendered `data-state-id`
131
+ attribute rather than assuming the bare name.
132
+ - No click listener is attached at all when `onStateClick` is omitted.
133
+
134
+ ### Imperative handle
135
+
136
+ `SfnDiagram` forwards a `ref` exposing `{ getSvg(): string | null }` — the rendered SVG
137
+ markup for `format="svg"`, or `null` for every other format (including an errored
138
+ render):
139
+
140
+ ```tsx
141
+ const ref = useRef<SfnDiagramHandle>(null)
142
+ <SfnDiagram definition={definition} ref={ref} />
143
+ // later: ref.current?.getSvg()
144
+ ```
145
+
146
+ There is no programmatic zoom/pan control — core doesn't expose the interactive
147
+ viewer's controller from a public subpath yet, so a meaningful pan/zoom handle needs a
148
+ core change first.
149
+
150
+ ### `useSfnDiagram`
151
+
152
+ For the raw output — the SVG/HTML string, Mermaid code, dimensions, and metadata —
153
+ without mounting anything, use the hook `<SfnDiagram>` is built on:
154
+
155
+ ```tsx
156
+ import { useSfnDiagram } from 'sfn-diagram-react'
157
+
158
+ function DownloadSvgButton({ definition }: { definition: object }) {
159
+ const result = useSfnDiagram({ definition })
160
+ if (result.type !== 'svg') return null
161
+
162
+ const href = `data:image/svg+xml;base64,${btoa(result.svg)}`
163
+ return (
164
+ <a download="diagram.svg" href={href}>
165
+ Download SVG
166
+ </a>
167
+ )
168
+ }
169
+ ```
170
+
171
+ It accepts the same option-bearing props as `<SfnDiagram>` (`before`, `definition`,
172
+ `format`, `history`, plus every diagram option below) — just not the presentational
173
+ ones (`className`, `onError`, `onStateClick`, `style`, `title`). Errors are returned as
174
+ `{ type: 'error', error }` rather than thrown.
175
+
61
176
  ## Props
62
177
 
63
- | Prop | Type | Default | Description |
64
- | --- | --- | --- | --- |
65
- | `definition` | `object \| string` | — (required) | ASL definition as an object or JSON string |
66
- | `format` | `'svg' \| 'mermaid'` | `'svg'` | Render an SVG diagram or emit Mermaid code |
67
- | `history` | `HistoryEvent[] \| GetExecutionHistoryCommandOutput \| string` | — | Execution history; when set, renders an execution overlay |
68
- | `layout` | `'TB' \| 'LR' \| 'RL' \| 'BT'` | `'TB'` | Graph layout direction |
69
- | `theme` | `'light' \| 'dark' \| CustomTheme` | `'light'` | Diagram theme |
70
- | `onError` | `(error: Error) => void` | — | Called when the definition fails to parse/render |
71
- | `className` | `string` | — | Applied to the wrapper element |
72
- | `style` | `React.CSSProperties` | — | Applied to the wrapper element |
73
-
74
- When the definition is invalid the component renders `null` and (if provided) calls `onError`.
178
+ | Prop | Type | Default | Applies to | Description |
179
+ | --- | --- | --- | --- | --- |
180
+ | `before` | `object \| string` | — | svg, mermaid | The *original* side of a diff; `definition` becomes the *after* side. See [Diff mode](#diff-mode). |
181
+ | `catchHandling` | `'hide' \| 'show'` | `'show'` | svg, mermaid¹ | `'hide'` drops Catch error edges and handler-only nodes. |
182
+ | `className` | `string` | — | all | Applied to the wrapper element (`div`, `pre`, or `iframe`). |
183
+ | `collapse` | `string[] \| boolean` | — | svg, mermaid¹ | Collapse Parallel/Map containers into a placeholder node. `true` collapses every container; an array collapses only the named ones. |
184
+ | `definition` | `object \| string` | — (required) | all | ASL definition as an object or JSON string. |
185
+ | `edgeOverrides` | `Record<string, EdgeStyleOverride>` | — | svg only | Per-edge style overrides keyed by `GraphEdge.id` (preferred, e.g. `Route->Work#choice#1`) or the legacy bare `${from}->${to}` (broad-matches every edge between that pair). |
186
+ | `edgeStyle` | `'straight' \| 'curved' \| 'orthogonal'` | `'curved'` | svg, mermaid¹ | Edge path rendering style. |
187
+ | `format` | `'svg' \| 'mermaid' \| 'html'` | `'svg'` | — | Output format. See [Interactive viewer](#interactive-viewer-formathtml) for `'html'`. |
188
+ | `history` | `HistoryEvent[] \| GetExecutionHistoryCommandOutput \| string` | — | svg, mermaid | Execution history; when set, renders an execution overlay. See [Execution overlay](#execution-overlay). |
189
+ | `iconPosition` | `'left' \| 'top' \| 'right'` | `'left'` | svg only | Position of AWS service icons relative to the node label. |
190
+ | `iconSize` | `number` | `24` | svg only | Size of AWS service icons in pixels. |
191
+ | `layout` | `'TB' \| 'LR' \| 'RL' \| 'BT'` | `'TB'` | svg, mermaid | Graph layout direction. |
192
+ | `nodeOverrides` | `Record<string, Partial<NodeStyle>>` | — | svg only | Per-node style overrides keyed by graph node id. |
193
+ | `onError` | `(error: Error) => void` | — | all | Called when the definition fails to parse/render. |
194
+ | `onStateClick` | `(params: { event, stateId }) => void` | — | svg only | See [`onStateClick`](#onstateclick). |
195
+ | `showIcons` | `boolean` | `false` | svg only | Whether to display AWS service icons on Task state nodes. |
196
+ | `showVariables` | `boolean` | `true` | svg, mermaid¹ | Whether to annotate nodes with the variables they assign via ASL `Assign`. |
197
+ | `style` | `React.CSSProperties` | — | all | Applied to the wrapper element. |
198
+ | `theme` | `'light' \| 'dark' \| CustomTheme` | `'light'` | svg, mermaid | Diagram theme. |
199
+ | `title` | `string` | `'Step Functions diagram'` | html only | Accessible title for the rendered iframe. |
200
+
201
+ ¹ Ignored when `format="mermaid"` is combined with `history` — see
202
+ [Execution overlay](#execution-overlay). Applies normally to plain `format="mermaid"`
203
+ (no `history`). `format="mermaid"` diff mode (the `before` prop) is a separate
204
+ carve-out with its own rule — see [Diff mode](#diff-mode).
205
+
206
+ When the definition is invalid the component renders `null` and (if provided) calls
207
+ `onError`.
208
+
209
+ **Memoize object-valued props.** `collapse` (as an array), `nodeOverrides`,
210
+ `edgeOverrides`, and a `CustomTheme` object for `theme` are all things a consumer might
211
+ inline in JSX. An inline object or array is referentially unstable — a different
212
+ reference on every render — which re-triggers this component's internal memo and a
213
+ full re-layout each time, even though nothing actually changed. Hoist them to a
214
+ constant or wrap them in `useMemo` if you re-render often.
75
215
 
76
216
  ## License
77
217
 
package/dist/index.cjs CHANGED
@@ -1,59 +1,200 @@
1
+ 'use client'
1
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
3
  let react = require("react");
3
4
  let sfn_diagram = require("sfn-diagram");
4
5
  let react_jsx_runtime = require("react/jsx-runtime");
5
- //#region src/SfnDiagram.tsx
6
- function SfnDiagram({ className, definition, format = "svg", history, layout = "TB", onError, style, theme = "light" }) {
7
- const asl = (0, react.useMemo)(() => typeof definition === "string" ? definition : JSON.stringify(definition), [definition]);
8
- const result = (0, react.useMemo)(() => {
6
+ //#region src/useSfnDiagram.ts
7
+ function toAslString(params) {
8
+ const { definition } = params;
9
+ return typeof definition === "string" ? definition : JSON.stringify(definition);
10
+ }
11
+ function omitUndefinedValues(options) {
12
+ const result = {};
13
+ for (const key of Object.keys(options)) if (options[key] !== void 0) result[key] = options[key];
14
+ return result;
15
+ }
16
+ /**
17
+ * Generate a Step Functions diagram and return the raw output - SVG or HTML
18
+ * markup, Mermaid code, dimensions, and metadata - without rendering it.
19
+ *
20
+ * This is the same generation logic `<SfnDiagram>` runs internally, exposed
21
+ * directly for a consumer who needs the underlying string or metadata rather
22
+ * than mounted markup - for example, building a data URI for `<img src>`, a
23
+ * download button, or a custom render target.
24
+ *
25
+ * @param params - The same option-bearing props `<SfnDiagram>` accepts, minus
26
+ * the presentational ones (`className`, `onError`, `onStateClick`, `style`,
27
+ * `title`).
28
+ * @returns A {@link SfnDiagramResult} discriminated union. Errors are returned
29
+ * as `{ type: 'error', error }` rather than thrown.
30
+ *
31
+ * @example
32
+ * ```tsx
33
+ * function DownloadSvgButton({ definition }: { definition: object }) {
34
+ * const result = useSfnDiagram({ definition })
35
+ * if (result.type !== 'svg') return null
36
+ *
37
+ * const href = `data:image/svg+xml;base64,${btoa(result.svg)}`
38
+ * return <a download="diagram.svg" href={href}>Download SVG</a>
39
+ * }
40
+ * ```
41
+ */
42
+ function useSfnDiagram(params) {
43
+ const { before, catchHandling, collapse, definition, edgeOverrides, edgeStyle, format = "svg", history, iconPosition, iconSize, layout = "TB", nodeOverrides, showIcons, showVariables, theme = "light" } = params;
44
+ const asl = (0, react.useMemo)(() => toAslString({ definition }), [definition]);
45
+ const beforeAsl = (0, react.useMemo)(() => before === void 0 ? void 0 : toAslString({ definition: before }), [before]);
46
+ return (0, react.useMemo)(() => {
9
47
  try {
10
- if (format === "mermaid") return {
11
- type: "mermaid",
12
- code: (history ? (0, sfn_diagram.generateMermaidExecution)({
48
+ const diagramOptions = omitUndefinedValues({
49
+ catchHandling,
50
+ collapse,
51
+ edgeOverrides,
52
+ edgeStyle,
53
+ iconPosition,
54
+ iconSize,
55
+ layout,
56
+ nodeOverrides,
57
+ showIcons,
58
+ showVariables,
59
+ theme
60
+ });
61
+ if (beforeAsl !== void 0) {
62
+ if (history) throw new Error("history and before cannot be combined");
63
+ if (format === "html") throw new Error("before and format=\"html\" cannot be combined");
64
+ if (format === "mermaid") {
65
+ const output = (0, sfn_diagram.generateMermaidDiff)({
66
+ after: asl,
67
+ before: beforeAsl
68
+ });
69
+ return {
70
+ code: output.code,
71
+ metadata: output.metadata,
72
+ type: "mermaid"
73
+ };
74
+ }
75
+ const output = (0, sfn_diagram.generateDiff)({
76
+ after: asl,
77
+ before: beforeAsl,
78
+ ...diagramOptions
79
+ });
80
+ return {
81
+ height: output.height,
82
+ metadata: output.metadata,
83
+ svg: output.svg,
84
+ type: "svg",
85
+ width: output.width
86
+ };
87
+ }
88
+ if (format === "html") {
89
+ if (history) throw new Error("history and format=\"html\" cannot be combined");
90
+ const output = (0, sfn_diagram.generateHtml)({
13
91
  aslDefinition: asl,
14
- history
15
- }) : (0, sfn_diagram.generateMermaid)({ aslDefinition: asl })).code
16
- };
17
- return {
18
- type: "svg",
19
- svg: (history ? (0, sfn_diagram.generateExecution)({
92
+ ...diagramOptions
93
+ });
94
+ return {
95
+ height: output.height,
96
+ html: output.html,
97
+ metadata: output.metadata,
98
+ type: "html",
99
+ width: output.width
100
+ };
101
+ }
102
+ if (format === "mermaid") {
103
+ const output = history ? (0, sfn_diagram.generateMermaidExecution)({
20
104
  aslDefinition: asl,
21
105
  history,
22
- layout,
23
- theme
24
- }) : (0, sfn_diagram.generateSvg)({
106
+ ...diagramOptions
107
+ }) : (0, sfn_diagram.generateMermaid)({
25
108
  aslDefinition: asl,
26
- layout,
27
- theme
28
- })).svg
109
+ ...diagramOptions
110
+ });
111
+ return {
112
+ code: output.code,
113
+ metadata: output.metadata,
114
+ type: "mermaid"
115
+ };
116
+ }
117
+ const output = history ? (0, sfn_diagram.generateExecution)({
118
+ aslDefinition: asl,
119
+ history,
120
+ ...diagramOptions
121
+ }) : (0, sfn_diagram.generateSvg)({
122
+ aslDefinition: asl,
123
+ ...diagramOptions
124
+ });
125
+ return {
126
+ height: output.height,
127
+ metadata: output.metadata,
128
+ svg: output.svg,
129
+ type: "svg",
130
+ width: output.width
29
131
  };
30
132
  } catch (err) {
31
133
  return {
32
- type: "error",
33
- error: err instanceof Error ? err : new Error(String(err))
134
+ error: err instanceof Error ? err : new Error(String(err)),
135
+ type: "error"
34
136
  };
35
137
  }
36
138
  }, [
37
139
  asl,
140
+ beforeAsl,
141
+ catchHandling,
142
+ collapse,
143
+ edgeOverrides,
144
+ edgeStyle,
38
145
  format,
39
146
  history,
147
+ iconPosition,
148
+ iconSize,
40
149
  layout,
150
+ nodeOverrides,
151
+ showIcons,
152
+ showVariables,
41
153
  theme
42
154
  ]);
43
- if (result.type === "error") {
155
+ }
156
+ //#endregion
157
+ //#region src/SfnDiagram.tsx
158
+ const SfnDiagram = (0, react.forwardRef)(function SfnDiagram({ className, onError, onStateClick, style, title = "Step Functions diagram", ...diagramParams }, ref) {
159
+ const result = useSfnDiagram(diagramParams);
160
+ (0, react.useImperativeHandle)(ref, () => ({ getSvg: () => result.type === "svg" ? result.svg : null }), [result]);
161
+ const handleStateClick = (0, react.useCallback)((event) => {
162
+ const stateId = (event.target instanceof Element ? event.target : null)?.closest("[data-state-id]")?.getAttribute("data-state-id");
163
+ if (stateId) onStateClick?.({
164
+ event,
165
+ stateId
166
+ });
167
+ }, [onStateClick]);
168
+ const reportedResult = (0, react.useRef)(null);
169
+ (0, react.useEffect)(() => {
170
+ if (result.type !== "error") {
171
+ reportedResult.current = null;
172
+ return;
173
+ }
174
+ if (reportedResult.current === result) return;
175
+ reportedResult.current = result;
44
176
  onError?.(result.error);
45
- return null;
46
- }
177
+ }, [onError, result]);
178
+ if (result.type === "error") return null;
47
179
  if (result.type === "mermaid") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
48
180
  className,
49
181
  style,
50
182
  children: result.code
51
183
  });
184
+ if (result.type === "html") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("iframe", {
185
+ className,
186
+ sandbox: "allow-scripts",
187
+ srcDoc: result.html,
188
+ style,
189
+ title
190
+ });
52
191
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
53
192
  className,
54
193
  dangerouslySetInnerHTML: { __html: result.svg },
194
+ onClick: onStateClick ? handleStateClick : void 0,
55
195
  style
56
196
  });
57
- }
197
+ });
58
198
  //#endregion
59
199
  exports.SfnDiagram = SfnDiagram;
200
+ exports.useSfnDiagram = useSfnDiagram;
package/dist/index.d.cts CHANGED
@@ -1,33 +1,124 @@
1
- import * as _$react from "react";
2
- import { ExecutionHistoryInput, LayoutDirection, ThemeOption } from "sfn-diagram";
3
1
 
4
- //#region src/SfnDiagram.d.ts
5
- interface SfnDiagramProps {
6
- className?: string;
2
+ import { DiagramOptions, DiffOutput, ExecutionHistoryInput, ExecutionOutput, HtmlOutput, MermaidDiffOutput, MermaidExecutionOutput, MermaidOutput, SvgOutput } from "sfn-diagram";
3
+ //#region src/useSfnDiagram.d.ts
4
+ interface UseSfnDiagramParams extends Pick<DiagramOptions, 'catchHandling' | 'collapse' | 'edgeOverrides' | 'edgeStyle' | 'iconPosition' | 'iconSize' | 'layout' | 'nodeOverrides' | 'showIcons' | 'showVariables' | 'theme'> {
5
+ /**
6
+ * When set, `definition` is treated as the *after* side of a diff and `before`
7
+ * as the *original* side: the rendered diagram highlights added, modified, and
8
+ * removed states rather than the plain machine. Accepts an ASL object or a
9
+ * JSON string. Cannot be combined with `history` or `format="html"` - core has
10
+ * no diff API for either combination.
11
+ *
12
+ * The Mermaid diff (`generateMermaidDiff`) ignores every option in this
13
+ * type's `Pick<DiagramOptions, ...>` list - `layout`, `theme`, and the rest -
14
+ * since core's diff-specific function accepts only the two definitions. The
15
+ * SVG diff honours them.
16
+ */
17
+ before?: object | string;
7
18
  definition: object | string;
8
- format?: 'mermaid' | 'svg';
19
+ /**
20
+ * Output format. `'svg'` and `'mermaid'` render static markup; `'html'` renders
21
+ * the interactive pan/zoom/search viewer as a self-contained HTML document.
22
+ * Cannot be combined with `history`.
23
+ * @default 'svg'
24
+ */
25
+ format?: 'html' | 'mermaid' | 'svg';
9
26
  /**
10
27
  * Optional execution history. When provided, the diagram is rendered as an
11
28
  * execution overlay: states are coloured by outcome, the taken path is
12
29
  * emphasized, and per-state duration / retry counts are annotated.
13
30
  * Accepts a GetExecutionHistory events array, the raw command output, or a
14
- * JSON string of either.
31
+ * JSON string of either. Cannot be combined with `format="html"`.
15
32
  */
16
33
  history?: ExecutionHistoryInput;
17
- layout?: LayoutDirection;
34
+ }
35
+ /** Result of {@link useSfnDiagram} - the raw output of whichever core function the params select. */
36
+ type SfnDiagramResult = {
37
+ error: Error;
38
+ type: 'error';
39
+ } | {
40
+ height: number;
41
+ html: string;
42
+ metadata: HtmlOutput['metadata'];
43
+ type: 'html';
44
+ width: number;
45
+ } | {
46
+ code: string;
47
+ metadata: MermaidDiffOutput['metadata'] | MermaidExecutionOutput['metadata'] | MermaidOutput['metadata'];
48
+ type: 'mermaid';
49
+ } | {
50
+ height: number;
51
+ metadata: DiffOutput['metadata'] | ExecutionOutput['metadata'] | SvgOutput['metadata'];
52
+ svg: string;
53
+ type: 'svg';
54
+ width: number;
55
+ };
56
+ /**
57
+ * Generate a Step Functions diagram and return the raw output - SVG or HTML
58
+ * markup, Mermaid code, dimensions, and metadata - without rendering it.
59
+ *
60
+ * This is the same generation logic `<SfnDiagram>` runs internally, exposed
61
+ * directly for a consumer who needs the underlying string or metadata rather
62
+ * than mounted markup - for example, building a data URI for `<img src>`, a
63
+ * download button, or a custom render target.
64
+ *
65
+ * @param params - The same option-bearing props `<SfnDiagram>` accepts, minus
66
+ * the presentational ones (`className`, `onError`, `onStateClick`, `style`,
67
+ * `title`).
68
+ * @returns A {@link SfnDiagramResult} discriminated union. Errors are returned
69
+ * as `{ type: 'error', error }` rather than thrown.
70
+ *
71
+ * @example
72
+ * ```tsx
73
+ * function DownloadSvgButton({ definition }: { definition: object }) {
74
+ * const result = useSfnDiagram({ definition })
75
+ * if (result.type !== 'svg') return null
76
+ *
77
+ * const href = `data:image/svg+xml;base64,${btoa(result.svg)}`
78
+ * return <a download="diagram.svg" href={href}>Download SVG</a>
79
+ * }
80
+ * ```
81
+ */
82
+ export declare function useSfnDiagram(params: UseSfnDiagramParams): SfnDiagramResult;
83
+ //#endregion
84
+ //#region src/SfnDiagram.d.ts
85
+ interface SfnDiagramProps extends UseSfnDiagramParams {
86
+ className?: string;
18
87
  onError?: (error: Error) => void;
88
+ /**
89
+ * Called when a state node is clicked. SVG format only - a click inside the
90
+ * sandboxed `format="html"` iframe does not reach this handler, and Mermaid
91
+ * output has no clickable elements. `stateId` is the graph node id, which
92
+ * equals the state name for every state whose name is unique across the
93
+ * machine; for a nested state whose name repeats elsewhere in the machine the
94
+ * id is qualified by its scope, so read it off the rendered `data-state-id`
95
+ * rather than assuming the bare name.
96
+ */
97
+ onStateClick?: (params: OnStateClickParams) => void;
19
98
  style?: React.CSSProperties;
20
- theme?: ThemeOption;
99
+ /**
100
+ * Accessible title for the rendered iframe when `format` is `'html'`. Ignored
101
+ * for every other format. Set this when a page renders more than one diagram
102
+ * so screen readers can distinguish between them.
103
+ * @default 'Step Functions diagram'
104
+ */
105
+ title?: string;
106
+ }
107
+ /** Parameters passed to {@link SfnDiagramProps.onStateClick}. */
108
+ interface OnStateClickParams {
109
+ event: React.MouseEvent<HTMLDivElement>;
110
+ stateId: string;
111
+ }
112
+ /**
113
+ * Imperative handle exposed via `ref` on {@link SfnDiagram}. There is no
114
+ * programmatic zoom/pan control - core has no public API for it yet (the
115
+ * interactive viewer's controller is not exported from a public subpath) -
116
+ * this handle only surfaces the rendered markup.
117
+ */
118
+ interface SfnDiagramHandle {
119
+ /** The rendered SVG markup for `format="svg"`, or `null` for every other format (including an errored render). */
120
+ getSvg(): string | null;
21
121
  }
22
- declare function SfnDiagram({
23
- className,
24
- definition,
25
- format,
26
- history,
27
- layout,
28
- onError,
29
- style,
30
- theme
31
- }: SfnDiagramProps): _$react.JSX.Element | null;
122
+ export declare const SfnDiagram: import("react").ForwardRefExoticComponent<SfnDiagramProps & import("react").RefAttributes<SfnDiagramHandle>>;
32
123
  //#endregion
33
- export { SfnDiagram, type SfnDiagramProps };
124
+ export type { OnStateClickParams, SfnDiagramHandle, SfnDiagramProps, SfnDiagramResult, UseSfnDiagramParams };
package/dist/index.d.ts CHANGED
@@ -1,33 +1,124 @@
1
- import * as _$react from "react";
2
- import { ExecutionHistoryInput, LayoutDirection, ThemeOption } from "sfn-diagram";
3
1
 
4
- //#region src/SfnDiagram.d.ts
5
- interface SfnDiagramProps {
6
- className?: string;
2
+ import { DiagramOptions, DiffOutput, ExecutionHistoryInput, ExecutionOutput, HtmlOutput, MermaidDiffOutput, MermaidExecutionOutput, MermaidOutput, SvgOutput } from "sfn-diagram";
3
+ //#region src/useSfnDiagram.d.ts
4
+ interface UseSfnDiagramParams extends Pick<DiagramOptions, 'catchHandling' | 'collapse' | 'edgeOverrides' | 'edgeStyle' | 'iconPosition' | 'iconSize' | 'layout' | 'nodeOverrides' | 'showIcons' | 'showVariables' | 'theme'> {
5
+ /**
6
+ * When set, `definition` is treated as the *after* side of a diff and `before`
7
+ * as the *original* side: the rendered diagram highlights added, modified, and
8
+ * removed states rather than the plain machine. Accepts an ASL object or a
9
+ * JSON string. Cannot be combined with `history` or `format="html"` - core has
10
+ * no diff API for either combination.
11
+ *
12
+ * The Mermaid diff (`generateMermaidDiff`) ignores every option in this
13
+ * type's `Pick<DiagramOptions, ...>` list - `layout`, `theme`, and the rest -
14
+ * since core's diff-specific function accepts only the two definitions. The
15
+ * SVG diff honours them.
16
+ */
17
+ before?: object | string;
7
18
  definition: object | string;
8
- format?: 'mermaid' | 'svg';
19
+ /**
20
+ * Output format. `'svg'` and `'mermaid'` render static markup; `'html'` renders
21
+ * the interactive pan/zoom/search viewer as a self-contained HTML document.
22
+ * Cannot be combined with `history`.
23
+ * @default 'svg'
24
+ */
25
+ format?: 'html' | 'mermaid' | 'svg';
9
26
  /**
10
27
  * Optional execution history. When provided, the diagram is rendered as an
11
28
  * execution overlay: states are coloured by outcome, the taken path is
12
29
  * emphasized, and per-state duration / retry counts are annotated.
13
30
  * Accepts a GetExecutionHistory events array, the raw command output, or a
14
- * JSON string of either.
31
+ * JSON string of either. Cannot be combined with `format="html"`.
15
32
  */
16
33
  history?: ExecutionHistoryInput;
17
- layout?: LayoutDirection;
34
+ }
35
+ /** Result of {@link useSfnDiagram} - the raw output of whichever core function the params select. */
36
+ type SfnDiagramResult = {
37
+ error: Error;
38
+ type: 'error';
39
+ } | {
40
+ height: number;
41
+ html: string;
42
+ metadata: HtmlOutput['metadata'];
43
+ type: 'html';
44
+ width: number;
45
+ } | {
46
+ code: string;
47
+ metadata: MermaidDiffOutput['metadata'] | MermaidExecutionOutput['metadata'] | MermaidOutput['metadata'];
48
+ type: 'mermaid';
49
+ } | {
50
+ height: number;
51
+ metadata: DiffOutput['metadata'] | ExecutionOutput['metadata'] | SvgOutput['metadata'];
52
+ svg: string;
53
+ type: 'svg';
54
+ width: number;
55
+ };
56
+ /**
57
+ * Generate a Step Functions diagram and return the raw output - SVG or HTML
58
+ * markup, Mermaid code, dimensions, and metadata - without rendering it.
59
+ *
60
+ * This is the same generation logic `<SfnDiagram>` runs internally, exposed
61
+ * directly for a consumer who needs the underlying string or metadata rather
62
+ * than mounted markup - for example, building a data URI for `<img src>`, a
63
+ * download button, or a custom render target.
64
+ *
65
+ * @param params - The same option-bearing props `<SfnDiagram>` accepts, minus
66
+ * the presentational ones (`className`, `onError`, `onStateClick`, `style`,
67
+ * `title`).
68
+ * @returns A {@link SfnDiagramResult} discriminated union. Errors are returned
69
+ * as `{ type: 'error', error }` rather than thrown.
70
+ *
71
+ * @example
72
+ * ```tsx
73
+ * function DownloadSvgButton({ definition }: { definition: object }) {
74
+ * const result = useSfnDiagram({ definition })
75
+ * if (result.type !== 'svg') return null
76
+ *
77
+ * const href = `data:image/svg+xml;base64,${btoa(result.svg)}`
78
+ * return <a download="diagram.svg" href={href}>Download SVG</a>
79
+ * }
80
+ * ```
81
+ */
82
+ export declare function useSfnDiagram(params: UseSfnDiagramParams): SfnDiagramResult;
83
+ //#endregion
84
+ //#region src/SfnDiagram.d.ts
85
+ interface SfnDiagramProps extends UseSfnDiagramParams {
86
+ className?: string;
18
87
  onError?: (error: Error) => void;
88
+ /**
89
+ * Called when a state node is clicked. SVG format only - a click inside the
90
+ * sandboxed `format="html"` iframe does not reach this handler, and Mermaid
91
+ * output has no clickable elements. `stateId` is the graph node id, which
92
+ * equals the state name for every state whose name is unique across the
93
+ * machine; for a nested state whose name repeats elsewhere in the machine the
94
+ * id is qualified by its scope, so read it off the rendered `data-state-id`
95
+ * rather than assuming the bare name.
96
+ */
97
+ onStateClick?: (params: OnStateClickParams) => void;
19
98
  style?: React.CSSProperties;
20
- theme?: ThemeOption;
99
+ /**
100
+ * Accessible title for the rendered iframe when `format` is `'html'`. Ignored
101
+ * for every other format. Set this when a page renders more than one diagram
102
+ * so screen readers can distinguish between them.
103
+ * @default 'Step Functions diagram'
104
+ */
105
+ title?: string;
106
+ }
107
+ /** Parameters passed to {@link SfnDiagramProps.onStateClick}. */
108
+ interface OnStateClickParams {
109
+ event: React.MouseEvent<HTMLDivElement>;
110
+ stateId: string;
111
+ }
112
+ /**
113
+ * Imperative handle exposed via `ref` on {@link SfnDiagram}. There is no
114
+ * programmatic zoom/pan control - core has no public API for it yet (the
115
+ * interactive viewer's controller is not exported from a public subpath) -
116
+ * this handle only surfaces the rendered markup.
117
+ */
118
+ interface SfnDiagramHandle {
119
+ /** The rendered SVG markup for `format="svg"`, or `null` for every other format (including an errored render). */
120
+ getSvg(): string | null;
21
121
  }
22
- declare function SfnDiagram({
23
- className,
24
- definition,
25
- format,
26
- history,
27
- layout,
28
- onError,
29
- style,
30
- theme
31
- }: SfnDiagramProps): _$react.JSX.Element | null;
122
+ export declare const SfnDiagram: import("react").ForwardRefExoticComponent<SfnDiagramProps & import("react").RefAttributes<SfnDiagramHandle>>;
32
123
  //#endregion
33
- export { SfnDiagram, type SfnDiagramProps };
124
+ export type { OnStateClickParams, SfnDiagramHandle, SfnDiagramProps, SfnDiagramResult, UseSfnDiagramParams };
package/dist/index.js CHANGED
@@ -1,58 +1,198 @@
1
- import { useMemo } from "react";
2
- import { generateExecution, generateMermaid, generateMermaidExecution, generateSvg } from "sfn-diagram";
1
+ 'use client'
2
+ import { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef } from "react";
3
+ import { generateDiff, generateExecution, generateHtml, generateMermaid, generateMermaidDiff, generateMermaidExecution, generateSvg } from "sfn-diagram";
3
4
  import { jsx } from "react/jsx-runtime";
4
- //#region src/SfnDiagram.tsx
5
- function SfnDiagram({ className, definition, format = "svg", history, layout = "TB", onError, style, theme = "light" }) {
6
- const asl = useMemo(() => typeof definition === "string" ? definition : JSON.stringify(definition), [definition]);
7
- const result = useMemo(() => {
5
+ //#region src/useSfnDiagram.ts
6
+ function toAslString(params) {
7
+ const { definition } = params;
8
+ return typeof definition === "string" ? definition : JSON.stringify(definition);
9
+ }
10
+ function omitUndefinedValues(options) {
11
+ const result = {};
12
+ for (const key of Object.keys(options)) if (options[key] !== void 0) result[key] = options[key];
13
+ return result;
14
+ }
15
+ /**
16
+ * Generate a Step Functions diagram and return the raw output - SVG or HTML
17
+ * markup, Mermaid code, dimensions, and metadata - without rendering it.
18
+ *
19
+ * This is the same generation logic `<SfnDiagram>` runs internally, exposed
20
+ * directly for a consumer who needs the underlying string or metadata rather
21
+ * than mounted markup - for example, building a data URI for `<img src>`, a
22
+ * download button, or a custom render target.
23
+ *
24
+ * @param params - The same option-bearing props `<SfnDiagram>` accepts, minus
25
+ * the presentational ones (`className`, `onError`, `onStateClick`, `style`,
26
+ * `title`).
27
+ * @returns A {@link SfnDiagramResult} discriminated union. Errors are returned
28
+ * as `{ type: 'error', error }` rather than thrown.
29
+ *
30
+ * @example
31
+ * ```tsx
32
+ * function DownloadSvgButton({ definition }: { definition: object }) {
33
+ * const result = useSfnDiagram({ definition })
34
+ * if (result.type !== 'svg') return null
35
+ *
36
+ * const href = `data:image/svg+xml;base64,${btoa(result.svg)}`
37
+ * return <a download="diagram.svg" href={href}>Download SVG</a>
38
+ * }
39
+ * ```
40
+ */
41
+ function useSfnDiagram(params) {
42
+ const { before, catchHandling, collapse, definition, edgeOverrides, edgeStyle, format = "svg", history, iconPosition, iconSize, layout = "TB", nodeOverrides, showIcons, showVariables, theme = "light" } = params;
43
+ const asl = useMemo(() => toAslString({ definition }), [definition]);
44
+ const beforeAsl = useMemo(() => before === void 0 ? void 0 : toAslString({ definition: before }), [before]);
45
+ return useMemo(() => {
8
46
  try {
9
- if (format === "mermaid") return {
10
- type: "mermaid",
11
- code: (history ? generateMermaidExecution({
47
+ const diagramOptions = omitUndefinedValues({
48
+ catchHandling,
49
+ collapse,
50
+ edgeOverrides,
51
+ edgeStyle,
52
+ iconPosition,
53
+ iconSize,
54
+ layout,
55
+ nodeOverrides,
56
+ showIcons,
57
+ showVariables,
58
+ theme
59
+ });
60
+ if (beforeAsl !== void 0) {
61
+ if (history) throw new Error("history and before cannot be combined");
62
+ if (format === "html") throw new Error("before and format=\"html\" cannot be combined");
63
+ if (format === "mermaid") {
64
+ const output = generateMermaidDiff({
65
+ after: asl,
66
+ before: beforeAsl
67
+ });
68
+ return {
69
+ code: output.code,
70
+ metadata: output.metadata,
71
+ type: "mermaid"
72
+ };
73
+ }
74
+ const output = generateDiff({
75
+ after: asl,
76
+ before: beforeAsl,
77
+ ...diagramOptions
78
+ });
79
+ return {
80
+ height: output.height,
81
+ metadata: output.metadata,
82
+ svg: output.svg,
83
+ type: "svg",
84
+ width: output.width
85
+ };
86
+ }
87
+ if (format === "html") {
88
+ if (history) throw new Error("history and format=\"html\" cannot be combined");
89
+ const output = generateHtml({
12
90
  aslDefinition: asl,
13
- history
14
- }) : generateMermaid({ aslDefinition: asl })).code
15
- };
16
- return {
17
- type: "svg",
18
- svg: (history ? generateExecution({
91
+ ...diagramOptions
92
+ });
93
+ return {
94
+ height: output.height,
95
+ html: output.html,
96
+ metadata: output.metadata,
97
+ type: "html",
98
+ width: output.width
99
+ };
100
+ }
101
+ if (format === "mermaid") {
102
+ const output = history ? generateMermaidExecution({
19
103
  aslDefinition: asl,
20
104
  history,
21
- layout,
22
- theme
23
- }) : generateSvg({
105
+ ...diagramOptions
106
+ }) : generateMermaid({
24
107
  aslDefinition: asl,
25
- layout,
26
- theme
27
- })).svg
108
+ ...diagramOptions
109
+ });
110
+ return {
111
+ code: output.code,
112
+ metadata: output.metadata,
113
+ type: "mermaid"
114
+ };
115
+ }
116
+ const output = history ? generateExecution({
117
+ aslDefinition: asl,
118
+ history,
119
+ ...diagramOptions
120
+ }) : generateSvg({
121
+ aslDefinition: asl,
122
+ ...diagramOptions
123
+ });
124
+ return {
125
+ height: output.height,
126
+ metadata: output.metadata,
127
+ svg: output.svg,
128
+ type: "svg",
129
+ width: output.width
28
130
  };
29
131
  } catch (err) {
30
132
  return {
31
- type: "error",
32
- error: err instanceof Error ? err : new Error(String(err))
133
+ error: err instanceof Error ? err : new Error(String(err)),
134
+ type: "error"
33
135
  };
34
136
  }
35
137
  }, [
36
138
  asl,
139
+ beforeAsl,
140
+ catchHandling,
141
+ collapse,
142
+ edgeOverrides,
143
+ edgeStyle,
37
144
  format,
38
145
  history,
146
+ iconPosition,
147
+ iconSize,
39
148
  layout,
149
+ nodeOverrides,
150
+ showIcons,
151
+ showVariables,
40
152
  theme
41
153
  ]);
42
- if (result.type === "error") {
154
+ }
155
+ //#endregion
156
+ //#region src/SfnDiagram.tsx
157
+ const SfnDiagram = forwardRef(function SfnDiagram({ className, onError, onStateClick, style, title = "Step Functions diagram", ...diagramParams }, ref) {
158
+ const result = useSfnDiagram(diagramParams);
159
+ useImperativeHandle(ref, () => ({ getSvg: () => result.type === "svg" ? result.svg : null }), [result]);
160
+ const handleStateClick = useCallback((event) => {
161
+ const stateId = (event.target instanceof Element ? event.target : null)?.closest("[data-state-id]")?.getAttribute("data-state-id");
162
+ if (stateId) onStateClick?.({
163
+ event,
164
+ stateId
165
+ });
166
+ }, [onStateClick]);
167
+ const reportedResult = useRef(null);
168
+ useEffect(() => {
169
+ if (result.type !== "error") {
170
+ reportedResult.current = null;
171
+ return;
172
+ }
173
+ if (reportedResult.current === result) return;
174
+ reportedResult.current = result;
43
175
  onError?.(result.error);
44
- return null;
45
- }
176
+ }, [onError, result]);
177
+ if (result.type === "error") return null;
46
178
  if (result.type === "mermaid") return /* @__PURE__ */ jsx("pre", {
47
179
  className,
48
180
  style,
49
181
  children: result.code
50
182
  });
183
+ if (result.type === "html") return /* @__PURE__ */ jsx("iframe", {
184
+ className,
185
+ sandbox: "allow-scripts",
186
+ srcDoc: result.html,
187
+ style,
188
+ title
189
+ });
51
190
  return /* @__PURE__ */ jsx("div", {
52
191
  className,
53
192
  dangerouslySetInnerHTML: { __html: result.svg },
193
+ onClick: onStateClick ? handleStateClick : void 0,
54
194
  style
55
195
  });
56
- }
196
+ });
57
197
  //#endregion
58
- export { SfnDiagram };
198
+ export { SfnDiagram, useSfnDiagram };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sfn-diagram-react",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "React component for AWS Step Functions ASL diagrams",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -28,7 +28,7 @@
28
28
  "scripts": {
29
29
  "build": "tsdown",
30
30
  "dev": "tsdown --watch",
31
- "test": "vitest run",
31
+ "test": "pnpm run build && vitest run",
32
32
  "typecheck": "tsc --noEmit",
33
33
  "lint": "eslint .",
34
34
  "prepublishOnly": "pnpm run build && pnpm run validate",
@@ -39,13 +39,17 @@
39
39
  "keywords": [
40
40
  "aws",
41
41
  "step-functions",
42
+ "stepfunctions",
42
43
  "react",
44
+ "component",
43
45
  "diagram",
44
46
  "asl",
45
47
  "state-machine",
46
48
  "visualization",
47
49
  "svg",
48
- "mermaid"
50
+ "mermaid",
51
+ "html",
52
+ "typescript"
49
53
  ],
50
54
  "author": "Yusuf Afzal",
51
55
  "license": "MIT",
@@ -57,14 +61,14 @@
57
61
  "bugs": {
58
62
  "url": "https://github.com/yusufaf/sfn-diagram/issues"
59
63
  },
60
- "homepage": "https://github.com/yusufaf/sfn-diagram/tree/main/packages/sfn-diagram-react#readme",
64
+ "homepage": "https://sfn.yusufaf.dev/ecosystem/react/",
61
65
  "peerDependencies": {
62
66
  "react": "^18.0.0 || ^19.0.0",
63
- "sfn-diagram": "^1.1.1"
67
+ "sfn-diagram": "^1.6.0"
64
68
  },
65
69
  "devDependencies": {
66
- "@arethetypeswrong/cli": "^0.18.2",
67
- "@testing-library/jest-dom": "^6.6.0",
70
+ "@arethetypeswrong/cli": "^0.18.5",
71
+ "@testing-library/jest-dom": "^7.0.1",
68
72
  "@testing-library/react": "^16.0.0",
69
73
  "@types/react": "^18.3.0",
70
74
  "@types/react-dom": "^18.3.0",
@@ -73,9 +77,9 @@
73
77
  "react": "^18.3.0",
74
78
  "react-dom": "^18.3.0",
75
79
  "sfn-diagram": "workspace:*",
76
- "tsdown": "^0.21.7",
80
+ "tsdown": "^0.23.0",
77
81
  "typescript": "^5.9.2",
78
- "vite": "^6.3.0",
79
- "vitest": "^3.2.6"
82
+ "vite": "^8.3.0",
83
+ "vitest": "^4.1.11"
80
84
  }
81
85
  }