@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.
Files changed (42) hide show
  1. package/package.json +18 -0
  2. package/src/comments/api.js +196 -0
  3. package/src/comments/api.test.js +194 -0
  4. package/src/comments/auth.js +79 -0
  5. package/src/comments/auth.test.js +60 -0
  6. package/src/comments/commentMode.js +63 -0
  7. package/src/comments/commentMode.test.js +87 -0
  8. package/src/comments/config.js +43 -0
  9. package/src/comments/config.test.js +76 -0
  10. package/src/comments/graphql.js +65 -0
  11. package/src/comments/graphql.test.js +95 -0
  12. package/src/comments/index.js +40 -0
  13. package/src/comments/metadata.js +52 -0
  14. package/src/comments/metadata.test.js +110 -0
  15. package/src/comments/queries.js +182 -0
  16. package/src/comments/ui/CommentOverlay.js +52 -0
  17. package/src/comments/ui/authModal.js +349 -0
  18. package/src/comments/ui/commentWindow.js +872 -0
  19. package/src/comments/ui/commentsDrawer.js +389 -0
  20. package/src/comments/ui/composer.js +248 -0
  21. package/src/comments/ui/mount.js +364 -0
  22. package/src/devtools.js +365 -0
  23. package/src/devtools.test.js +81 -0
  24. package/src/dotPath.js +53 -0
  25. package/src/dotPath.test.js +114 -0
  26. package/src/hashSubscribe.js +19 -0
  27. package/src/hashSubscribe.test.js +62 -0
  28. package/src/hideMode.js +421 -0
  29. package/src/hideMode.test.js +224 -0
  30. package/src/index.js +38 -0
  31. package/src/interceptHideParams.js +35 -0
  32. package/src/interceptHideParams.test.js +90 -0
  33. package/src/loader.js +212 -0
  34. package/src/loader.test.js +232 -0
  35. package/src/localStorage.js +134 -0
  36. package/src/localStorage.test.js +148 -0
  37. package/src/sceneDebug.js +108 -0
  38. package/src/sceneDebug.test.js +128 -0
  39. package/src/session.js +76 -0
  40. package/src/session.test.js +91 -0
  41. package/src/viewfinder.js +47 -0
  42. 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
+ }