@dfosco/storyboard-react 4.2.0-beta.4 → 4.2.1

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 (89) hide show
  1. package/package.json +10 -11
  2. package/src/AuthModal/AuthModal.jsx +6 -8
  3. package/src/BranchBar/BranchBar.jsx +20 -6
  4. package/src/BranchBar/BranchBar.module.css +13 -4
  5. package/src/BranchBar/useBranches.js +20 -6
  6. package/src/BranchBar/useBranches.test.js +68 -0
  7. package/src/CommandPalette/CommandPalette.jsx +480 -187
  8. package/src/CommandPalette/command-palette.css +142 -78
  9. package/src/Icon.jsx +157 -58
  10. package/src/Viewfinder.jsx +562 -207
  11. package/src/Viewfinder.module.css +434 -93
  12. package/src/Workspace.jsx +7 -0
  13. package/src/canvas/CanvasPage.bridge.test.jsx +14 -6
  14. package/src/canvas/CanvasPage.dragdrop.test.jsx +11 -7
  15. package/src/canvas/CanvasPage.jsx +739 -219
  16. package/src/canvas/CanvasPage.module.css +13 -15
  17. package/src/canvas/CanvasPage.multiselect.test.jsx +17 -6
  18. package/src/canvas/ConnectorLayer.jsx +121 -165
  19. package/src/canvas/ConnectorLayer.module.css +69 -0
  20. package/src/canvas/PageSelector.test.jsx +15 -6
  21. package/src/canvas/canvasApi.js +68 -2
  22. package/src/canvas/canvasReloadGuard.test.js +1 -1
  23. package/src/canvas/connectorGeometry.js +132 -0
  24. package/src/canvas/hotPoolDevLogs.js +25 -0
  25. package/src/canvas/useCanvas.js +1 -1
  26. package/src/canvas/useMarqueeSelect.js +30 -4
  27. package/src/canvas/widgets/CodePenEmbed.jsx +1 -0
  28. package/src/canvas/widgets/ComponentSetWidget.jsx +199 -0
  29. package/src/canvas/widgets/ComponentSetWidget.module.css +89 -0
  30. package/src/canvas/widgets/ComponentWidget.jsx +1 -0
  31. package/src/canvas/widgets/CropOverlay.jsx +219 -0
  32. package/src/canvas/widgets/CropOverlay.module.css +118 -0
  33. package/src/canvas/widgets/ExpandedPane.jsx +474 -0
  34. package/src/canvas/widgets/ExpandedPane.module.css +179 -0
  35. package/src/canvas/widgets/ExpandedPane.test.jsx +240 -0
  36. package/src/canvas/widgets/ExpandedPaneTopBar.jsx +111 -0
  37. package/src/canvas/widgets/ExpandedPaneTopBar.module.css +59 -0
  38. package/src/canvas/widgets/ExpandedPaneTopBar.test.jsx +45 -0
  39. package/src/canvas/widgets/FigmaEmbed.jsx +62 -47
  40. package/src/canvas/widgets/FigmaEmbed.module.css +61 -0
  41. package/src/canvas/widgets/ImageWidget.jsx +130 -9
  42. package/src/canvas/widgets/ImageWidget.module.css +30 -0
  43. package/src/canvas/widgets/LinkPreview.jsx +113 -5
  44. package/src/canvas/widgets/LinkPreview.module.css +127 -0
  45. package/src/canvas/widgets/MarkdownBlock.jsx +167 -17
  46. package/src/canvas/widgets/MarkdownBlock.module.css +148 -0
  47. package/src/canvas/widgets/PromptWidget.jsx +414 -0
  48. package/src/canvas/widgets/PromptWidget.module.css +273 -0
  49. package/src/canvas/widgets/PrototypeEmbed.jsx +77 -39
  50. package/src/canvas/widgets/PrototypeEmbed.module.css +117 -0
  51. package/src/canvas/widgets/PrototypeEmbed.test.jsx +2 -2
  52. package/src/canvas/widgets/ResizeHandle.jsx +17 -6
  53. package/src/canvas/widgets/StoryWidget.jsx +73 -15
  54. package/src/canvas/widgets/TerminalReadWidget.jsx +146 -0
  55. package/src/canvas/widgets/TerminalReadWidget.module.css +94 -0
  56. package/src/canvas/widgets/TerminalWidget.jsx +445 -67
  57. package/src/canvas/widgets/TerminalWidget.module.css +271 -8
  58. package/src/canvas/widgets/TilesWidget.jsx +300 -0
  59. package/src/canvas/widgets/TilesWidget.module.css +133 -0
  60. package/src/canvas/widgets/WidgetChrome.jsx +74 -153
  61. package/src/canvas/widgets/WidgetChrome.module.css +30 -1
  62. package/src/canvas/widgets/embedInteraction.test.jsx +24 -26
  63. package/src/canvas/widgets/expandUtils.js +560 -0
  64. package/src/canvas/widgets/expandUtils.test.js +155 -0
  65. package/src/canvas/widgets/index.js +9 -0
  66. package/src/canvas/widgets/snapshotDisplay.test.jsx +23 -71
  67. package/src/canvas/widgets/tilePool.js +23 -0
  68. package/src/canvas/widgets/tiles/diagonal-bl.png +0 -0
  69. package/src/canvas/widgets/tiles/diagonal-br.png +0 -0
  70. package/src/canvas/widgets/tiles/diagonal-tl.png +0 -0
  71. package/src/canvas/widgets/tiles/leaf.png +0 -0
  72. package/src/canvas/widgets/tiles/quarter-tl.png +0 -0
  73. package/src/canvas/widgets/tiles/quarter-tr.png +0 -0
  74. package/src/canvas/widgets/tiles/solid-a.png +0 -0
  75. package/src/canvas/widgets/tiles/solid-b.png +0 -0
  76. package/src/canvas/widgets/widgetConfig.js +55 -4
  77. package/src/canvas/widgets/widgetIcons.jsx +190 -0
  78. package/src/canvas/widgets/widgetProps.js +1 -0
  79. package/src/context.jsx +48 -20
  80. package/src/hooks/useConfig.js +14 -0
  81. package/src/hooks/usePrototypeReloadGuard.js +64 -0
  82. package/src/hooks/useSceneData.js +1 -0
  83. package/src/hooks/useThemeState.test.js +1 -1
  84. package/src/index.js +8 -2
  85. package/src/story/ComponentSetPage.jsx +186 -0
  86. package/src/story/ComponentSetPage.module.css +121 -0
  87. package/src/story/StoryPage.jsx +32 -2
  88. package/src/vite/data-plugin.js +407 -67
  89. package/src/vite/data-plugin.test.js +1 -1
