@dfosco/storyboard-react 4.2.0-alpha.15 → 4.2.0-alpha.16

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.
@@ -1,207 +0,0 @@
1
- /**
2
- * Embed Controller — manages iframe lifecycle for canvas embed widgets.
3
- *
4
- * Behaviors:
5
- * - Performance mode (per-canvas setting): embeds don't render until clicked
6
- * - Viewport threshold: if >7 embeds visible, none render (zoom in to reduce)
7
- * - Viewport exit: embeds deactivate 5s after leaving the viewport
8
- *
9
- * Usage:
10
- * <EmbedControllerProvider performanceMode={bool} scrollRef={ref}>
11
- * <StoryWidget ... />
12
- * </EmbedControllerProvider>
13
- *
14
- * // Inside a widget:
15
- * const { active, activate } = useEmbedActive(widgetId, containerRef)
16
- */
17
- import { createContext, useContext, useCallback, useEffect, useRef, useState } from 'react'
18
-
19
- const DEACTIVATE_DELAY = 5000
20
- const MAX_VISIBLE_EMBEDS = 7
21
-
22
- // ── Shared state (module-level, one per page) ──────────────────────────
23
-
24
- let performanceMode = false
25
- let visibleEmbedIds = new Set()
26
- let activeEmbedIds = new Set()
27
- let manuallyActivatedIds = new Set()
28
- let listeners = new Set()
29
-
30
- function notify() {
31
- for (const fn of listeners) fn()
32
- }
33
-
34
- function subscribe(fn) {
35
- listeners.add(fn)
36
- return () => listeners.delete(fn)
37
- }
38
-
39
- function setPerformanceMode(value) {
40
- performanceMode = value
41
- if (value) {
42
- // Entering perf mode: deactivate all non-manually-activated embeds
43
- activeEmbedIds = new Set(manuallyActivatedIds)
44
- }
45
- notify()
46
- }
47
-
48
- function getPerformanceMode() {
49
- return performanceMode
50
- }
51
-
52
- function registerEmbed(id) {
53
- // In normal mode with few embeds, auto-activate
54
- if (!performanceMode && visibleEmbedIds.size <= MAX_VISIBLE_EMBEDS) {
55
- activeEmbedIds.add(id)
56
- }
57
- notify()
58
- }
59
-
60
- function unregisterEmbed(id) {
61
- visibleEmbedIds.delete(id)
62
- activeEmbedIds.delete(id)
63
- manuallyActivatedIds.delete(id)
64
- notify()
65
- }
66
-
67
- function markVisible(id) {
68
- visibleEmbedIds.add(id)
69
- // Auto-activate if not in perf mode and under threshold
70
- if (!performanceMode && visibleEmbedIds.size <= MAX_VISIBLE_EMBEDS) {
71
- activeEmbedIds.add(id)
72
- }
73
- // If was manually activated, keep it active
74
- if (manuallyActivatedIds.has(id)) {
75
- activeEmbedIds.add(id)
76
- }
77
- notify()
78
- }
79
-
80
- function markHidden(id) {
81
- visibleEmbedIds.delete(id)
82
- // Check if other embeds should now activate (dropped below threshold)
83
- if (!performanceMode && visibleEmbedIds.size <= MAX_VISIBLE_EMBEDS) {
84
- for (const vid of visibleEmbedIds) {
85
- activeEmbedIds.add(vid)
86
- }
87
- }
88
- notify()
89
- }
90
-
91
- function deactivateEmbed(id) {
92
- activeEmbedIds.delete(id)
93
- manuallyActivatedIds.delete(id)
94
- notify()
95
- }
96
-
97
- function activateEmbed(id) {
98
- activeEmbedIds.add(id)
99
- manuallyActivatedIds.add(id)
100
- notify()
101
- }
102
-
103
- function isActive(id) {
104
- return activeEmbedIds.has(id)
105
- }
106
-
107
- function isTooManyVisible() {
108
- return visibleEmbedIds.size > MAX_VISIBLE_EMBEDS
109
- }
110
-
111
- // ── React context ──────────────────────────────────────────────────────
112
-
113
- const EmbedControllerContext = createContext(null)
114
-
115
- export function EmbedControllerProvider({ performanceMode: perfModeProp, scrollRef, children }) {
116
- // Sync prop to module state
117
- useEffect(() => {
118
- setPerformanceMode(perfModeProp)
119
- }, [perfModeProp])
120
-
121
- // Reset on unmount
122
- useEffect(() => {
123
- return () => {
124
- visibleEmbedIds = new Set()
125
- activeEmbedIds = new Set()
126
- manuallyActivatedIds = new Set()
127
- performanceMode = false
128
- notify()
129
- }
130
- }, [])
131
-
132
- return (
133
- <EmbedControllerContext.Provider value={scrollRef}>
134
- {children}
135
- </EmbedControllerContext.Provider>
136
- )
137
- }
138
-
139
- /**
140
- * Hook for embed widgets. Returns { active, activate, performanceMode, tooMany }.
141
- * - active: whether the iframe should be rendered
142
- * - activate: call to manually activate (user clicked)
143
- * - performanceMode: whether perf mode is on
144
- * - tooMany: whether there are too many visible embeds
145
- */
146
- export function useEmbedActive(widgetId, containerRef) {
147
- const scrollRef = useContext(EmbedControllerContext)
148
- const deactivateTimerRef = useRef(null)
149
-
150
- // Track state with useState + subscribe (avoids useSyncExternalStore object identity issues)
151
- const [, forceUpdate] = useState(0)
152
- useEffect(() => {
153
- return subscribe(() => forceUpdate(c => c + 1))
154
- }, [])
155
-
156
- const active = isActive(widgetId)
157
- const perf = getPerformanceMode()
158
- const tooMany = isTooManyVisible()
159
-
160
- // Register/unregister
161
- useEffect(() => {
162
- registerEmbed(widgetId)
163
- return () => {
164
- unregisterEmbed(widgetId)
165
- if (deactivateTimerRef.current) clearTimeout(deactivateTimerRef.current)
166
- }
167
- }, [widgetId])
168
-
169
- // IntersectionObserver for viewport tracking
170
- useEffect(() => {
171
- const el = containerRef?.current
172
- if (!el) return
173
-
174
- const root = scrollRef?.current || null
175
-
176
- const observer = new IntersectionObserver(
177
- ([entry]) => {
178
- if (entry.isIntersecting) {
179
- // Entered viewport
180
- if (deactivateTimerRef.current) {
181
- clearTimeout(deactivateTimerRef.current)
182
- deactivateTimerRef.current = null
183
- }
184
- markVisible(widgetId)
185
- } else {
186
- // Left viewport — start deactivation timer
187
- markHidden(widgetId)
188
- if (deactivateTimerRef.current) clearTimeout(deactivateTimerRef.current)
189
- deactivateTimerRef.current = setTimeout(() => {
190
- deactivateEmbed(widgetId)
191
- deactivateTimerRef.current = null
192
- }, DEACTIVATE_DELAY)
193
- }
194
- },
195
- { root, threshold: 0 }
196
- )
197
-
198
- observer.observe(el)
199
- return () => observer.disconnect()
200
- }, [widgetId, containerRef, scrollRef])
201
-
202
- const activate = useCallback(() => {
203
- activateEmbed(widgetId)
204
- }, [widgetId])
205
-
206
- return { active, activate, performanceMode: perf, tooMany }
207
- }