@dfosco/storyboard-react 4.2.0-beta.0 → 4.2.0-beta.17

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 (48) hide show
  1. package/package.json +5 -4
  2. package/src/AuthModal/AuthModal.jsx +6 -2
  3. package/src/BranchBar/BranchBar.jsx +17 -5
  4. package/src/BranchBar/BranchBar.module.css +11 -2
  5. package/src/CommandPalette/CommandPalette.jsx +267 -164
  6. package/src/CommandPalette/command-palette.css +130 -78
  7. package/src/Icon.jsx +112 -48
  8. package/src/Viewfinder.jsx +511 -61
  9. package/src/Viewfinder.module.css +414 -2
  10. package/src/canvas/CanvasPage.bridge.test.jsx +14 -6
  11. package/src/canvas/CanvasPage.dragdrop.test.jsx +10 -6
  12. package/src/canvas/CanvasPage.jsx +157 -174
  13. package/src/canvas/CanvasPage.module.css +0 -15
  14. package/src/canvas/CanvasPage.multiselect.test.jsx +10 -6
  15. package/src/canvas/ConnectorLayer.jsx +5 -5
  16. package/src/canvas/PageSelector.test.jsx +15 -6
  17. package/src/canvas/useCanvas.js +1 -1
  18. package/src/canvas/widgets/ActionWidget.jsx +200 -0
  19. package/src/canvas/widgets/ActionWidget.module.css +122 -0
  20. package/src/canvas/widgets/FigmaEmbed.jsx +97 -29
  21. package/src/canvas/widgets/FigmaEmbed.module.css +61 -0
  22. package/src/canvas/widgets/ImageWidget.jsx +1 -1
  23. package/src/canvas/widgets/LinkPreview.jsx +64 -5
  24. package/src/canvas/widgets/LinkPreview.module.css +127 -0
  25. package/src/canvas/widgets/MarkdownBlock.jsx +39 -17
  26. package/src/canvas/widgets/MarkdownBlock.module.css +123 -0
  27. package/src/canvas/widgets/PrototypeEmbed.jsx +183 -20
  28. package/src/canvas/widgets/PrototypeEmbed.module.css +117 -0
  29. package/src/canvas/widgets/PrototypeEmbed.test.jsx +2 -2
  30. package/src/canvas/widgets/SplitExpandModal.jsx +234 -0
  31. package/src/canvas/widgets/SplitExpandModal.module.css +335 -0
  32. package/src/canvas/widgets/SplitScreenTopBar.jsx +30 -0
  33. package/src/canvas/widgets/SplitScreenTopBar.module.css +58 -0
  34. package/src/canvas/widgets/StoryWidget.jsx +7 -4
  35. package/src/canvas/widgets/TerminalReadWidget.jsx +140 -0
  36. package/src/canvas/widgets/TerminalReadWidget.module.css +92 -0
  37. package/src/canvas/widgets/TerminalWidget.jsx +299 -49
  38. package/src/canvas/widgets/TerminalWidget.module.css +155 -1
  39. package/src/canvas/widgets/WidgetChrome.jsx +19 -14
  40. package/src/canvas/widgets/WidgetChrome.module.css +10 -0
  41. package/src/canvas/widgets/embedInteraction.test.jsx +24 -26
  42. package/src/canvas/widgets/expandUtils.js +188 -0
  43. package/src/canvas/widgets/index.js +5 -0
  44. package/src/canvas/widgets/snapshotDisplay.test.jsx +23 -71
  45. package/src/canvas/widgets/widgetConfig.js +19 -1
  46. package/src/hooks/useConfig.js +14 -0
  47. package/src/index.js +4 -0
  48. package/src/vite/data-plugin.js +264 -14
@@ -4,10 +4,11 @@
4
4
  * Replaces the old list-based Viewfinder with a sidebar + grid layout.
5
5
  * Wired to real data from buildPrototypeIndex and listStories.
6
6
  */
7
- import { useState, useEffect, useMemo, useCallback, useSyncExternalStore } from 'react'
7
+ import { useState, useEffect, useRef, useMemo, useCallback, useSyncExternalStore } from 'react'
8
8
  import { buildPrototypeIndex, listStories, getStoryData, getLocal, setLocal } from '@dfosco/storyboard-core'
9
- import { MarkGithubIcon, GitBranchIcon, ChevronDownIcon, ChevronRightIcon, FileDirectoryFillIcon, PlusIcon, StarIcon, StarFillIcon, ThreeBarsIcon, XIcon, StackIcon } from '@primer/octicons-react'
9
+ import { MarkGithubIcon, GitBranchIcon, ChevronDownIcon, ChevronRightIcon, FileDirectoryFillIcon, PlusIcon, StarIcon, StarFillIcon, ThreeBarsIcon, XIcon, StackIcon, TrashIcon, ShieldLockIcon, KebabHorizontalIcon, PencilIcon } from '@primer/octicons-react'
10
10
  import { Menu } from '@base-ui/react/menu'
