@plannotator/ui 0.25.0 → 0.27.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.
@@ -23,6 +23,16 @@ interface UseResizablePanelOptions {
23
23
  * (still rAF-coalesced).
24
24
  */
25
25
  apply?: (width: number) => void;
26
+ /**
27
+ * Fires on pointer-up when the pointer never moved beyond `clickThreshold` —
28
+ * i.e. a genuine click on the handle, not a drag. The hook owns the pointer
29
+ * state machine, so this is the only reliable way to tell a click from a
30
+ * drag-start. Lets a host make the whole handle a click target (e.g. click
31
+ * anywhere on the handle to collapse the panel). Not fired on a snap-close.
32
+ */
33
+ onClick?: () => void;
34
+ /** Max pointer travel (px) still counted as a click for `onClick`. Default 4. */
35
+ clickThreshold?: number;
26
36
  }
27
37
 
28
38
  export interface ResizeHandleProps {
@@ -43,6 +53,8 @@ export function useResizablePanel({
43
53
  onSnapClose,
44
54
  snapCloseRatio = 0.6,
45
55
  apply,
56
+ onClick,
57
+ clickThreshold = 4,
46
58
  }: UseResizablePanelOptions) {
47
59
  const [width, setWidth] = useState(() => {
48
60
  const saved = storage.getItem(storageKey);
@@ -61,6 +73,9 @@ export function useResizablePanel({
61
73
  const startWidthRef = useRef(0);
62
74
  const draggingRef = useRef(false);
63
75
  const snappedRef = useRef(false);
76
+ // Latches true once the pointer travels past clickThreshold — distinguishes a
77
+ // click from a drag. Reset on each pointerdown.
78
+ const movedRef = useRef(false);
64
79
  const latestXRef = useRef(0);
65
80
  const rafRef = useRef<number | null>(null);
66
81
 
@@ -69,6 +84,8 @@ export function useResizablePanel({
69
84
  applyRef.current = apply;
70
85
  const onSnapCloseRef = useRef(onSnapClose);
71
86
  onSnapCloseRef.current = onSnapClose;
87
+ const onClickRef = useRef(onClick);
88
+ onClickRef.current = onClick;
72
89
 
73
90
  // rAF tick: compute the width from the most recent pointer position and apply
74
91
  // it. At most one DOM/state write per frame regardless of pointer-event rate.
@@ -110,6 +127,7 @@ export function useResizablePanel({
110
127
  startWidthRef.current = widthRef.current;
111
128
  latestXRef.current = pos;
112
129
  snappedRef.current = false;
130
+ movedRef.current = false;
113
131
  draggingRef.current = true;
114
132
  setIsDragging(true);
115
133
 
@@ -120,6 +138,8 @@ export function useResizablePanel({
120
138
  const onMove = (ev: PointerEvent) => {
121
139
  if (!draggingRef.current) return;
122
140
  latestXRef.current = axis === 'y' ? ev.clientY : ev.clientX;
141
+ if (Math.abs(latestXRef.current - startXRef.current) > clickThreshold)
142
+ movedRef.current = true;
123
143
  if (rafRef.current == null) rafRef.current = requestAnimationFrame(flush);
124
144
  };
125
145
  const cleanup = () => {
@@ -127,7 +147,11 @@ export function useResizablePanel({
127
147
  window.removeEventListener('pointerup', onUp);
128
148
  window.removeEventListener('pointercancel', onUp);
129
149
  };
130
- function onUp() {
150
+ function onUp(e: PointerEvent) {
151
+ // pointercancel = the browser aborted the gesture (palm rejection, a
152
+ // system gesture, focus loss). It is NOT a completed click — only clean
153
+ // up drag state, never treat it as a click-to-collapse.
154
+ const cancelled = e?.type === 'pointercancel';
131
155
  const wasSnapped = snappedRef.current;
132
156
  draggingRef.current = false;
133
157
  snappedRef.current = false;
@@ -137,9 +161,16 @@ export function useResizablePanel({
137
161
  }
138
162
  setIsDragging(false);
139
163
  if (!wasSnapped) {
140
- // Commit the live width to React state + persist.
141
- setWidth(widthRef.current);
142
- storage.setItem(storageKey, String(widthRef.current));
164
+ if (onClickRef.current && !movedRef.current && !cancelled) {
165
+ // A pointerdown+up that never crossed the threshold is a click, not
166
+ // a resize — fire onClick and leave the width untouched. Only when a
167
+ // host opts in via onClick; otherwise commit exactly as before.
168
+ onClickRef.current();
169
+ } else {
170
+ // Commit the live width to React state + persist.
171
+ setWidth(widthRef.current);
172
+ storage.setItem(storageKey, String(widthRef.current));
173
+ }
143
174
  }
144
175
  cleanup();
145
176
  }
@@ -148,7 +179,7 @@ export function useResizablePanel({
148
179
  window.addEventListener('pointerup', onUp);
149
180
  window.addEventListener('pointercancel', onUp);
150
181
  },
151
- [flush, storageKey, axis],
182
+ [flush, storageKey, axis, clickThreshold],
152
183
  );
153
184
 
154
185
  const resetWidth = useCallback(() => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plannotator/ui",
3
- "version": "0.25.0",
3
+ "version": "0.27.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  "./components/*": "./components/*.tsx",
@@ -65,9 +65,9 @@
65
65
  "@lezer/common": "^1.5.2",
66
66
  "@lezer/highlight": "^1.2.3",
67
67
  "@pierre/diffs": "1.2.8",
68
- "@plannotator/atomic-editor": "^0.5.0",
68
+ "@plannotator/atomic-editor": "^0.7.0",
69
69
  "@plannotator/core": "0.22.0",
70
- "@plannotator/markdown-editor": "^0.2.0",
70
+ "@plannotator/markdown-editor": "^0.3.2",
71
71
  "@plannotator/web-highlighter": "^0.8.1",
72
72
  "@tanstack/react-table": "^8.21.3",
73
73
  "@viz-js/viz": "^3.25.0",