@dfosco/storyboard-core 1.1.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 +18 -0
- package/src/comments/api.js +196 -0
- package/src/comments/api.test.js +194 -0
- package/src/comments/auth.js +79 -0
- package/src/comments/auth.test.js +60 -0
- package/src/comments/commentMode.js +63 -0
- package/src/comments/commentMode.test.js +87 -0
- package/src/comments/config.js +43 -0
- package/src/comments/config.test.js +76 -0
- package/src/comments/graphql.js +65 -0
- package/src/comments/graphql.test.js +95 -0
- package/src/comments/index.js +40 -0
- package/src/comments/metadata.js +52 -0
- package/src/comments/metadata.test.js +110 -0
- package/src/comments/queries.js +182 -0
- package/src/comments/ui/CommentOverlay.js +52 -0
- package/src/comments/ui/authModal.js +349 -0
- package/src/comments/ui/commentWindow.js +872 -0
- package/src/comments/ui/commentsDrawer.js +389 -0
- package/src/comments/ui/composer.js +248 -0
- package/src/comments/ui/mount.js +364 -0
- package/src/devtools.js +365 -0
- package/src/devtools.test.js +81 -0
- package/src/dotPath.js +53 -0
- package/src/dotPath.test.js +114 -0
- package/src/hashSubscribe.js +19 -0
- package/src/hashSubscribe.test.js +62 -0
- package/src/hideMode.js +421 -0
- package/src/hideMode.test.js +224 -0
- package/src/index.js +38 -0
- package/src/interceptHideParams.js +35 -0
- package/src/interceptHideParams.test.js +90 -0
- package/src/loader.js +212 -0
- package/src/loader.test.js +232 -0
- package/src/localStorage.js +134 -0
- package/src/localStorage.test.js +148 -0
- package/src/sceneDebug.js +108 -0
- package/src/sceneDebug.test.js +128 -0
- package/src/session.js +76 -0
- package/src/session.test.js +91 -0
- package/src/viewfinder.js +47 -0
- package/src/viewfinder.test.js +87 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Comment overlay UI utilities (vanilla JS).
|
|
3
|
+
*
|
|
4
|
+
* Provides menu items for the DevTools integration.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { isAuthenticated } from '../auth.js'
|
|
8
|
+
import { toggleCommentMode } from '../commentMode.js'
|
|
9
|
+
import { openAuthModal, signOut } from './authModal.js'
|
|
10
|
+
import { openCommentsDrawer } from './commentsDrawer.js'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Get menu items for the DevTools comments section.
|
|
14
|
+
* @returns {Array<{ label: string, icon: string, onClick: () => void }>}
|
|
15
|
+
*/
|
|
16
|
+
export function getCommentsMenuItems() {
|
|
17
|
+
const items = []
|
|
18
|
+
|
|
19
|
+
if (!isAuthenticated()) {
|
|
20
|
+
items.push({
|
|
21
|
+
label: 'Sign in for comments',
|
|
22
|
+
icon: '💬',
|
|
23
|
+
onClick: () => {
|
|
24
|
+
openAuthModal()
|
|
25
|
+
},
|
|
26
|
+
})
|
|
27
|
+
} else {
|
|
28
|
+
items.push({
|
|
29
|
+
label: 'Toggle comments',
|
|
30
|
+
icon: '💬',
|
|
31
|
+
onClick: () => {
|
|
32
|
+
toggleCommentMode()
|
|
33
|
+
},
|
|
34
|
+
})
|
|
35
|
+
items.push({
|
|
36
|
+
label: 'See all comments',
|
|
37
|
+
icon: '📋',
|
|
38
|
+
onClick: () => {
|
|
39
|
+
openCommentsDrawer()
|
|
40
|
+
},
|
|
41
|
+
})
|
|
42
|
+
items.push({
|
|
43
|
+
label: 'Sign out of comments',
|
|
44
|
+
icon: '🚪',
|
|
45
|
+
onClick: () => {
|
|
46
|
+
signOut()
|
|
47
|
+
},
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return items
|
|
52
|
+
}
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth modal — vanilla JS modal for entering a GitHub PAT.
|
|
3
|
+
*
|
|
4
|
+
* Styled to match the devtools dark theme. Uses the native <dialog> element.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { setToken, validateToken, clearToken, getCachedUser } from '../auth.js'
|
|
8
|
+
|
|
9
|
+
const MODAL_ID = 'sb-auth-modal'
|
|
10
|
+
const STYLE_ID = 'sb-auth-modal-style'
|
|
11
|
+
|
|
12
|
+
function injectStyles() {
|
|
13
|
+
if (document.getElementById(STYLE_ID)) return
|
|
14
|
+
const style = document.createElement('style')
|
|
15
|
+
style.id = STYLE_ID
|
|
16
|
+
style.textContent = `
|
|
17
|
+
.sb-auth-backdrop {
|
|
18
|
+
position: fixed;
|
|
19
|
+
inset: 0;
|
|
20
|
+
z-index: 100000;
|
|
21
|
+
background: rgba(0, 0, 0, 0.6);
|
|
22
|
+
backdrop-filter: blur(4px);
|
|
23
|
+
display: flex;
|
|
24
|
+
align-items: center;
|
|
25
|
+
justify-content: center;
|
|
26
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.sb-auth-modal {
|
|
30
|
+
width: 420px;
|
|
31
|
+
max-width: calc(100vw - 32px);
|
|
32
|
+
background: #161b22;
|
|
33
|
+
border: 1px solid #30363d;
|
|
34
|
+
border-radius: 12px;
|
|
35
|
+
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.5);
|
|
36
|
+
color: #c9d1d9;
|
|
37
|
+
overflow: hidden;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.sb-auth-header {
|
|
41
|
+
display: flex;
|
|
42
|
+
align-items: center;
|
|
43
|
+
justify-content: space-between;
|
|
44
|
+
padding: 16px 20px;
|
|
45
|
+
border-bottom: 1px solid #21262d;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.sb-auth-header h2 {
|
|
49
|
+
margin: 0;
|
|
50
|
+
font-size: 16px;
|
|
51
|
+
font-weight: 600;
|
|
52
|
+
color: #f0f6fc;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.sb-auth-close {
|
|
56
|
+
display: flex;
|
|
57
|
+
align-items: center;
|
|
58
|
+
justify-content: center;
|
|
59
|
+
width: 28px;
|
|
60
|
+
height: 28px;
|
|
61
|
+
background: none;
|
|
62
|
+
border: none;
|
|
63
|
+
border-radius: 6px;
|
|
64
|
+
color: #8b949e;
|
|
65
|
+
cursor: pointer;
|
|
66
|
+
font-size: 18px;
|
|
67
|
+
line-height: 1;
|
|
68
|
+
}
|
|
69
|
+
.sb-auth-close:hover {
|
|
70
|
+
background: #21262d;
|
|
71
|
+
color: #c9d1d9;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.sb-auth-body {
|
|
75
|
+
padding: 20px;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.sb-auth-description {
|
|
79
|
+
margin: 0 0 16px;
|
|
80
|
+
font-size: 13px;
|
|
81
|
+
color: #8b949e;
|
|
82
|
+
line-height: 1.5;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.sb-auth-description a {
|
|
86
|
+
color: #58a6ff;
|
|
87
|
+
text-decoration: none;
|
|
88
|
+
}
|
|
89
|
+
.sb-auth-description a:hover {
|
|
90
|
+
text-decoration: underline;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.sb-auth-label {
|
|
94
|
+
display: block;
|
|
95
|
+
margin-bottom: 6px;
|
|
96
|
+
font-size: 13px;
|
|
97
|
+
font-weight: 500;
|
|
98
|
+
color: #c9d1d9;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.sb-auth-input {
|
|
102
|
+
width: 100%;
|
|
103
|
+
padding: 8px 12px;
|
|
104
|
+
background: #0d1117;
|
|
105
|
+
border: 1px solid #30363d;
|
|
106
|
+
border-radius: 6px;
|
|
107
|
+
color: #c9d1d9;
|
|
108
|
+
font-size: 14px;
|
|
109
|
+
font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace;
|
|
110
|
+
outline: none;
|
|
111
|
+
box-sizing: border-box;
|
|
112
|
+
}
|
|
113
|
+
.sb-auth-input:focus {
|
|
114
|
+
border-color: #58a6ff;
|
|
115
|
+
box-shadow: 0 0 0 3px rgba(88, 166, 255, 0.15);
|
|
116
|
+
}
|
|
117
|
+
.sb-auth-input::placeholder {
|
|
118
|
+
color: #484f58;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.sb-auth-scopes {
|
|
122
|
+
margin: 12px 0 0;
|
|
123
|
+
padding: 10px 12px;
|
|
124
|
+
background: #0d1117;
|
|
125
|
+
border: 1px solid #21262d;
|
|
126
|
+
border-radius: 6px;
|
|
127
|
+
font-size: 12px;
|
|
128
|
+
color: #8b949e;
|
|
129
|
+
line-height: 1.6;
|
|
130
|
+
}
|
|
131
|
+
.sb-auth-scopes code {
|
|
132
|
+
display: inline-block;
|
|
133
|
+
padding: 1px 5px;
|
|
134
|
+
background: rgba(110, 118, 129, 0.15);
|
|
135
|
+
border-radius: 4px;
|
|
136
|
+
font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace;
|
|
137
|
+
font-size: 11px;
|
|
138
|
+
color: #c9d1d9;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.sb-auth-footer {
|
|
142
|
+
display: flex;
|
|
143
|
+
align-items: center;
|
|
144
|
+
justify-content: flex-end;
|
|
145
|
+
gap: 8px;
|
|
146
|
+
padding: 16px 20px;
|
|
147
|
+
border-top: 1px solid #21262d;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.sb-auth-btn {
|
|
151
|
+
padding: 6px 16px;
|
|
152
|
+
border-radius: 6px;
|
|
153
|
+
font-size: 13px;
|
|
154
|
+
font-weight: 500;
|
|
155
|
+
font-family: inherit;
|
|
156
|
+
cursor: pointer;
|
|
157
|
+
border: 1px solid transparent;
|
|
158
|
+
transition: background 100ms ease;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
.sb-auth-btn-cancel {
|
|
162
|
+
background: #21262d;
|
|
163
|
+
border-color: #30363d;
|
|
164
|
+
color: #c9d1d9;
|
|
165
|
+
}
|
|
166
|
+
.sb-auth-btn-cancel:hover {
|
|
167
|
+
background: #30363d;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
.sb-auth-btn-submit {
|
|
171
|
+
background: #238636;
|
|
172
|
+
color: #fff;
|
|
173
|
+
}
|
|
174
|
+
.sb-auth-btn-submit:hover {
|
|
175
|
+
background: #2ea043;
|
|
176
|
+
}
|
|
177
|
+
.sb-auth-btn-submit:disabled {
|
|
178
|
+
opacity: 0.5;
|
|
179
|
+
cursor: not-allowed;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
.sb-auth-error {
|
|
183
|
+
margin: 10px 0 0;
|
|
184
|
+
padding: 8px 12px;
|
|
185
|
+
background: rgba(248, 81, 73, 0.1);
|
|
186
|
+
border: 1px solid rgba(248, 81, 73, 0.3);
|
|
187
|
+
border-radius: 6px;
|
|
188
|
+
font-size: 13px;
|
|
189
|
+
color: #f85149;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
.sb-auth-success {
|
|
193
|
+
display: flex;
|
|
194
|
+
align-items: center;
|
|
195
|
+
gap: 12px;
|
|
196
|
+
padding: 4px 0;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
.sb-auth-avatar {
|
|
200
|
+
width: 40px;
|
|
201
|
+
height: 40px;
|
|
202
|
+
border-radius: 50%;
|
|
203
|
+
border: 2px solid #30363d;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
.sb-auth-user-info {
|
|
207
|
+
font-size: 14px;
|
|
208
|
+
color: #f0f6fc;
|
|
209
|
+
}
|
|
210
|
+
.sb-auth-user-info span {
|
|
211
|
+
display: block;
|
|
212
|
+
font-size: 12px;
|
|
213
|
+
color: #3fb950;
|
|
214
|
+
margin-top: 2px;
|
|
215
|
+
}
|
|
216
|
+
`
|
|
217
|
+
document.head.appendChild(style)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Open the auth modal. Returns a promise that resolves with the user info
|
|
222
|
+
* on successful sign-in, or null if cancelled.
|
|
223
|
+
* @returns {Promise<{ login: string, avatarUrl: string }|null>}
|
|
224
|
+
*/
|
|
225
|
+
export function openAuthModal() {
|
|
226
|
+
injectStyles()
|
|
227
|
+
|
|
228
|
+
return new Promise((resolve) => {
|
|
229
|
+
// Remove any existing modal
|
|
230
|
+
const existing = document.getElementById(MODAL_ID)
|
|
231
|
+
if (existing) existing.remove()
|
|
232
|
+
|
|
233
|
+
const backdrop = document.createElement('div')
|
|
234
|
+
backdrop.id = MODAL_ID
|
|
235
|
+
backdrop.className = 'sb-auth-backdrop'
|
|
236
|
+
|
|
237
|
+
const modal = document.createElement('div')
|
|
238
|
+
modal.className = 'sb-auth-modal'
|
|
239
|
+
|
|
240
|
+
modal.innerHTML = `
|
|
241
|
+
<div class="sb-auth-header">
|
|
242
|
+
<h2>Sign in for comments</h2>
|
|
243
|
+
<button class="sb-auth-close" data-action="close" aria-label="Close">×</button>
|
|
244
|
+
</div>
|
|
245
|
+
<div class="sb-auth-body">
|
|
246
|
+
<p class="sb-auth-description">
|
|
247
|
+
Enter a <a href="https://github.com/settings/tokens/new" target="_blank" rel="noopener">GitHub Personal Access Token</a>
|
|
248
|
+
to leave comments on this prototype. Your token is stored locally in your browser.
|
|
249
|
+
</p>
|
|
250
|
+
<label class="sb-auth-label" for="sb-auth-token-input">Personal Access Token</label>
|
|
251
|
+
<input class="sb-auth-input" id="sb-auth-token-input" type="password" placeholder="ghp_xxxxxxxxxxxx" autocomplete="off" spellcheck="false" />
|
|
252
|
+
<div class="sb-auth-scopes">Required scopes: <code>repo</code> <code>read:user</code></div>
|
|
253
|
+
<div data-slot="feedback"></div>
|
|
254
|
+
</div>
|
|
255
|
+
<div class="sb-auth-footer">
|
|
256
|
+
<button class="sb-auth-btn sb-auth-btn-cancel" data-action="close">Cancel</button>
|
|
257
|
+
<button class="sb-auth-btn sb-auth-btn-submit" data-action="submit">Sign in</button>
|
|
258
|
+
</div>
|
|
259
|
+
`
|
|
260
|
+
|
|
261
|
+
backdrop.appendChild(modal)
|
|
262
|
+
document.body.appendChild(backdrop)
|
|
263
|
+
|
|
264
|
+
const input = modal.querySelector('#sb-auth-token-input')
|
|
265
|
+
const submitBtn = modal.querySelector('[data-action="submit"]')
|
|
266
|
+
const feedbackSlot = modal.querySelector('[data-slot="feedback"]')
|
|
267
|
+
|
|
268
|
+
function close(result) {
|
|
269
|
+
backdrop.remove()
|
|
270
|
+
resolve(result)
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Close on backdrop click
|
|
274
|
+
backdrop.addEventListener('click', (e) => {
|
|
275
|
+
if (e.target === backdrop) close(null)
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
// Close buttons
|
|
279
|
+
modal.querySelectorAll('[data-action="close"]').forEach((btn) => {
|
|
280
|
+
btn.addEventListener('click', () => close(null))
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
// Escape key
|
|
284
|
+
function onKeyDown(e) {
|
|
285
|
+
if (e.key === 'Escape') {
|
|
286
|
+
e.preventDefault()
|
|
287
|
+
e.stopPropagation()
|
|
288
|
+
window.removeEventListener('keydown', onKeyDown, true)
|
|
289
|
+
close(null)
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
window.addEventListener('keydown', onKeyDown, true)
|
|
293
|
+
|
|
294
|
+
// Submit
|
|
295
|
+
async function submit() {
|
|
296
|
+
const token = input.value.trim()
|
|
297
|
+
if (!token) {
|
|
298
|
+
input.focus()
|
|
299
|
+
return
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
submitBtn.disabled = true
|
|
303
|
+
submitBtn.textContent = 'Validating…'
|
|
304
|
+
feedbackSlot.innerHTML = ''
|
|
305
|
+
|
|
306
|
+
try {
|
|
307
|
+
const user = await validateToken(token)
|
|
308
|
+
setToken(token)
|
|
309
|
+
|
|
310
|
+
feedbackSlot.innerHTML = `
|
|
311
|
+
<div class="sb-auth-success">
|
|
312
|
+
<img class="sb-auth-avatar" src="${user.avatarUrl}" alt="${user.login}" />
|
|
313
|
+
<div class="sb-auth-user-info">
|
|
314
|
+
${user.login}
|
|
315
|
+
<span>✓ Signed in</span>
|
|
316
|
+
</div>
|
|
317
|
+
</div>
|
|
318
|
+
`
|
|
319
|
+
submitBtn.textContent = 'Done'
|
|
320
|
+
submitBtn.disabled = false
|
|
321
|
+
submitBtn.onclick = () => {
|
|
322
|
+
window.removeEventListener('keydown', onKeyDown, true)
|
|
323
|
+
close(user)
|
|
324
|
+
}
|
|
325
|
+
} catch (err) {
|
|
326
|
+
feedbackSlot.innerHTML = `<div class="sb-auth-error">${err.message}</div>`
|
|
327
|
+
submitBtn.disabled = false
|
|
328
|
+
submitBtn.textContent = 'Sign in'
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
submitBtn.addEventListener('click', submit)
|
|
333
|
+
input.addEventListener('keydown', (e) => {
|
|
334
|
+
if (e.key === 'Enter') submit()
|
|
335
|
+
})
|
|
336
|
+
|
|
337
|
+
// Auto-focus
|
|
338
|
+
requestAnimationFrame(() => input.focus())
|
|
339
|
+
})
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Open a sign-out confirmation. Clears token immediately.
|
|
344
|
+
*/
|
|
345
|
+
export function signOut() {
|
|
346
|
+
const user = getCachedUser()
|
|
347
|
+
clearToken()
|
|
348
|
+
console.log(`[storyboard] Signed out${user ? ` (was ${user.login})` : ''}`)
|
|
349
|
+
}
|