11
+ import { Dialog } from '@base-ui/react/dialog'
11
12
  import Icon from './Icon.jsx'
12
13
  import css from './Viewfinder.module.css'
13
14
 
@@ -45,6 +46,52 @@ function useToolbarTheme() {
45
46
  return attrs
46
47
  }
47
48
 
49
+ /* ─── GitHub user hook ─── */
50
+
51
+ const COMMENTS_USER_KEY = 'sb-comments-user'
52
+ const COMMENTS_TOKEN_KEY = 'sb-comments-token'
53
+
54
+ /**
55
+ * Resolve the current GitHub user for display in the sidebar.
56
+ * Priority: 1) PAT-cached user (from comments auth), 2) gh CLI login via git-user endpoint.
57
+ */
58
+ function useGitHubUser(basePath) {
59
+ const [user, setUser] = useState(() => {
60
+ try {
61
+ const raw = localStorage.getItem(COMMENTS_USER_KEY)
62
+ const token = localStorage.getItem(COMMENTS_TOKEN_KEY)
63
+ if (token && raw) {
64
+ const parsed = JSON.parse(raw)
65
+ if (parsed?.login) return parsed
66
+ }
67
+ } catch { /* ignore */ }
68
+ return null
69
+ })
70
+
71
+ // Listen for auth changes (when user signs in via AuthModal)
72
+ useEffect(() => {
73
+ const handler = () => {
74
+ try {
75
+ const raw = localStorage.getItem(COMMENTS_USER_KEY)
76
+ const token = localStorage.getItem(COMMENTS_TOKEN_KEY)
77
+ if (token && raw) {
78
+ const parsed = JSON.parse(raw)
79
+ if (parsed?.login) { setUser(parsed); return }
80
+ }
81
+ setUser(null)
82
+ } catch { setUser(null) }
83
+ }
84
+ window.addEventListener('storage', handler)
85
+ document.addEventListener('storyboard:auth-changed', handler)
86
+ return () => {
87
+ window.removeEventListener('storage', handler)
88
+ document.removeEventListener('storyboard:auth-changed', handler)
89
+ }
90
+ }, [])
91
+
92
+ return user
93
+ }
94
+
48
95
  /* ─── localStorage helpers ─── */
49
96
 
50
97
  const STARRED_KEY = 'sb-viewfinder-starred'
