@flamingo-stack/openframe-frontend-core 0.0.177 → 0.0.178

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/dist/{chunk-6LDN3CIY.js → chunk-AAX27BCR.js} +189 -348
  2. package/dist/chunk-AAX27BCR.js.map +1 -0
  3. package/dist/{chunk-WX7PT5C7.cjs → chunk-ALW3D72O.cjs} +61 -2
  4. package/dist/chunk-ALW3D72O.cjs.map +1 -0
  5. package/dist/{chunk-KB2N44BY.js → chunk-FMWHOUFE.js} +61 -2
  6. package/dist/chunk-FMWHOUFE.js.map +1 -0
  7. package/dist/{chunk-C6ZMI4UB.cjs → chunk-L4T24AN4.cjs} +113 -272
  8. package/dist/chunk-L4T24AN4.cjs.map +1 -0
  9. package/dist/components/features/index.cjs +3 -5
  10. package/dist/components/features/index.cjs.map +1 -1
  11. package/dist/components/features/index.js +2 -4
  12. package/dist/components/features/video-player.d.ts +17 -20
  13. package/dist/components/features/video-player.d.ts.map +1 -1
  14. package/dist/components/features/youtube-embed.d.ts +18 -4
  15. package/dist/components/features/youtube-embed.d.ts.map +1 -1
  16. package/dist/components/index.cjs +3 -5
  17. package/dist/components/index.cjs.map +1 -1
  18. package/dist/components/index.js +2 -4
  19. package/dist/components/navigation/index.cjs +3 -3
  20. package/dist/components/navigation/index.js +2 -2
  21. package/dist/components/ui/index.cjs +3 -3
  22. package/dist/components/ui/index.js +2 -2
  23. package/dist/hooks/index.cjs +4 -2
  24. package/dist/hooks/index.cjs.map +1 -1
  25. package/dist/hooks/index.d.ts +1 -0
  26. package/dist/hooks/index.d.ts.map +1 -1
  27. package/dist/hooks/index.js +3 -1
  28. package/dist/hooks/use-near-viewport.d.ts +42 -0
  29. package/dist/hooks/use-near-viewport.d.ts.map +1 -0
  30. package/dist/index.cjs +3 -3
  31. package/dist/index.cjs.map +1 -1
  32. package/dist/index.js +4 -4
  33. package/package.json +1 -1
  34. package/src/components/features/video-player.tsx +39 -176
  35. package/src/components/features/youtube-embed.tsx +107 -224
  36. package/src/hooks/index.ts +3 -0
  37. package/src/hooks/use-near-viewport.ts +118 -0
  38. package/dist/chunk-6LDN3CIY.js.map +0 -1
  39. package/dist/chunk-C6ZMI4UB.cjs.map +0 -1
  40. package/dist/chunk-KB2N44BY.js.map +0 -1
  41. package/dist/chunk-WX7PT5C7.cjs.map +0 -1
  42. package/src/components/features/__tests__/video-player.test.tsx +0 -142
@@ -15,3 +15,6 @@ export * from './state'
15
15
 
16
16
  // NATS hooks
17
17
  export * from './nats/use-nats-client'