@@ -1,14 +1,16 @@
1
1
  /**
2
- * Viewfinder — SaaS-style homescreen for Storyboard.
2
+ * Workspace — SaaS-style homescreen for Storyboard.
3
3
  *
4
- * Replaces the old list-based Viewfinder with a sidebar + grid layout.
5
- * Wired to real data from buildPrototypeIndex and listStories.
4
+ * Sidebar + grid layout wired to real data from buildPrototypeIndex and listStories.
5
+ * Formerly known as Viewfinder renamed to match the /workspace route.
6
6
  */
7
- import { useState, useEffect, useMemo, useCallback, useSyncExternalStore } from 'react'
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'
7
+ import { useState, useEffect, useRef, useMemo, useCallback, useSyncExternalStore } from 'react'
8
+ import { buildPrototypeIndex, listStories, getStoryData, BranchSelect } from '@dfosco/storyboard-core'
9
+ import { MarkGithubIcon, GitBranchIcon, ChevronDownIcon, ChevronRightIcon, 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'
13
+ import { useBranches } from './BranchBar/useBranches.js'
12
14
  import css from './Viewfinder.module.css'
13
15
 
14
16
  /* ─── Theme sync: read toolbar theme from DOM and apply to Primer/BaseUI ─── */
@@ -45,12 +47,58 @@ function useToolbarTheme() {
45
47
  return attrs
46
48
  }
47
49
 
50
+ /* ─── GitHub user hook ─── */
51
+
52
+ const COMMENTS_USER_KEY = 'sb-comments-user'
53
+ const COMMENTS_TOKEN_KEY = 'sb-comments-token'
54
+
55
+ /**
56
+ * Resolve the current GitHub user for display in the sidebar.
57
+ * Priority: 1) PAT-cached user (from comments auth), 2) gh CLI login via git-user endpoint.
58
+ */
59
+ function useGitHubUser() {
60
+ const [user, setUser] = useState(() => {
61
+ try {
62
+ const raw = localStorage.getItem(COMMENTS_USER_KEY)
63
+ const token = localStorage.getItem(COMMENTS_TOKEN_KEY)
64
+ if (token && raw) {
65
+ const parsed = JSON.parse(raw)
66
+ if (parsed?.login) return parsed
67
+ }
68
+ } catch { /* ignore */ }
69
+ return null
70
+ })
71
+
72
+ // Listen for auth changes (when user signs in via AuthModal)
73
+ useEffect(() => {
74
+ const handler = () => {
75
+ try {
76
+ const raw = localStorage.getItem(COMMENTS_USER_KEY)
77
+ const token = localStorage.getItem(COMMENTS_TOKEN_KEY)
78
+ if (token && raw) {
79
+ const parsed = JSON.parse(raw)
80
+ if (parsed?.login) { setUser(parsed); return }
81
+ }
82
+ setUser(null)
83
+ } catch { setUser(null) }
84
+ }
85
+ window.addEventListener('storage', handler)
86
+ document.addEventListener('storyboard:auth-changed', handler)
87
+ return () => {
88
+ window.removeEventListener('storage', handler)
89
+ document.removeEventListener('storyboard:auth-changed', handler)
90
+ }
91
+ }, [])
92
+
93
+ return user
94
+ }
95
+
48
96
  /* ─── localStorage helpers ─── */
49
97
 
50
- const STARRED_KEY = 'sb-viewfinder-starred'
51
- const RECENT_KEY = 'sb-viewfinder-recent'
98
+ const STARRED_KEY = 'sb-workspace-starred'
99
+ const RECENT_KEY = 'sb-workspace-recent'
52
100
  const MAX_RECENT = 30
53
- const GROUP_BY_FOLDERS_KEY = 'sb-viewfinder-group-folders'
101
+ const GROUP_BY_FOLDERS_KEY = 'sb-workspace-group-folders'
54
102
 
55
103
  function readJSON(key, fallback) {
56
104
  try { return JSON.parse(localStorage.getItem(key)) || fallback }
@@ -106,16 +154,6 @@ function withBase(basePath, route) {
106
154
  return `${normalizedBase}${normalizedRoute}`.replace(/\/+/g, '/')
107
155
  }
108
156
 
109
- /* ─── Thumbnail color from name hash ─── */
110
-
111
- const THUMB_CLASSES = ['thumbBlue', 'thumbAmber', 'thumbGreen', 'thumbPurple', 'thumbRose', 'thumbSlate']
112
-
113
- function thumbClass(name) {
114
- let h = 0
115
- for (let i = 0; i < name.length; i++) h = ((h << 5) - h + name.charCodeAt(i)) | 0
116
- return css[THUMB_CLASSES[Math.abs(h) % THUMB_CLASSES.length]]
117
- }
118
-
119
157
  /* ─── Type helpers ─── */
120
158
 
121
159
  function getTypeLabel(type) {
@@ -128,7 +166,7 @@ function getTypeLabel(type) {
128
166
  function getTypeIcon(type, size = 14) {
129
167
  if (type === 'prototype') return <Icon name="prototype" size={size} />
130
168
  if (type === 'canvas') return <Icon name="canvas" size={size} />
131
- if (type === 'component') return <Icon name="component" size={size} />
169
+ if (type === 'component') return <Icon name="iconoir/keyframe" size={size} />
132
170
  return null
133
171
  }
134
172
 
@@ -157,10 +195,13 @@ function AvatarStack({ authors }) {
157
195
 
158
196
  /* ─── Star Button ─── */
159
197
 
160
- function StarBtn({ active, onClick }) {
198
+ function StarBtn({ active, onClick, inline }) {
199
+ const cls = inline
200
+ ? (active ? css.iconBtnInlineActive : css.iconBtnInline)
201
+ : (active ? css.iconBtnActive : css.iconBtn)
161
202
  return (
162
203
  <button
163
- className={active ? css.iconBtnActive : css.iconBtn}
204
+ className={cls}
164
205
  onClick={(e) => { e.preventDefault(); e.stopPropagation(); onClick() }}
165
206
  aria-label={active ? 'Remove favorite' : 'Favorite'}
166
207
  title={active ? 'Remove favorite' : 'Favorite'}
@@ -170,9 +211,251 @@ function StarBtn({ active, onClick }) {
170
211
  )
171
212
  }
172
213
 
214
+ /* ─── Card Actions Menu ─── */
215
+
216
+ function CardActionsMenu({ typeLabel, onEdit, onDelete }) {
217
+ return (
218
+ <Menu.Root>
219
+ <Menu.Trigger
220
+ className={css.iconBtn}
221
+ onClick={(e) => { e.preventDefault(); e.stopPropagation() }}
222
+ aria-label="Actions"
223
+ render={<button />}
224
+ >
225
+ <KebabHorizontalIcon size={16} />
226
+ </Menu.Trigger>
227
+ <Menu.Portal>
228
+ <Menu.Positioner className={css.actionsMenuPositioner} side="bottom" alignment="end">
229
+ <Menu.Popup className={css.actionsMenu} onClick={(e) => { e.preventDefault(); e.stopPropagation() }}>
230
+ <Menu.Item
231
+ className={css.actionsMenuItem}
232
+ onClick={(e) => { e.preventDefault(); e.stopPropagation(); onEdit() }}
233
+ render={<button />}
234
+ >
235
+ <PencilIcon size={16} />
236
+ Edit {typeLabel}
237
+ </Menu.Item>
238
+ <Menu.Item
239
+ className={css.actionsMenuItemDanger}
240
+ onClick={(e) => { e.preventDefault(); e.stopPropagation(); onDelete() }}
241
+ render={<button />}
242
+ >
243
+ <TrashIcon size={16} />
244
+ Delete {typeLabel}
245
+ </Menu.Item>
246
+ </Menu.Popup>
247
+ </Menu.Positioner>
248
+ </Menu.Portal>
249
+ </Menu.Root>
250
+ )
251
+ }
252
+
253
+ /* ─── Edit Artifact Modal ─── */
254
+
255
+ function EditArtifactModal({ item, dirName, basePath, onClose }) {
256
+ const [name, setName] = useState(item.name || '')
257
+ const [description, setDescription] = useState(item.description || '')
258
+ const [author, setAuthor] = useState(
259
+ item.author
260
+ ? (Array.isArray(item.author) ? item.author.join(', ') : item.author)
261
+ : ''
262
+ )
263
+ const [error, setError] = useState('')
264
+ const [submitting, setSubmitting] = useState(false)
265
+ const overlayRef = useRef(null)
266
+
267
+ const typeLabel = item.type === 'canvas' ? 'Canvas' : item.type === 'prototype' ? 'Prototype' : 'Component'
268
+
269
+ const handleSubmit = async (e) => {
270
+ e.preventDefault()
271
+ setError('')
272
+ setSubmitting(true)
273
+
274
+ const apiBase = (basePath || '/').replace(/\/+$/, '')
275
+ let endpoint
276
+
277
+ if (item.type === 'canvas') {
278
+ endpoint = `${apiBase}/_storyboard/canvas/update-meta`
279
+ } else if (item.type === 'prototype') {
280
+ endpoint = `${apiBase}/_storyboard/workshop/prototypes`
281
+ } else {
282
+ setError('Editing this type is not supported')
283
+ setSubmitting(false)
284
+ return
285
+ }
286
+
287
+ const body = {
288
+ name: dirName,
289
+ title: name.trim(),
290
+ description: description.trim(),
291
+ author: author.trim(),
292
+ }
293
+ if (item.folder) body.folder = item.folder
294
+
295
+ try {
296
+ const res = await fetch(endpoint, {
297
+ method: 'PUT',
298
+ headers: { 'Content-Type': 'application/json' },
299
+ body: JSON.stringify(body),
300
+ })
301
+ if (!res.ok) {
302
+ const text = await res.text()
303
+ throw new Error(text || `Request failed (${res.status})`)
304
+ }
305
+ window.location.reload()
306
+ } catch (err) {
307
+ setError(err.message)
308
+ setSubmitting(false)
309
+ }
310
+ }
311
+
312
+ useEffect(() => {
313
+ const handleKey = (e) => { if (e.key === 'Escape') onClose() }
314
+ document.addEventListener('keydown', handleKey)
315
+ return () => document.removeEventListener('keydown', handleKey)
316
+ }, [onClose])
317
+
318
+ return (
319
+ <div className={css.modalOverlay} onClick={(e) => { if (e.target === overlayRef.current) onClose() }} ref={overlayRef}>
320
+ <div className={css.modalContent} onClick={(e) => e.stopPropagation()}>
321
+ <form onSubmit={handleSubmit}>
322
+ <div className={css.createFormHeader}>
323
+ <div className={css.createMenuTitle}>Edit {typeLabel}</div>
324
+ <button type="button" className={css.createFormClose} onClick={onClose} aria-label="Close">
325
+ <XIcon size={16} />
326
+ </button>
327
+ </div>
328
+
329
+ <div className={css.createFormField}>
330
+ <label className={css.createFormLabel}>Name</label>
331
+ <input
332
+ className={css.createFormInput}
333
+ value={name}
334
+ onChange={e => setName(e.target.value)}
335
+ autoFocus
336
+ />
337
+ </div>
338
+
339
+ <div className={css.createFormField}>
340
+ <label className={css.createFormLabel}>Description</label>
341
+ <input
342
+ className={css.createFormInput}
343
+ value={description}
344
+ onChange={e => setDescription(e.target.value)}
345
+ placeholder="Optional description"
346
+ />
347
+ </div>
348
+
349
+ <div className={css.createFormField}>
350
+ <label className={css.createFormLabel}>Author</label>
351
+ <input
352
+ className={css.createFormInput}
353
+ value={author}
354
+ onChange={e => setAuthor(e.target.value)}
355
+ placeholder="GitHub username(s), comma-separated"
356
+ />
357
+ </div>
358
+
359
+ {error && <div className={css.createFormError}>{error}</div>}
360
+
361
+ <div className={css.modalActions}>
362
+ <button type="button" className={css.modalCancelBtn} onClick={onClose}>Cancel</button>
363
+ <button type="submit" className={css.modalSubmitBtn} disabled={submitting}>
364
+ {submitting ? 'Saving…' : 'Save Changes'}
365
+ </button>
366
+ </div>
367
+ </form>
368
+ </div>
369
+ </div>
370
+ )
371
+ }
372
+
373
+ /* ─── Delete Artifact Modal ─── */
374
+
375
+ function DeleteArtifactModal({ item, dirName, basePath, typeLabel, onClose, onDeleted }) {
376
+ const [error, setError] = useState('')
377
+ const [deleting, setDeleting] = useState(false)
378
+ const overlayRef = useRef(null)
379
+
380
+ const handleDelete = async () => {
381
+ setError('')
382
+ setDeleting(true)
383
+
384
+ const apiBase = (basePath || '/').replace(/\/+$/, '')
385
+ let endpoint
386
+
387
+ if (item.type === 'canvas') {
388
+ endpoint = `${apiBase}/_storyboard/canvas/delete-canvas`
389
+ } else if (item.type === 'prototype') {
390
+ endpoint = `${apiBase}/_storyboard/workshop/prototypes`
391
+ } else {
392
+ setError('Deleting this type is not supported')
393
+ setDeleting(false)
394
+ return
395
+ }
396
+
397
+ const body = { name: dirName }
398
+ if (item.folder) body.folder = item.folder
399
+
400
+ try {
401
+ const res = await fetch(endpoint, {
402
+ method: 'DELETE',
403
+ headers: { 'Content-Type': 'application/json' },
404
+ body: JSON.stringify(body),
405
+ })
406
+ if (!res.ok) {
407
+ const text = await res.text()
408
+ throw new Error(text || `Request failed (${res.status})`)
409
+ }
410
+ onDeleted?.()
411
+ onClose()
412
+ } catch (err) {
413
+ setError(err.message)
414
+ setDeleting(false)
415
+ }
416
+ }
417
+
418
+ useEffect(() => {
419
+ const handleKey = (e) => { if (e.key === 'Escape') onClose() }
420
+ document.addEventListener('keydown', handleKey)
421
+ return () => document.removeEventListener('keydown', handleKey)
422
+ }, [onClose])
423
+
424
+ return (
425
+ <div className={css.modalOverlay} onClick={(e) => { if (e.target === overlayRef.current) onClose() }} ref={overlayRef}>
426
+ <div className={css.modalContent} onClick={(e) => e.stopPropagation()}>
427
+ <div className={css.createFormHeader}>
428
+ <div className={css.createMenuTitle}>Delete {typeLabel}</div>
429
+ <button type="button" className={css.createFormClose} onClick={onClose} aria-label="Close">
430
+ <XIcon size={16} />
431
+ </button>
432
+ </div>
433
+
434
+ <p className={css.deleteMessage}>
435
+ Are you sure you want to delete <strong>{item.name}</strong>? This action cannot be undone.
436
+ </p>
437
+
438
+ {error && <div className={css.createFormError}>{error}</div>}
439
+
440
+ <div className={css.modalActions}>
441
+ <button type="button" className={css.modalCancelBtn} onClick={onClose}>Cancel</button>
442
+ <button
443
+ type="button"
444
+ className={css.deleteConfirmBtn}
445
+ onClick={handleDelete}
446
+ disabled={deleting}
447
+ >
448
+ {deleting ? 'Deleting…' : `Delete ${typeLabel}`}
449
+ </button>
450
+ </div>
451
+ </div>
452
+ </div>
453
+ )
454
+ }
455
+
173
456
  /* ─── Artifact Card ─── */
174
457
 
175
- function ArtifactCard({ item, basePath, starred, onToggleStar }) {
458
+ function ArtifactCard({ item, basePath, starred, onToggleStar, onItemDeleted }) {
176
459
  const href = item.route ? withBase(basePath, item.route) : '#'
177
460
  const isExternal = item.isExternal
178
461
 
@@ -189,36 +472,73 @@ function ArtifactCard({ item, basePath, starred, onToggleStar }) {
189
472
  ? (Array.isArray(item.author) ? item.author : [item.author])
190
473
  : item.gitAuthor ? [item.gitAuthor] : []
191
474
 
475
+ const [showEdit, setShowEdit] = useState(false)
476
+ const [showDelete, setShowDelete] = useState(false)
477
+
478
+ // Extract dirName from item.id (format: "type:dirName")
479
+ const dirName = item.id.split(':').slice(1).join(':')
480
+ const typeLabel = item.type === 'canvas' ? 'Canvas' : item.type === 'prototype' ? 'Prototype' : 'Component'
481
+ const canEditDelete = item.type === 'canvas' || item.type === 'prototype'
482
+
192
483
  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>}
484
+ <>
485
+ <Tag className={css.card} {...linkProps} onClick={handleClick}>
486
+ <div className={css.cardHeader}>
487
+ <span className={css.cardBadge}>{getTypeLabel(item.type)}</span>
488
+ <div className={css.cardActions}>
489
+ {item.flows?.length > 0 && <FlowsDropdown flows={item.flows} basePath={basePath} />}
490
+ {item.pages?.length > 1 && <PagesDropdown pages={item.pages} basePath={basePath} />}
491
+ {canEditDelete && (
492
+ <CardActionsMenu
493
+ typeLabel={typeLabel}
494
+ onEdit={() => setShowEdit(true)}
495
+ onDelete={() => setShowDelete(true)}
496
+ />
497
+ )}
207
498
  </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>}
499
+ </div>
500
+ <div className={css.cardBody}>
501
+ <div className={css.cardBodyContent}>
502
+ <div className={css.cardTitleRow}>
503
+ <div className={css.cardTitle}>
504
+ {item.name}
505
+ {isExternal && <span className={css.externalBadge}>↗</span>}
506
+ </div>
507
+ <StarBtn active={starred} onClick={() => onToggleStar(item.id)} inline />
508
+ </div>
509
+ {item.description && (
510
+ <div className={css.cardDescription}>{item.description}</div>
511
+ )}
512
+ <div className={css.cardFooter}>
513
+ <AvatarStack authors={authorList} />
514
+ <div className={css.cardMeta}>
515
+ {authorList.length > 0 && <span>{authorList.join(', ')}</span>}
516
+ {authorList.length > 0 && formatRelativeTime(item.lastModified) && <span className={css.cardMetaDot} />}
517
+ {formatRelativeTime(item.lastModified) && <span>{formatRelativeTime(item.lastModified)}</span>}
518
+ </div>
217
519
  </div>
218
520
  </div>
219
521
  </div>
220
- </div>
221
- </Tag>
522
+ </Tag>
523
+ {showEdit && (
524
+ <EditArtifactModal
525
+ item={item}
526
+ dirName={dirName}
527
+ basePath={basePath}
528
+ onClose={() => setShowEdit(false)}
529
+ />
530
+ )}
531
+ {showDelete && (
532
+ <DeleteArtifactModal
533
+ item={item}
534
+ dirName={dirName}
535
+ basePath={basePath}
536
+ typeLabel={typeLabel}
537
+ onClose={() => setShowDelete(false)}
538
+ onDeleted={() => onItemDeleted?.(item.id)}
539
+ />
540
+ )}
541
+ </>
222
542
  )
223
543
  }
224
544
 
@@ -316,7 +636,7 @@ function PagesDropdown({ pages, basePath }) {
316
636
 
317
637
  /* ─── Folder Section ─── */
318
638
 
319
- function FolderSection({ folder, collapsed, onToggle, basePath, starred, onToggleStar }) {
639
+ function FolderSection({ folder, collapsed, onToggle, basePath, starred, onToggleStar, onItemDeleted }) {
320
640
  return (
321
641
  <section className={collapsed ? css.folderSectionCollapsed : css.folderSection}>
322
642
  <button className={css.folderHeader} onClick={onToggle}>
@@ -337,6 +657,7 @@ function FolderSection({ folder, collapsed, onToggle, basePath, starred, onToggl
337
657
  basePath={basePath}
338
658
  starred={starred.has(item.id)}
339
659
  onToggleStar={onToggleStar}
660
+ onItemDeleted={onItemDeleted}
340
661
  />
341
662
  ))}
342
663
  </div>
@@ -545,7 +866,7 @@ function CreateMenu({ onClose, basePath }) {
545
866
  const items = [
546
867
  { icon: <Icon name="canvas" size={18} />, title: 'Canvas', desc: 'Interactive board for prototypes, components, and documents' },
547
868
  { icon: <Icon name="prototype" size={18} />, title: 'Prototype', desc: 'Interactive page flow' },
548
- { icon: <Icon name="component" size={18} />, title: 'Component', desc: 'Reusable component' },
869
+ { icon: <Icon name="iconoir/-couple-solid" size={18} />, title: 'Component', desc: 'Reusable component' },
549
870
  ]
550
871
 
551
872
  const moreItems = [
@@ -615,162 +936,157 @@ const NAV_ITEMS = [
615
936
 
616
937
  const TAB_FILTERS = ['All', 'Recent', 'Starred']
617
938
 
618
- /* ─── Branch Dropdown ─── */
619
-
620
- function useBranches(basePath) {
621
- const [branches, setBranches] = useState(() => {
622
- if (typeof window !== 'undefined' && Array.isArray(window.__SB_BRANCHES__)) {
623
- return window.__SB_BRANCHES__
624
- }
625
- return null
626
- })
627
-
628
- const [gitUser, setGitUser] = useState(null)
629
-
630
- useEffect(() => {
631
- const apiBase = (basePath || '/').replace(/\/$/, '')
632
-
633
- // Fetch git user info for "my branches" filtering
634
- fetch(`${apiBase}/_storyboard/git-user`).then(r => r.ok ? r.json() : null)
635
- .then(data => { if (data?.name) setGitUser(data.name) })
636
- .catch(() => {})
637
-
638
- // Always fetch live branch list from server API
639
- fetch(`${apiBase}/_storyboard/worktrees`).then(r => r.ok ? r.json() : null)
640
- .then(data => { if (Array.isArray(data) && data.length > 0) setBranches(data) })
641
- .catch(() => {})
642
- }, [])
643
-
644
- const currentBranch = useMemo(() => {
645
- const m = (basePath || '').match(/\/branch--([^/]+)\/?$/)
646
- return m ? m[1] : 'main'
647
- }, [basePath])
939
+ /* ─── Branch Navigation ─── */
648
940
 
649
- const branchBasePath = (basePath || '/').replace(/\/branch--[^/]*\/$/, '/')
650
-
651
- return { branches, currentBranch, branchBasePath, gitUser }
652
- }
653
-
654
- function BranchDropdown({ basePath }) {
655
- // Dev: hide dropdown — use CLI to switch branches
941
+ function BranchNav({ basePath }) {
656
942
  const isLocalDev = typeof window !== 'undefined' && window.__SB_LOCAL_DEV__ === true
657
- if (isLocalDev) return null
658
-
659
- const { branches, currentBranch, branchBasePath, gitUser } = useBranches(basePath)
660
- const [showAll, setShowAll] = useState(false)
943
+ const { branches, currentBranch, branchBasePath } = useBranches(basePath)
661
944
  const [switching, setSwitching] = useState(null)
662
- const [switchError, setSwitchError] = useState(null)
663
945
 
664
946
  if (!branches || branches.length === 0) return null
665
947
 
666
- const twoWeeksAgo = Date.now() - 14 * 24 * 60 * 60 * 1000
948
+ const branchNames = branches.map(b => b.branch)
667
949
 
668
- // Split into "my branches" vs others
669
- const myBranches = gitUser
670
- ? branches.filter(b => b.author === gitUser || b.branch === currentBranch)
671
- : branches.filter(b => b.branch === currentBranch)
950
+ const navigate = async (branch) => {
951
+ if (switching) return
952
+ const target = branches.find(b => b.branch === branch)
953
+ const folder = target?.folder || (branch === 'main' ? '' : `branch--${branch}/`)
954
+ const directUrl = `${branchBasePath}${folder}`
672
955
 
673
- const otherBranches = branches.filter(b => !myBranches.some(m => m.branch === b.branch))
674
-
675
- // Recent = last 2 weeks (or all if showAll)
676
- const recentBranches = showAll
677
- ? [...otherBranches].sort((a, b) => (a.branch || '').localeCompare(b.branch || ''))
678
- : otherBranches
679
- .filter(b => !b.lastModified || new Date(b.lastModified).getTime() > twoWeeksAgo)
680
- .sort((a, b) => (a.branch || '').localeCompare(b.branch || ''))
956
+ if (!isLocalDev) {
957
+ window.location.href = directUrl
958
+ return
959
+ }
681
960
 
682
- const switchBranch = (branch) => {
961
+ // Local dev: ask server to spin up the branch, then navigate
683
962
  setSwitching(branch)
684
- const target = branches?.find(b => b.branch === branch)
685
- window.location.href = `${branchBasePath}${target?.folder || (branch === 'main' ? '' : `branch--${branch}/`)}`
963
+ const apiBase = (basePath || '/').replace(/\/$/, '')
964
+ try {
965
+ const res = await fetch(`${apiBase}/_storyboard/switch-branch`, {
966
+ method: 'POST',
967
+ headers: { 'Content-Type': 'application/json' },
968
+ body: JSON.stringify({ branch }),
969
+ })
970
+ const data = await res.json()
971
+ window.location.href = (res.ok && data.url) ? data.url : directUrl
972
+ } catch {
973
+ window.location.href = directUrl
974
+ }
686
975
  }
687
976
 
688
977
  return (
689
- <Menu.Root>
690
- <Menu.Trigger className={css.branchBtn} disabled={!!switching}>
978
+ <>
979
+ <div className={css.branchNav}>
691
980
  <GitBranchIcon size={14} />
692
- <span className={css.branchBtnText}>{switching ? `Switching to ${switching}…` : currentBranch}</span>
693
- {!switching && <ChevronDownIcon size={12} />}
694
- </Menu.Trigger>
695
- <Menu.Portal>
696
- <Menu.Positioner className={css.branchPositioner} side="bottom" align="end" sideOffset={4}>
697
- <Menu.Popup className={css.branchPopup}>
698
- {myBranches.length > 0 && (
699
- <>
700
- <div className={css.branchSectionLabel}>My branches</div>
701
- {myBranches.map(b => (
702
- <Menu.Item
703
- key={b.branch}
704
- className={`${css.branchItem}${b.branch === currentBranch ? ` ${css.branchItemActive}` : ''}`}
705
- onClick={() => switchBranch(b.branch)}
706
- >
707
- <GitBranchIcon size={12} />
708
- {b.branch}
709
- </Menu.Item>
710
- ))}
711
- </>
712
- )}
981
+ <BranchSelect
982
+ branches={branchNames}
983
+ value={currentBranch}
984
+ onChange={(e) => navigate(e.target.value)}
985
+ disabled={!!switching}
986
+ />
987
+ </div>
988
+ {switching && <div className={css.switchOverlay}>
989
+ <div className={css.switchSpinner} />
990
+ <span>Starting {switching}…</span>
991
+ </div>}
992
+ </>
993
+ )
994
+ }
713
995
 
714
- {myBranches.length > 0 && recentBranches.length > 0 && (
715
- <div className={css.branchSeparator} />
716
- )}
996
+ /* ─── User Settings Dialog ─── */
717
997
 
718
- {recentBranches.length > 0 && (
719
- <>
720
- <div className={css.branchSectionLabel}>
721
- {showAll ? 'All branches' : 'Recent branches'}
722
- </div>
723
- <Menu.Viewport className={css.branchViewport}>
724
- {recentBranches.map(b => (
725
- <Menu.Item
726
- key={b.branch}
727
- className={`${css.branchItem}${b.branch === currentBranch ? ` ${css.branchItemActive}` : ''}`}
728
- onClick={() => switchBranch(b.branch)}
729
- >
730
- <GitBranchIcon size={12} />
731
- {b.branch}
732
- </Menu.Item>
733
- ))}
734
- </Menu.Viewport>
735
- </>
736
- )}
998
+ function UserSettingsDialog({ open, onOpenChange, user, onRemoveToken }) {
999
+ const hasToken = (() => {
1000
+ try { return !!localStorage.getItem(COMMENTS_TOKEN_KEY) } catch { return false }
1001
+ })()
1002
+ const scopes = user?.scopes || []
1003
+ const isFineGrained = hasToken && scopes.length === 0
737
1004
 
738
- {!showAll && otherBranches.length > recentBranches.length && (
739
- <>
740
- <div className={css.branchSeparator} />
741
- <button
742
- className={css.branchShowAll}
743
- onClick={(e) => { e.stopPropagation(); setShowAll(true) }}
744
- >
745
- See all branches ({otherBranches.length})
746
- </button>
747
- </>
748
- )}
749
- </Menu.Popup>
750
- </Menu.Positioner>
751
- </Menu.Portal>
752
- </Menu.Root>
1005
+ return (
1006
+ <Dialog.Root open={open} onOpenChange={onOpenChange}>
1007
+ <Dialog.Portal>
1008
+ <Dialog.Backdrop className={css.settingsBackdrop} />
1009
+ <div className={css.settingsPopupWrap}>
1010
+ <Dialog.Popup className={css.settingsPopup}>
1011
+ <Dialog.Title className={css.settingsTitle}>Settings</Dialog.Title>
1012
+ <Dialog.Close className={css.settingsCloseBtn} aria-label="Close">×</Dialog.Close>
1013
+
1014
+ {/* GitHub connection section */}
1015
+ <div className={css.settingsSection}>
1016
+ <div className={css.settingsSectionHeader}>
1017
+ <ShieldLockIcon size={16} />
1018
+ <span>GitHub Connection</span>
1019
+ </div>
1020
+
1021
+ {hasToken ? (
1022
+ <div className={css.settingsTokenCard}>
1023
+ <div className={css.settingsTokenRow}>
1024
+ <span className={css.settingsTokenLabel}>Token</span>
1025
+ <code className={css.settingsTokenValue}>••••••••••••••••</code>
1026
+ </div>
1027
+ <div className={css.settingsTokenRow}>
1028
+ <span className={css.settingsTokenLabel}>Permissions</span>
1029
+ <span className={css.settingsTokenValue}>
1030
+ {isFineGrained
1031
+ ? 'Fine-grained token'
1032
+ : scopes.map(s => <code key={s} className={css.settingsScope}>{s}</code>)
1033
+ }
1034
+ </span>
1035
+ </div>
1036
+ <button className={css.settingsRemoveBtn} onClick={onRemoveToken}>
1037
+ <TrashIcon size={14} />
1038
+ Remove token
1039
+ </button>
1040
+ </div>
1041
+ ) : (
1042
+ <div className={css.settingsNoToken}>
1043
+ <p>No GitHub token configured.</p>
1044
+ <button
1045
+ className={css.settingsSignInBtn}
1046
+ onClick={() => {
1047
+ onOpenChange(false)
1048
+ document.dispatchEvent(new CustomEvent('storyboard:open-auth-modal'))
1049
+ }}
1050
+ >
1051
+ <MarkGithubIcon size={16} />
1052
+ Sign in with GitHub
1053
+ </button>
1054
+ </div>
1055
+ )}
1056
+ </div>
1057
+ </Dialog.Popup>
1058
+ </div>
1059
+ </Dialog.Portal>
1060
+ </Dialog.Root>
753
1061
  )
754
1062
  }
755
1063
 
756
1064
  /* ─── Main Component ─── */
757
1065
 
758
- export default function Viewfinder({
1066
+ export default function Workspace({
759
1067
  pageModules = {},
760
1068
  basePath,
761
1069
  title = 'Storyboard',
762
1070
  subtitle,
763
- hideDefaultFlow,
764
- hideDefaultScene = false,
765
1071
  }) {
766
- const shouldHideDefault = hideDefaultFlow ?? hideDefaultScene
767
1072
  const themeAttrs = useToolbarTheme()
1073
+ const ghUser = useGitHubUser(basePath)
1074
+ const [settingsOpen, setSettingsOpen] = useState(false)
1075
+
1076
+ const handleRemoveToken = useCallback(() => {
1077
+ try {
1078
+ localStorage.removeItem(COMMENTS_TOKEN_KEY)
1079
+ localStorage.removeItem(COMMENTS_USER_KEY)
1080
+ } catch { /* ignore */ }
1081
+ document.dispatchEvent(new CustomEvent('storyboard:auth-changed'))
1082
+ setSettingsOpen(false)
1083
+ }, [])
768
1084
 
769
1085
  // Build data index from real prototype/canvas/story data
770
1086
  const knownRoutes = useMemo(() =>
771
1087
  Object.keys(pageModules)
772
1088
  .map(p => p.replace('/src/prototypes/', '').replace('.jsx', ''))
773
- .filter(n => !n.startsWith('_') && n !== 'index' && n !== 'viewfinder'),
1089
+ .filter(n => !n.startsWith('_') && n !== 'index' && n !== 'workspace' && n !== 'viewfinder'),
774
1090
  [pageModules],
775
1091
  )
776
1092
 
@@ -860,20 +1176,23 @@ export default function Viewfinder({
860
1176
  const [activeNav, setActiveNav] = useState('all')
861
1177
  const [activeTab, setActiveTab] = useState('All')
862
1178
  const [showCreate, setShowCreate] = useState(false)
1179
+ const isLocalDev = typeof window !== 'undefined' && window.__SB_LOCAL_DEV__ === true
863
1180
  const [sidebarOpen, setSidebarOpen] = useState(false)
864
1181
  const [groupByFolders, setGroupByFolders] = useState(() => {
865
1182
  try { return localStorage.getItem(GROUP_BY_FOLDERS_KEY) !== 'false' } catch { return true }
866
1183
  })
867
1184
  const [collapsedFolders, setCollapsedFolders] = useState(new Set())
1185
+ const [hiddenItems, setHiddenItems] = useState(new Set())
868
1186
  const { starred, toggle: toggleStar } = useStarred()
869
1187
  const recentIds = useRecent()
870
1188
 
871
1189
  // Filter by nav category
1190
+ const typeMap = { prototypes: 'prototype', canvases: 'canvas', components: 'component' }
872
1191
  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])
1192
+ let filtered = activeNav === 'all' ? allItems : allItems.filter(i => i.type === typeMap[activeNav])
1193
+ if (hiddenItems.size > 0) filtered = filtered.filter(i => !hiddenItems.has(i.id))
1194
+ return filtered
1195
+ }, [allItems, activeNav, hiddenItems])
877
1196
 
878
1197
  // Filter by tab
879
1198
  const items = useMemo(() => {
@@ -926,7 +1245,7 @@ export default function Viewfinder({
926
1245
  const toggleGrouping = useCallback(() => {
927
1246
  setGroupByFolders(prev => {
928
1247
  const next = !prev
929
- try { localStorage.setItem(GROUP_BY_FOLDERS_KEY, String(next)) } catch {}
1248
+ try { localStorage.setItem(GROUP_BY_FOLDERS_KEY, String(next)) } catch { /* empty */ }
930
1249
  return next
931
1250
  })
932
1251
  }, [])
@@ -940,18 +1259,24 @@ export default function Viewfinder({
940
1259
  })
941
1260
  }, [])
942
1261
 
1262
+ const handleItemDeleted = useCallback((itemId) => {
1263
+ setHiddenItems(prev => new Set(prev).add(itemId))
1264
+ }, [])
1265
+
943
1266
  // Counts
1267
+ const visibleItems = useMemo(() =>
1268
+ hiddenItems.size > 0 ? allItems.filter(i => !hiddenItems.has(i.id)) : allItems
1269
+ , [allItems, hiddenItems])
1270
+
944
1271
  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])
1272
+ all: visibleItems.length,
1273
+ prototypes: visibleItems.filter(i => i.type === 'prototype').length,
1274
+ canvases: visibleItems.filter(i => i.type === 'canvas').length,
1275
+ components: visibleItems.filter(i => i.type === 'component').length,
1276
+ }), [visibleItems])
950
1277
 
951
1278
  // Starred items for sidebar
952
- const starredItems = useMemo(() => allItems.filter(i => starred.has(i.id)), [allItems, starred])
953
-
954
- const pageTitle = NAV_ITEMS.find(n => n.id === activeNav)?.label || 'All artifacts'
1279
+ const starredItems = useMemo(() => visibleItems.filter(i => starred.has(i.id)), [visibleItems, starred])
955
1280
 
956
1281
  return (
957
1282
  <div className={css.layout} {...themeAttrs}>
@@ -972,19 +1297,21 @@ export default function Viewfinder({
972
1297
  </div>
973
1298
  </div>
974
1299
  <div className={css.topActions}>
975
- <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>
1300
+ <BranchNav basePath={basePath} />
1301
+ {isLocalDev && (
1302
+ <Menu.Root open={showCreate} onOpenChange={setShowCreate}>
1303
+ <Menu.Trigger className={css.createBtn}>
1304
+ <PlusIcon size={14} /> Create
1305
+ </Menu.Trigger>
1306
+ <Menu.Portal>
1307
+ <Menu.Positioner className={css.createDropdownPositioner} side="bottom" align="end" sideOffset={4}>
1308
+ <Menu.Popup className={css.createDropdown}>
1309
+ <CreateMenu onClose={() => setShowCreate(false)} basePath={basePath} />
1310
+ </Menu.Popup>
1311
+ </Menu.Positioner>
1312
+ </Menu.Portal>
1313
+ </Menu.Root>
1314
+ )}
988
1315
  </div>
989
1316
  </header>
990
1317
 
@@ -1027,16 +1354,41 @@ export default function Viewfinder({
1027
1354
  ))}
1028
1355
  </div>
1029
1356
 
1030
- {/* User profile / login */}
1357
+ {/* User profile / settings */}
1031
1358
  <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>
1359
+ {ghUser ? (
1360
+ <div className={css.footerRow}>
1361
+ <button className={css.userBtn} onClick={() => setSettingsOpen(true)}>
1362
+ <img
1363
+ className={css.userAvatar}
1364
+ src={ghUser.avatarUrl || `https://github.com/${ghUser.login}.png?size=64`}
1365
+ alt={ghUser.login}
1366
+ width={32}
1367
+ height={32}
1368
+ />
1369
+ <div className={css.userInfo}>
1370
+ <div className={css.userName}>{ghUser.login}</div>
1371
+ </div>
1372
+ </button>
1037
1373
  </div>
1038
- </button>
1374
+ ) : (
1375
+ <div className={css.footerRow}>
1376
+ <button className={css.loginBtn} onClick={() => document.dispatchEvent(new CustomEvent('storyboard:open-auth-modal'))}>
1377
+ <span className={css.avatar}><MarkGithubIcon size={16} /></span>
1378
+ <div>
1379
+ <div className={css.userName}>Sign in</div>
1380
+ <div className={css.userSub}>Connect with GitHub</div>
1381
+ </div>
1382
+ </button>
1383
+ </div>
1384
+ )}
1039
1385
  </div>
1386
+ <UserSettingsDialog
1387
+ open={settingsOpen}
1388
+ onOpenChange={setSettingsOpen}
1389
+ user={ghUser}
1390
+ onRemoveToken={handleRemoveToken}
1391
+ />
1040
1392
  </aside>
1041
1393
 
1042
1394
  {/* ─── Main ─── */}
@@ -1071,7 +1423,7 @@ export default function Viewfinder({
1071
1423
  {activeTab === 'Starred' && 'No starred items. Click ☆ on a card to star it.'}
1072
1424
  {activeTab === 'All' && 'No items found. Create a prototype, canvas, or component to get started.'}
1073
1425
  </div>
1074
- ) : groupByFolders && grouped && activeTab === 'All' && activeNav === 'all' ? (
1426
+ ) : groupByFolders && grouped && activeTab === 'All' ? (
1075
1427
  <>
1076
1428
  {grouped.folders.map(folder => (
1077
1429
  <FolderSection
@@ -1082,6 +1434,7 @@ export default function Viewfinder({
1082
1434
  basePath={basePath}
1083
1435
  starred={starred}
1084
1436
  onToggleStar={toggleStar}
1437
+ onItemDeleted={handleItemDeleted}
1085
1438
  />
1086
1439
  ))}
1087
1440
  {grouped.ungrouped.length > 0 && (
@@ -1093,6 +1446,7 @@ export default function Viewfinder({
1093
1446
  basePath={basePath}
1094
1447
  starred={starred.has(item.id)}
1095
1448
  onToggleStar={toggleStar}
1449
+ onItemDeleted={handleItemDeleted}
1096
1450
  />
1097
1451
  ))}
1098
1452
  </div>
@@ -1107,6 +1461,7 @@ export default function Viewfinder({
1107
1461
  basePath={basePath}
1108
1462
  starred={starred.has(item.id)}
1109
1463
  onToggleStar={toggleStar}
1464
+ onItemDeleted={handleItemDeleted}
1110
1465
  />
1111
1466
  ))}
1112
1467
  </div>