@mhkeller/layercake-annotations 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.
Files changed (42) hide show
  1. package/README.md +184 -0
  2. package/dist/Annotations.svelte +15 -0
  3. package/dist/Annotations.svelte.d.ts +14 -0
  4. package/dist/Editor.svelte +213 -0
  5. package/dist/Editor.svelte.d.ts +19 -0
  6. package/dist/README.md +200 -0
  7. package/dist/Static.svelte +24 -0
  8. package/dist/Static.svelte.d.ts +12 -0
  9. package/dist/components/AnnotationEditor.svelte +127 -0
  10. package/dist/components/AnnotationEditor.svelte.d.ts +14 -0
  11. package/dist/components/AnnotationsData.svelte +56 -0
  12. package/dist/components/AnnotationsData.svelte.d.ts +15 -0
  13. package/dist/components/ArrowZone.svelte +366 -0
  14. package/dist/components/ArrowZone.svelte.d.ts +27 -0
  15. package/dist/components/ArrowheadMarker.svelte +18 -0
  16. package/dist/components/ArrowheadMarker.svelte.d.ts +14 -0
  17. package/dist/components/Arrows.svelte +175 -0
  18. package/dist/components/Arrows.svelte.d.ts +20 -0
  19. package/dist/components/Draggable.svelte +121 -0
  20. package/dist/components/Draggable.svelte.d.ts +33 -0
  21. package/dist/components/EditableText.svelte +131 -0
  22. package/dist/components/EditableText.svelte.d.ts +16 -0
  23. package/dist/components/ResizeHandles.svelte +146 -0
  24. package/dist/components/ResizeHandles.svelte.d.ts +21 -0
  25. package/dist/index.d.ts +7 -0
  26. package/dist/index.js +11 -0
  27. package/dist/modules/arrowUtils.d.ts +17 -0
  28. package/dist/modules/arrowUtils.js +32 -0
  29. package/dist/modules/coordinates.d.ts +85 -0
  30. package/dist/modules/coordinates.js +139 -0
  31. package/dist/modules/createRef.svelte.d.ts +7 -0
  32. package/dist/modules/createRef.svelte.js +10 -0
  33. package/dist/modules/filterObject.d.ts +7 -0
  34. package/dist/modules/filterObject.js +14 -0
  35. package/dist/modules/invertScale.d.ts +1 -0
  36. package/dist/modules/invertScale.js +5 -0
  37. package/dist/modules/newAnnotation.d.ts +9 -0
  38. package/dist/modules/newAnnotation.js +26 -0
  39. package/dist/modules/ordinalInvert.d.ts +9 -0
  40. package/dist/modules/ordinalInvert.js +22 -0
  41. package/dist/types.d.ts +162 -0
  42. package/package.json +61 -0
