@dfosco/storyboard-react 4.2.0-alpha.14 → 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.
- package/package.json +3 -3
- package/src/BranchBar/BranchBar.jsx +7 -4
- package/src/BranchBar/BranchBar.module.css +7 -2
- package/src/CommandPalette/CommandPalette.jsx +20 -2
- package/src/Icon.jsx +4 -0
- package/src/Viewfinder.jsx +1 -1
- package/src/canvas/CanvasPage.bridge.test.jsx +14 -6
- package/src/canvas/CanvasPage.dragdrop.test.jsx +10 -6
- package/src/canvas/CanvasPage.jsx +22 -30
- package/src/canvas/CanvasPage.module.css +0 -15
- package/src/canvas/CanvasPage.multiselect.test.jsx +10 -6
- package/src/canvas/PageSelector.test.jsx +15 -6
- package/src/canvas/widgets/ImageWidget.jsx +1 -1
- package/src/canvas/widgets/PrototypeEmbed.jsx +16 -18
- package/src/canvas/widgets/PrototypeEmbed.test.jsx +2 -2
- package/src/canvas/widgets/StoryWidget.jsx +18 -21
- package/src/canvas/widgets/TerminalWidget.jsx +100 -98
- package/src/canvas/widgets/TerminalWidget.module.css +49 -1
- package/src/canvas/widgets/embedInteraction.test.jsx +24 -26
- package/src/canvas/widgets/snapshotDisplay.test.jsx +23 -71
- package/src/canvas/widgets/useEmbedsPaused.js +19 -0
- package/src/hooks/useConfig.js +14 -0
- package/src/index.js +1 -0
- package/src/vite/data-plugin.js +230 -13
- package/src/canvas/widgets/useEmbedController.jsx +0 -223
|
@@ -1,223 +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, useSyncExternalStore } 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
|
-
// Subscribe to state changes
|
|
151
|
-
const snapshot = useSyncExternalStore(subscribe, () => ({
|
|
152
|
-
active: isActive(widgetId),
|
|
153
|
-
performanceMode: getPerformanceMode(),
|
|
154
|
-
tooMany: isTooManyVisible(),
|
|
155
|
-
}), () => ({
|
|
156
|
-
active: false,
|
|
157
|
-
performanceMode: false,
|
|
158
|
-
tooMany: false,
|
|
159
|
-
}))
|
|
160
|
-
|
|
161
|
-
// Need a stable reference check since useSyncExternalStore compares by reference
|
|
162
|
-
const activeRef = useRef(false)
|
|
163
|
-
const perfRef = useRef(false)
|
|
164
|
-
const tooManyRef = useRef(false)
|
|
165
|
-
|
|
166
|
-
const active = isActive(widgetId)
|
|
167
|
-
const perf = getPerformanceMode()
|
|
168
|
-
const tooMany = isTooManyVisible()
|
|
169
|
-
|
|
170
|
-
if (activeRef.current !== active || perfRef.current !== perf || tooManyRef.current !== tooMany) {
|
|
171
|
-
activeRef.current = active
|
|
172
|
-
perfRef.current = perf
|
|
173
|
-
tooManyRef.current = tooMany
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// Register/unregister
|
|
177
|
-
useEffect(() => {
|
|
178
|
-
registerEmbed(widgetId)
|
|
179
|
-
return () => {
|
|
180
|
-
unregisterEmbed(widgetId)
|
|
181
|
-
if (deactivateTimerRef.current) clearTimeout(deactivateTimerRef.current)
|
|
182
|
-
}
|
|
183
|
-
}, [widgetId])
|
|
184
|
-
|
|
185
|
-
// IntersectionObserver for viewport tracking
|
|
186
|
-
useEffect(() => {
|
|
187
|
-
const el = containerRef?.current
|
|
188
|
-
if (!el) return
|
|
189
|
-
|
|
190
|
-
const root = scrollRef?.current || null
|
|
191
|
-
|
|
192
|
-
const observer = new IntersectionObserver(
|
|
193
|
-
([entry]) => {
|
|
194
|
-
if (entry.isIntersecting) {
|
|
195
|
-
// Entered viewport
|
|
196
|
-
if (deactivateTimerRef.current) {
|
|
197
|
-
clearTimeout(deactivateTimerRef.current)
|
|
198
|
-
deactivateTimerRef.current = null
|
|
199
|
-
}
|
|
200
|
-
markVisible(widgetId)
|
|
201
|
-
} else {
|
|
202
|
-
// Left viewport — start deactivation timer
|
|
203
|
-
markHidden(widgetId)
|
|
204
|
-
if (deactivateTimerRef.current) clearTimeout(deactivateTimerRef.current)
|
|
205
|
-
deactivateTimerRef.current = setTimeout(() => {
|
|
206
|
-
deactivateEmbed(widgetId)
|
|
207
|
-
deactivateTimerRef.current = null
|
|
208
|
-
}, DEACTIVATE_DELAY)
|
|
209
|
-
}
|
|
210
|
-
},
|
|
211
|
-
{ root, threshold: 0 }
|
|
212
|
-
)
|
|
213
|
-
|
|
214
|
-
observer.observe(el)
|
|
215
|
-
return () => observer.disconnect()
|
|
216
|
-
}, [widgetId, containerRef, scrollRef])
|
|
217
|
-
|
|
218
|
-
const activate = useCallback(() => {
|
|
219
|
-
activateEmbed(widgetId)
|
|
220
|
-
}, [widgetId])
|
|
221
|
-
|
|
222
|
-
return { active, activate, performanceMode: perf, tooMany }
|
|
223
|
-
}
|