@@ -157,10 +204,13 @@ function AvatarStack({ authors }) {
157
204
 
158
205
  /* ─── Star Button ─── */
159
206
 
160
- function StarBtn({ active, onClick }) {
207
+ function StarBtn({ active, onClick, inline }) {
208
+ const cls = inline
209
+ ? (active ? css.iconBtnInlineActive : css.iconBtnInline)
210
+ : (active ? css.iconBtnActive : css.iconBtn)
161
211
  return (
162
212
  <button
163
- className={active ? css.iconBtnActive : css.iconBtn}
213
+ className={cls}
164
214
  onClick={(e) => { e.preventDefault(); e.stopPropagation(); onClick() }}
165
215
  aria-label={active ? 'Remove favorite' : 'Favorite'}
166
216
  title={active ? 'Remove favorite' : 'Favorite'}
@@ -170,9 +220,251 @@ function StarBtn({ active, onClick }) {
170
220
  )
171
221
  }
172
222
 
223
+ /* ─── Card Actions Menu ─── */
224
+
225
+ function CardActionsMenu({ typeLabel, onEdit, onDelete }) {
226
+ return (
227
+ <Menu.Root>
228
+ <Menu.Trigger
229
+ className={css.iconBtn}
230
+ onClick={(e) => { e.preventDefault(); e.stopPropagation() }}
231
+ aria-label="Actions"
232
+ render={<button />}
233
+ >
234
+ <KebabHorizontalIcon size={16} />
235
+ </Menu.Trigger>
236
+ <Menu.Portal>
237
+ <Menu.Positioner className={css.actionsMenuPositioner} side="bottom" alignment="end">
238
+ <Menu.Popup className={css.actionsMenu} onClick={(e) => { e.preventDefault(); e.stopPropagation() }}>
239
+ <Menu.Item
240
+ className={css.actionsMenuItem}
241
+ onClick={(e) => { e.preventDefault(); e.stopPropagation(); onEdit() }}
242
+ render={<button />}
243
+ >
244
+ <PencilIcon size={16} />
245
+ Edit {typeLabel}
246
+ </Menu.Item>
247
+ <Menu.Item
248
+ className={css.actionsMenuItemDanger}
249
+ onClick={(e) => { e.preventDefault(); e.stopPropagation(); onDelete() }}
250
+ render={<button />}
251
+ >
252
+ <TrashIcon size={16} />
253
+ Delete {typeLabel}
254
+ </Menu.Item>
255
+ </Menu.Popup>
256
+ </Menu.Positioner>
257
+ </Menu.Portal>
258
+ </Menu.Root>
259
+ )
260
+ }
261
+
262
+ /* ─── Edit Artifact Modal ─── */
263
+
264
+ function EditArtifactModal({ item, dirName, basePath, onClose }) {
265
+ const [name, setName] = useState(item.name || '')
266
+ const [description, setDescription] = useState(item.description || '')
267
+ const [author, setAuthor] = useState(
268
+ item.author
269
+ ? (Array.isArray(item.author) ? item.author.join(', ') : item.author)
270
+ : ''
271
+ )
272
+ const [error, setError] = useState('')
273
+ const [submitting, setSubmitting] = useState(false)
274
+ const overlayRef = useRef(null)
275
+
276
+ const typeLabel = item.type === 'canvas' ? 'Canvas' : item.type === 'prototype' ? 'Prototype' : 'Component'
277
+
278
+ const handleSubmit = async (e) => {
279
+ e.preventDefault()
280
+ setError('')
281
+ setSubmitting(true)
282
+
283
+ const apiBase = (basePath || '/').replace(/\/+$/, '')
284
+ let endpoint
285
+
286
+ if (item.type === 'canvas') {
287
+ endpoint = `${apiBase}/_storyboard/canvas/update-meta`
288
+ } else if (item.type === 'prototype') {
289
+ endpoint = `${apiBase}/_storyboard/workshop/prototypes`
290
+ } else {
291
+ setError('Editing this type is not supported')
292
+ setSubmitting(false)
293
+ return
294
+ }
295
+
296
+ const body = {
297
+ name: dirName,
298
+ title: name.trim(),
299
+ description: description.trim(),
300
+ author: author.trim(),
301
+ }
302
+ if (item.folder) body.folder = item.folder
303
+
304
+ try {
305
+ const res = await fetch(endpoint, {
306
+ method: 'PUT',
307
+ headers: { 'Content-Type': 'application/json' },
308
+ body: JSON.stringify(body),
309
+ })
310
+ if (!res.ok) {
311
+ const text = await res.text()
312
+ throw new Error(text || `Request failed (${res.status})`)
313
+ }
314
+ window.location.reload()
315
+ } catch (err) {
316
+ setError(err.message)
317
+ setSubmitting(false)
318
+ }
319
+ }
320
+
321
+ useEffect(() => {
322
+ const handleKey = (e) => { if (e.key === 'Escape') onClose() }
323
+ document.addEventListener('keydown', handleKey)
324
+ return () => document.removeEventListener('keydown', handleKey)
325
+ }, [onClose])
326
+
327
+ return (
328
+ <div className={css.modalOverlay} onClick={(e) => { if (e.target === overlayRef.current) onClose() }} ref={overlayRef}>
329
+ <div className={css.modalContent} onClick={(e) => e.stopPropagation()}>
330
+ <form onSubmit={handleSubmit}>
331
+ <div className={css.createFormHeader}>
332
+ <div className={css.createMenuTitle}>Edit {typeLabel}</div>
333
+ <button type="button" className={css.createFormClose} onClick={onClose} aria-label="Close">
334
+ <XIcon size={16} />
335
+ </button>
336
+ </div>
337
+
338
+ <div className={css.createFormField}>
339
+ <label className={css.createFormLabel}>Name</label>
340
+ <input
341
+ className={css.createFormInput}
342
+ value={name}
343
+ onChange={e => setName(e.target.value)}
344
+ autoFocus
345
+ />
346
+ </div>
347
+
348
+ <div className={css.createFormField}>
349
+ <label className={css.createFormLabel}>Description</label>
350
+ <input
351
+ className={css.createFormInput}
352
+ value={description}
353
+ onChange={e => setDescription(e.target.value)}
354
+ placeholder="Optional description"
355
+ />
356
+ </div>
357
+
358
+ <div className={css.createFormField}>
359
+ <label className={css.createFormLabel}>Author</label>
360
+ <input
361
+ className={css.createFormInput}
362
+ value={author}
363
+ onChange={e => setAuthor(e.target.value)}
364
+ placeholder="GitHub username(s), comma-separated"
365
+ />
366
+ </div>
367
+
368
+ {error && <div className={css.createFormError}>{error}</div>}
369
+
370
+ <div className={css.modalActions}>
371
+ <button type="button" className={css.modalCancelBtn} onClick={onClose}>Cancel</button>
372
+ <button type="submit" className={css.modalSubmitBtn} disabled={submitting}>
373
+ {submitting ? 'Saving…' : 'Save Changes'}
374
+ </button>
375
+ </div>
376
+ </form>
377
+ </div>
378
+ </div>
379
+ )
380
+ }
381
+
382
+ /* ─── Delete Artifact Modal ─── */
383
+
384
+ function DeleteArtifactModal({ item, dirName, basePath, typeLabel, onClose, onDeleted }) {
385
+ const [error, setError] = useState('')
386
+ const [deleting, setDeleting] = useState(false)
387
+ const overlayRef = useRef(null)
388
+
389
+ const handleDelete = async () => {
390
+ setError('')
391
+ setDeleting(true)
392
+
393
+ const apiBase = (basePath || '/').replace(/\/+$/, '')
394
+ let endpoint
395
+
396
+ if (item.type === 'canvas') {
397
+ endpoint = `${apiBase}/_storyboard/canvas/delete-canvas`
398
+ } else if (item.type === 'prototype') {
399
+ endpoint = `${apiBase}/_storyboard/workshop/prototypes`
400
+ } else {
401
+ setError('Deleting this type is not supported')
402
+ setDeleting(false)
403
+ return
404
+ }
405
+
406
+ const body = { name: dirName }
407
+ if (item.folder) body.folder = item.folder
408
+
409
+ try {
410
+ const res = await fetch(endpoint, {
411
+ method: 'DELETE',
412
+ headers: { 'Content-Type': 'application/json' },
413
+ body: JSON.stringify(body),
414
+ })
415
+ if (!res.ok) {
416
+ const text = await res.text()
417
+ throw new Error(text || `Request failed (${res.status})`)
418
+ }
419
+ onDeleted?.()
420
+ onClose()
421
+ } catch (err) {
422
+ setError(err.message)
423
+ setDeleting(false)
424
+ }
425
+ }
426
+
427
+ useEffect(() => {
428
+ const handleKey = (e) => { if (e.key === 'Escape') onClose() }
429
+ document.addEventListener('keydown', handleKey)
430
+ return () => document.removeEventListener('keydown', handleKey)
431
+ }, [onClose])
432
+
433
+ return (
434
+ <div className={css.modalOverlay} onClick={(e) => { if (e.target === overlayRef.current) onClose() }} ref={overlayRef}>
435
+ <div className={css.modalContent} onClick={(e) => e.stopPropagation()}>
436
+ <div className={css.createFormHeader}>
437
+ <div className={css.createMenuTitle}>Delete {typeLabel}</div>
438
+ <button type="button" className={css.createFormClose} onClick={onClose} aria-label="Close">
439
+ <XIcon size={16} />
440
+ </button>
441
+ </div>
442
+
443
+ <p className={css.deleteMessage}>
444
+ Are you sure you want to delete <strong>{item.name}</strong>? This action cannot be undone.
445
+ </p>
446
+
447
+ {error && <div className={css.createFormError}>{error}</div>}
448
+
449
+ <div className={css.modalActions}>
450
+ <button type="button" className={css.modalCancelBtn} onClick={onClose}>Cancel</button>
451
+ <button
452
+ type="button"
453
+ className={css.deleteConfirmBtn}
454
+ onClick={handleDelete}
455
+ disabled={deleting}
456
+ >
457
+ {deleting ? 'Deleting…' : `Delete ${typeLabel}`}
458
+ </button>
459
+ </div>
460
+ </div>
461
+ </div>
462
+ )
463
+ }
464
+
173
465
  /* ─── Artifact Card ─── */
174
466
 
175
- function ArtifactCard({ item, basePath, starred, onToggleStar }) {
467
+ function ArtifactCard({ item, basePath, starred, onToggleStar, onItemDeleted }) {
176
468
  const href = item.route ? withBase(basePath, item.route) : '#'
177
469
  const isExternal = item.isExternal
178
470
 
@@ -189,36 +481,73 @@ function ArtifactCard({ item, basePath, starred, onToggleStar }) {
189
481
  ? (Array.isArray(item.author) ? item.author : [item.author])
190
482
  : item.gitAuthor ? [item.gitAuthor] : []
191
483
 
484
+ const [showEdit, setShowEdit] = useState(false)
485
+ const [showDelete, setShowDelete] = useState(false)
486
+
487
+ // Extract dirName from item.id (format: "type:dirName")
488
+ const dirName = item.id.split(':').slice(1).join(':')
489
+ const typeLabel = item.type === 'canvas' ? 'Canvas' : item.type === 'prototype' ? 'Prototype' : 'Component'
490
+ const canEditDelete = item.type === 'canvas' || item.type === 'prototype'
491
+
192
492
  return (
193
- <Tag className={css.card} {...linkProps} onClick={handleClick}>
194
- <div className={css.cardHeader}>
195
- <span className={css.cardBadge}>{getTypeLabel(item.type)}</span>
196
- <div className={css.cardActions}>
197
- {item.flows?.length > 0 && <FlowsDropdown flows={item.flows} basePath={basePath} />}
198
- {item.pages?.length > 1 && <PagesDropdown pages={item.pages} basePath={basePath} />}
199
- <StarBtn active={starred} onClick={() => onToggleStar(item.id)} />
200
- </div>
201
- </div>
202
- <div className={css.cardBody}>
203
- <div className={css.cardBodyContent}>
204
- <div className={css.cardTitle}>
205
- {item.name}
206
- {isExternal && <span className={css.externalBadge}>↗</span>}
493
+ <>
494
+ <Tag className={css.card} {...linkProps} onClick={handleClick}>
495
+ <div className={css.cardHeader}>
496
+ <span className={css.cardBadge}>{getTypeLabel(item.type)}</span>
497
+ <div className={css.cardActions}>
498
+ {item.flows?.length > 0 && <FlowsDropdown flows={item.flows} basePath={basePath} />}
499
+ {item.pages?.length > 1 && <PagesDropdown pages={item.pages} basePath={basePath} />}
500
+ {canEditDelete && (
501
+ <CardActionsMenu
502
+ typeLabel={typeLabel}
503
+ onEdit={() => setShowEdit(true)}
504
+ onDelete={() => setShowDelete(true)}
505
+ />
506
+ )}
207
507
  </div>
208
- {item.description && (
209
- <div className={css.cardDescription}>{item.description}</div>
210
- )}
211
- <div className={css.cardFooter}>
212
- <AvatarStack authors={authorList} />
213
- <div className={css.cardMeta}>
214
- {authorList.length > 0 && <span>{authorList.join(', ')}</span>}
215
- {authorList.length > 0 && formatRelativeTime(item.lastModified) && <span className={css.cardMetaDot} />}
216
- {formatRelativeTime(item.lastModified) && <span>{formatRelativeTime(item.lastModified)}</span>}
508
+ </div>
509
+ <div className={css.cardBody}>
510
+ <div className={css.cardBodyContent}>
511
+ <div className={css.cardTitleRow}>
512
+ <div className={css.cardTitle}>
513
+ {item.name}
514
+ {isExternal && <span className={css.externalBadge}>↗</span>}
515
+ </div>
516
+ <StarBtn active={starred} onClick={() => onToggleStar(item.id)} inline />
517
+ </div>
518
+ {item.description && (
519
+ <div className={css.cardDescription}>{item.description}</div>
520
+ )}
521
+ <div className={css.cardFooter}>
522
+ <AvatarStack authors={authorList} />
523
+ <div className={css.cardMeta}>
524
+ {authorList.length > 0 && <span>{authorList.join(', ')}</span>}
525
+ {authorList.length > 0 && formatRelativeTime(item.lastModified) && <span className={css.cardMetaDot} />}
526
+ {formatRelativeTime(item.lastModified) && <span>{formatRelativeTime(item.lastModified)}</span>}
527
+ </div>
217
528
  </div>
218
529
  </div>
219
530
  </div>
220
- </div>
221
- </Tag>
531
+ </Tag>
532
+ {showEdit && (
533
+ <EditArtifactModal
534
+ item={item}
535
+ dirName={dirName}
536
+ basePath={basePath}
537
+ onClose={() => setShowEdit(false)}
538
+ />
539
+ )}
540
+ {showDelete && (
541
+ <DeleteArtifactModal
542
+ item={item}
543
+ dirName={dirName}
544
+ basePath={basePath}
545
+ typeLabel={typeLabel}
546
+ onClose={() => setShowDelete(false)}
547
+ onDeleted={() => onItemDeleted?.(item.id)}
548
+ />
549
+ )}
550
+ </>
222
551
  )
223
552
  }
224
553
 
@@ -316,7 +645,7 @@ function PagesDropdown({ pages, basePath }) {
316
645
 
317
646
  /* ─── Folder Section ─── */
318
647
 
319
- function FolderSection({ folder, collapsed, onToggle, basePath, starred, onToggleStar }) {
648
+ function FolderSection({ folder, collapsed, onToggle, basePath, starred, onToggleStar, onItemDeleted }) {
320
649
  return (
321
650
  <section className={collapsed ? css.folderSectionCollapsed : css.folderSection}>
322
651
  <button className={css.folderHeader} onClick={onToggle}>
@@ -337,6 +666,7 @@ function FolderSection({ folder, collapsed, onToggle, basePath, starred, onToggl
337
666
  basePath={basePath}
338
667
  starred={starred.has(item.id)}
339
668
  onToggleStar={onToggleStar}
669
+ onItemDeleted={onItemDeleted}
340
670
  />
341
671
  ))}
342
672
  </div>
@@ -753,6 +1083,74 @@ function BranchDropdown({ basePath }) {
753
1083
  )
754
1084
  }
755
1085
 
1086
+ /* ─── User Settings Dialog ─── */
1087
+
1088
+ function UserSettingsDialog({ open, onOpenChange, user, onRemoveToken }) {
1089
+ const hasToken = (() => {
1090
+ try { return !!localStorage.getItem(COMMENTS_TOKEN_KEY) } catch { return false }
1091
+ })()
1092
+ const scopes = user?.scopes || []
1093
+ const isFineGrained = hasToken && scopes.length === 0
1094
+
1095
+ return (
1096
+ <Dialog.Root open={open} onOpenChange={onOpenChange}>
1097
+ <Dialog.Portal>
1098
+ <Dialog.Backdrop className={css.settingsBackdrop} />
1099
+ <div className={css.settingsPopupWrap}>
1100
+ <Dialog.Popup className={css.settingsPopup}>
1101
+ <Dialog.Title className={css.settingsTitle}>Settings</Dialog.Title>
1102
+ <Dialog.Close className={css.settingsCloseBtn} aria-label="Close">×</Dialog.Close>
1103
+
1104
+ {/* GitHub connection section */}
1105
+ <div className={css.settingsSection}>
1106
+ <div className={css.settingsSectionHeader}>
1107
+ <ShieldLockIcon size={16} />
1108
+ <span>GitHub Connection</span>
1109
+ </div>
1110
+
1111
+ {hasToken ? (
1112
+ <div className={css.settingsTokenCard}>
1113
+ <div className={css.settingsTokenRow}>
1114
+ <span className={css.settingsTokenLabel}>Token</span>
1115
+ <code className={css.settingsTokenValue}>••••••••••••••••</code>
1116
+ </div>
1117
+ <div className={css.settingsTokenRow}>
1118
+ <span className={css.settingsTokenLabel}>Permissions</span>
1119
+ <span className={css.settingsTokenValue}>
1120
+ {isFineGrained
1121
+ ? 'Fine-grained token'
1122
+ : scopes.map(s => <code key={s} className={css.settingsScope}>{s}</code>)
1123
+ }
1124
+ </span>
1125
+ </div>
1126
+ <button className={css.settingsRemoveBtn} onClick={onRemoveToken}>
1127
+ <TrashIcon size={14} />
1128
+ Remove token
1129
+ </button>
1130
+ </div>
1131
+ ) : (
1132
+ <div className={css.settingsNoToken}>
1133
+ <p>No GitHub token configured.</p>
1134
+ <button
1135
+ className={css.settingsSignInBtn}
1136
+ onClick={() => {
1137
+ onOpenChange(false)
1138
+ document.dispatchEvent(new CustomEvent('storyboard:open-auth-modal'))
1139
+ }}
1140
+ >
1141
+ <MarkGithubIcon size={16} />
1142
+ Sign in with GitHub
1143
+ </button>
1144
+ </div>
1145
+ )}
1146
+ </div>
1147
+ </Dialog.Popup>
1148
+ </div>
1149
+ </Dialog.Portal>
1150
+ </Dialog.Root>
1151
+ )
1152
+ }
1153
+
756
1154
  /* ─── Main Component ─── */
757
1155
 
758
1156
  export default function Viewfinder({
@@ -765,6 +1163,17 @@ export default function Viewfinder({
765
1163
  }) {
766
1164
  const shouldHideDefault = hideDefaultFlow ?? hideDefaultScene
767
1165
  const themeAttrs = useToolbarTheme()
1166
+ const ghUser = useGitHubUser(basePath)
1167
+ const [settingsOpen, setSettingsOpen] = useState(false)
1168
+
1169
+ const handleRemoveToken = useCallback(() => {
1170
+ try {
1171
+ localStorage.removeItem(COMMENTS_TOKEN_KEY)
1172
+ localStorage.removeItem(COMMENTS_USER_KEY)
1173
+ } catch { /* ignore */ }
1174
+ document.dispatchEvent(new CustomEvent('storyboard:auth-changed'))
1175
+ setSettingsOpen(false)
1176
+ }, [])
768
1177
 
769
1178
  // Build data index from real prototype/canvas/story data
770
1179
  const knownRoutes = useMemo(() =>
@@ -860,20 +1269,23 @@ export default function Viewfinder({
860
1269
  const [activeNav, setActiveNav] = useState('all')
861
1270
  const [activeTab, setActiveTab] = useState('All')
862
1271
  const [showCreate, setShowCreate] = useState(false)
1272
+ const isLocalDev = typeof window !== 'undefined' && window.__SB_LOCAL_DEV__ === true
863
1273
  const [sidebarOpen, setSidebarOpen] = useState(false)
864
1274
  const [groupByFolders, setGroupByFolders] = useState(() => {
865
1275
  try { return localStorage.getItem(GROUP_BY_FOLDERS_KEY) !== 'false' } catch { return true }
866
1276
  })
867
1277
  const [collapsedFolders, setCollapsedFolders] = useState(new Set())
1278
+ const [hiddenItems, setHiddenItems] = useState(new Set())
868
1279
  const { starred, toggle: toggleStar } = useStarred()
869
1280
  const recentIds = useRecent()
870
1281
 
871
1282
  // Filter by nav category
1283
+ const typeMap = { prototypes: 'prototype', canvases: 'canvas', components: 'component' }
872
1284
  const navFiltered = useMemo(() => {
873
- if (activeNav === 'all') return allItems
874
- const typeMap = { prototypes: 'prototype', canvases: 'canvas', components: 'component' }
875
- return allItems.filter(i => i.type === typeMap[activeNav])
876
- }, [allItems, activeNav])
1285
+ let filtered = activeNav === 'all' ? allItems : allItems.filter(i => i.type === typeMap[activeNav])
1286
+ if (hiddenItems.size > 0) filtered = filtered.filter(i => !hiddenItems.has(i.id))
1287
+ return filtered
1288
+ }, [allItems, activeNav, hiddenItems])
877
1289
 
878
1290
  // Filter by tab
879
1291
  const items = useMemo(() => {
@@ -940,16 +1352,24 @@ export default function Viewfinder({
940
1352
  })
941
1353
  }, [])
942
1354
 
1355
+ const handleItemDeleted = useCallback((itemId) => {
1356
+ setHiddenItems(prev => new Set(prev).add(itemId))
1357
+ }, [])
1358
+
943
1359
  // Counts
1360
+ const visibleItems = useMemo(() =>
1361
+ hiddenItems.size > 0 ? allItems.filter(i => !hiddenItems.has(i.id)) : allItems
1362
+ , [allItems, hiddenItems])
1363
+
944
1364
  const counts = useMemo(() => ({
945
- all: allItems.length,
946
- prototypes: allItems.filter(i => i.type === 'prototype').length,
947
- canvases: allItems.filter(i => i.type === 'canvas').length,
948
- components: allItems.filter(i => i.type === 'component').length,
949
- }), [allItems])
1365
+ all: visibleItems.length,
1366
+ prototypes: visibleItems.filter(i => i.type === 'prototype').length,
1367
+ canvases: visibleItems.filter(i => i.type === 'canvas').length,
1368
+ components: visibleItems.filter(i => i.type === 'component').length,
1369
+ }), [visibleItems])
950
1370
 
951
1371
  // Starred items for sidebar
952
- const starredItems = useMemo(() => allItems.filter(i => starred.has(i.id)), [allItems, starred])
1372
+ const starredItems = useMemo(() => visibleItems.filter(i => starred.has(i.id)), [visibleItems, starred])
953
1373
 
954
1374
  const pageTitle = NAV_ITEMS.find(n => n.id === activeNav)?.label || 'All artifacts'
955
1375
 
@@ -973,18 +1393,20 @@ export default function Viewfinder({
973
1393
  </div>
974
1394
  <div className={css.topActions}>
975
1395
  <BranchDropdown basePath={basePath} />
976
- <Menu.Root open={showCreate} onOpenChange={setShowCreate}>
977
- <Menu.Trigger className={css.createBtn}>
978
- <PlusIcon size={14} /> Create
979
- </Menu.Trigger>
980
- <Menu.Portal>
981
- <Menu.Positioner className={css.createDropdownPositioner} side="bottom" align="end" sideOffset={4}>
982
- <Menu.Popup className={css.createDropdown}>
983
- <CreateMenu onClose={() => setShowCreate(false)} basePath={basePath} />
984
- </Menu.Popup>
985
- </Menu.Positioner>
986
- </Menu.Portal>
987
- </Menu.Root>
1396
+ {isLocalDev && (
1397
+ <Menu.Root open={showCreate} onOpenChange={setShowCreate}>
1398
+ <Menu.Trigger className={css.createBtn}>
1399
+ <PlusIcon size={14} /> Create
1400
+ </Menu.Trigger>
1401
+ <Menu.Portal>
1402
+ <Menu.Positioner className={css.createDropdownPositioner} side="bottom" align="end" sideOffset={4}>
1403
+ <Menu.Popup className={css.createDropdown}>
1404
+ <CreateMenu onClose={() => setShowCreate(false)} basePath={basePath} />
1405
+ </Menu.Popup>
1406
+ </Menu.Positioner>
1407
+ </Menu.Portal>
1408
+ </Menu.Root>
1409
+ )}
988
1410
  </div>
989
1411
  </header>
990
1412
 
@@ -1027,16 +1449,41 @@ export default function Viewfinder({
1027
1449
  ))}
1028
1450
  </div>
1029
1451
 
1030
- {/* User profile / login */}
1452
+ {/* User profile / settings */}
1031
1453
  <div className={css.sidebarFooter}>
1032
- <button className={css.loginBtn} onClick={() => document.dispatchEvent(new CustomEvent('storyboard:open-auth-modal'))}>
1033
- <span className={css.avatar}><MarkGithubIcon size={16} /></span>
1034
- <div>
1035
- <div className={css.userName}>Sign in</div>
1036
- <div className={css.userSub}>Connect with GitHub</div>
1454
+ {ghUser ? (
1455
+ <div className={css.footerRow}>
1456
+ <button className={css.userBtn} onClick={() => setSettingsOpen(true)}>
1457
+ <img
1458
+ className={css.userAvatar}
1459
+ src={ghUser.avatarUrl || `https://github.com/${ghUser.login}.png?size=64`}
1460
+ alt={ghUser.login}
1461
+ width={32}
1462
+ height={32}
1463
+ />
1464
+ <div className={css.userInfo}>
1465
+ <div className={css.userName}>{ghUser.login}</div>
1466
+ </div>
1467
+ </button>
1037
1468
  </div>
1038
- </button>
1469
+ ) : (
1470
+ <div className={css.footerRow}>
1471
+ <button className={css.loginBtn} onClick={() => document.dispatchEvent(new CustomEvent('storyboard:open-auth-modal'))}>
1472
+ <span className={css.avatar}><MarkGithubIcon size={16} /></span>
1473
+ <div>
1474
+ <div className={css.userName}>Sign in</div>
1475
+ <div className={css.userSub}>Connect with GitHub</div>
1476
+ </div>
1477
+ </button>
1478
+ </div>
1479
+ )}
1039
1480
  </div>
1481
+ <UserSettingsDialog
1482
+ open={settingsOpen}
1483
+ onOpenChange={setSettingsOpen}
1484
+ user={ghUser}
1485
+ onRemoveToken={handleRemoveToken}
1486
+ />
1040
1487
  </aside>
1041
1488
 
1042
1489
  {/* ─── Main ─── */}
@@ -1071,7 +1518,7 @@ export default function Viewfinder({
1071
1518
  {activeTab === 'Starred' && 'No starred items. Click ☆ on a card to star it.'}
1072
1519
  {activeTab === 'All' && 'No items found. Create a prototype, canvas, or component to get started.'}
1073
1520
  </div>
1074
- ) : groupByFolders && grouped && activeTab === 'All' && activeNav === 'all' ? (
1521
+ ) : groupByFolders && grouped && activeTab === 'All' ? (
1075
1522
  <>
1076
1523
  {grouped.folders.map(folder => (
1077
1524
  <FolderSection
@@ -1082,6 +1529,7 @@ export default function Viewfinder({
1082
1529
  basePath={basePath}
1083
1530
  starred={starred}
1084
1531
  onToggleStar={toggleStar}
1532
+ onItemDeleted={handleItemDeleted}
1085
1533
  />
1086
1534
  ))}
1087
1535
  {grouped.ungrouped.length > 0 && (
@@ -1093,6 +1541,7 @@ export default function Viewfinder({
1093
1541
  basePath={basePath}
1094
1542
  starred={starred.has(item.id)}
1095
1543
  onToggleStar={toggleStar}
1544
+ onItemDeleted={handleItemDeleted}
1096
1545
  />
1097
1546
  ))}
1098
1547
  </div>
@@ -1107,6 +1556,7 @@ export default function Viewfinder({
1107
1556
  basePath={basePath}
1108
1557
  starred={starred.has(item.id)}
1109
1558
  onToggleStar={toggleStar}
1559
+ onItemDeleted={handleItemDeleted}
1110
1560
  />
1111
1561
  ))}
1112
1562
  </div>