@thomasfosterau/prosekit-flowchart 0.0.0 → 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.
@@ -0,0 +1,188 @@
1
+ <!-- @component The modal that hosts the editable `Flowchart` canvas, loaded on open; Save hands the diagram to `onSave`. -->
2
+
3
+ <!--
4
+ FlowchartEditDialog — the modal that hosts the full WYSIWYG canvas.
5
+
6
+ Opens either from the bindable `open` prop or from the imperative
7
+ `open(data)` instance method the node view uses. On Save it pulls the current
8
+ diagram out of `Flowchart` with `getSnapshot()` and hands it to `onSave`,
9
+ which may be async; Cancel discards.
10
+
11
+ ## bits-ui, not a native `<dialog>`
12
+
13
+ The app this is ported from used a native `<dialog class="modal">`. bits-ui's
14
+ `Dialog` is used instead because it owns the whole modal contract — the focus
15
+ trap, the initial focus move, `aria-modal`, the `aria-labelledby` /
16
+ `aria-describedby` wiring to `Dialog.Title` and `Dialog.Description`, the
17
+ scroll lock and the focus return — and because a native `<dialog>`'s built-in
18
+ Escape handling cannot be gated: it closes the dialog out from under an
19
+ in-flight save. Here Escape goes through `onOpenChange`, which refuses to
20
+ close while `pending`, so a save in flight is never abandoned halfway.
21
+
22
+ ## The canvas is loaded on open, not on import
23
+
24
+ `Flowchart.svelte` is reached through a dynamic `import()` in the effect
25
+ below, and the type-only import is what keeps that true. It is the package's
26
+ heaviest module — `@xyflow/svelte` plus its stylesheet — and the node view
27
+ that mounts this dialog renders every flowchart in a document. A static
28
+ import here would put the whole canvas engine in the chunk of any page with
29
+ an editor on it, which is precisely what the SVG preview exists to avoid.
30
+
31
+ Affordances inside the canvas: the "Add node" button appends a node; drag
32
+ from a node's bottom handle to another's top to connect; double-click a node
33
+ to edit its label; select a node or edge and press Delete to remove it.
34
+ -->
35
+ <script lang="ts">import { Dialog } from "bits-ui";
36
+ import { EMPTY_FLOWCHART } from "./node/data.js";
37
+ // The `open` PROP is bound to `isOpen` because this component also exports an
38
+ // `open(data)` METHOD (the node view's entry point) and the two cannot share
39
+ // a name in one scope.
40
+ let { open: isOpen = $bindable(false), initialData = EMPTY_FLOWCHART, onSave, titleLabel = "Edit flowchart", addNodeLabel = "Add node", helpText = "Drag from a node's bottom to another's top to connect. Double-click a node to edit its label. Select and press Delete to remove.", saveLabel = "Save", savingLabel = "Saving…", cancelLabel = "Cancel", closeLabel = "Close", failureMessage = "Failed to save diagram.", newNodeLabel = "New", nodeLabelLabel = "Node label", untitledLabel = "Untitled", class: className, id, ...rest } = $props();
41
+ // A `bind:this` target, so the declared type must literally be `T |
42
+ // undefined`: Svelte assigns the component instance itself into it, and
43
+ // `undefined` before mount and after unmount.
44
+ let flowchart = $state();
45
+ /** The canvas component itself, once the dialog has been opened at least once. */
46
+ let FlowchartCanvas = $state();
47
+ let pending = $state(false);
48
+ let errorMessage = $state("");
49
+ /**
50
+ * The payload queued by `open(data)` for the canvas that does not exist yet.
51
+ * Deliberately NOT `$state`: it is read and cleared inside the effect below,
52
+ * and making it reactive would make that effect invalidate itself.
53
+ */
54
+ let queued;
55
+ /**
56
+ * Open the dialog on the given payload (default: `initialData`).
57
+ *
58
+ * The imperative entry point, kept for the ProseKit node view, which has an
59
+ * instance to reach through `bind:this` but no place to thread a bound prop.
60
+ */
61
+ export function open(data) {
62
+ errorMessage = "";
63
+ queued = data ?? initialData;
64
+ isOpen = true;
65
+ }
66
+ // Pull the canvas in the first time the dialog opens. Everything it drags
67
+ // behind it — `@xyflow/svelte` and its stylesheet — stays out of the chunk
68
+ // that merely renders the node view's SVG preview.
69
+ async function loadCanvas() {
70
+ FlowchartCanvas = (await import("./Flowchart.svelte")).default;
71
+ }
72
+ $effect(() => {
73
+ if (!isOpen || FlowchartCanvas !== undefined) return;
74
+ void loadCanvas();
75
+ });
76
+ // Seed the canvas only once the dialog's content has mounted: `fitView` needs
77
+ // a real layout box, and bits-ui does not render `Dialog.Content` until the
78
+ // dialog is open. The effect re-runs when `bind:this` fills `flowchart` in;
79
+ // `queueMicrotask` then defers past the mount effect that measures it.
80
+ $effect(() => {
81
+ if (!isOpen) return;
82
+ const canvas = flowchart;
83
+ if (canvas === undefined) return;
84
+ const payload = queued ?? initialData;
85
+ queued = undefined;
86
+ queueMicrotask(() => {
87
+ canvas.resetTo(payload);
88
+ });
89
+ });
90
+ /**
91
+ * bits-ui's request to change the open state — Escape, an outside click, or
92
+ * the close button. Refused while a save is in flight, so the dialog cannot
93
+ * disappear while `onSave` is still running.
94
+ */
95
+ function handleOpenChange(next) {
96
+ if (!next && pending) return;
97
+ if (!next) errorMessage = "";
98
+ isOpen = next;
99
+ }
100
+ function close() {
101
+ handleOpenChange(false);
102
+ }
103
+ async function save() {
104
+ const snapshot = flowchart?.getSnapshot();
105
+ if (snapshot === undefined) return;
106
+ pending = true;
107
+ errorMessage = "";
108
+ try {
109
+ await onSave(snapshot);
110
+ isOpen = false;
111
+ } catch (error) {
112
+ errorMessage = error instanceof Error ? error.message : failureMessage;
113
+ } finally {
114
+ pending = false;
115
+ }
116
+ }
117
+ function addNode() {
118
+ flowchart?.addNode(newNodeLabel);
119
+ }
120
+ </script>
121
+
122
+ <Dialog.Root bind:open={() => isOpen, handleOpenChange}>
123
+ <Dialog.Portal>
124
+ <!-- daisyUI's `modal-backdrop` is a child of an open `.modal`; this is the
125
+ same scrim as an element bits-ui can dismiss from. -->
126
+ <Dialog.Overlay class="bg-base-300/60 fixed inset-0 z-40" />
127
+ <Dialog.Content
128
+ {...rest}
129
+ {...(id === undefined ? {} : { id })}
130
+ class={[
131
+ 'modal-box fixed top-1/2 left-1/2 z-50 max-h-[calc(100svh-2rem)] w-[calc(100vw-2rem)] max-w-6xl -translate-x-1/2 -translate-y-1/2',
132
+ className,
133
+ ]}
134
+ >
135
+ <div class="flex items-center justify-between gap-3">
136
+ <Dialog.Title class="text-lg font-bold">{titleLabel}</Dialog.Title>
137
+ <div class="flex items-center gap-2">
138
+ <button type="button" class="btn btn-sm btn-primary" disabled={pending} onclick={addNode}>
139
+ {addNodeLabel}
140
+ </button>
141
+ <Dialog.Close>
142
+ {#snippet child({ props })}
143
+ <button
144
+ {...props}
145
+ type="button"
146
+ class="btn btn-sm btn-ghost btn-square"
147
+ disabled={pending}
148
+ aria-label={closeLabel}
149
+ >
150
+ <span aria-hidden="true">✕</span>
151
+ </button>
152
+ {/snippet}
153
+ </Dialog.Close>
154
+ </div>
155
+ </div>
156
+ <Dialog.Description class="text-base-content/70 mt-1 text-sm">
157
+ {helpText}
158
+ </Dialog.Description>
159
+ <div class="border-base-300 mt-4 rounded border" style="height: 32rem;">
160
+ {#if FlowchartCanvas}
161
+ <!-- `nodeLabelLabel` and `untitledLabel` are forwarded rather than
162
+ left to the canvas's own defaults: the nodes an author edits in
163
+ here are the dialog's surface, and an application that
164
+ translated the dialog would otherwise find English inside it. -->
165
+ <FlowchartCanvas
166
+ bind:this={flowchart}
167
+ data={initialData}
168
+ interactive
169
+ class="h-full"
170
+ {nodeLabelLabel}
171
+ {untitledLabel}
172
+ />
173
+ {/if}
174
+ </div>
175
+ {#if errorMessage !== ''}
176
+ <p class="text-error pt-3 text-sm" role="alert">{errorMessage}</p>
177
+ {/if}
178
+ <div class="modal-action">
179
+ <button type="button" class="btn btn-ghost" disabled={pending} onclick={close}>
180
+ {cancelLabel}
181
+ </button>
182
+ <button type="button" class="btn btn-primary" disabled={pending} onclick={save}>
183
+ {pending ? savingLabel : saveLabel}
184
+ </button>
185
+ </div>
186
+ </Dialog.Content>
187
+ </Dialog.Portal>
188
+ </Dialog.Root>
@@ -0,0 +1,42 @@
1
+ import type { HTMLAttributes } from 'svelte/elements';
2
+ import type { FlowchartData } from './node/schema.ts';
3
+ interface Props extends Omit<HTMLAttributes<HTMLDivElement>, 'children' | 'class' | 'id'> {
4
+ /** The dialog element's `id`. */
5
+ id?: string | undefined;
6
+ /** Whether the dialog is shown. Bindable. */
7
+ open?: boolean | undefined;
8
+ /** The payload the canvas opens with, when `open(data)` is not given one. */
9
+ initialData?: FlowchartData | undefined;
10
+ /** Called with the snapshot on Save; may be async, and may reject. */
11
+ onSave: (data: FlowchartData) => void | Promise<void>;
12
+ /** Heading — the dialog's accessible name. */
13
+ titleLabel?: string | undefined;
14
+ /** The button that appends a node to the canvas. */
15
+ addNodeLabel?: string | undefined;
16
+ /** The supporting sentence under the heading — the accessible description. */
17
+ helpText?: string | undefined;
18
+ /** The confirm button, at rest. */
19
+ saveLabel?: string | undefined;
20
+ /** The confirm button, while `onSave` is in flight. */
21
+ savingLabel?: string | undefined;
22
+ /** The discard button. */
23
+ cancelLabel?: string | undefined;
24
+ /** Accessible name of the corner close button. */
25
+ closeLabel?: string | undefined;
26
+ /** Shown when `onSave` rejects with something that is not an `Error`. */
27
+ failureMessage?: string | undefined;
28
+ /** Label given to a node added by the "Add node" button. */
29
+ newNodeLabel?: string | undefined;
30
+ /** Accessible name of a node's label field, forwarded to the canvas. */
31
+ nodeLabelLabel?: string | undefined;
32
+ /** Shown, and committed, when a node's label is blank; forwarded to the canvas. */
33
+ untitledLabel?: string | undefined;
34
+ /** Extra classes on the `modal-box`; merged last, so they win. */
35
+ class?: string | undefined;
36
+ }
37
+ /** The modal that hosts the editable `Flowchart` canvas, loaded on open; Save hands the diagram to `onSave`. */
38
+ declare const FlowchartEditDialog: import("svelte").Component<Props, {
39
+ open: (data?: FlowchartData) => void;
40
+ }, "open">;
41
+ type FlowchartEditDialog = ReturnType<typeof FlowchartEditDialog>;
42
+ export default FlowchartEditDialog;
@@ -0,0 +1,71 @@
1
+ <!-- @component The ProseKit node view for a `flowchart` node: previews it with the server-rendered SVG and edits it in `FlowchartEditDialog`. -->
2
+
3
+ <!--
4
+ FlowchartNodeView — the Svelte node view ProseKit mounts for a `flowchart`
5
+ node.
6
+
7
+ The preview is the SAME server-renderable SVG a reader page gets
8
+ (`generateFlowchartSvg`, from this package's `./node` entry), so the editor
9
+ and the published article cannot drift, and so the `@xyflow/svelte` bundle
10
+ only loads when the author actually opens the edit dialog.
11
+
12
+ ## The one place stores are allowed
13
+
14
+ This workspace's Svelte code is runes-only. ProseKit's node-view contract is
15
+ the documented exception: the adapter hands the view a `Writable<Node>` and a
16
+ `Writable<boolean>` plus a `setAttrs` callback, and they are consumed here
17
+ through Svelte's `$store` auto-subscription. Nothing else in this package
18
+ touches a store.
19
+ -->
20
+ <script lang="ts">import FlowchartEditDialog from "./FlowchartEditDialog.svelte";
21
+ import { EMPTY_FLOWCHART } from "./node/data.js";
22
+ import { generateFlowchartSvg } from "./node/svg.js";
23
+ // `node` is read as `$node` (store auto-subscription), which fallow's
24
+ // component-prop check does not follow.
25
+ // fallow-ignore-next-line unused-component-prop
26
+ let { node, selected, setAttrs, editLabel = "Edit" } = $props();
27
+ // A `bind:this` target, so the declared type must literally be `T |
28
+ // undefined`: Svelte assigns the component instance itself into it, and
29
+ // `undefined` before mount and after unmount.
30
+ let dialog = $state();
31
+ const data = $derived($node.attrs.data ?? EMPTY_FLOWCHART);
32
+ const svg = $derived(generateFlowchartSvg(data));
33
+ function openEditor() {
34
+ dialog?.open(data);
35
+ }
36
+ function handleSave(next) {
37
+ setAttrs({ data: next });
38
+ }
39
+ </script>
40
+
41
+ <div
42
+ class="flowchart-node-view group relative my-3 rounded border p-2"
43
+ class:border-primary={$selected}
44
+ class:border-base-300={!$selected}
45
+ contenteditable="false"
46
+ data-prosemirror-node-name="flowchart"
47
+ >
48
+ <div class="flowchart-preview overflow-hidden">
49
+ <!-- The SVG comes from this package's own escape-aware serialiser
50
+ (`./node/svg.ts`), which XML-escapes every label; it is never
51
+ user-supplied HTML. -->
52
+ {@html svg}
53
+ </div>
54
+ <button
55
+ type="button"
56
+ class="btn btn-xs btn-primary absolute top-2 right-2 opacity-0 transition-opacity group-hover:opacity-100 focus-visible:opacity-100"
57
+ class:opacity-100={$selected}
58
+ onclick={openEditor}
59
+ >
60
+ {editLabel}
61
+ </button>
62
+ </div>
63
+
64
+ <FlowchartEditDialog bind:this={dialog} initialData={data} onSave={handleSave} />
65
+
66
+ <style>
67
+ .flowchart-preview :global(svg) {
68
+ max-height: 20rem;
69
+ display: block;
70
+ }
71
+ </style>
@@ -0,0 +1,16 @@
1
+ import type { Attrs, Node as ProseMirrorNode } from '@prosekit/pm/model';
2
+ import type { Writable } from 'svelte/store';
3
+ interface Props {
4
+ /** The ProseMirror node this view renders, as ProseKit's store. */
5
+ node: Writable<ProseMirrorNode>;
6
+ /** Whether ProseMirror currently has this node selected. */
7
+ selected: Writable<boolean>;
8
+ /** Writes attributes back onto the node — the save channel. */
9
+ setAttrs: (attrs: Attrs) => void;
10
+ /** The button that opens the edit dialog. */
11
+ editLabel?: string | undefined;
12
+ }
13
+ /** The ProseKit node view for a `flowchart` node: previews it with the server-rendered SVG and edits it in `FlowchartEditDialog`. */
14
+ declare const FlowchartNodeView: import("svelte").Component<Props, {}, "">;
15
+ type FlowchartNodeView = ReturnType<typeof FlowchartNodeView>;
16
+ export default FlowchartNodeView;
@@ -0,0 +1,25 @@
1
+ /**
2
+ * The ProseKit node-view extension for the `flowchart` block.
3
+ *
4
+ * Kept apart from `./node/flowchart-node.ts` — which registers the node spec
5
+ * and the `updateFlowchartData` command — because the view pulls in Svelte, the
6
+ * edit dialog and (through it) `@xyflow/svelte`, none of which may reach the
7
+ * `./node` entry. A consuming app composes the two at editor-instantiation
8
+ * time: `defineFlowchart()` for the schema, `defineFlowchartNodeView()` for the
9
+ * UI.
10
+ *
11
+ * `defineSvelteNodeView` is a no-op on the server, so this is safe to include
12
+ * in an extension that is also built server-side.
13
+ *
14
+ * @module @thomasfosterau/prosekit-flowchart/flowchart-node-view
15
+ */
16
+ /**
17
+ * Registers {@link FlowchartNodeView} as the `flowchart` node's view, hosted in
18
+ * a `div`.
19
+ *
20
+ * Both mutation guards return `true` on purpose: the rendered diagram rewrites
21
+ * its own DOM (the SVG preview, and xyflow's canvas while the dialog is open),
22
+ * and without them ProseMirror would read those rewrites as document edits and
23
+ * fight the view for control of the node.
24
+ */
25
+ export declare function defineFlowchartNodeView(): import("@prosekit/core").Extension<import("@prosekit/core").ExtensionTyping<any, any, any>>;
@@ -0,0 +1,35 @@
1
+ /**
2
+ * The ProseKit node-view extension for the `flowchart` block.
3
+ *
4
+ * Kept apart from `./node/flowchart-node.ts` — which registers the node spec
5
+ * and the `updateFlowchartData` command — because the view pulls in Svelte, the
6
+ * edit dialog and (through it) `@xyflow/svelte`, none of which may reach the
7
+ * `./node` entry. A consuming app composes the two at editor-instantiation
8
+ * time: `defineFlowchart()` for the schema, `defineFlowchartNodeView()` for the
9
+ * UI.
10
+ *
11
+ * `defineSvelteNodeView` is a no-op on the server, so this is safe to include
12
+ * in an extension that is also built server-side.
13
+ *
14
+ * @module @thomasfosterau/prosekit-flowchart/flowchart-node-view
15
+ */
16
+ import { defineSvelteNodeView } from 'prosekit/svelte';
17
+ import FlowchartNodeView from './FlowchartNodeView.svelte';
18
+ /**
19
+ * Registers {@link FlowchartNodeView} as the `flowchart` node's view, hosted in
20
+ * a `div`.
21
+ *
22
+ * Both mutation guards return `true` on purpose: the rendered diagram rewrites
23
+ * its own DOM (the SVG preview, and xyflow's canvas while the dialog is open),
24
+ * and without them ProseMirror would read those rewrites as document edits and
25
+ * fight the view for control of the node.
26
+ */
27
+ export function defineFlowchartNodeView() {
28
+ return defineSvelteNodeView({
29
+ name: 'flowchart',
30
+ component: FlowchartNodeView,
31
+ as: 'div',
32
+ ignoreMutation: () => true,
33
+ stopEvent: () => true,
34
+ });
35
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * `@thomasfosterau/prosekit-flowchart` — the flowchart canvas, edit dialog
3
+ * and ProseKit node view on `@xyflow/svelte`.
4
+ *
5
+ * `Flowchart.svelte` carries this package's only side-effecting import,
6
+ * `@xyflow/svelte/dist/style.css`. The ProseKit node spec, its Effect Schema
7
+ * payload and the server SVG renderer live under `./node`, which imports
8
+ * neither Svelte nor xyflow and is a separate entry point — nothing from it is
9
+ * re-exported here, so a server bundle never reaches this module.
10
+ *
11
+ * @module
12
+ */
13
+ export { default as EditableNode } from './EditableNode.svelte';
14
+ export { default as Flowchart } from './Flowchart.svelte';
15
+ export { defineFlowchartNodeView } from './flowchart-node-view.ts';
16
+ export { default as FlowchartEditDialog } from './FlowchartEditDialog.svelte';
17
+ export { default as FlowchartNodeView } from './FlowchartNodeView.svelte';
18
+ export { flowchartSlashCommand, flowchartToolbarItem } from './toolbar.ts';
package/dist/index.js ADDED
@@ -0,0 +1,18 @@
1
+ /**
2
+ * `@thomasfosterau/prosekit-flowchart` — the flowchart canvas, edit dialog
3
+ * and ProseKit node view on `@xyflow/svelte`.
4
+ *
5
+ * `Flowchart.svelte` carries this package's only side-effecting import,
6
+ * `@xyflow/svelte/dist/style.css`. The ProseKit node spec, its Effect Schema
7
+ * payload and the server SVG renderer live under `./node`, which imports
8
+ * neither Svelte nor xyflow and is a separate entry point — nothing from it is
9
+ * re-exported here, so a server bundle never reaches this module.
10
+ *
11
+ * @module
12
+ */
13
+ export { default as EditableNode } from './EditableNode.svelte';
14
+ export { default as Flowchart } from './Flowchart.svelte';
15
+ export { defineFlowchartNodeView } from "./flowchart-node-view.js";
16
+ export { default as FlowchartEditDialog } from './FlowchartEditDialog.svelte';
17
+ export { default as FlowchartNodeView } from './FlowchartNodeView.svelte';
18
+ export { flowchartSlashCommand, flowchartToolbarItem } from "./toolbar.js";
@@ -0,0 +1,28 @@
1
+ /**
2
+ * The flowchart payload's parser and empty-state default.
3
+ *
4
+ * `FlowchartPosition` is the one convenience type `./schema.ts` deliberately
5
+ * doesn't export on its own (see that module's doc comment); `FlowchartNode`,
6
+ * `FlowchartEdge` and `FlowchartData` are re-exported from `./schema.ts`
7
+ * directly by the package barrel, since each already merges the Effect Schema
8
+ * value and its decoded type under one name.
9
+ *
10
+ * Ported verbatim (semantics unchanged) from Website's
11
+ * `src/editor/flowchart/types.ts`.
12
+ *
13
+ * @module @thomasfosterau/prosekit-flowchart/node/data
14
+ */
15
+ import { FlowchartData as FlowchartDataSchema, FlowchartNode as FlowchartNodeSchema } from './schema.ts';
16
+ /** Position of a node on the flowchart canvas. */
17
+ export type FlowchartPosition = FlowchartNodeSchema['position'];
18
+ /** Empty flowchart payload — used as the default attr value and on parse failure. */
19
+ export declare const EMPTY_FLOWCHART: FlowchartDataSchema;
20
+ /**
21
+ * Parses a JSON string into a `FlowchartData` payload, returning
22
+ * {@link EMPTY_FLOWCHART} on any failure. Validates the shape just enough to
23
+ * make downstream rendering safe.
24
+ *
25
+ * Nodes and edges are validated individually against `FlowchartNode` /
26
+ * `FlowchartEdge` — a single malformed entry is dropped, not the whole array.
27
+ */
28
+ export declare function parseFlowchartData(input: string): FlowchartDataSchema;
@@ -0,0 +1,57 @@
1
+ /**
2
+ * The flowchart payload's parser and empty-state default.
3
+ *
4
+ * `FlowchartPosition` is the one convenience type `./schema.ts` deliberately
5
+ * doesn't export on its own (see that module's doc comment); `FlowchartNode`,
6
+ * `FlowchartEdge` and `FlowchartData` are re-exported from `./schema.ts`
7
+ * directly by the package barrel, since each already merges the Effect Schema
8
+ * value and its decoded type under one name.
9
+ *
10
+ * Ported verbatim (semantics unchanged) from Website's
11
+ * `src/editor/flowchart/types.ts`.
12
+ *
13
+ * @module @thomasfosterau/prosekit-flowchart/node/data
14
+ */
15
+ import * as Arr from 'effect/Array';
16
+ import * as Option from 'effect/Option';
17
+ import * as Predicate from 'effect/Predicate';
18
+ import * as Schema from 'effect/Schema';
19
+ import { FlowchartData as FlowchartDataSchema, FlowchartEdge as FlowchartEdgeSchema, FlowchartNode as FlowchartNodeSchema, } from "./schema.js";
20
+ /**
21
+ * Parses a JSON string to `unknown`, or `Option.none()` when it is not JSON.
22
+ *
23
+ * Built once at module scope, alongside the per-element decoders below:
24
+ * `parseFlowchartData` runs per flowchart block on every server render, so
25
+ * none of these decoders must be rebuilt per call.
26
+ */
27
+ const decodeJson = /* @__PURE__ */ Schema.decodeUnknownOption(
28
+ /* @__PURE__ */ Schema.fromJsonString(Schema.Unknown));
29
+ /** Decodes a single node, dropping it (via `Option.none()`) if malformed. */
30
+ const decodeFlowchartNode = /* @__PURE__ */ Schema.decodeUnknownOption(FlowchartNodeSchema);
31
+ /** Decodes a single edge, dropping it (via `Option.none()`) if malformed. */
32
+ const decodeFlowchartEdge = /* @__PURE__ */ Schema.decodeUnknownOption(FlowchartEdgeSchema);
33
+ /** Empty flowchart payload — used as the default attr value and on parse failure. */
34
+ export const EMPTY_FLOWCHART = { nodes: [], edges: [] };
35
+ /**
36
+ * Parses a JSON string into a `FlowchartData` payload, returning
37
+ * {@link EMPTY_FLOWCHART} on any failure. Validates the shape just enough to
38
+ * make downstream rendering safe.
39
+ *
40
+ * Nodes and edges are validated individually against `FlowchartNode` /
41
+ * `FlowchartEdge` — a single malformed entry is dropped, not the whole array.
42
+ */
43
+ export function parseFlowchartData(input) {
44
+ const decoded = decodeJson(input);
45
+ if (Option.isNone(decoded))
46
+ return { ...EMPTY_FLOWCHART };
47
+ const parsed = decoded.value;
48
+ if (!Predicate.isObjectOrArray(parsed))
49
+ return { ...EMPTY_FLOWCHART };
50
+ const nodes = Predicate.hasProperty(parsed, 'nodes') && Array.isArray(parsed.nodes)
51
+ ? Arr.getSomes(parsed.nodes.map((node) => decodeFlowchartNode(node)))
52
+ : [];
53
+ const edges = Predicate.hasProperty(parsed, 'edges') && Array.isArray(parsed.edges)
54
+ ? Arr.getSomes(parsed.edges.map((edge) => decodeFlowchartEdge(edge)))
55
+ : [];
56
+ return { nodes, edges };
57
+ }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Flowchart block node for the ProseKit editor.
3
+ *
4
+ * Stores the diagram payload as a single `data` attribute containing a
5
+ * {@link FlowchartData}. The DOM representation is
6
+ * `<div data-flowchart>{JSON}</div>` so SSR and paste both round-trip the
7
+ * data through textContent.
8
+ *
9
+ * Ported verbatim from Website's `src/editor/flowchart/flowchart-node.ts`.
10
+ * The Svelte-backed node view lives at the flowchart package's root; this
11
+ * module only registers the schema-side node spec and the editing command.
12
+ *
13
+ * @module @thomasfosterau/prosekit-flowchart/node/flowchart-node
14
+ */
15
+ /**
16
+ * Schema-side extension for the flowchart block: registers the node spec and
17
+ * the `updateFlowchartData` editing command. The Svelte-backed node view
18
+ * lives at this package's root and is composed in by the consuming app's
19
+ * editor component at instantiation time.
20
+ */
21
+ export declare function defineFlowchart(): import("@prosekit/core").Union<readonly [import("@prosekit/core").Extension<{
22
+ Nodes: {
23
+ flowchart: {
24
+ data: {
25
+ readonly nodes: readonly {
26
+ readonly id: string;
27
+ readonly position: {
28
+ readonly x: number;
29
+ readonly y: number;
30
+ };
31
+ readonly data: {
32
+ readonly label: string;
33
+ };
34
+ }[];
35
+ readonly edges: readonly {
36
+ readonly id: string;
37
+ readonly source: string;
38
+ readonly target: string;
39
+ }[];
40
+ };
41
+ };
42
+ };
43
+ }>, import("@prosekit/core").Extension<{
44
+ Commands: {
45
+ updateFlowchartData: [pos: number, data: {
46
+ readonly nodes: readonly {
47
+ readonly id: string;
48
+ readonly position: {
49
+ readonly x: number;
50
+ readonly y: number;
51
+ };
52
+ readonly data: {
53
+ readonly label: string;
54
+ };
55
+ }[];
56
+ readonly edges: readonly {
57
+ readonly id: string;
58
+ readonly source: string;
59
+ readonly target: string;
60
+ }[];
61
+ }];
62
+ };
63
+ }>]>;
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Flowchart block node for the ProseKit editor.
3
+ *
4
+ * Stores the diagram payload as a single `data` attribute containing a
5
+ * {@link FlowchartData}. The DOM representation is
6
+ * `<div data-flowchart>{JSON}</div>` so SSR and paste both round-trip the
7
+ * data through textContent.
8
+ *
9
+ * Ported verbatim from Website's `src/editor/flowchart/flowchart-node.ts`.
10
+ * The Svelte-backed node view lives at the flowchart package's root; this
11
+ * module only registers the schema-side node spec and the editing command.
12
+ *
13
+ * @module @thomasfosterau/prosekit-flowchart/node/flowchart-node
14
+ */
15
+ import * as Option from 'effect/Option';
16
+ import * as Schema from 'effect/Schema';
17
+ import { defineCommands, defineNodeSpec, union } from 'prosekit/core';
18
+ import { EMPTY_FLOWCHART, parseFlowchartData } from "./data.js";
19
+ /**
20
+ * JSON encoder for the `data` attr payload. Built once at module scope —
21
+ * `toDOM` runs per flowchart node on every editor render.
22
+ */
23
+ const encodeJson = /* @__PURE__ */ Schema.encodeSync(
24
+ /* @__PURE__ */ Schema.fromJsonString(Schema.Unknown));
25
+ /**
26
+ * JSON encoding with HTML-sensitive characters unicode-escaped so the result
27
+ * can be embedded in an HTML attribute value without breaking out, regardless
28
+ * of which quote the serializer chooses.
29
+ */
30
+ function htmlSafeStringify(value) {
31
+ return encodeJson(value)
32
+ .replace(/</g, '\\u003c')
33
+ .replace(/>/g, '\\u003e')
34
+ .replace(/&/g, '\\u0026')
35
+ .replace(/'/g, '\\u0027');
36
+ }
37
+ function defineFlowchartNodeSpec() {
38
+ return defineNodeSpec({
39
+ name: 'flowchart',
40
+ group: 'block',
41
+ atom: true,
42
+ draggable: true,
43
+ selectable: true,
44
+ isolating: true,
45
+ attrs: {
46
+ data: { default: EMPTY_FLOWCHART },
47
+ },
48
+ parseDOM: [
49
+ {
50
+ tag: 'div[data-flowchart]',
51
+ getAttrs: (el) => {
52
+ // Prefer the `data-payload` attribute (current emit format). Fall
53
+ // back to text content for older serialised documents where the
54
+ // JSON lived inside the element.
55
+ const payload = el.getAttribute('data-payload') ?? el.textContent ?? '';
56
+ return { data: parseFlowchartData(payload) };
57
+ },
58
+ },
59
+ ],
60
+ toDOM: (node) => {
61
+ // ProseMirror `attrs` is a foreign plain object; convert at the
62
+ // boundary rather than pushing `Option` into the node spec.
63
+ // SAFETY: `data` is only ever written by this spec's default
64
+ // (`EMPTY_FLOWCHART`), by `parseDOM` (`parseFlowchartData`) and by
65
+ // `updateFlowchartData`, all of which produce a `FlowchartData`; a
66
+ // caller-supplied nullish `data` is still caught below.
67
+ const attrs = node.attrs;
68
+ const data = Option.getOrElse(Option.fromNullishOr(attrs.data), () => EMPTY_FLOWCHART);
69
+ // Persist the JSON in an attribute so the element's inner content is
70
+ // free for the post-rendered SVG (see `./html.ts`). The JSON is
71
+ // unicode-escaped so user-supplied labels containing `<`, `>`, `&`, `'`
72
+ // can never break out of the attribute context. The empty text child
73
+ // ensures happy-dom emits a proper closing tag.
74
+ return ['div', { 'data-flowchart': '', 'data-payload': htmlSafeStringify(data) }, ''];
75
+ },
76
+ });
77
+ }
78
+ function defineFlowchartCommands() {
79
+ return defineCommands({
80
+ updateFlowchartData: (pos, data) => (state, dispatch) => {
81
+ const node = state.doc.nodeAt(pos);
82
+ if (!node || node.type.name !== 'flowchart')
83
+ return false;
84
+ if (dispatch) {
85
+ const tr = state.tr.setNodeMarkup(pos, undefined, { ...node.attrs, data });
86
+ dispatch(tr);
87
+ }
88
+ return true;
89
+ },
90
+ });
91
+ }
92
+ /**
93
+ * Schema-side extension for the flowchart block: registers the node spec and
94
+ * the `updateFlowchartData` editing command. The Svelte-backed node view
95
+ * lives at this package's root and is composed in by the consuming app's
96
+ * editor component at instantiation time.
97
+ */
98
+ export function defineFlowchart() {
99
+ return union(defineFlowchartNodeSpec(), defineFlowchartCommands());
100
+ }