@plannotator/ui 0.54.0 → 0.55.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,94 @@
1
+ import { useCallback, useEffect, useRef, useState } from 'react';
2
+
3
+ /** How long the pointer rests on an anchor before the preview is fetched. */
4
+ export const HOVER_PREVIEW_SHOW_DELAY_MS = 150;
5
+ /** How long a preview stays after the pointer leaves, so it can move into the preview. */
6
+ export const HOVER_PREVIEW_HIDE_DELAY_MS = 200;
7
+
8
+ export interface HoverPreview<T> {
9
+ /** The loaded preview while it is open, else null. */
10
+ preview: T | null;
11
+ /** Attach to the anchor element; the preview positions against it. */
12
+ anchorRef: React.MutableRefObject<HTMLElement | null>;
13
+ /** Pointer or focus arrives on the anchor: load after the show delay. */
14
+ onEnter: () => void;
15
+ /** Pointer or focus leaves the anchor: close after the hide delay. */
16
+ onLeave: () => void;
17
+ /** Spread onto the preview so moving into it keeps it open. */
18
+ previewHandlers: { onMouseEnter: () => void; onMouseLeave: () => void };
19
+ /** Close at once (a click, a scroll). */
20
+ dismiss: () => void;
21
+ }
22
+
23
+ /**
24
+ * Dwell-then-load hover for a floating preview: the timing shared by the
25
+ * code-file link preview (InlineMarkdown) and Call Flow's source locations.
26
+ * Nothing loads before the pointer rests for `HOVER_PREVIEW_SHOW_DELAY_MS`; a
27
+ * load that resolves after the pointer left is dropped. `load` resolving
28
+ * null means "nothing to show".
29
+ */
30
+ export function useHoverPreview<T>(load: () => Promise<T | null>, enabled = true): HoverPreview<T> {
31
+ const [preview, setPreview] = useState<T | null>(null);
32
+ const previewRef = useRef(preview);
33
+ previewRef.current = preview;
34
+ const anchorRef = useRef<HTMLElement | null>(null);
35
+ const showTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
36
+ const hideTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
37
+ // Bumped on every leave so a load still in flight cannot open a stale preview.
38
+ const generationRef = useRef(0);
39
+ const loadRef = useRef(load);
40
+ loadRef.current = load;
41
+
42
+ const cancelShow = useCallback(() => {
43
+ if (showTimerRef.current) { clearTimeout(showTimerRef.current); showTimerRef.current = null; }
44
+ }, []);
45
+ const cancelHide = useCallback(() => {
46
+ if (hideTimerRef.current) { clearTimeout(hideTimerRef.current); hideTimerRef.current = null; }
47
+ }, []);
48
+
49
+ const scheduleHide = useCallback(() => {
50
+ cancelHide();
51
+ hideTimerRef.current = setTimeout(() => setPreview(null), HOVER_PREVIEW_HIDE_DELAY_MS);
52
+ }, [cancelHide]);
53
+
54
+ const onEnter = useCallback(() => {
55
+ if (!enabled) return;
56
+ cancelHide();
57
+ if (previewRef.current || showTimerRef.current) return;
58
+ const generation = generationRef.current;
59
+ showTimerRef.current = setTimeout(async () => {
60
+ showTimerRef.current = null;
61
+ try {
62
+ const data = await loadRef.current();
63
+ if (data != null && generation === generationRef.current) setPreview(data);
64
+ } catch {}
65
+ }, HOVER_PREVIEW_SHOW_DELAY_MS);
66
+ }, [enabled, cancelHide]);
67
+
68
+ const onLeave = useCallback(() => {
69
+ cancelShow();
70
+ generationRef.current += 1;
71
+ scheduleHide();
72
+ }, [cancelShow, scheduleHide]);
73
+
74
+ const dismiss = useCallback(() => {
75
+ cancelShow();
76
+ cancelHide();
77
+ generationRef.current += 1;
78
+ setPreview(null);
79
+ }, [cancelShow, cancelHide]);
80
+
81
+ useEffect(() => () => {
82
+ cancelShow();
83
+ cancelHide();
84
+ }, [cancelShow, cancelHide]);
85
+
86
+ return {
87
+ preview,
88
+ anchorRef,
89
+ onEnter,
90
+ onLeave,
91
+ previewHandlers: { onMouseEnter: cancelHide, onMouseLeave: scheduleHide },
92
+ dismiss,
93
+ };
94
+ }
@@ -0,0 +1,23 @@
1
+ // Moved from the Workspaces app's `shell-motion.ts` with its contents rail
2
+ // (MarkdownContentsRail, approved 2026-10-03). Port: none.
3
+
4
+ import { useEffect, useState } from 'react';
5
+
6
+ /** Whether the reader asked for reduced motion; follows the setting live. */
7
+ export function usePrefersReducedMotion(): boolean {
8
+ const [reduced, setReduced] = useState(
9
+ () =>
10
+ typeof window !== 'undefined' &&
11
+ typeof window.matchMedia === 'function' &&
12
+ window.matchMedia('(prefers-reduced-motion: reduce)').matches,
13
+ );
14
+ useEffect(() => {
15
+ if (typeof window.matchMedia !== 'function') return;
16
+ const query = window.matchMedia('(prefers-reduced-motion: reduce)');
17
+ const onChange = () => setReduced(query.matches);
18
+ // Older WebKit shims lack addEventListener on MediaQueryList.
19
+ query.addEventListener?.('change', onChange);
20
+ return () => query.removeEventListener?.('change', onChange);
21
+ }, []);
22
+ return reduced;
23
+ }