@liminis/diagrams 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE
CHANGED
|
@@ -19,16 +19,3 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
|
19
19
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
20
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
21
|
SOFTWARE.
|
|
22
|
-
|
|
23
|
-
---
|
|
24
|
-
|
|
25
|
-
This package vendors a modified copy of `mdast-util-wiki-link` (MIT,
|
|
26
|
-
Copyright (c) 2020 Mark Hudnall). Its license and a description of the
|
|
27
|
-
modifications ship alongside it, at
|
|
28
|
-
`dist/markdown/vendor/mdast-util-wiki-link/`.
|
|
29
|
-
|
|
30
|
-
This package derives from the `webview-ui` package of SlashMD
|
|
31
|
-
(https://github.com/wolfdavo/SlashMD) by David Wolfenden, which that project's
|
|
32
|
-
README declares to be MIT-licensed. SlashMD carries no LICENSE file and no
|
|
33
|
-
copyright notice, so none is reproduced here; this notice records the
|
|
34
|
-
derivation and the stated license in their absence.
|
package/README.md
CHANGED
|
@@ -7,6 +7,15 @@ editing.
|
|
|
7
7
|
Extracted from [`@liminis/editor`](https://github.com/verveguy/liminis-editor), where it
|
|
8
8
|
renders ` ```c4 ` fenced code blocks. Nothing here is bound to that editor.
|
|
9
9
|
|
|
10
|
+
## Demo
|
|
11
|
+
|
|
12
|
+
**[https://v3rv.com/liminis-diagrams/](https://v3rv.com/liminis-diagrams/)** — edit
|
|
13
|
+
C4-PlantUML source and see it re-render live, drag nodes to reposition them, toggle dark
|
|
14
|
+
mode, and switch between a few preset diagrams. The demo keeps dragged positions in
|
|
15
|
+
memory only, for as long as the tab is open — this package has no persistence of its
|
|
16
|
+
own (see [Recipe 3](docs/recipes.md#recipe-3-position-persistence--the-hosts-choice)),
|
|
17
|
+
and neither does this demo.
|
|
18
|
+
|
|
10
19
|
## Install
|
|
11
20
|
|
|
12
21
|
```bash
|
|
@@ -281,7 +281,14 @@ function C4InteractiveSvg({ layout, isDarkMode, isEditMode, draggedNodeId, svgRe
|
|
|
281
281
|
cursor: isEditMode ? (draggedNodeId ? 'grabbing' : 'default') : 'default',
|
|
282
282
|
}, children: [_jsx(C4RendererContent, { layout: layout, isDarkMode: isDarkMode, legendPositionOverride: legendPositionOverride }), isEditMode && (_jsx("g", { className: "interactive-layer", children: hitAreas.map((area) => (_jsx("rect", { "data-node-id": area.id, x: area.x, y: area.y, width: area.width, height: area.height, fill: "transparent", stroke: "transparent", style: {
|
|
283
283
|
cursor: draggedNodeId === area.id ? 'grabbing' : 'grab',
|
|
284
|
-
|
|
284
|
+
// Without this the browser claims the gesture for panning and
|
|
285
|
+
// zooming before any pointermove reaches us, so a touch drag
|
|
286
|
+
// scrolls the page instead of moving the node. `none` is
|
|
287
|
+
// deliberate rather than `pan-y`: the hit area is a drag
|
|
288
|
+
// target, and a partial allowance still lets the browser
|
|
289
|
+
// steal the gesture mid-drag and fire pointercancel.
|
|
290
|
+
touchAction: 'none',
|
|
291
|
+
}, onPointerDown: (e) => onNodeMouseDown(area.id, area.x, area.y, e) }, area.id))) })), isEditMode && (_jsx("g", { className: "drag-handles-layer", children: hitAreas.map((area) => (_jsx(DragHandle, { x: area.x + area.width - 20, y: area.y + 4, color: handleColor, isDragging: draggedNodeId === area.id }, `handle-${area.id}`))) }))] }) }));
|
|
285
292
|
}
|
|
286
293
|
/**
|
|
287
294
|
* Drag handle indicator (grip icon).
|
|
@@ -4,6 +4,15 @@
|
|
|
4
4
|
* Provides drag-and-drop functionality for repositioning C4 diagram elements.
|
|
5
5
|
* Uses window-level listeners during drag so the interaction continues even
|
|
6
6
|
* when the cursor leaves the SVG bounds.
|
|
7
|
+
*
|
|
8
|
+
* Pointer events, not mouse events. This started as mouse-only and did not work
|
|
9
|
+
* on touch devices at all: iPadOS and mobile Safari synthesize `mousedown` and
|
|
10
|
+
* `click` *after* a gesture resolves, but never emit the `mousemove` stream a
|
|
11
|
+
* drag needs, so the node never moved and the page panned instead. Pointer
|
|
12
|
+
* events unify mouse, touch and pen, and are the only way to get one code path
|
|
13
|
+
* for all three. `pointercancel` matters here too — the browser fires it when it
|
|
14
|
+
* decides a touch is a scroll or a system gesture, and without handling it the
|
|
15
|
+
* drag would stay stuck open with no terminating `pointerup`.
|
|
7
16
|
*/
|
|
8
17
|
import { RefObject } from 'react';
|
|
9
18
|
export interface UseC4DiagramDragProps {
|
|
@@ -22,7 +31,12 @@ export interface UseC4DiagramDragReturn {
|
|
|
22
31
|
/** Whether a drag is in progress */
|
|
23
32
|
isDragging: boolean;
|
|
24
33
|
/** Start dragging a node */
|
|
25
|
-
|
|
34
|
+
/**
|
|
35
|
+
* Start dragging a node. Accepts a pointer or mouse event: the parameter is
|
|
36
|
+
* widened rather than switched so existing callers passing a React.MouseEvent
|
|
37
|
+
* still typecheck.
|
|
38
|
+
*/
|
|
39
|
+
startNodeDrag: (nodeId: string, nodeX: number, nodeY: number, e: React.PointerEvent | React.MouseEvent) => void;
|
|
26
40
|
/** Convert screen coordinates to SVG coordinates */
|
|
27
41
|
screenToSvg: (clientX: number, clientY: number) => {
|
|
28
42
|
x: number;
|
|
@@ -32,7 +46,7 @@ export interface UseC4DiagramDragReturn {
|
|
|
32
46
|
/**
|
|
33
47
|
* Hook for managing drag interactions on C4 diagram nodes.
|
|
34
48
|
*
|
|
35
|
-
* During a drag,
|
|
36
|
-
* interaction continues seamlessly when the
|
|
49
|
+
* During a drag, pointermove/pointerup/pointercancel are handled on `window` so
|
|
50
|
+
* the interaction continues seamlessly when the pointer leaves the SVG.
|
|
37
51
|
*/
|
|
38
52
|
export declare function useC4DiagramDrag({ svgRef, onNodeDrag, onNodeDragEnd, enabled, }: UseC4DiagramDragProps): UseC4DiagramDragReturn;
|
|
@@ -4,13 +4,22 @@
|
|
|
4
4
|
* Provides drag-and-drop functionality for repositioning C4 diagram elements.
|
|
5
5
|
* Uses window-level listeners during drag so the interaction continues even
|
|
6
6
|
* when the cursor leaves the SVG bounds.
|
|
7
|
+
*
|
|
8
|
+
* Pointer events, not mouse events. This started as mouse-only and did not work
|
|
9
|
+
* on touch devices at all: iPadOS and mobile Safari synthesize `mousedown` and
|
|
10
|
+
* `click` *after* a gesture resolves, but never emit the `mousemove` stream a
|
|
11
|
+
* drag needs, so the node never moved and the page panned instead. Pointer
|
|
12
|
+
* events unify mouse, touch and pen, and are the only way to get one code path
|
|
13
|
+
* for all three. `pointercancel` matters here too — the browser fires it when it
|
|
14
|
+
* decides a touch is a scroll or a system gesture, and without handling it the
|
|
15
|
+
* drag would stay stuck open with no terminating `pointerup`.
|
|
7
16
|
*/
|
|
8
17
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
9
18
|
/**
|
|
10
19
|
* Hook for managing drag interactions on C4 diagram nodes.
|
|
11
20
|
*
|
|
12
|
-
* During a drag,
|
|
13
|
-
* interaction continues seamlessly when the
|
|
21
|
+
* During a drag, pointermove/pointerup/pointercancel are handled on `window` so
|
|
22
|
+
* the interaction continues seamlessly when the pointer leaves the SVG.
|
|
14
23
|
*/
|
|
15
24
|
export function useC4DiagramDrag({ svgRef, onNodeDrag, onNodeDragEnd, enabled, }) {
|
|
16
25
|
const [draggedNodeId, setDraggedNodeId] = useState(null);
|
|
@@ -18,6 +27,14 @@ export function useC4DiagramDrag({ svgRef, onNodeDrag, onNodeDragEnd, enabled, }
|
|
|
18
27
|
const dragOffsetRef = useRef({ x: 0, y: 0 });
|
|
19
28
|
// Track the last known position for dragEnd callback
|
|
20
29
|
const lastPositionRef = useRef({ x: 0, y: 0 });
|
|
30
|
+
// The pointer that owns the current drag. Mouse events have exactly one
|
|
31
|
+
// stream; pointer events do not — a touch device can deliver concurrent
|
|
32
|
+
// streams for several fingers at once. Without this, a second finger landing
|
|
33
|
+
// on another node overwrites the single-slot drag state, so the first
|
|
34
|
+
// finger's moves start dragging the second node and either finger's release
|
|
35
|
+
// ends the drag. Null when no drag is active; null for a mouse-event caller,
|
|
36
|
+
// which cannot be concurrent anyway.
|
|
37
|
+
const activePointerIdRef = useRef(null);
|
|
21
38
|
// Locked CTM inverse captured at drag start — prevents the accelerating
|
|
22
39
|
// feedback loop where canvas expansion changes the scale mid-drag
|
|
23
40
|
const lockedCtmInverseRef = useRef(null);
|
|
@@ -54,8 +71,14 @@ export function useC4DiagramDrag({ svgRef, onNodeDrag, onNodeDragEnd, enabled, }
|
|
|
54
71
|
const startNodeDrag = useCallback((nodeId, nodeX, nodeY, e) => {
|
|
55
72
|
if (!enabled)
|
|
56
73
|
return;
|
|
74
|
+
// One drag at a time. A pointerdown arriving while a drag is already in
|
|
75
|
+
// progress is a second finger, not a new intent — ignore it rather than
|
|
76
|
+
// letting it hijack the drag already underway.
|
|
77
|
+
if (draggedNodeIdRef.current !== null)
|
|
78
|
+
return;
|
|
57
79
|
e.stopPropagation();
|
|
58
80
|
e.preventDefault();
|
|
81
|
+
activePointerIdRef.current = 'pointerId' in e ? e.pointerId : null;
|
|
59
82
|
// Lock the CTM at drag start
|
|
60
83
|
const svg = svgRef.current;
|
|
61
84
|
const ctm = svg?.getScreenCTM();
|
|
@@ -75,10 +98,13 @@ export function useC4DiagramDrag({ svgRef, onNodeDrag, onNodeDragEnd, enabled, }
|
|
|
75
98
|
useEffect(() => {
|
|
76
99
|
if (!draggedNodeId)
|
|
77
100
|
return;
|
|
78
|
-
const
|
|
101
|
+
const handlePointerMove = (e) => {
|
|
79
102
|
const nodeId = draggedNodeIdRef.current;
|
|
80
103
|
if (!nodeId)
|
|
81
104
|
return;
|
|
105
|
+
// Movement from any other finger is not this drag.
|
|
106
|
+
if (activePointerIdRef.current !== null && e.pointerId !== activePointerIdRef.current)
|
|
107
|
+
return;
|
|
82
108
|
const svgPoint = screenToSvgRef.current(e.clientX, e.clientY);
|
|
83
109
|
if (!svgPoint)
|
|
84
110
|
return;
|
|
@@ -87,20 +113,29 @@ export function useC4DiagramDrag({ svgRef, onNodeDrag, onNodeDragEnd, enabled, }
|
|
|
87
113
|
lastPositionRef.current = { x: newX, y: newY };
|
|
88
114
|
onNodeDragRef.current?.(nodeId, newX, newY);
|
|
89
115
|
};
|
|
90
|
-
const
|
|
116
|
+
const endDrag = (e) => {
|
|
91
117
|
const nodeId = draggedNodeIdRef.current;
|
|
92
118
|
if (!nodeId)
|
|
93
119
|
return;
|
|
120
|
+
// A second finger lifting must not end the drag the first one owns.
|
|
121
|
+
if (activePointerIdRef.current !== null && e.pointerId !== activePointerIdRef.current)
|
|
122
|
+
return;
|
|
94
123
|
onNodeDragEndRef.current?.(nodeId, lastPositionRef.current.x, lastPositionRef.current.y);
|
|
95
124
|
draggedNodeIdRef.current = null;
|
|
125
|
+
activePointerIdRef.current = null;
|
|
96
126
|
lockedCtmInverseRef.current = null;
|
|
97
127
|
setDraggedNodeId(null);
|
|
98
128
|
};
|
|
99
|
-
window.addEventListener('
|
|
100
|
-
window.addEventListener('
|
|
129
|
+
window.addEventListener('pointermove', handlePointerMove);
|
|
130
|
+
window.addEventListener('pointerup', endDrag);
|
|
131
|
+
// Fired when the browser takes the gesture over (scroll, system swipe) or
|
|
132
|
+
// the pointer is otherwise lost. Without it a touch drag can end with no
|
|
133
|
+
// `pointerup` at all, leaving the node stuck to the finger.
|
|
134
|
+
window.addEventListener('pointercancel', endDrag);
|
|
101
135
|
return () => {
|
|
102
|
-
window.removeEventListener('
|
|
103
|
-
window.removeEventListener('
|
|
136
|
+
window.removeEventListener('pointermove', handlePointerMove);
|
|
137
|
+
window.removeEventListener('pointerup', endDrag);
|
|
138
|
+
window.removeEventListener('pointercancel', endDrag);
|
|
104
139
|
};
|
|
105
140
|
}, [draggedNodeId]);
|
|
106
141
|
return {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@liminis/diagrams",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "C4 architecture diagrams: parse C4-PlantUML, lay out with dagre, render to SVG",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"//repository": "Not cosmetic, and not optional. npm matches this URL against the GitHub Actions OIDC claim when publishing with --provenance; without it the registry rejects the publish outright (E422) after the release tag has already been cut. That is exactly how 0.1.0's first release attempt failed (#6). The `git+https://` scheme and the `.git` suffix are both part of the match — the SSH form does not work.",
|
|
@@ -48,7 +48,8 @@
|
|
|
48
48
|
"./server": {
|
|
49
49
|
"types": "./dist/server.d.ts",
|
|
50
50
|
"default": "./dist/server.js"
|
|
51
|
-
}
|
|
51
|
+
},
|
|
52
|
+
"./package.json": "./package.json"
|
|
52
53
|
},
|
|
53
54
|
"publishConfig": {
|
|
54
55
|
"access": "public"
|
|
@@ -61,6 +62,7 @@
|
|
|
61
62
|
"clean": "rm -rf dist",
|
|
62
63
|
"prepack": "pnpm run build",
|
|
63
64
|
"prepublishOnly": "node scripts/guard-publish.mjs",
|
|
65
|
+
"verify:package": "node scripts/verify-package.mjs",
|
|
64
66
|
"lint": "eslint src/",
|
|
65
67
|
"lint:fix": "eslint src/ --fix",
|
|
66
68
|
"typecheck": "tsc --noEmit",
|