@dfosco/storyboard-react-primer 1.17.3 → 1.18.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.
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { useState, useEffect, useCallback } from 'react'
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { BeakerIcon, InfoIcon, SyncIcon, XIcon, ScreenFullIcon } from '@primer/octicons-react'
|
|
2
|
+
import { loadScene, getFlagKeys } from '@dfosco/storyboard-core'
|
|
3
|
+
import { BeakerIcon, InfoIcon, SyncIcon, XIcon, ScreenFullIcon, ZapIcon } from '@primer/octicons-react'
|
|
5
4
|
import styles from './DevTools.module.css'
|
|
5
|
+
import FeatureFlagsPanel from './FeatureFlagsPanel.jsx'
|
|
6
6
|
|
|
7
7
|
function getSceneName() {
|
|
8
8
|
return new URLSearchParams(window.location.search).get('scene') || 'default'
|
|
@@ -11,18 +11,30 @@ function getSceneName() {
|
|
|
11
11
|
/**
|
|
12
12
|
* Storyboard DevTools — a floating toolbar for development.
|
|
13
13
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* - "Show scene info" — translucent overlay panel with resolved scene JSON
|
|
17
|
-
* - "Reset all params" — clears all URL hash session params
|
|
18
|
-
* - Cmd+. (Mac) / Ctrl+. (other) toggles the toolbar visibility
|
|
14
|
+
* Uses a custom dropdown menu (no Primer ActionMenu) so that
|
|
15
|
+
* view-swapping and flag toggling don't auto-close the panel.
|
|
19
16
|
*/
|
|
20
17
|
export default function DevTools() {
|
|
21
18
|
const [visible, setVisible] = useState(true)
|
|
19
|
+
const [menuOpen, setMenuOpen] = useState(false)
|
|
22
20
|
const [panelOpen, setPanelOpen] = useState(false)
|
|
21
|
+
const [flagsPanelOpen, setFlagsPanelOpen] = useState(false)
|
|
23
22
|
const [sceneData, setSceneData] = useState(null)
|
|
24
23
|
const [sceneError, setSceneError] = useState(null)
|
|
25
24
|
|
|
25
|
+
// Close menu on outside click — use mousedown so it fires before
|
|
26
|
+
// React re-renders remove the clicked element from the DOM
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
if (!menuOpen) return
|
|
29
|
+
function handleMouseDown(e) {
|
|
30
|
+
if (!e.target.closest(`.${styles.wrapper}`)) {
|
|
31
|
+
setMenuOpen(false)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
document.addEventListener('mousedown', handleMouseDown)
|
|
35
|
+
return () => document.removeEventListener('mousedown', handleMouseDown)
|
|
36
|
+
}, [menuOpen])
|
|
37
|
+
|
|
26
38
|
// Cmd+. keyboard shortcut to toggle toolbar
|
|
27
39
|
useEffect(() => {
|
|
28
40
|
function handleKeyDown(e) {
|
|
@@ -31,6 +43,8 @@ export default function DevTools() {
|
|
|
31
43
|
setVisible((v) => !v)
|
|
32
44
|
if (visible) {
|
|
33
45
|
setPanelOpen(false)
|
|
46
|
+
setFlagsPanelOpen(false)
|
|
47
|
+
setMenuOpen(false)
|
|
34
48
|
}
|
|
35
49
|
}
|
|
36
50
|
}
|
|
@@ -38,13 +52,16 @@ export default function DevTools() {
|
|
|
38
52
|
return () => window.removeEventListener('keydown', handleKeyDown)
|
|
39
53
|
}, [visible])
|
|
40
54
|
|
|
55
|
+
const openMenu = useCallback(() => {
|
|
56
|
+
setMenuOpen((v) => !v)
|
|
57
|
+
}, [])
|
|
58
|
+
|
|
41
59
|
const handleShowSceneInfo = useCallback(() => {
|
|
42
|
-
|
|
60
|
+
setMenuOpen(false)
|
|
43
61
|
setPanelOpen(true)
|
|
44
62
|
setSceneError(null)
|
|
45
|
-
|
|
46
63
|
try {
|
|
47
|
-
setSceneData(loadScene(
|
|
64
|
+
setSceneData(loadScene(getSceneName()))
|
|
48
65
|
} catch (err) {
|
|
49
66
|
setSceneError(err.message)
|
|
50
67
|
}
|
|
@@ -52,88 +69,76 @@ export default function DevTools() {
|
|
|
52
69
|
|
|
53
70
|
const handleResetParams = useCallback(() => {
|
|
54
71
|
window.location.hash = ''
|
|
72
|
+
setMenuOpen(false)
|
|
55
73
|
}, [])
|
|
56
74
|
|
|
57
75
|
const handleViewfinder = useCallback(() => {
|
|
76
|
+
setMenuOpen(false)
|
|
58
77
|
window.location.href = (document.querySelector('base')?.href || '/') + 'viewfinder'
|
|
59
78
|
}, [])
|
|
60
79
|
|
|
80
|
+
const handleOpenFlagsPanel = useCallback(() => {
|
|
81
|
+
setMenuOpen(false)
|
|
82
|
+
setFlagsPanelOpen(true)
|
|
83
|
+
}, [])
|
|
84
|
+
|
|
61
85
|
if (!visible) return null
|
|
62
86
|
|
|
87
|
+
const hasFlags = getFlagKeys().length > 0
|
|
88
|
+
|
|
63
89
|
return (
|
|
64
90
|
<>
|
|
65
91
|
{/* Scene info overlay panel */}
|
|
66
92
|
{panelOpen && (
|
|
67
93
|
<div className={styles.overlay}>
|
|
68
|
-
<div
|
|
69
|
-
className={styles.overlayBackdrop}
|
|
70
|
-
onClick={() => setPanelOpen(false)}
|
|
71
|
-
/>
|
|
94
|
+
<div className={styles.overlayBackdrop} onClick={() => setPanelOpen(false)} />
|
|
72
95
|
<div className={styles.panel}>
|
|
73
96
|
<div className={styles.panelHeader}>
|
|
74
|
-
<span className={styles.panelTitle}>
|
|
75
|
-
|
|
76
|
-
</span>
|
|
77
|
-
<button
|
|
78
|
-
className={styles.panelClose}
|
|
79
|
-
onClick={() => setPanelOpen(false)}
|
|
80
|
-
aria-label="Close panel"
|
|
81
|
-
>
|
|
97
|
+
<span className={styles.panelTitle}>Scene: {getSceneName()}</span>
|
|
98
|
+
<button className={styles.panelClose} onClick={() => setPanelOpen(false)} aria-label="Close panel">
|
|
82
99
|
<XIcon size={16} />
|
|
83
100
|
</button>
|
|
84
101
|
</div>
|
|
85
102
|
<div className={styles.panelBody}>
|
|
86
|
-
{sceneError &&
|
|
87
|
-
<span className={styles.error}>{sceneError}</span>
|
|
88
|
-
)}
|
|
103
|
+
{sceneError && <span className={styles.error}>{sceneError}</span>}
|
|
89
104
|
{!sceneError && sceneData && (
|
|
90
|
-
<pre className={styles.codeBlock}>
|
|
91
|
-
{JSON.stringify(sceneData, null, 2)}
|
|
92
|
-
</pre>
|
|
105
|
+
<pre className={styles.codeBlock}>{JSON.stringify(sceneData, null, 2)}</pre>
|
|
93
106
|
)}
|
|
94
107
|
</div>
|
|
95
108
|
</div>
|
|
96
109
|
</div>
|
|
97
110
|
)}
|
|
111
|
+
<FeatureFlagsPanel open={flagsPanelOpen} onClose={() => setFlagsPanelOpen(false)} />
|
|
98
112
|
|
|
99
113
|
{/* Floating toolbar */}
|
|
100
114
|
<div className={styles.wrapper}>
|
|
101
|
-
|
|
102
|
-
<
|
|
103
|
-
<button
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
>
|
|
107
|
-
<
|
|
115
|
+
{menuOpen && (
|
|
116
|
+
<div className={styles.menu}>
|
|
117
|
+
<button className={styles.menuItem} onClick={handleViewfinder}>
|
|
118
|
+
<ScreenFullIcon size={16} /> See viewfinder
|
|
119
|
+
</button>
|
|
120
|
+
<button className={styles.menuItem} onClick={handleShowSceneInfo}>
|
|
121
|
+
<InfoIcon size={16} /> Show scene info
|
|
108
122
|
</button>
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
Reset all params
|
|
129
|
-
</ActionList.Item>
|
|
130
|
-
<div className={styles.shortcutHint}>
|
|
131
|
-
Press <code>⌘ + .</code> to hide
|
|
132
|
-
</div>
|
|
133
|
-
|
|
134
|
-
</ActionList>
|
|
135
|
-
</ActionMenu.Overlay>
|
|
136
|
-
</ActionMenu>
|
|
123
|
+
<button className={styles.menuItem} onClick={handleResetParams}>
|
|
124
|
+
<SyncIcon size={16} /> Reset all params
|
|
125
|
+
</button>
|
|
126
|
+
{hasFlags && (
|
|
127
|
+
<>
|
|
128
|
+
<div className={styles.separator} />
|
|
129
|
+
<button className={styles.menuItem} onClick={handleOpenFlagsPanel}>
|
|
130
|
+
<ZapIcon size={16} /> Feature Flags
|
|
131
|
+
</button>
|
|
132
|
+
</>
|
|
133
|
+
)}
|
|
134
|
+
<div className={styles.shortcutHint}>
|
|
135
|
+
Press <code>⌘ + .</code> to hide
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
138
|
+
)}
|
|
139
|
+
<button className={styles.trigger} aria-label="Storyboard DevTools" onClick={openMenu}>
|
|
140
|
+
<BeakerIcon className={styles.triggerIcon} size={16} />
|
|
141
|
+
</button>
|
|
137
142
|
</div>
|
|
138
143
|
</>
|
|
139
144
|
)
|
|
@@ -151,3 +151,96 @@ code {
|
|
|
151
151
|
margin-left: auto;
|
|
152
152
|
font-family: var(--fontStack-monospace);
|
|
153
153
|
}
|
|
154
|
+
|
|
155
|
+
/* Custom dropdown menu */
|
|
156
|
+
.menu {
|
|
157
|
+
position: absolute;
|
|
158
|
+
bottom: 56px;
|
|
159
|
+
right: 0;
|
|
160
|
+
min-width: 200px;
|
|
161
|
+
background-color: var(--overlay-bgColor);
|
|
162
|
+
border: 1px solid var(--borderColor-default);
|
|
163
|
+
border-radius: var(--borderRadius-large);
|
|
164
|
+
box-shadow: var(--shadow-floating-large);
|
|
165
|
+
overflow: hidden;
|
|
166
|
+
padding: var(--base-size-4) 0;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.menuItem {
|
|
170
|
+
display: flex;
|
|
171
|
+
align-items: center;
|
|
172
|
+
gap: var(--base-size-8);
|
|
173
|
+
width: 100%;
|
|
174
|
+
padding: var(--base-size-8) var(--base-size-16);
|
|
175
|
+
background: none;
|
|
176
|
+
border: none;
|
|
177
|
+
color: var(--fgColor-default);
|
|
178
|
+
font-size: var(--text-body-size-medium);
|
|
179
|
+
font-family: var(--fontStack-system);
|
|
180
|
+
cursor: pointer;
|
|
181
|
+
text-align: left;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
.menuItem:hover {
|
|
185
|
+
background-color: var(--control-transparent-bgColor-hover);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
.menuItem svg {
|
|
189
|
+
flex-shrink: 0;
|
|
190
|
+
color: var(--fgColor-muted);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
.separator {
|
|
194
|
+
height: 1px;
|
|
195
|
+
background-color: var(--borderColor-muted);
|
|
196
|
+
margin: var(--base-size-4) 0;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
.flagIcon {
|
|
200
|
+
display: flex;
|
|
201
|
+
align-items: center;
|
|
202
|
+
justify-content: center;
|
|
203
|
+
width: 16px;
|
|
204
|
+
height: 16px;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
.flagsList {
|
|
208
|
+
display: flex;
|
|
209
|
+
flex-direction: column;
|
|
210
|
+
gap: var(--base-size-4);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
.flagPanelItem {
|
|
214
|
+
display: flex;
|
|
215
|
+
align-items: center;
|
|
216
|
+
gap: var(--base-size-8);
|
|
217
|
+
width: 100%;
|
|
218
|
+
padding: var(--base-size-8) var(--base-size-10);
|
|
219
|
+
background: none;
|
|
220
|
+
border: 1px solid transparent;
|
|
221
|
+
border-radius: var(--borderRadius-medium);
|
|
222
|
+
color: var(--fgColor-default);
|
|
223
|
+
font-size: var(--text-body-size-medium);
|
|
224
|
+
font-family: var(--fontStack-system);
|
|
225
|
+
text-align: left;
|
|
226
|
+
cursor: pointer;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
.flagPanelItem:hover {
|
|
230
|
+
background-color: var(--control-transparent-bgColor-hover);
|
|
231
|
+
border-color: var(--borderColor-muted);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
.flagPanelCheck {
|
|
235
|
+
display: flex;
|
|
236
|
+
align-items: center;
|
|
237
|
+
justify-content: center;
|
|
238
|
+
width: 16px;
|
|
239
|
+
height: 16px;
|
|
240
|
+
color: var(--fgColor-accent);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
.noFlags {
|
|
244
|
+
color: var(--fgColor-muted);
|
|
245
|
+
font-size: var(--text-body-size-small);
|
|
246
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { useSyncExternalStore } from 'react'
|
|
2
|
+
import { getAllFlags, getFlagKeys, toggleFlag, subscribeToHash, getHashSnapshot } from '@dfosco/storyboard-core'
|
|
3
|
+
import { CheckIcon, XIcon } from '@primer/octicons-react'
|
|
4
|
+
import styles from './DevTools.module.css'
|
|
5
|
+
|
|
6
|
+
export default function FeatureFlagsPanel({ open, onClose }) {
|
|
7
|
+
useSyncExternalStore(subscribeToHash, getHashSnapshot)
|
|
8
|
+
|
|
9
|
+
if (!open) return null
|
|
10
|
+
|
|
11
|
+
const flagKeys = getFlagKeys()
|
|
12
|
+
const flags = getAllFlags()
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<div className={styles.overlay}>
|
|
16
|
+
<div className={styles.overlayBackdrop} onClick={onClose} />
|
|
17
|
+
<div className={styles.panel}>
|
|
18
|
+
<div className={styles.panelHeader}>
|
|
19
|
+
<span className={styles.panelTitle}>Feature Flags</span>
|
|
20
|
+
<button className={styles.panelClose} onClick={onClose} aria-label="Close feature flags panel">
|
|
21
|
+
<XIcon size={16} />
|
|
22
|
+
</button>
|
|
23
|
+
</div>
|
|
24
|
+
<div className={styles.panelBody}>
|
|
25
|
+
{flagKeys.length === 0 ? (
|
|
26
|
+
<div className={styles.noFlags}>No feature flags are configured.</div>
|
|
27
|
+
) : (
|
|
28
|
+
<div className={styles.flagsList}>
|
|
29
|
+
{flagKeys.map((key) => (
|
|
30
|
+
<button key={key} className={styles.flagPanelItem} onClick={() => toggleFlag(key)}>
|
|
31
|
+
<span className={styles.flagPanelCheck}>
|
|
32
|
+
{flags[key]?.current ? <CheckIcon size={16} /> : null}
|
|
33
|
+
</span>
|
|
34
|
+
{key}
|
|
35
|
+
</button>
|
|
36
|
+
))}
|
|
37
|
+
</div>
|
|
38
|
+
)}
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
)
|
|
43
|
+
}
|