18
+
19
+ // Viewport / lazy-mount primitive (shared IO singleton)
20
+ export * from './use-near-viewport'
@@ -0,0 +1,118 @@
1
+ /**
2
+ * useNearViewport — module-level shared IntersectionObserver in hook form.
3
+ *
4
+ * Single IO instance per `rootMargin` value, shared across every component
5
+ * that mounts the hook. Reduces overhead vs. one IO per component on
6
+ * grid/list pages where many subscribers observe the viewport with the same
7
+ * margin. Promoted from the inline singleton at
8
+ * `multi-platform-hub/components/shared/video-bites-display.tsx:21-43`,
9
+ * which is the only IO pattern in either repo today.
10
+ *
11
+ * Usage:
12
+ * ```tsx
13
+ * function MyCard() {
14
+ * const { ref, isNear } = useNearViewport('500px');
15
+ * return <div ref={ref}>{isNear ? <HeavyChild /> : <Placeholder />}</div>;
16
+ * }
17
+ * ```
18
+ *
19
+ * StrictMode safety: cleanup uses an identity check on the registered
20
+ * callback so React's dev double-mount (mount → cleanup → re-mount) does
21
+ * not drop the second mount's freshly-set subscription. The IO callback
22
+ * also checks `subscribers.get(target)` before invoking so a fire that
23
+ * races with unmount cannot crash on a torn-down component.
24
+ *
25
+ * The hook fires once — on first intersection it sets `isNear=true` and
26
+ * unobserves the element. Callers that need re-observation should
27
+ * unmount and remount (or fork the hook for two-way behavior).
28
+ */
29
+
30
+ import { useEffect, useRef, useState, useCallback } from 'react';
31
+
32
+ // Per-rootMargin IO map. Multiple call sites with different margins each
33
+ // get their own singleton observer.
34
+ const observers = new Map<string, IntersectionObserver>();
35
+ const subscribers = new WeakMap<Element, () => void>();
36
+
37
+ function getObserverFor(rootMargin: string): IntersectionObserver {
38
+ const existing = observers.get(rootMargin);
39
+ if (existing) return existing;
40
+
41
+ const io = new IntersectionObserver(
42
+ (entries) => {
43
+ entries.forEach((entry) => {
44
+ if (!entry.isIntersecting) return;
45
+ // Race-safe: re-read the callback at fire time. A late IO firing
46
+ // after cleanup must not invoke a stale callback.
47
+ const cb = subscribers.get(entry.target);
48
+ if (cb) {
49
+ cb();
50
+ io.unobserve(entry.target);
51
+ subscribers.delete(entry.target);
52
+ }
53
+ });
54
+ },
55
+ { rootMargin }
56
+ );
57
+ observers.set(rootMargin, io);
58
+ return io;
59
+ }
60
+
61
+ export interface UseNearViewportResult<T extends Element = HTMLElement> {
62
+ /** Ref to attach to the element you want to gate on visibility. */
63
+ ref: (node: T | null) => void;
64
+ /** Flips to `true` once the element enters within `rootMargin` of the viewport. Never flips back. */
65
+ isNear: boolean;
66
+ }
67
+
68
+ /**
69
+ * @param rootMargin Margin around the viewport (CSS-style string).
70
+ * '500px' = element starts mounting 500px before scroll-in.
71
+ * '1000px' = a full viewport's worth of lookahead.
72
+ * '0px' = strict on-screen detection.
73
+ */
74
+ export function useNearViewport<T extends Element = HTMLElement>(
75
+ rootMargin: string = '500px'
76
+ ): UseNearViewportResult<T> {
77
+ const [isNear, setIsNear] = useState(false);
78
+ const elRef = useRef<T | null>(null);
79
+
80
+ // Subscribe/unsubscribe on element change.
81
+ const ref = useCallback(
82
+ (node: T | null) => {
83
+ const prev = elRef.current;
84
+
85
+ // Unsubscribe previous, if any. Identity-check the callback so a
86
+ // StrictMode re-mount that has already re-registered keeps its sub.
87
+ if (prev) {
88
+ const stillOurs = subscribers.get(prev);
89
+ if (stillOurs) {
90
+ subscribers.delete(prev);
91
+ observers.get(rootMargin)?.unobserve(prev);
92
+ }
93
+ }
94
+
95
+ elRef.current = node;
96
+ if (!node) return;
97
+
98
+ const cb = () => setIsNear(true);
99
+ subscribers.set(node, cb);
100
+ getObserverFor(rootMargin).observe(node);
101
+ },
102
+ [rootMargin]
103
+ );
104
+
105
+ // Unsubscribe on unmount. Identity check guards the StrictMode race.
106
+ useEffect(() => {
107
+ return () => {
108
+ const el = elRef.current;
109
+ if (!el) return;
110
+ if (subscribers.get(el)) {
111
+ subscribers.delete(el);
112
+ observers.get(rootMargin)?.unobserve(el);
113
+ }
114
+ };
115
+ }, [rootMargin]);
116
+
117
+ return { ref, isNear };
118
+ }