@plannotator/ui 0.40.0 → 0.41.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/HANDOFF.md +46 -5
- package/README.md +6 -2
- package/components/DiagramBlock.tsx +376 -0
- package/components/GraphvizBlock.tsx +22 -597
- package/components/MermaidBlock.tsx +17 -664
- package/components/Viewer.tsx +42 -2
- package/components/diagram/DiagramCanvas.tsx +431 -0
- package/components/diagram/DiagramComposer.tsx +135 -0
- package/components/diagram/DiagramOverlay.tsx +215 -0
- package/components/diagram/DiagramPopout.tsx +70 -0
- package/components/diagram/DiagramSourcePane.tsx +244 -0
- package/components/diagram/DiagramViewer.tsx +276 -0
- package/components/diagram/anchorClaims.ts +71 -0
- package/components/diagram/index.ts +36 -0
- package/components/diagram/useDiagramComments.ts +341 -0
- package/components/diagram/useDiagramRender.ts +91 -0
- package/components/diagram/useDiagramSourceDraft.ts +143 -0
- package/components/diagram/useDiagramViewport.ts +156 -0
- package/components/html-viewer/bridge-script.asset.js +25 -0
- package/components/html-viewer/bridge-script.lite.ts +1 -1
- package/components/html-viewer/bridge-script.ts +37 -0
- package/hooks/useAnnotationHighlighter.ts +5 -0
- package/package.json +5 -3
- package/styles.css +1 -1
- package/types.ts +5 -0
- package/utils/diagram-anchor-graphviz.ts +143 -0
- package/utils/diagram-anchor.ts +401 -0
- package/utils/diagram-projection.ts +66 -0
- package/utils/diagram-render.ts +668 -0
- package/utils/graphviz.ts +93 -0
- package/utils/parser.ts +15 -1
- package/components/mermaidSvg.ts +0 -33
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useState, type ReactNode } from 'react';
|
|
2
|
+
import type { DiagramKind } from '@plannotator/core/diagram-anchor';
|
|
3
|
+
import { cn } from '../../lib/utils';
|
|
4
|
+
import { diagramFamilyOf } from '../../utils/diagram-anchor';
|
|
5
|
+
import { diagramFinder, type DiagramTheme } from '../../utils/diagram-render';
|
|
6
|
+
import { DiagramCanvas, type DiagramCanvasHandle, type DiagramEscapeOutcome } from './DiagramCanvas';
|
|
7
|
+
import { DiagramComposer } from './DiagramComposer';
|
|
8
|
+
import { DiagramOverlay } from './DiagramOverlay';
|
|
9
|
+
import { DiagramSourcePane } from './DiagramSourcePane';
|
|
10
|
+
import { useDiagramComments, type DiagramComment, type DiagramCreateComment } from './useDiagramComments';
|
|
11
|
+
import { useDiagramRender, type DiagramRenderState } from './useDiagramRender';
|
|
12
|
+
import { useDiagramSourceDraft, type SaveResult } from './useDiagramSourceDraft';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The one diagram viewer: the canvas with its overlay, the Source pane on
|
|
16
|
+
* its LEFT (stacked under it on the phone) when the host can save, the
|
|
17
|
+
* parse-error strip over a dimmed last-good render. It takes the diagram
|
|
18
|
+
* source and the host's comments on it, and reports a new comment through
|
|
19
|
+
* `onCreateComment`; the comments rail, storage and identity are the
|
|
20
|
+
* host's. Plannotator's fence blocks render through it in the document
|
|
21
|
+
* (`components/DiagramBlock`) and again at full size in the popout; a host
|
|
22
|
+
* with its own document store renders it wherever a diagram lives.
|
|
23
|
+
*/
|
|
24
|
+
export interface DiagramViewerProps {
|
|
25
|
+
readonly kind: DiagramKind;
|
|
26
|
+
/** The diagram text. With `onSave` this is the saved baseline the pane's
|
|
27
|
+
* draft starts from; without it, what the canvas renders. */
|
|
28
|
+
readonly source: string;
|
|
29
|
+
readonly theme: DiagramTheme;
|
|
30
|
+
readonly comments: readonly DiagramComment[];
|
|
31
|
+
/** A comment composed on a part. Absent: clicks open nothing. */
|
|
32
|
+
readonly onCreateComment?: DiagramCreateComment;
|
|
33
|
+
/** Save the pane's text. Absent: there is no Source pane. */
|
|
34
|
+
readonly onSave?: (source: string) => Promise<SaveResult>;
|
|
35
|
+
/** With `onSave`: show the pane read-only (no edit access). */
|
|
36
|
+
readonly readOnlySource?: boolean;
|
|
37
|
+
/** Whether the Source pane is open (the host owns the toggle). */
|
|
38
|
+
readonly sourceOpen?: boolean;
|
|
39
|
+
readonly selectedCommentId?: string | null;
|
|
40
|
+
readonly onSelectComment?: (id: string | null) => void;
|
|
41
|
+
/** The comments whose part is gone from the current render, once per
|
|
42
|
+
* membership change, after every render. */
|
|
43
|
+
readonly onUnanchoredChange?: (ids: ReadonlySet<string>) => void;
|
|
44
|
+
/** Every comment's verdict (true: its part is in this render), whenever
|
|
45
|
+
* a verdict or the comment list changes. */
|
|
46
|
+
readonly onResolutionChange?: (resolution: ReadonlyMap<string, boolean>) => void;
|
|
47
|
+
/** Escape with no composer open and nothing selected: a popout closes. */
|
|
48
|
+
readonly onDismiss?: () => void;
|
|
49
|
+
/** A stable prefix for the rendered element ids; two viewers over one
|
|
50
|
+
* document need two. */
|
|
51
|
+
readonly renderId?: string;
|
|
52
|
+
/** Lines to add so a comment's `sourceLine` names DOCUMENT lines: 0 when
|
|
53
|
+
* the document is the diagram, the fence's opening line for a fence. */
|
|
54
|
+
readonly sourceLineOffset?: number;
|
|
55
|
+
/** Shift-click targets one comment may also cover. Default 0: a comment
|
|
56
|
+
* covers one part. */
|
|
57
|
+
readonly maxAdditionalTargets?: number;
|
|
58
|
+
/** Commenting is off, and this is why (shown in place of the composer). */
|
|
59
|
+
readonly commentingDisabledReason?: string;
|
|
60
|
+
/** Bumped by the host's Retry after a failed engine load. */
|
|
61
|
+
readonly retryToken?: number;
|
|
62
|
+
/** Every render state change (the host sizes itself, shows its own
|
|
63
|
+
* pending or error chrome). */
|
|
64
|
+
readonly onRenderState?: (state: DiagramRenderState) => void;
|
|
65
|
+
/** What to show while there is no svg yet (the first render pending, or
|
|
66
|
+
* the first render failed). Default: a quiet status line. */
|
|
67
|
+
readonly renderFallback?: (state: DiagramRenderState) => ReactNode;
|
|
68
|
+
/** Take the keyboard on mount (a popout). */
|
|
69
|
+
readonly autoFocus?: boolean;
|
|
70
|
+
readonly className?: string;
|
|
71
|
+
/** Classes for the canvas host. The canvas lets a finger scroll the page
|
|
72
|
+
* past it (`touch-action: pan-y`); a viewer that owns the whole screen
|
|
73
|
+
* passes `touch-none`. */
|
|
74
|
+
readonly canvasClassName?: string;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function DiagramViewer({
|
|
78
|
+
kind,
|
|
79
|
+
source,
|
|
80
|
+
theme,
|
|
81
|
+
comments,
|
|
82
|
+
onCreateComment,
|
|
83
|
+
onSave,
|
|
84
|
+
readOnlySource = false,
|
|
85
|
+
sourceOpen = false,
|
|
86
|
+
selectedCommentId = null,
|
|
87
|
+
onSelectComment,
|
|
88
|
+
onUnanchoredChange,
|
|
89
|
+
onResolutionChange,
|
|
90
|
+
onDismiss,
|
|
91
|
+
renderId = 'diagram',
|
|
92
|
+
sourceLineOffset = 0,
|
|
93
|
+
maxAdditionalTargets = 0,
|
|
94
|
+
commentingDisabledReason,
|
|
95
|
+
retryToken,
|
|
96
|
+
onRenderState,
|
|
97
|
+
renderFallback,
|
|
98
|
+
autoFocus,
|
|
99
|
+
className,
|
|
100
|
+
canvasClassName,
|
|
101
|
+
}: DiagramViewerProps) {
|
|
102
|
+
const finder = diagramFinder(kind);
|
|
103
|
+
const familyOf = useCallback((svg: Element) => (kind === 'graphviz' ? ('graphviz' as const) : diagramFamilyOf(svg)), [kind]);
|
|
104
|
+
const hasPane = onSave !== undefined;
|
|
105
|
+
const editable = hasPane && !readOnlySource;
|
|
106
|
+
|
|
107
|
+
const draft = useDiagramSourceDraft({ source, editable, onSave });
|
|
108
|
+
const rendered = hasPane ? draft.preview : source;
|
|
109
|
+
const savedSource = hasPane ? draft.baseline : source;
|
|
110
|
+
const sourceDirty = hasPane && draft.dirty;
|
|
111
|
+
|
|
112
|
+
const render = useDiagramRender(kind, renderId, rendered, theme, { retryToken });
|
|
113
|
+
useEffect(() => {
|
|
114
|
+
onRenderState?.(render);
|
|
115
|
+
}, [onRenderState, render]);
|
|
116
|
+
|
|
117
|
+
const [svgRoot, setSvgRoot] = useState<SVGSVGElement | null>(null);
|
|
118
|
+
const onSvgRoot = useCallback((root: SVGSVGElement | null) => setSvgRoot(root), []);
|
|
119
|
+
|
|
120
|
+
const canCreate = onCreateComment !== undefined || commentingDisabledReason !== undefined;
|
|
121
|
+
const commentsState = useDiagramComments({
|
|
122
|
+
finder,
|
|
123
|
+
svgRoot,
|
|
124
|
+
renderId: render.renderId,
|
|
125
|
+
renderVersion: render.renderVersion,
|
|
126
|
+
comments,
|
|
127
|
+
savedSource,
|
|
128
|
+
sourceLineOffset,
|
|
129
|
+
sourceDirty,
|
|
130
|
+
maxAdditionalTargets,
|
|
131
|
+
canCreate,
|
|
132
|
+
onCreateComment,
|
|
133
|
+
onSelectComment,
|
|
134
|
+
onUnanchoredChange,
|
|
135
|
+
onResolutionChange,
|
|
136
|
+
familyOf,
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// The selected comment's source line for the pane's gutter mark. The
|
|
140
|
+
// anchor names DOCUMENT lines; the pane shows the diagram text.
|
|
141
|
+
const markedLines = useMemo(() => {
|
|
142
|
+
if (selectedCommentId === null) return null;
|
|
143
|
+
const line = comments.find((entry) => entry.id === selectedCommentId)?.anchor.sourceLine;
|
|
144
|
+
if (line === null || line === undefined) return null;
|
|
145
|
+
return [line[0] - sourceLineOffset, line[1] - sourceLineOffset] as const;
|
|
146
|
+
}, [comments, selectedCommentId, sourceLineOffset]);
|
|
147
|
+
|
|
148
|
+
const onEscape = useCallback((): DiagramEscapeOutcome => {
|
|
149
|
+
if (commentsState.composer !== null) {
|
|
150
|
+
commentsState.cancel();
|
|
151
|
+
return 'consumed';
|
|
152
|
+
}
|
|
153
|
+
if (selectedCommentId !== null && onSelectComment !== undefined) {
|
|
154
|
+
onSelectComment(null);
|
|
155
|
+
return 'consumed';
|
|
156
|
+
}
|
|
157
|
+
onDismiss?.();
|
|
158
|
+
return 'pass';
|
|
159
|
+
}, [commentsState, onDismiss, onSelectComment, selectedCommentId]);
|
|
160
|
+
|
|
161
|
+
// Closing the pane only closes an open composer so the layout shift does
|
|
162
|
+
// not strand it; the draft survives a reopen.
|
|
163
|
+
const cancelComposer = commentsState.cancel;
|
|
164
|
+
useEffect(() => {
|
|
165
|
+
if (!sourceOpen) cancelComposer();
|
|
166
|
+
}, [cancelComposer, sourceOpen]);
|
|
167
|
+
|
|
168
|
+
const overlay = useCallback(
|
|
169
|
+
(handle: DiagramCanvasHandle) => (
|
|
170
|
+
<DiagramOverlay
|
|
171
|
+
handle={handle}
|
|
172
|
+
resolved={commentsState.resolved}
|
|
173
|
+
hover={commentsState.hover}
|
|
174
|
+
composer={commentsState.composer}
|
|
175
|
+
selectedCommentId={selectedCommentId}
|
|
176
|
+
onSelectComment={onSelectComment}
|
|
177
|
+
renderComposer={(anchorRect) =>
|
|
178
|
+
commentsState.composer === null ? null : (
|
|
179
|
+
<DiagramComposer
|
|
180
|
+
key={`${commentsState.composer.primary.target.id ?? ''}:${commentsState.composer.primary.target.from ?? ''}:${commentsState.composer.primary.target.to ?? ''}`}
|
|
181
|
+
draft={commentsState.composer}
|
|
182
|
+
anchorRect={anchorRect}
|
|
183
|
+
hostWidth={handle.hostRef.current?.getBoundingClientRect().width ?? 0}
|
|
184
|
+
sourceDirty={sourceDirty}
|
|
185
|
+
submitting={commentsState.submitting}
|
|
186
|
+
error={commentsState.submitError}
|
|
187
|
+
disabledReason={commentingDisabledReason}
|
|
188
|
+
onSubmit={(text) => void commentsState.submit(text)}
|
|
189
|
+
onCancel={commentsState.cancel}
|
|
190
|
+
/>
|
|
191
|
+
)
|
|
192
|
+
}
|
|
193
|
+
/>
|
|
194
|
+
),
|
|
195
|
+
[commentingDisabledReason, commentsState, onSelectComment, selectedCommentId, sourceDirty],
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
const showFallback = render.svgNode === null;
|
|
199
|
+
|
|
200
|
+
return (
|
|
201
|
+
// `annotation-exclude`: the document's text highlighter never enters a
|
|
202
|
+
// diagram, so a text restore can never wrap a <mark> inside the svg.
|
|
203
|
+
<div data-diagram-viewer="" className={cn('annotation-exclude flex h-full min-h-0 w-full flex-col md:flex-row', className)}>
|
|
204
|
+
{/* The pane comes FIRST in the row (owner ruling: the source sits on
|
|
205
|
+
the left of the diagram while editing). On the phone the row is a
|
|
206
|
+
column and the pane stays stacked UNDER the canvas, which
|
|
207
|
+
`order-last` keeps while `md:order-first` puts it left from `md`. */}
|
|
208
|
+
{hasPane && sourceOpen && (
|
|
209
|
+
<DiagramSourcePane
|
|
210
|
+
draft={draft}
|
|
211
|
+
editable={editable}
|
|
212
|
+
markedLines={markedLines}
|
|
213
|
+
className="order-last min-h-0 shrink-0 basis-2/5 border-t border-border md:order-first md:w-80 md:basis-auto md:border-r md:border-t-0"
|
|
214
|
+
/>
|
|
215
|
+
)}
|
|
216
|
+
<div className="relative min-h-0 min-w-0 flex-1">
|
|
217
|
+
{showFallback ? (
|
|
218
|
+
<div data-diagram-fallback="" className="h-full min-h-0 w-full">
|
|
219
|
+
{renderFallback !== undefined ? (
|
|
220
|
+
renderFallback(render)
|
|
221
|
+
) : (
|
|
222
|
+
<div className="flex h-full items-center justify-center p-4 text-xs text-muted-foreground" aria-live="polite">
|
|
223
|
+
{render.error !== null
|
|
224
|
+
? `The diagram does not parse${render.error.line === null ? '' : ` (line ${render.error.line})`}: ${render.error.message}`
|
|
225
|
+
: render.pending
|
|
226
|
+
? 'Rendering diagram…'
|
|
227
|
+
: 'Nothing to render yet.'}
|
|
228
|
+
</div>
|
|
229
|
+
)}
|
|
230
|
+
</div>
|
|
231
|
+
) : (
|
|
232
|
+
<DiagramCanvas
|
|
233
|
+
svgNode={render.svgNode}
|
|
234
|
+
targetSelector={finder.targetSelector}
|
|
235
|
+
pickTarget={commentsState.pickTarget}
|
|
236
|
+
className={canvasClassName}
|
|
237
|
+
dimmed={render.error !== null}
|
|
238
|
+
onSvgRoot={onSvgRoot}
|
|
239
|
+
onHoverElement={commentsState.setHoverElement}
|
|
240
|
+
onClickElement={commentsState.clickElement}
|
|
241
|
+
onEscape={onEscape}
|
|
242
|
+
overlay={overlay}
|
|
243
|
+
autoFocus={autoFocus}
|
|
244
|
+
>
|
|
245
|
+
{render.error !== null && (
|
|
246
|
+
<div
|
|
247
|
+
data-diagram-parse-error=""
|
|
248
|
+
role="alert"
|
|
249
|
+
className="absolute inset-x-3 top-3 z-10 rounded-md border border-warning/40 bg-card/95 px-3 py-2 text-xs shadow-lg backdrop-blur"
|
|
250
|
+
>
|
|
251
|
+
<div className="font-medium text-foreground">
|
|
252
|
+
{render.error.line === null ? 'The diagram does not parse' : `The diagram does not parse (line ${render.error.line})`}
|
|
253
|
+
</div>
|
|
254
|
+
<div className="mt-0.5 text-muted-foreground">
|
|
255
|
+
{render.error.message}
|
|
256
|
+
{' The last good render stays below.'}
|
|
257
|
+
</div>
|
|
258
|
+
</div>
|
|
259
|
+
)}
|
|
260
|
+
{/* The chip earns its place only while the pane is closed (the
|
|
261
|
+
draft survives a close); with the pane open its header carries
|
|
262
|
+
the one "Draft · unsaved". */}
|
|
263
|
+
{editable && draft.dirty && !sourceOpen && (
|
|
264
|
+
<span
|
|
265
|
+
data-diagram-preview-state=""
|
|
266
|
+
className="absolute left-3 top-3 z-10 rounded-sm border border-border bg-card/85 px-1.5 py-0.5 text-[10px] text-muted-foreground backdrop-blur"
|
|
267
|
+
>
|
|
268
|
+
Draft preview · unsaved
|
|
269
|
+
</span>
|
|
270
|
+
)}
|
|
271
|
+
</DiagramCanvas>
|
|
272
|
+
)}
|
|
273
|
+
</div>
|
|
274
|
+
</div>
|
|
275
|
+
);
|
|
276
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { createContext } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Who shows a diagram comment that names no diagram block of the document.
|
|
5
|
+
*
|
|
6
|
+
* A comment composed in a block carries that block's `blockId`. One that
|
|
7
|
+
* arrives through `POST /api/external-annotations` carries `blockId:
|
|
8
|
+
* "external"`, and one whose fence was deleted carries an id that no longer
|
|
9
|
+
* exists; neither says which diagram it belongs to, but its anchor does.
|
|
10
|
+
* Every diagram block tries such a comment against its own render and
|
|
11
|
+
* reports the verdict here; the FIRST block in document order whose finder
|
|
12
|
+
* resolves it shows it, and when every block has answered and none did, it
|
|
13
|
+
* is unanchored (listed in the rail with the chip, never silently dropped).
|
|
14
|
+
* A block that has not answered yet keeps the verdict pending, so nothing is
|
|
15
|
+
* called unanchored while a diagram is still rendering.
|
|
16
|
+
*/
|
|
17
|
+
export class DiagramAnchorClaims {
|
|
18
|
+
private readonly reports = new Map<string, Map<string, boolean>>();
|
|
19
|
+
private readonly listeners = new Set<() => void>();
|
|
20
|
+
private version = 0;
|
|
21
|
+
|
|
22
|
+
/** The diagram blocks of the document, in document order. */
|
|
23
|
+
constructor(readonly blockIds: readonly string[]) {}
|
|
24
|
+
|
|
25
|
+
/** A block's verdicts for the unowned comments it tried. Verdicts for ids
|
|
26
|
+
* it no longer tries are kept: a block stops trying a comment once another
|
|
27
|
+
* block owns it, and that must not read as "could not resolve". */
|
|
28
|
+
report(blockId: string, results: ReadonlyMap<string, boolean>): void {
|
|
29
|
+
let mine = this.reports.get(blockId);
|
|
30
|
+
let changed = false;
|
|
31
|
+
if (mine === undefined) {
|
|
32
|
+
mine = new Map();
|
|
33
|
+
this.reports.set(blockId, mine);
|
|
34
|
+
changed = true;
|
|
35
|
+
}
|
|
36
|
+
for (const [id, resolved] of results) {
|
|
37
|
+
if (mine.get(id) !== resolved) {
|
|
38
|
+
mine.set(id, resolved);
|
|
39
|
+
changed = true;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (!changed) return;
|
|
43
|
+
this.version += 1;
|
|
44
|
+
for (const listener of [...this.listeners]) listener();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** The block that shows the comment; `null` when every block answered and
|
|
48
|
+
* none resolved it (unanchored); `undefined` while an earlier block has
|
|
49
|
+
* not answered yet. */
|
|
50
|
+
owner(annotationId: string): string | null | undefined {
|
|
51
|
+
for (const blockId of this.blockIds) {
|
|
52
|
+
const verdict = this.reports.get(blockId)?.get(annotationId);
|
|
53
|
+
if (verdict === undefined) return undefined;
|
|
54
|
+
if (verdict) return blockId;
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
readonly subscribe = (listener: () => void): (() => void) => {
|
|
60
|
+
this.listeners.add(listener);
|
|
61
|
+
return () => {
|
|
62
|
+
this.listeners.delete(listener);
|
|
63
|
+
};
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
readonly getVersion = (): number => this.version;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Provided by `Viewer` with the document's diagram block ids; a diagram
|
|
70
|
+
* block rendered on its own makes a one-block coordinator for itself. */
|
|
71
|
+
export const DiagramAnchorClaimsContext = createContext<DiagramAnchorClaims | null>(null);
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The diagram engine's host surface (0.41.0): the one viewer over the
|
|
3
|
+
* renderer slot, its canvas, overlay, composer and Source pane, and the
|
|
4
|
+
* hooks and types a host wires them with. The renderer slot itself, the
|
|
5
|
+
* runtime slots and the anchor codecs live under `utils/` (`diagram-render`,
|
|
6
|
+
* `graphviz`, `mermaid`, `diagram-anchor`, `diagram-anchor-graphviz`,
|
|
7
|
+
* `diagram-projection`).
|
|
8
|
+
*/
|
|
9
|
+
export { DiagramViewer, type DiagramViewerProps } from './DiagramViewer';
|
|
10
|
+
export { DiagramPopout } from './DiagramPopout';
|
|
11
|
+
export { DiagramCanvas, svgContentSize, KEY_PAN_PX, type DiagramCanvasHandle, type DiagramEscapeOutcome } from './DiagramCanvas';
|
|
12
|
+
export { DiagramOverlay } from './DiagramOverlay';
|
|
13
|
+
export { DiagramComposer } from './DiagramComposer';
|
|
14
|
+
export { DiagramSourcePane } from './DiagramSourcePane';
|
|
15
|
+
export {
|
|
16
|
+
useDiagramComments,
|
|
17
|
+
type DiagramComment,
|
|
18
|
+
type DiagramComposerDraft,
|
|
19
|
+
type DiagramCreateComment,
|
|
20
|
+
type DiagramHover,
|
|
21
|
+
type ResolvedDiagramComment,
|
|
22
|
+
} from './useDiagramComments';
|
|
23
|
+
export { useDiagramRender, type DiagramRenderState } from './useDiagramRender';
|
|
24
|
+
export { useDiagramSourceDraft, PREVIEW_DEBOUNCE_MS, type DiagramSourceDraft, type SaveResult } from './useDiagramSourceDraft';
|
|
25
|
+
export {
|
|
26
|
+
useDiagramViewport,
|
|
27
|
+
DRAG_THRESHOLD_PX,
|
|
28
|
+
TOUCH_DRAG_THRESHOLD_PX,
|
|
29
|
+
FIT_PADDING_PX,
|
|
30
|
+
WHEEL_ZOOM_SENSITIVITY,
|
|
31
|
+
ZOOM_MAX,
|
|
32
|
+
ZOOM_MIN,
|
|
33
|
+
ZOOM_STEP,
|
|
34
|
+
type ContentSize,
|
|
35
|
+
type Viewport,
|
|
36
|
+
} from './useDiagramViewport';
|