@dfosco/storyboard-core 3.6.0 → 3.7.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/dist/storyboard-ui.css +1 -1
- package/dist/storyboard-ui.js +12274 -11387
- package/dist/storyboard-ui.js.map +1 -1
- package/dist/tailwind.css +1 -1
- package/package.json +1 -1
- package/src/CanvasZoomControl.svelte +8 -8
- package/src/CommentsMenuButton.svelte +7 -21
- package/src/CoreUIBar.svelte +19 -3
- package/src/CreateMenuButton.svelte +8 -12
- package/src/InspectorPanel.svelte +12 -15
- package/src/SidePanel.svelte +14 -14
- package/src/assets/fonts/IoskeleyMono-Bold.woff2 +0 -0
- package/src/assets/fonts/IoskeleyMono-Italic.woff2 +0 -0
- package/src/assets/fonts/IoskeleyMono-Medium.woff2 +0 -0
- package/src/assets/fonts/IoskeleyMono-Regular.woff2 +0 -0
- package/src/assets/fonts/IoskeleyMono-SemiBold.woff2 +0 -0
- package/src/comments/ui/AuthModal.svelte +45 -12
- package/src/comments/ui/authModal.js +6 -1
- package/src/comments/ui/comment-layout.css +15 -15
- package/src/comments/ui/commentWindow.js +6 -1
- package/src/comments/ui/comments.css +57 -57
- package/src/comments/ui/commentsDrawer.js +2 -0
- package/src/comments/ui/composer.js +7 -2
- package/src/comments/ui/mount.js +252 -33
- package/src/comments/ui/mount.test.js +138 -0
- package/src/core-ui-colors.css +28 -28
- package/src/inspector/mouseMode.js +2 -2
- package/src/lib/components/ui/button/button.svelte +9 -9
- package/src/lib/components/ui/panel/panel-content.svelte +2 -2
- package/src/lib/components/ui/select/select-trigger.svelte +1 -1
- package/src/lib/components/ui/toggle/toggle.svelte +1 -1
- package/src/lib/components/ui/toggle-group/toggle-group.svelte +2 -2
- package/src/lib/components/ui/trigger-button/trigger-button.svelte +13 -13
- package/src/modes.css +21 -21
- package/src/mountStoryboardCore.js +4 -4
- package/src/sidepanel.css +11 -11
- package/src/styles/tailwind.css +89 -1
- package/src/svelte-plugin-ui/components/ModeSwitch.svelte +3 -3
- package/src/svelte-plugin-ui/components/Viewfinder.svelte +31 -11
- package/src/svelte-plugin-ui/styles/base.css +41 -41
- package/src/workshop/features/createFlow/CreateFlowForm.svelte +187 -25
- package/src/workshop/features/createFlow/server.js +437 -40
- package/src/workshop/features/createPage/CreatePageForm.svelte +249 -0
- package/src/workshop/features/createPage/index.js +11 -0
- package/src/workshop/features/createPrototype/CreatePrototypeForm.svelte +77 -24
- package/src/workshop/features/createPrototype/server.js +14 -16
- package/src/workshop/features/registry-server.js +1 -0
- package/src/workshop/features/registry.js +2 -0
- package/src/workshop/features/templateIndex.js +155 -0
- package/toolbar.config.json +2 -1
package/src/comments/ui/mount.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Call mountComments() once at app startup (after initCommentsConfig).
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { isCommentsEnabled } from '../config.js'
|
|
7
|
+
import { getCommentsConfig, isCommentsEnabled } from '../config.js'
|
|
8
8
|
import { isAuthenticated, getCachedUser } from '../auth.js'
|
|
9
9
|
import { toggleCommentMode, setCommentMode, isCommentModeActive, subscribeToCommentMode } from '../commentMode.js'
|
|
10
10
|
import { fetchRouteCommentsSummary, fetchCommentDetail, moveComment, createComment } from '../api.js'
|
|
@@ -13,11 +13,20 @@ import { showComposer } from './composer.js'
|
|
|
13
13
|
import { openAuthModal } from './authModal.js'
|
|
14
14
|
import { showCommentWindow, closeCommentWindow } from './commentWindow.js'
|
|
15
15
|
|
|
16
|
+
const INVALID_PAT_ERROR_MESSAGE = 'GitHub PAT is invalid or expired. Please sign in again.'
|
|
17
|
+
const TOKEN_ACCESS_ERROR_MESSAGE =
|
|
18
|
+
`Token doesn't have access to repository discussions. ` +
|
|
19
|
+
'Fine-grained tokens need "Discussions: Read and write". ' +
|
|
20
|
+
'Classic tokens need the "repo" scope.'
|
|
21
|
+
|
|
16
22
|
let banner = null
|
|
17
23
|
let overlay = null
|
|
18
24
|
let activeComposer = null
|
|
19
25
|
let renderedPins = []
|
|
20
26
|
let cachedDiscussion = null
|
|
27
|
+
const CANVAS_SCROLL_SELECTOR = '[data-storyboard-canvas-scroll]'
|
|
28
|
+
const CANVAS_ZOOM_SELECTOR = '[data-storyboard-canvas-zoom]'
|
|
29
|
+
const CANVAS_SURFACE_SELECTOR = '.tc-canvas'
|
|
21
30
|
|
|
22
31
|
function esc(str) {
|
|
23
32
|
const d = document.createElement('div')
|
|
@@ -25,12 +34,97 @@ function esc(str) {
|
|
|
25
34
|
return d.innerHTML
|
|
26
35
|
}
|
|
27
36
|
|
|
37
|
+
function roundPct(value) {
|
|
38
|
+
return Math.round(value * 10) / 10
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function parseScale(transform) {
|
|
42
|
+
if (!transform || transform === 'none') return 1
|
|
43
|
+
const scaleMatch = transform.match(/scale\(([^)]+)\)/)
|
|
44
|
+
if (scaleMatch) {
|
|
45
|
+
const parsed = Number.parseFloat(scaleMatch[1])
|
|
46
|
+
if (Number.isFinite(parsed) && parsed > 0) return parsed
|
|
47
|
+
}
|
|
48
|
+
const matrixMatch = transform.match(/matrix\(([^)]+)\)/)
|
|
49
|
+
if (matrixMatch) {
|
|
50
|
+
const first = Number.parseFloat(matrixMatch[1].split(',')[0])
|
|
51
|
+
if (Number.isFinite(first) && first > 0) return first
|
|
52
|
+
}
|
|
53
|
+
return 1
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function getCanvasContext() {
|
|
57
|
+
const scrollEl = document.querySelector(CANVAS_SCROLL_SELECTOR)
|
|
58
|
+
const zoomEl = document.querySelector(CANVAS_ZOOM_SELECTOR)
|
|
59
|
+
const canvasEl = (zoomEl && zoomEl.querySelector(CANVAS_SURFACE_SELECTOR)) || null
|
|
60
|
+
if (!scrollEl || !zoomEl || !canvasEl) return null
|
|
61
|
+
const scale = parseScale(zoomEl.style.transform || getComputedStyle(zoomEl).transform)
|
|
62
|
+
const width = canvasEl.offsetWidth || canvasEl.clientWidth
|
|
63
|
+
const height = canvasEl.offsetHeight || canvasEl.clientHeight
|
|
64
|
+
if (!width || !height) return null
|
|
65
|
+
return { scrollEl, scale, width, height }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function getAnchorPosition(xPct, yPct) {
|
|
69
|
+
const canvas = getCanvasContext()
|
|
70
|
+
if (!canvas) {
|
|
71
|
+
return {
|
|
72
|
+
left: `${xPct}%`,
|
|
73
|
+
top: `${yPct}%`,
|
|
74
|
+
canvas: false,
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const rect = canvas.scrollEl.getBoundingClientRect()
|
|
79
|
+
const canvasX = (xPct / 100) * canvas.width
|
|
80
|
+
const canvasY = (yPct / 100) * canvas.height
|
|
81
|
+
const left = rect.left + (canvasX * canvas.scale) - canvas.scrollEl.scrollLeft
|
|
82
|
+
const top = rect.top + (canvasY * canvas.scale) - canvas.scrollEl.scrollTop
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
left: `${left}px`,
|
|
86
|
+
top: `${top}px`,
|
|
87
|
+
canvas: true,
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function getPercentFromPointer(clientX, clientY) {
|
|
92
|
+
const canvas = getCanvasContext()
|
|
93
|
+
if (canvas) {
|
|
94
|
+
const rect = canvas.scrollEl.getBoundingClientRect()
|
|
95
|
+
const canvasX = (clientX - rect.left + canvas.scrollEl.scrollLeft) / canvas.scale
|
|
96
|
+
const canvasY = (clientY - rect.top + canvas.scrollEl.scrollTop) / canvas.scale
|
|
97
|
+
const xPct = roundPct((canvasX / canvas.width) * 100)
|
|
98
|
+
const yPct = roundPct((canvasY / canvas.height) * 100)
|
|
99
|
+
return { xPct, yPct, canvas: true }
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const xPct = roundPct((clientX / window.innerWidth) * 100)
|
|
103
|
+
const docHeight = document.documentElement.scrollHeight
|
|
104
|
+
const yPct = roundPct(((clientY + window.scrollY) / docHeight) * 100)
|
|
105
|
+
return { xPct, yPct, canvas: false }
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function syncOverlayCoordinateSpace() {
|
|
109
|
+
if (!overlay) return
|
|
110
|
+
if (getCanvasContext()) {
|
|
111
|
+
overlay.style.position = 'fixed'
|
|
112
|
+
overlay.style.width = '100vw'
|
|
113
|
+
overlay.style.height = '100vh'
|
|
114
|
+
} else {
|
|
115
|
+
overlay.style.position = 'absolute'
|
|
116
|
+
overlay.style.width = ''
|
|
117
|
+
overlay.style.height = ''
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
28
121
|
function ensureOverlay() {
|
|
29
122
|
if (overlay) return overlay
|
|
30
123
|
|
|
31
124
|
overlay = document.createElement('div')
|
|
32
125
|
overlay.className = 'sb-comment-overlay'
|
|
33
126
|
document.body.appendChild(overlay)
|
|
127
|
+
syncOverlayCoordinateSpace()
|
|
34
128
|
|
|
35
129
|
// Click handler for placing comments lives on the overlay itself
|
|
36
130
|
overlay.addEventListener('click', (e) => {
|
|
@@ -38,6 +132,26 @@ function ensureOverlay() {
|
|
|
38
132
|
if (e.target.closest('.sb-composer') || e.target.closest('.sb-comment-pin') || e.target.closest('.sb-comment-window')) return
|
|
39
133
|
handleOverlayClick(e)
|
|
40
134
|
})
|
|
135
|
+
// Keep canvas scroll usable while comment mode is active.
|
|
136
|
+
overlay.addEventListener('wheel', (e) => {
|
|
137
|
+
if (!isCommentModeActive()) return
|
|
138
|
+
const target = e.target
|
|
139
|
+
if (
|
|
140
|
+
target instanceof Element &&
|
|
141
|
+
(target.closest('.sb-composer') || target.closest('.sb-comment-pin') || target.closest('.sb-comment-window'))
|
|
142
|
+
) {
|
|
143
|
+
return
|
|
144
|
+
}
|
|
145
|
+
const canvas = getCanvasContext()
|
|
146
|
+
if (!canvas) return
|
|
147
|
+
if (typeof canvas.scrollEl.scrollBy === 'function') {
|
|
148
|
+
canvas.scrollEl.scrollBy({ left: e.deltaX, top: e.deltaY, behavior: 'auto' })
|
|
149
|
+
} else {
|
|
150
|
+
canvas.scrollEl.scrollLeft += e.deltaX
|
|
151
|
+
canvas.scrollEl.scrollTop += e.deltaY
|
|
152
|
+
}
|
|
153
|
+
e.preventDefault()
|
|
154
|
+
}, { passive: false })
|
|
41
155
|
|
|
42
156
|
return overlay
|
|
43
157
|
}
|
|
@@ -64,6 +178,48 @@ function getCurrentRoute() {
|
|
|
64
178
|
return window.location.pathname
|
|
65
179
|
}
|
|
66
180
|
|
|
181
|
+
function getAuthErrorMessage(err) {
|
|
182
|
+
const message = typeof err === 'string'
|
|
183
|
+
? err
|
|
184
|
+
: (typeof err?.message === 'string' ? err.message : String(err ?? ''))
|
|
185
|
+
|
|
186
|
+
if (message.includes('invalid or expired')) {
|
|
187
|
+
return INVALID_PAT_ERROR_MESSAGE
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (message.includes('Not authenticated — no GitHub PAT found')) {
|
|
191
|
+
return 'Not authenticated — no GitHub PAT found. Please sign in.'
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (
|
|
195
|
+
message.includes('Resource not accessible by personal access token') ||
|
|
196
|
+
message.includes('insufficient') ||
|
|
197
|
+
message.includes("doesn't have access")
|
|
198
|
+
) {
|
|
199
|
+
return TOKEN_ACCESS_ERROR_MESSAGE
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (message.includes('Could not resolve to a Repository with the name')) {
|
|
203
|
+
const config = getCommentsConfig()
|
|
204
|
+
const repo = config?.repo?.owner && config?.repo?.name
|
|
205
|
+
? `${config.repo.owner}/${config.repo.name}`
|
|
206
|
+
: 'the configured repository'
|
|
207
|
+
|
|
208
|
+
return `Token cannot access repository \`${repo}\`. Please set the PAT repository access to \`${repo}\` and include Discussions read/write (classic tokens need repo scope).`
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return null
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async function promptReauthForAuthError(err) {
|
|
215
|
+
const errorMessage = getAuthErrorMessage(err)
|
|
216
|
+
if (!errorMessage) return false
|
|
217
|
+
|
|
218
|
+
setCommentMode(false)
|
|
219
|
+
openAuthModal({ initialError: errorMessage })
|
|
220
|
+
return true
|
|
221
|
+
}
|
|
222
|
+
|
|
67
223
|
function clearPins() {
|
|
68
224
|
for (const pin of renderedPins) pin.remove()
|
|
69
225
|
renderedPins = []
|
|
@@ -82,8 +238,9 @@ function renderOptimisticPin(ov, xPct, yPct, text, user) {
|
|
|
82
238
|
const pendingId = `pending-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
|
|
83
239
|
const pin = document.createElement('div')
|
|
84
240
|
pin.className = 'sb-comment-pin sb-comment-pin-pending absolute br-100 sb-bg pointer sb-shadow pe-auto overflow-hidden'
|
|
85
|
-
|
|
86
|
-
pin.style.
|
|
241
|
+
const anchor = getAnchorPosition(xPct, yPct)
|
|
242
|
+
pin.style.left = anchor.left
|
|
243
|
+
pin.style.top = anchor.top
|
|
87
244
|
pin.title = `${user?.login ?? 'you'}: ${text.slice(0, 80)}`
|
|
88
245
|
|
|
89
246
|
pin.innerHTML = user?.avatarUrl
|
|
@@ -118,7 +275,8 @@ function renderOptimisticPin(ov, xPct, yPct, text, user) {
|
|
|
118
275
|
removePendingComment(route, pendingId)
|
|
119
276
|
pin.classList.remove('sb-comment-pin-pending')
|
|
120
277
|
reloadComments()
|
|
121
|
-
} catch {
|
|
278
|
+
} catch (err) {
|
|
279
|
+
if (await promptReauthForAuthError(err)) return
|
|
122
280
|
pin.classList.remove('sb-comment-pin-pending')
|
|
123
281
|
pin.classList.add('sb-comment-pin-failed')
|
|
124
282
|
pin.title = `⚠ Failed to post — click to retry: ${text.slice(0, 60)}`
|
|
@@ -137,8 +295,9 @@ function renderPendingPins(ov) {
|
|
|
137
295
|
for (const p of pending) {
|
|
138
296
|
const pin = document.createElement('div')
|
|
139
297
|
pin.className = 'sb-comment-pin sb-comment-pin-failed absolute br-100 sb-bg pointer sb-shadow pe-auto overflow-hidden'
|
|
140
|
-
|
|
141
|
-
pin.style.
|
|
298
|
+
const anchor = getAnchorPosition(p.x, p.y)
|
|
299
|
+
pin.style.left = anchor.left
|
|
300
|
+
pin.style.top = anchor.top
|
|
142
301
|
pin.title = `⚠ Failed to post — click to retry: ${p.text?.slice(0, 60) ?? ''}`
|
|
143
302
|
|
|
144
303
|
pin.innerHTML = p.author?.avatarUrl
|
|
@@ -155,7 +314,8 @@ function renderPendingPins(ov) {
|
|
|
155
314
|
removePendingComment(route, p.id)
|
|
156
315
|
pin.remove()
|
|
157
316
|
reloadComments()
|
|
158
|
-
} catch {
|
|
317
|
+
} catch (err) {
|
|
318
|
+
if (await promptReauthForAuthError(err)) return
|
|
159
319
|
pin.classList.remove('sb-comment-pin-pending')
|
|
160
320
|
pin.classList.add('sb-comment-pin-failed')
|
|
161
321
|
pin.title = `⚠ Failed to post — click to retry: ${p.text?.slice(0, 60) ?? ''}`
|
|
@@ -171,8 +331,9 @@ function renderPin(ov, comment, index) {
|
|
|
171
331
|
const hue = Math.round((index * 137.5) % 360)
|
|
172
332
|
const pin = document.createElement('div')
|
|
173
333
|
pin.className = 'sb-comment-pin absolute br-100 sb-bg pointer sb-shadow pe-auto overflow-hidden'
|
|
174
|
-
|
|
175
|
-
pin.style.
|
|
334
|
+
const startAnchor = getAnchorPosition(comment.meta?.x ?? 0, comment.meta?.y ?? 0)
|
|
335
|
+
pin.style.left = startAnchor.left
|
|
336
|
+
pin.style.top = startAnchor.top
|
|
176
337
|
pin.style.setProperty('--pin-hue', String(hue))
|
|
177
338
|
|
|
178
339
|
if (comment.meta?.resolved) pin.setAttribute('data-resolved', 'true')
|
|
@@ -192,19 +353,29 @@ function renderPin(ov, comment, index) {
|
|
|
192
353
|
dragged = false
|
|
193
354
|
const startX = e.clientX
|
|
194
355
|
const startY = e.clientY
|
|
195
|
-
const
|
|
196
|
-
const
|
|
356
|
+
const startCoords = getPercentFromPointer(e.clientX, e.clientY)
|
|
357
|
+
const startLeftPct = startCoords.xPct
|
|
358
|
+
const startTopPct = startCoords.yPct
|
|
359
|
+
let lastCoords = { xPct: startLeftPct, yPct: startTopPct }
|
|
197
360
|
|
|
198
361
|
const onMove = (ev) => {
|
|
199
362
|
const dx = ev.clientX - startX
|
|
200
363
|
const dy = ev.clientY - startY
|
|
201
364
|
if (!dragged && Math.abs(dx) < 4 && Math.abs(dy) < 4) return
|
|
202
365
|
dragged = true
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
366
|
+
|
|
367
|
+
if (startCoords.canvas) {
|
|
368
|
+
lastCoords = getPercentFromPointer(ev.clientX, ev.clientY)
|
|
369
|
+
pin.style.left = `${ev.clientX}px`
|
|
370
|
+
pin.style.top = `${ev.clientY}px`
|
|
371
|
+
} else {
|
|
372
|
+
const xPct = roundPct(startLeftPct + (dx / window.innerWidth) * 100)
|
|
373
|
+
const docHeight = document.documentElement.scrollHeight
|
|
374
|
+
const yPct = roundPct(startTopPct + (dy / docHeight) * 100)
|
|
375
|
+
lastCoords = { xPct, yPct }
|
|
376
|
+
pin.style.left = `${xPct}%`
|
|
377
|
+
pin.style.top = `${yPct}%`
|
|
378
|
+
}
|
|
208
379
|
}
|
|
209
380
|
|
|
210
381
|
const onUp = async (ev) => {
|
|
@@ -212,11 +383,15 @@ function renderPin(ov, comment, index) {
|
|
|
212
383
|
document.removeEventListener('mouseup', onUp)
|
|
213
384
|
if (!dragged) return
|
|
214
385
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
386
|
+
let xPct = lastCoords.xPct
|
|
387
|
+
let yPct = lastCoords.yPct
|
|
388
|
+
if (!startCoords.canvas) {
|
|
389
|
+
const dx = ev.clientX - startX
|
|
390
|
+
const dy = ev.clientY - startY
|
|
391
|
+
xPct = roundPct(startLeftPct + (dx / window.innerWidth) * 100)
|
|
392
|
+
const docHeight = document.documentElement.scrollHeight
|
|
393
|
+
yPct = roundPct(startTopPct + (dy / docHeight) * 100)
|
|
394
|
+
}
|
|
220
395
|
comment.meta = { ...comment.meta, x: xPct, y: yPct }
|
|
221
396
|
|
|
222
397
|
try {
|
|
@@ -224,6 +399,7 @@ function renderPin(ov, comment, index) {
|
|
|
224
399
|
comment._rawBody = null
|
|
225
400
|
clearCachedComments(getCurrentRoute())
|
|
226
401
|
} catch (err) {
|
|
402
|
+
if (await promptReauthForAuthError(err)) return
|
|
227
403
|
console.error('[storyboard] Failed to move pin:', err)
|
|
228
404
|
}
|
|
229
405
|
}
|
|
@@ -246,6 +422,7 @@ function renderPin(ov, comment, index) {
|
|
|
246
422
|
if (detail) {
|
|
247
423
|
detail._rawBody = detail.body
|
|
248
424
|
showCommentWindow(ov, detail, cachedDiscussion, {
|
|
425
|
+
getAnchorPosition,
|
|
249
426
|
onClose: () => {},
|
|
250
427
|
onMove: () => reloadComments(),
|
|
251
428
|
})
|
|
@@ -254,6 +431,7 @@ function renderPin(ov, comment, index) {
|
|
|
254
431
|
console.warn('[storyboard] Could not load comment detail:', err.message)
|
|
255
432
|
// Fall back to summary data
|
|
256
433
|
showCommentWindow(ov, comment, cachedDiscussion, {
|
|
434
|
+
getAnchorPosition,
|
|
257
435
|
onClose: () => {},
|
|
258
436
|
onMove: () => reloadComments(),
|
|
259
437
|
})
|
|
@@ -312,6 +490,7 @@ async function loadAndRenderComments() {
|
|
|
312
490
|
|
|
313
491
|
autoOpenCommentFromUrl(ov, discussion)
|
|
314
492
|
} catch (err) {
|
|
493
|
+
if (await promptReauthForAuthError(err)) return
|
|
315
494
|
console.warn('[storyboard] Could not load comments:', err.message)
|
|
316
495
|
}
|
|
317
496
|
}
|
|
@@ -324,13 +503,25 @@ async function autoOpenCommentFromUrl(ov, discussion) {
|
|
|
324
503
|
if (!comment) return
|
|
325
504
|
|
|
326
505
|
if (comment.meta?.y != null) {
|
|
327
|
-
const
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
const
|
|
333
|
-
|
|
506
|
+
const canvas = getCanvasContext()
|
|
507
|
+
if (canvas) {
|
|
508
|
+
const canvasY = (comment.meta.y / 100) * canvas.height
|
|
509
|
+
const yPx = canvasY * canvas.scale
|
|
510
|
+
const viewTop = canvas.scrollEl.scrollTop
|
|
511
|
+
const viewBottom = viewTop + canvas.scrollEl.clientHeight
|
|
512
|
+
if (yPx < viewTop || yPx > viewBottom) {
|
|
513
|
+
const scrollTarget = Math.max(0, yPx - canvas.scrollEl.clientHeight / 3)
|
|
514
|
+
canvas.scrollEl.scrollTo({ top: scrollTarget, behavior: 'smooth' })
|
|
515
|
+
}
|
|
516
|
+
} else {
|
|
517
|
+
const docHeight = document.documentElement.scrollHeight
|
|
518
|
+
const yPx = (comment.meta.y / 100) * docHeight
|
|
519
|
+
const viewTop = window.scrollY
|
|
520
|
+
const viewBottom = viewTop + window.innerHeight
|
|
521
|
+
if (yPx < viewTop || yPx > viewBottom) {
|
|
522
|
+
const scrollTarget = Math.max(0, yPx - window.innerHeight / 3)
|
|
523
|
+
window.scrollTo({ top: scrollTarget, behavior: 'smooth' })
|
|
524
|
+
}
|
|
334
525
|
}
|
|
335
526
|
}
|
|
336
527
|
|
|
@@ -340,18 +531,21 @@ async function autoOpenCommentFromUrl(ov, discussion) {
|
|
|
340
531
|
if (detail) {
|
|
341
532
|
detail._rawBody = detail.body
|
|
342
533
|
showCommentWindow(ov, detail, discussion, {
|
|
534
|
+
getAnchorPosition,
|
|
343
535
|
onClose: () => {},
|
|
344
536
|
onMove: () => reloadComments(),
|
|
345
537
|
})
|
|
346
538
|
return
|
|
347
539
|
}
|
|
348
540
|
} catch (err) {
|
|
541
|
+
if (await promptReauthForAuthError(err)) return
|
|
349
542
|
console.warn('[storyboard] Could not load comment detail:', err.message)
|
|
350
543
|
}
|
|
351
544
|
|
|
352
545
|
// Fallback to summary data
|
|
353
546
|
comment._rawBody = comment.body
|
|
354
547
|
showCommentWindow(ov, comment, discussion, {
|
|
548
|
+
getAnchorPosition,
|
|
355
549
|
onClose: () => {},
|
|
356
550
|
onMove: () => reloadComments(),
|
|
357
551
|
})
|
|
@@ -363,10 +557,7 @@ function handleOverlayClick(e) {
|
|
|
363
557
|
|
|
364
558
|
closeCommentWindow()
|
|
365
559
|
|
|
366
|
-
|
|
367
|
-
const xPct = Math.round((e.clientX / window.innerWidth) * 1000) / 10
|
|
368
|
-
const docHeight = document.documentElement.scrollHeight
|
|
369
|
-
const yPct = Math.round(((e.clientY + window.scrollY) / docHeight) * 1000) / 10
|
|
560
|
+
const { xPct, yPct } = getPercentFromPointer(e.clientX, e.clientY)
|
|
370
561
|
|
|
371
562
|
// Move existing composer instead of destroying and recreating
|
|
372
563
|
if (activeComposer) {
|
|
@@ -377,6 +568,7 @@ function handleOverlayClick(e) {
|
|
|
377
568
|
const ov = ensureOverlay()
|
|
378
569
|
const route = getCurrentRoute()
|
|
379
570
|
activeComposer = showComposer(ov, xPct, yPct, route, {
|
|
571
|
+
getAnchorPosition,
|
|
380
572
|
onCancel: () => { activeComposer = null },
|
|
381
573
|
onSubmitOptimistic: (text, x, y) => {
|
|
382
574
|
activeComposer = null
|
|
@@ -388,8 +580,9 @@ function handleOverlayClick(e) {
|
|
|
388
580
|
opt.succeed()
|
|
389
581
|
reloadComments()
|
|
390
582
|
})
|
|
391
|
-
.catch((err) => {
|
|
583
|
+
.catch(async (err) => {
|
|
392
584
|
console.error('[storyboard] Failed to post comment:', err)
|
|
585
|
+
if (await promptReauthForAuthError(err)) return
|
|
393
586
|
opt.fail()
|
|
394
587
|
})
|
|
395
588
|
},
|
|
@@ -401,6 +594,7 @@ function setBodyCommentMode(active) {
|
|
|
401
594
|
document.body.classList.add('sb-comment-mode')
|
|
402
595
|
showBanner()
|
|
403
596
|
ensureOverlay()
|
|
597
|
+
syncOverlayCoordinateSpace()
|
|
404
598
|
renderCachedPins()
|
|
405
599
|
loadAndRenderComments()
|
|
406
600
|
} else {
|
|
@@ -431,6 +625,31 @@ export function mountComments() {
|
|
|
431
625
|
_mounted = true
|
|
432
626
|
|
|
433
627
|
subscribeToCommentMode(setBodyCommentMode)
|
|
628
|
+
window.addEventListener('popstate', () => {
|
|
629
|
+
if (isCommentModeActive()) {
|
|
630
|
+
setCommentMode(false)
|
|
631
|
+
}
|
|
632
|
+
})
|
|
633
|
+
|
|
634
|
+
document.addEventListener('storyboard:canvas:mounted', () => {
|
|
635
|
+
syncOverlayCoordinateSpace()
|
|
636
|
+
if (isCommentModeActive()) renderCachedPins()
|
|
637
|
+
})
|
|
638
|
+
document.addEventListener('storyboard:canvas:unmounted', () => {
|
|
639
|
+
syncOverlayCoordinateSpace()
|
|
640
|
+
if (isCommentModeActive()) renderCachedPins()
|
|
641
|
+
})
|
|
642
|
+
document.addEventListener('storyboard:canvas:zoom-changed', () => {
|
|
643
|
+
syncOverlayCoordinateSpace()
|
|
644
|
+
if (isCommentModeActive()) renderCachedPins()
|
|
645
|
+
})
|
|
646
|
+
document.addEventListener('scroll', (e) => {
|
|
647
|
+
const target = e.target
|
|
648
|
+
if (!(target instanceof Element)) return
|
|
649
|
+
if (!target.matches(CANVAS_SCROLL_SELECTOR)) return
|
|
650
|
+
if (!isCommentModeActive()) return
|
|
651
|
+
renderCachedPins()
|
|
652
|
+
}, true)
|
|
434
653
|
|
|
435
654
|
window.addEventListener('keydown', (e) => {
|
|
436
655
|
const tag = e.target.tagName
|
|
@@ -48,6 +48,9 @@ describe('mount.js', () => {
|
|
|
48
48
|
let initCommentsConfig
|
|
49
49
|
let setToken
|
|
50
50
|
let clearToken
|
|
51
|
+
let createComment
|
|
52
|
+
let openAuthModal
|
|
53
|
+
let showComposer
|
|
51
54
|
|
|
52
55
|
beforeEach(async () => {
|
|
53
56
|
// Reset DOM
|
|
@@ -85,6 +88,9 @@ describe('mount.js', () => {
|
|
|
85
88
|
const commentModeMod = await import('../commentMode.js')
|
|
86
89
|
const configMod = await import('../config.js')
|
|
87
90
|
const authMod = await import('../auth.js')
|
|
91
|
+
const apiMod = await import('../api.js')
|
|
92
|
+
const authModalMod = await import('./authModal.js')
|
|
93
|
+
const composerMod = await import('./composer.js')
|
|
88
94
|
|
|
89
95
|
mountComments = mountMod.mountComments
|
|
90
96
|
setCommentMode = commentModeMod.setCommentMode
|
|
@@ -92,6 +98,9 @@ describe('mount.js', () => {
|
|
|
92
98
|
initCommentsConfig = configMod.initCommentsConfig
|
|
93
99
|
setToken = authMod.setToken
|
|
94
100
|
clearToken = authMod.clearToken
|
|
101
|
+
createComment = apiMod.createComment
|
|
102
|
+
openAuthModal = authModalMod.openAuthModal
|
|
103
|
+
showComposer = composerMod.showComposer
|
|
95
104
|
|
|
96
105
|
// Reset storyboard state
|
|
97
106
|
setCommentMode(false)
|
|
@@ -195,4 +204,133 @@ describe('mount.js', () => {
|
|
|
195
204
|
// No error thrown — _mounted guard prevents double init
|
|
196
205
|
})
|
|
197
206
|
})
|
|
207
|
+
|
|
208
|
+
describe('navigation and canvas coordinates', () => {
|
|
209
|
+
it('turns comment mode off on popstate navigation', () => {
|
|
210
|
+
initCommentsConfig({ comments: { repo: { owner: 'o', name: 'r' } } })
|
|
211
|
+
setToken('ghp_test')
|
|
212
|
+
mountComments()
|
|
213
|
+
|
|
214
|
+
setCommentMode(true)
|
|
215
|
+
expect(document.body.classList.contains('sb-comment-mode')).toBe(true)
|
|
216
|
+
|
|
217
|
+
window.dispatchEvent(new PopStateEvent('popstate'))
|
|
218
|
+
expect(document.body.classList.contains('sb-comment-mode')).toBe(false)
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
it('computes click coordinates relative to canvas absolute space', () => {
|
|
222
|
+
initCommentsConfig({ comments: { repo: { owner: 'o', name: 'r' } } })
|
|
223
|
+
setToken('ghp_test')
|
|
224
|
+
mountComments()
|
|
225
|
+
|
|
226
|
+
const scroll = document.createElement('div')
|
|
227
|
+
scroll.setAttribute('data-storyboard-canvas-scroll', '')
|
|
228
|
+
Object.defineProperty(scroll, 'scrollLeft', { value: 100, writable: true })
|
|
229
|
+
Object.defineProperty(scroll, 'scrollTop', { value: 50, writable: true })
|
|
230
|
+
Object.defineProperty(scroll, 'clientHeight', { value: 700, writable: true })
|
|
231
|
+
scroll.getBoundingClientRect = () => ({ left: 20, top: 10, right: 1020, bottom: 710, width: 1000, height: 700 })
|
|
232
|
+
|
|
233
|
+
const zoom = document.createElement('div')
|
|
234
|
+
zoom.setAttribute('data-storyboard-canvas-zoom', '')
|
|
235
|
+
zoom.style.transform = 'scale(2)'
|
|
236
|
+
|
|
237
|
+
const surface = document.createElement('main')
|
|
238
|
+
surface.className = 'tc-canvas'
|
|
239
|
+
Object.defineProperty(surface, 'offsetWidth', { value: 10000, writable: true })
|
|
240
|
+
Object.defineProperty(surface, 'offsetHeight', { value: 10000, writable: true })
|
|
241
|
+
zoom.appendChild(surface)
|
|
242
|
+
scroll.appendChild(zoom)
|
|
243
|
+
document.body.appendChild(scroll)
|
|
244
|
+
|
|
245
|
+
let captured = null
|
|
246
|
+
showComposer.mockImplementation((ov, x, y) => {
|
|
247
|
+
captured = { x, y }
|
|
248
|
+
return { destroy: vi.fn(), moveTo: vi.fn() }
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
setCommentMode(true)
|
|
252
|
+
const overlay = document.body.querySelector('.sb-comment-overlay')
|
|
253
|
+
overlay.dispatchEvent(new MouseEvent('click', { bubbles: true, clientX: 220, clientY: 210 }))
|
|
254
|
+
|
|
255
|
+
expect(captured).toEqual({ x: 1.5, y: 1.3 })
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
it('scrolls canvas on wheel while comment mode is active', () => {
|
|
259
|
+
initCommentsConfig({ comments: { repo: { owner: 'o', name: 'r' } } })
|
|
260
|
+
setToken('ghp_test')
|
|
261
|
+
mountComments()
|
|
262
|
+
|
|
263
|
+
const scroll = document.createElement('div')
|
|
264
|
+
scroll.setAttribute('data-storyboard-canvas-scroll', '')
|
|
265
|
+
Object.defineProperty(scroll, 'scrollLeft', { value: 0, writable: true })
|
|
266
|
+
Object.defineProperty(scroll, 'scrollTop', { value: 0, writable: true })
|
|
267
|
+
scroll.scrollBy = vi.fn(({ left = 0, top = 0 }) => {
|
|
268
|
+
scroll.scrollLeft += left
|
|
269
|
+
scroll.scrollTop += top
|
|
270
|
+
})
|
|
271
|
+
scroll.getBoundingClientRect = () => ({ left: 0, top: 0, right: 1000, bottom: 700, width: 1000, height: 700 })
|
|
272
|
+
|
|
273
|
+
const zoom = document.createElement('div')
|
|
274
|
+
zoom.setAttribute('data-storyboard-canvas-zoom', '')
|
|
275
|
+
zoom.style.transform = 'scale(1)'
|
|
276
|
+
|
|
277
|
+
const surface = document.createElement('main')
|
|
278
|
+
surface.className = 'tc-canvas'
|
|
279
|
+
Object.defineProperty(surface, 'offsetWidth', { value: 10000, writable: true })
|
|
280
|
+
Object.defineProperty(surface, 'offsetHeight', { value: 10000, writable: true })
|
|
281
|
+
zoom.appendChild(surface)
|
|
282
|
+
scroll.appendChild(zoom)
|
|
283
|
+
document.body.appendChild(scroll)
|
|
284
|
+
|
|
285
|
+
setCommentMode(true)
|
|
286
|
+
const overlay = document.body.querySelector('.sb-comment-overlay')
|
|
287
|
+
const wheelEvent = new WheelEvent('wheel', { bubbles: true, cancelable: true, deltaX: 8, deltaY: 24 })
|
|
288
|
+
overlay.dispatchEvent(wheelEvent)
|
|
289
|
+
|
|
290
|
+
expect(scroll.scrollBy).toHaveBeenCalledTimes(1)
|
|
291
|
+
expect(scroll.scrollLeft).toBe(8)
|
|
292
|
+
expect(scroll.scrollTop).toBe(24)
|
|
293
|
+
expect(wheelEvent.defaultPrevented).toBe(true)
|
|
294
|
+
})
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
describe('auth error handling', () => {
|
|
298
|
+
it('opens auth modal with repo-specific message when PAT lacks repository access during submit', async () => {
|
|
299
|
+
initCommentsConfig({
|
|
300
|
+
comments: { discussions: { category: 'Comments' } },
|
|
301
|
+
repository: { owner: 'correct', name: 'repository' },
|
|
302
|
+
})
|
|
303
|
+
setToken('ghp_test')
|
|
304
|
+
|
|
305
|
+
createComment.mockRejectedValueOnce(
|
|
306
|
+
new Error("GraphQL error: Could not resolve to a Repository with the name 'github/storyboard'.")
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
showComposer.mockImplementation((ov, x, y, route, opts) => {
|
|
310
|
+
queueMicrotask(() => {
|
|
311
|
+
opts.onSubmitOptimistic('Hello', x, y)
|
|
312
|
+
})
|
|
313
|
+
return {
|
|
314
|
+
destroy: vi.fn(),
|
|
315
|
+
moveTo: vi.fn(),
|
|
316
|
+
}
|
|
317
|
+
})
|
|
318
|
+
|
|
319
|
+
mountComments()
|
|
320
|
+
setCommentMode(true)
|
|
321
|
+
|
|
322
|
+
const overlay = document.body.querySelector('.sb-comment-overlay')
|
|
323
|
+
overlay.dispatchEvent(new MouseEvent('click', { bubbles: true, clientX: 10, clientY: 10 }))
|
|
324
|
+
|
|
325
|
+
await Promise.resolve()
|
|
326
|
+
await Promise.resolve()
|
|
327
|
+
await new Promise((resolve) => setTimeout(resolve, 0))
|
|
328
|
+
|
|
329
|
+
expect(openAuthModal).toHaveBeenCalledTimes(1)
|
|
330
|
+
const call = openAuthModal.mock.calls[0]?.[0]
|
|
331
|
+
expect(call?.initialError).toContain('`correct/repository`')
|
|
332
|
+
expect(call?.initialError).toContain('PAT repository access')
|
|
333
|
+
expect(document.body.classList.contains('sb-comment-mode')).toBe(false)
|
|
334
|
+
})
|
|
335
|
+
})
|
|
198
336
|
})
|