@@ -0,0 +1,175 @@
1
+ <!--
2
+ @component
3
+ Renders SVG arrows for annotations. Source position is relative to annotation (pixel offsets),
4
+ target position is in data space with optional percentage offsets for ordinal scales.
5
+ During drag, uses live pixel coordinates from dragState.
6
+ -->
7
+ <script>
8
+ /** @typedef {import('../types.js').Annotation} Annotation */
9
+ /** @typedef {import('../types.js').DragState} DragState */
10
+ /** @typedef {import('../types.js').ModifyArrowFn} ModifyArrowFn */
11
+ /**
12
+ * @template T
13
+ * @typedef {import('../types.js').Ref<T>} Ref
14
+ */
15
+
16
+ import { getContext } from 'svelte';
17
+
18
+ import { createArrowPath } from '../modules/arrowUtils.js';
19
+ import { getArrowSource, getArrowTarget } from '../modules/coordinates.js';
20
+
21
+ /** @type {{ annotations?: Annotation[] }} */
22
+ let { annotations = [] } = $props();
23
+
24
+ const { xScale, yScale, x, y, width, height } = getContext('LayerCake');
25
+
26
+ /** @type {Ref<DragState | null> | undefined} - Only available in Editor mode */
27
+ const dragStateRef = getContext('previewArrow');
28
+
29
+ /** @type {ModifyArrowFn | undefined} - Only available in Editor mode */
30
+ const modifyArrow = getContext('modifyArrow');
31
+
32
+ /**
33
+ * Build scales object for coordinate utilities
34
+ */
35
+ function getScales() {
36
+ return {
37
+ xScale: $xScale,
38
+ yScale: $yScale,
39
+ x: $x,
40
+ y: $y,
41
+ width: $width,
42
+ height: $height
43
+ };
44
+ }
45
+
46
+ /**
47
+ * Compute the SVG path for a saved arrow
48
+ */
49
+ function getStaticPath(anno, arrow) {
50
+ const scales = getScales();
51
+ const source = getArrowSource(anno, arrow, scales);
52
+ const target = getArrowTarget(arrow, scales);
53
+ const clockwise = arrow.clockwise !== undefined ? arrow.clockwise : true;
54
+
55
+ return createArrowPath(source, target, clockwise);
56
+ }
57
+
58
+ /**
59
+ * Toggle clockwise on cmd+click - cycle order depends on side
60
+ */
61
+ function handleArrowClick(e, anno, arrow) {
62
+ if (!e.metaKey || !modifyArrow) return;
63
+
64
+ const side = arrow.side;
65
+ const clockwise =
66
+ arrow.clockwise !== undefined ? arrow.clockwise : side === 'west' ? false : true;
67
+
68
+ let newClockwise;
69
+ if (side === 'east') {
70
+ // East: clockwise → straight → counter-clockwise → clockwise
71
+ if (clockwise === true) {
72
+ newClockwise = null;
73
+ } else if (clockwise === null) {
74
+ newClockwise = false;
75
+ } else {
76
+ newClockwise = true;
77
+ }
78
+ } else {
79
+ // West: counter-clockwise → straight → clockwise → counter-clockwise
80
+ if (clockwise === false) {
81
+ newClockwise = null;
82
+ } else if (clockwise === null) {
83
+ newClockwise = true;
84
+ } else {
85
+ newClockwise = false;
86
+ }
87
+ }
88
+
89
+ modifyArrow(anno.id, side, { clockwise: newClockwise });
90
+ }
91
+
92
+ /**
93
+ * Check if a specific arrow is currently being dragged
94
+ */
95
+ let draggingArrowKey = $derived.by(() => {
96
+ if (!dragStateRef) return null;
97
+ const ds = dragStateRef.value;
98
+ if (!ds) return null;
99
+ return `${ds.annotationId}_${ds.side}`;
100
+ });
101
+
102
+ /**
103
+ * Reactive drag path - renders arrow being dragged (new or existing)
104
+ */
105
+ let dragPath = $derived.by(() => {
106
+ if (!dragStateRef) return '';
107
+ const ds = dragStateRef.value;
108
+ if (!ds || ds.annotationId === null || ds.annotationId === undefined) return '';
109
+ if (ds.sourceX == null || ds.targetX == null) return '';
110
+
111
+ const clockwise = ds.clockwise !== undefined ? ds.clockwise : true;
112
+ return createArrowPath(
113
+ { x: ds.sourceX, y: ds.sourceY },
114
+ { x: ds.targetX, y: ds.targetY },
115
+ clockwise
116
+ );
117
+ });
118
+ </script>
119
+
120
+ <g class="swoops">
121
+ <!-- Render saved arrows (hide if this specific arrow is being dragged) -->
122
+ {#each annotations as anno}
123
+ {#if anno.arrows}
124
+ {#each anno.arrows as arrow}
125
+ {@const arrowKey = `${anno.id}_${arrow.side}`}
126
+ {@const isBeingDragged = draggingArrowKey === arrowKey}
127
+ {#if !isBeingDragged}
128
+ {@const pathD = getStaticPath(anno, arrow)}
129
+ <!-- Visible arrow -->
130
+ <path class="arrow-visible" marker-end="url(#layercake-annotation-arrowhead)" d={pathD}
131
+ ></path>
132
+ <!-- Invisible hit area for clicking (edit mode only) -->
133
+ {#if modifyArrow}
134
+ <path
135
+ class="arrow-hitarea"
136
+ d={pathD}
137
+ onclick={(e) => handleArrowClick(e, anno, arrow)}
138
+ onkeydown={(e) => e.key === 'Enter' && handleArrowClick(e, anno, arrow)}
139
+ role="button"
140
+ tabindex="0"
141
+ aria-label="Arrow - Cmd+Enter to toggle curve direction"
142
+ ></path>
143
+ {/if}
144
+ {/if}
145
+ {/each}
146
+ {/if}
147
+ {/each}
148
+
149
+ <!-- Arrow being dragged (new or existing) - rendered with live coordinates -->
150
+ {#if dragPath}
151
+ <path class="arrow-visible" marker-end="url(#layercake-annotation-arrowhead)" d={dragPath}
152
+ ></path>
153
+ {/if}
154
+ </g>
155
+
156
+ <style>
157
+ .swoops {
158
+ position: absolute;
159
+ max-width: 200px;
160
+ line-height: 14px;
161
+ }
162
+ .arrow-visible {
163
+ fill: none;
164
+ stroke: #000;
165
+ stroke-width: 1;
166
+ pointer-events: none;
167
+ }
168
+ .arrow-hitarea {
169
+ fill: none;
170
+ stroke: transparent;
171
+ stroke-width: 12;
172
+ cursor: pointer;
173
+ pointer-events: stroke;
174
+ }
175
+ </style>
@@ -0,0 +1,20 @@
1
+ export default Arrows;
2
+ export type Annotation = import("../types.js").Annotation;
3
+ export type DragState = import("../types.js").DragState;
4
+ export type ModifyArrowFn = import("../types.js").ModifyArrowFn;
5
+ export type Ref<T> = import("../types.js").Ref<T>;
6
+ type Arrows = {
7
+ $on?(type: string, callback: (e: any) => void): () => void;
8
+ $set?(props: Partial<$$ComponentProps>): void;
9
+ };
10
+ /**
11
+ * Renders SVG arrows for annotations. Source position is relative to annotation (pixel offsets),
12
+ * target position is in data space with optional percentage offsets for ordinal scales.
13
+ * During drag, uses live pixel coordinates from dragState.
14
+ */
15
+ declare const Arrows: import("svelte").Component<{
16
+ annotations?: Annotation[];
17
+ }, {}, "">;
18
+ type $$ComponentProps = {
19
+ annotations?: Annotation[];
20
+ };
@@ -0,0 +1,121 @@
1
+ <script>
2
+ /** @typedef {import('../types.js').HoverState} HoverState */
3
+ /**
4
+ * @template T
5
+ * @typedef {import('../types.js').Ref<T>} Ref
6
+ */
7
+
8
+ import { getContext } from 'svelte';
9
+
10
+ let {
11
+ id,
12
+ left,
13
+ top,
14
+ ondrag,
15
+ canDrag = true,
16
+ bannedTargets = [],
17
+ noteDimensions = $bindable(),
18
+ containerClass = '.chart-container',
19
+ width,
20
+ onclick,
21
+ children
22
+ } = $props();
23
+
24
+ /**
25
+ * State vars
26
+ */
27
+ let el = $state();
28
+ let isBanned = $state(false);
29
+ let thisMoving = $state(false);
30
+
31
+ /** @type {Ref<HoverState | null>} */
32
+ const hovering = getContext('hovering');
33
+ /** @type {Ref<boolean>} */
34
+ const moving = getContext('moving');
35
+ const { padding } = getContext('LayerCake');
36
+
37
+ function onmousedown(e) {
38
+ moving.value = true;
39
+ thisMoving = true;
40
+ isBanned = [...e.target.classList].some((c) => bannedTargets.includes(c));
41
+ }
42
+
43
+ /**
44
+ * Broadcast the elements movements on drag
45
+ */
46
+ function onmousemove(e) {
47
+ if (thisMoving && canDrag && !isBanned) {
48
+ const { left, top } = el.getBoundingClientRect();
49
+
50
+ const parent = el.closest(containerClass).getBoundingClientRect();
51
+
52
+ ondrag([
53
+ left - parent.left - $padding.left + e.movementX,
54
+ top - parent.top - $padding.top - 0 + e.movementY
55
+ ]);
56
+ }
57
+ }
58
+
59
+ function onmouseup() {
60
+ moving.value = false;
61
+ thisMoving = false;
62
+ }
63
+ function onmouseover() {
64
+ if (moving.value) return;
65
+ hovering.value = { annotationId: id, type: 'body' };
66
+ }
67
+ function onmouseout() {
68
+ if (moving.value) return;
69
+ hovering.value = null;
70
+ }
71
+ </script>
72
+
73
+ <div
74
+ bind:this={el}
75
+ style:left
76
+ style:top
77
+ style:width
78
+ class="draggable"
79
+ class:canDrag
80
+ class:hovering={hovering.value?.annotationId === id}
81
+ {onclick}
82
+ {onmousedown}
83
+ {onmouseover}
84
+ {onmouseout}
85
+ onfocus={onmouseover}
86
+ onblur={onmouseout}
87
+ onkeydown={(e) => e.key === 'Delete' && onclick(e)}
88
+ role="button"
89
+ tabindex="0"
90
+ aria-label="Annotation - drag to move, press Delete to remove"
91
+ bind:clientWidth={noteDimensions[0]}
92
+ bind:clientHeight={noteDimensions[1]}
93
+ >
94
+ {@render children()}
95
+ </div>
96
+
97
+ <svelte:window {onmouseup} {onmousemove} />
98
+
99
+ <style>
100
+ .draggable {
101
+ position: absolute;
102
+ display: inline-block;
103
+ box-sizing: border-box;
104
+ transition: border-color 250ms;
105
+ border-radius: 2px;
106
+ padding: 3px;
107
+ border: 1px solid transparent;
108
+ }
109
+ .draggable.hovering {
110
+ border-color: red;
111
+ }
112
+
113
+ .draggable.canDrag {
114
+ user-select: none;
115
+ cursor: move;
116
+ }
117
+
118
+ .draggable.hovering :global(.grabber) {
119
+ opacity: 1;
120
+ }
121
+ </style>
@@ -0,0 +1,33 @@
1
+ export default Draggable;
2
+ export type HoverState = import("../types.js").HoverState;
3
+ export type Ref<T> = import("../types.js").Ref<T>;
4
+ type Draggable = {
5
+ $on?(type: string, callback: (e: any) => void): () => void;
6
+ $set?(props: Partial<$$ComponentProps>): void;
7
+ };
8
+ declare const Draggable: import("svelte").Component<{
9
+ id: any;
10
+ left: any;
11
+ top: any;
12
+ ondrag: any;
13
+ canDrag?: boolean;
14
+ bannedTargets?: any[];
15
+ noteDimensions?: any;
16
+ containerClass?: string;
17
+ width: any;
18
+ onclick: any;
19
+ children: any;
20
+ }, {}, "noteDimensions">;
21
+ type $$ComponentProps = {
22
+ id: any;
23
+ left: any;
24
+ top: any;
25
+ ondrag: any;
26
+ canDrag?: boolean;
27
+ bannedTargets?: any[];
28
+ noteDimensions?: any;
29
+ containerClass?: string;
30
+ width: any;
31
+ onclick: any;
32
+ children: any;
33
+ };
@@ -0,0 +1,131 @@
1
+ <script>
2
+ /**
3
+ * @template T
4
+ * @typedef {import('../types.js').Ref<T>} Ref
5
+ */
6
+
7
+ import { getContext } from 'svelte';
8
+
9
+ /** @type {Ref<boolean>} */
10
+ const isEditing = getContext('isEditing');
11
+
12
+ let { text = $bindable(), isEditable = $bindable(false), alignment } = $props();
13
+
14
+ let textarea = $state(null);
15
+
16
+ function selectAllTextInContentEditable(element) {
17
+ const selection = window.getSelection();
18
+ const range = document.createRange();
19
+ range.selectNodeContents(element);
20
+ selection.removeAllRanges();
21
+ selection.addRange(range);
22
+ }
23
+
24
+ function cancelEdit() {
25
+ isEditable = false;
26
+ text = text.trim();
27
+ window.removeEventListener('keydown', handleKeydown);
28
+ document.removeEventListener('click', handleClickOutside);
29
+
30
+ // Wait for the click event to propagate before setting isEditing to false
31
+ setTimeout(() => {
32
+ isEditing.value = false;
33
+ }, 200);
34
+ }
35
+
36
+ function handleClickOutside(event) {
37
+ if (isEditable && textarea && !textarea.contains(event.target)) {
38
+ textarea.blur();
39
+ }
40
+ }
41
+
42
+ function handleKeydown(e) {
43
+ if (e.key === 'Escape' || e.key === 'Tab') {
44
+ textarea.blur();
45
+ }
46
+ // Enter without shift saves, shift+enter allows line break
47
+ if (e.key === 'Enter' && !e.shiftKey) {
48
+ e.preventDefault();
49
+ textarea.blur();
50
+ }
51
+ }
52
+
53
+ $effect(() => {
54
+ if (textarea && isEditable) {
55
+ textarea.focus();
56
+ selectAllTextInContentEditable(textarea);
57
+ window.addEventListener('keydown', handleKeydown);
58
+ }
59
+ });
60
+
61
+ function handleDoubleClick(e) {
62
+ // Don't enter edit mode if Cmd is held (used for alignment cycling)
63
+ if (e.metaKey) return;
64
+ isEditable = true;
65
+ isEditing.value = true;
66
+ document.addEventListener('click', handleClickOutside);
67
+ }
68
+ function onclick(e) {
69
+ if (isEditable) {
70
+ e.stopPropagation();
71
+ // If we are inside a contenteditable element, don't propagate the click event
72
+ e.preventDefault();
73
+ return false;
74
+ }
75
+ }
76
+ </script>
77
+
78
+ {#if isEditable}
79
+ <!-- svelte-ignore a11y_click_events_have_key_events -->
80
+ <div
81
+ class="textarea"
82
+ role="textbox"
83
+ aria-multiline="true"
84
+ tabindex="0"
85
+ bind:this={textarea}
86
+ onblur={cancelEdit}
87
+ {onclick}
88
+ ondblclick={handleDoubleClick}
89
+ contenteditable
90
+ bind:innerText={text}
91
+ style:text-align={alignment}
92
+ ></div>
93
+ {:else}
94
+ <div
95
+ class="text-display"
96
+ ondblclick={handleDoubleClick}
97
+ onkeydown={(e) => e.key === 'Enter' && handleDoubleClick()}
98
+ role="button"
99
+ tabindex="0"
100
+ aria-label="Double-click or press Enter to edit"
101
+ style:text-align={alignment}
102
+ >
103
+ <pre>{text}</pre>
104
+ </div>
105
+ {/if}
106
+
107
+ <style>
108
+ .textarea[contenteditable] {
109
+ outline: none;
110
+ position: relative;
111
+ white-space: pre-wrap;
112
+ }
113
+ .textarea[contenteditable]:after {
114
+ position: absolute;
115
+ content: '';
116
+ top: -2px;
117
+ right: -4px;
118
+ bottom: -2px;
119
+ left: -4px;
120
+ pointer-events: none;
121
+ border-radius: 3px;
122
+ border: 2px solid #007bff;
123
+ box-shadow: 0 0 5px #007bff50;
124
+ }
125
+ pre {
126
+ margin: 0;
127
+ font-family: inherit;
128
+ white-space: pre-wrap;
129
+ word-wrap: break-word;
130
+ }
131
+ </style>
@@ -0,0 +1,16 @@
1
+ export default EditableText;
2
+ export type Ref<T> = import("../types.js").Ref<T>;
3
+ type EditableText = {
4
+ $on?(type: string, callback: (e: any) => void): () => void;
5
+ $set?(props: Partial<$$ComponentProps>): void;
6
+ };
7
+ declare const EditableText: import("svelte").Component<{
8
+ text?: any;
9
+ isEditable?: boolean;
10
+ alignment: any;
11
+ }, {}, "text" | "isEditable">;
12
+ type $$ComponentProps = {
13
+ text?: any;
14
+ isEditable?: boolean;
15
+ alignment: any;
16
+ };
@@ -0,0 +1,146 @@
1
+ <!--
2
+ @component
3
+ Horizontal resize handles for annotation text boxes.
4
+ Supports west (left) and east (right) resizing only.
5
+ -->
6
+ <script>
7
+ import { getContext } from 'svelte';
8
+
9
+ let {
10
+ /** Which handles to show: 'west', 'east', or both */
11
+ grabbers = ['west', 'east'],
12
+ /** Current width in pixels (bound) - number or "Npx" string */
13
+ width = $bindable(),
14
+ /** Callback when resizing */
15
+ ondrag,
16
+ /** Container selector for position calculations */
17
+ containerClass = '.chart-container'
18
+ } = $props();
19
+
20
+ /** Parse width to number */
21
+ function parseWidth(w) {
22
+ if (typeof w === 'number') return w;
23
+ if (typeof w === 'string') return parseInt(w) || 0;
24
+ return 0;
25
+ }
26
+
27
+ const { padding } = getContext('LayerCake');
28
+
29
+ let active = $state(null);
30
+ let initialRect = $state(null);
31
+ let initialPos = $state(null);
32
+
33
+ function onmousedown(event) {
34
+ event.stopPropagation();
35
+ active = event.target;
36
+ const rect = active.parentElement.getBoundingClientRect();
37
+ initialRect = {
38
+ width: rect.width,
39
+ left: rect.left,
40
+ top: rect.top
41
+ };
42
+ initialPos = { x: event.pageX };
43
+ active.classList.add('selected');
44
+
45
+ window.addEventListener('mousemove', onmousemove);
46
+ window.addEventListener('mouseup', onmouseup);
47
+ }
48
+
49
+ function onmouseup() {
50
+ if (!active) return;
51
+
52
+ active.classList.remove('selected');
53
+ active = null;
54
+ initialRect = null;
55
+ initialPos = null;
56
+
57
+ window.removeEventListener('mousemove', onmousemove);
58
+ window.removeEventListener('mouseup', onmouseup);
59
+ }
60
+
61
+ function onmousemove(event) {
62
+ if (!active) return;
63
+
64
+ const isEast = active.classList.contains('east');
65
+ const isWest = active.classList.contains('west');
66
+
67
+ if (isEast) {
68
+ const delta = event.pageX - initialPos.x;
69
+ const newWidth = Math.round(initialRect.width + delta);
70
+ if (newWidth < 50) return;
71
+ width = `${newWidth}px`;
72
+ ondrag();
73
+ }
74
+
75
+ if (isWest) {
76
+ const delta = initialPos.x - event.pageX;
77
+ const newWidth = Math.round(initialRect.width + delta);
78
+ if (newWidth < 50) return;
79
+
80
+ width = `${newWidth}px`;
81
+
82
+ // Calculate new position - the left edge moves with the mouse
83
+ const parent = active.parentElement.closest(containerClass)?.getBoundingClientRect();
84
+ if (parent) {
85
+ const newLeft = event.pageX - parent.left - $padding.left;
86
+ const newTop = initialRect.top - parent.top - $padding.top;
87
+ ondrag([newLeft, newTop]);
88
+ } else {
89
+ ondrag();
90
+ }
91
+ }
92
+ }
93
+
94
+ /** Keyboard resize handler */
95
+ function onResize(delta) {
96
+ const currentWidth = parseWidth(width);
97
+ const newWidth = Math.max(50, currentWidth + delta);
98
+ width = `${newWidth}px`;
99
+ ondrag();
100
+ }
101
+ </script>
102
+
103
+ {#each grabbers as grabber}
104
+ <div
105
+ class="grabber {grabber}"
106
+ {onmousedown}
107
+ onkeydown={(e) => {
108
+ if (e.key === 'ArrowLeft') { onResize(-10); e.preventDefault(); }
109
+ if (e.key === 'ArrowRight') { onResize(10); e.preventDefault(); }
110
+ }}
111
+ role="slider"
112
+ tabindex="0"
113
+ aria-label="Resize handle - use arrow keys to adjust width"
114
+ aria-valuenow={width}
115
+ ></div>
116
+ {/each}
117
+
118
+ <style>
119
+ .grabber {
120
+ position: absolute;
121
+ box-sizing: border-box;
122
+ transition: opacity 250ms;
123
+ opacity: 0;
124
+ z-index: 9999;
125
+ width: 3px;
126
+ height: 70%;
127
+ top: 50%;
128
+ background: red;
129
+ border-radius: 2px;
130
+ cursor: col-resize;
131
+ }
132
+
133
+ .grabber.west {
134
+ left: -0.5px;
135
+ transform: translateX(-50%) translateY(-50%);
136
+ }
137
+
138
+ .grabber.east {
139
+ right: -0.5px;
140
+ transform: translateX(50%) translateY(-50%);
141
+ }
142
+
143
+ .grabber.selected {
144
+ opacity: 1;
145
+ }
146
+ </style>
@@ -0,0 +1,21 @@
1
+ export default ResizeHandles;
2
+ type ResizeHandles = {
3
+ $on?(type: string, callback: (e: any) => void): () => void;
4
+ $set?(props: Partial<$$ComponentProps>): void;
5
+ };
6
+ /**
7
+ * Horizontal resize handles for annotation text boxes.
8
+ * Supports west (left) and east (right) resizing only.
9
+ */
10
+ declare const ResizeHandles: import("svelte").Component<{
11
+ grabbers?: any[];
12
+ width?: any;
13
+ ondrag: any;
14
+ containerClass?: string;
15
+ }, {}, "width">;
16
+ type $$ComponentProps = {
17
+ grabbers?: any[];
18
+ width?: any;
19
+ ondrag: any;
20
+ containerClass?: string;
21
+ };
@@ -0,0 +1,7 @@
1
+ export { default as Annotations } from "./Annotations.svelte";
2
+ export { default as AnnotationsEditor } from "./Editor.svelte";
3
+ export { default as AnnotationsStatic } from "./Static.svelte";
4
+ export type Annotation = import("./types.js").Annotation;
5
+ export type Arrow = import("./types.js").Arrow;
6
+ export type ArrowSource = import("./types.js").ArrowSource;
7
+ export type ArrowTarget = import("./types.js").ArrowTarget;
package/dist/index.js ADDED
@@ -0,0 +1,11 @@
1
+ export { default as Annotations } from './Annotations.svelte';
2
+ export { default as AnnotationsEditor } from './Editor.svelte';
3
+ export { default as AnnotationsStatic } from './Static.svelte';
4
+
5
+ // Re-export types for consumers
6
+ /**
7
+ * @typedef {import('./types.js').Annotation} Annotation
8
+ * @typedef {import('./types.js').Arrow} Arrow
9
+ * @typedef {import('./types.js').ArrowSource} ArrowSource
10
+ * @typedef {import('./types.js').ArrowTarget} ArrowTarget
11
+ */
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Create an SVG arc path between two points.
3
+ * Adapted from bizweekgraphics/swoopyarrows
4
+ *
5
+ * @param {{ x: number, y: number }} source - Start point
6
+ * @param {{ x: number, y: number }} target - End point
7
+ * @param {boolean|null} clockwise - Arc direction (null for straight line)
8
+ * @param {number} [angle=Math.PI/2] - Arc angle in radians
9
+ * @returns {string} SVG path string
10
+ */
11
+ export function createArrowPath(source: {
12
+ x: number;
13
+ y: number;
14
+ }, target: {
15
+ x: number;
16
+ y: number;
17
+ }, clockwise: boolean | null, angle?: number): string;