@dfosco/storyboard-react 4.2.0-beta.1 → 4.2.0-beta.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +5 -4
- package/src/AuthModal/AuthModal.jsx +6 -2
- package/src/BranchBar/BranchBar.jsx +20 -6
- package/src/BranchBar/BranchBar.module.css +13 -4
- package/src/BranchBar/useBranches.js +20 -6
- package/src/BranchBar/useBranches.test.js +68 -0
- package/src/CommandPalette/CommandPalette.jsx +478 -186
- package/src/CommandPalette/command-palette.css +142 -78
- package/src/Icon.jsx +157 -58
- package/src/Viewfinder.jsx +561 -191
- package/src/Viewfinder.module.css +434 -93
- package/src/Workspace.jsx +7 -0
- package/src/canvas/CanvasPage.bridge.test.jsx +14 -6
- package/src/canvas/CanvasPage.dragdrop.test.jsx +10 -6
- package/src/canvas/CanvasPage.jsx +738 -216
- package/src/canvas/CanvasPage.module.css +13 -15
- package/src/canvas/CanvasPage.multiselect.test.jsx +17 -6
- package/src/canvas/ConnectorLayer.jsx +121 -153
- package/src/canvas/ConnectorLayer.module.css +69 -0
- package/src/canvas/PageSelector.test.jsx +15 -6
- package/src/canvas/canvasApi.js +68 -2
- package/src/canvas/connectorGeometry.js +132 -0
- package/src/canvas/hotPoolDevLogs.js +25 -0
- package/src/canvas/useCanvas.js +1 -1
- package/src/canvas/useMarqueeSelect.js +30 -4
- package/src/canvas/widgets/CodePenEmbed.jsx +1 -0
- package/src/canvas/widgets/ComponentSetWidget.jsx +199 -0
- package/src/canvas/widgets/ComponentSetWidget.module.css +89 -0
- package/src/canvas/widgets/ComponentWidget.jsx +1 -0
- package/src/canvas/widgets/CropOverlay.jsx +219 -0
- package/src/canvas/widgets/CropOverlay.module.css +118 -0
- package/src/canvas/widgets/ExpandedPane.jsx +472 -0
- package/src/canvas/widgets/ExpandedPane.module.css +179 -0
- package/src/canvas/widgets/ExpandedPane.test.jsx +240 -0
- package/src/canvas/widgets/ExpandedPaneTopBar.jsx +111 -0
- package/src/canvas/widgets/ExpandedPaneTopBar.module.css +59 -0
- package/src/canvas/widgets/ExpandedPaneTopBar.test.jsx +45 -0
- package/src/canvas/widgets/FigmaEmbed.jsx +62 -47
- package/src/canvas/widgets/FigmaEmbed.module.css +61 -0
- package/src/canvas/widgets/ImageWidget.jsx +130 -9
- package/src/canvas/widgets/ImageWidget.module.css +30 -0
- package/src/canvas/widgets/LinkPreview.jsx +112 -4
- package/src/canvas/widgets/LinkPreview.module.css +127 -0
- package/src/canvas/widgets/MarkdownBlock.jsx +164 -17
- package/src/canvas/widgets/MarkdownBlock.module.css +148 -0
- package/src/canvas/widgets/PromptWidget.jsx +414 -0
- package/src/canvas/widgets/PromptWidget.module.css +273 -0
- package/src/canvas/widgets/PrototypeEmbed.jsx +77 -38
- package/src/canvas/widgets/PrototypeEmbed.module.css +117 -0
- package/src/canvas/widgets/PrototypeEmbed.test.jsx +2 -2
- package/src/canvas/widgets/ResizeHandle.jsx +17 -6
- package/src/canvas/widgets/StoryWidget.jsx +72 -15
- package/src/canvas/widgets/TerminalReadWidget.jsx +146 -0
- package/src/canvas/widgets/TerminalReadWidget.module.css +94 -0
- package/src/canvas/widgets/TerminalWidget.jsx +496 -69
- package/src/canvas/widgets/TerminalWidget.module.css +271 -8
- package/src/canvas/widgets/TilesWidget.jsx +302 -0
- package/src/canvas/widgets/TilesWidget.module.css +133 -0
- package/src/canvas/widgets/WidgetChrome.jsx +73 -153
- package/src/canvas/widgets/WidgetChrome.module.css +30 -1
- package/src/canvas/widgets/embedInteraction.test.jsx +24 -26
- package/src/canvas/widgets/expandUtils.js +557 -0
- package/src/canvas/widgets/expandUtils.test.js +155 -0
- package/src/canvas/widgets/index.js +9 -0
- package/src/canvas/widgets/snapshotDisplay.test.jsx +23 -71
- package/src/canvas/widgets/tilePool.js +23 -0
- package/src/canvas/widgets/tiles/diagonal-bl.png +0 -0
- package/src/canvas/widgets/tiles/diagonal-br.png +0 -0
- package/src/canvas/widgets/tiles/diagonal-tl.png +0 -0
- package/src/canvas/widgets/tiles/leaf.png +0 -0
- package/src/canvas/widgets/tiles/quarter-tl.png +0 -0
- package/src/canvas/widgets/tiles/quarter-tr.png +0 -0
- package/src/canvas/widgets/tiles/solid-a.png +0 -0
- package/src/canvas/widgets/tiles/solid-b.png +0 -0
- package/src/canvas/widgets/widgetConfig.js +55 -4
- package/src/canvas/widgets/widgetIcons.jsx +190 -0
- package/src/canvas/widgets/widgetProps.js +1 -0
- package/src/context.jsx +47 -19
- package/src/hooks/useConfig.js +14 -0
- package/src/hooks/usePrototypeReloadGuard.js +64 -0
- package/src/index.js +8 -2
- package/src/story/ComponentSetPage.jsx +186 -0
- package/src/story/ComponentSetPage.module.css +121 -0
- package/src/story/StoryPage.jsx +32 -2
- package/src/vite/data-plugin.js +324 -30
package/src/Viewfinder.jsx
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Workspace — SaaS-style homescreen for Storyboard.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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, getLocal, setLocal, BranchSelect } from '@dfosco/storyboard-core'
|
|
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'
|
|
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(basePath) {
|
|
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-
|
|
51
|
-
const RECENT_KEY = 'sb-
|
|
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-
|
|
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 }
|
|
@@ -128,7 +176,7 @@ function getTypeLabel(type) {
|
|
|
128
176
|
function getTypeIcon(type, size = 14) {
|
|
129
177
|
if (type === 'prototype') return <Icon name="prototype" size={size} />
|
|
130
178
|
if (type === 'canvas') return <Icon name="canvas" size={size} />
|
|
131
|
-
if (type === 'component') return <Icon name="
|
|
179
|
+
if (type === 'component') return <Icon name="iconoir/keyframe" size={size} />
|
|
132
180
|
return null
|
|
133
181
|
}
|
|
134
182
|
|
|
@@ -157,10 +205,13 @@ function AvatarStack({ authors }) {
|
|
|
157
205
|
|
|
158
206
|
/* ─── Star Button ─── */
|
|
159
207
|
|
|
160
|
-
function StarBtn({ active, onClick }) {
|
|
208
|
+
function StarBtn({ active, onClick, inline }) {
|
|
209
|
+
const cls = inline
|
|
210
|
+
? (active ? css.iconBtnInlineActive : css.iconBtnInline)
|
|
211
|
+
: (active ? css.iconBtnActive : css.iconBtn)
|
|
161
212
|
return (
|
|
162
213
|
<button
|
|
163
|
-
className={
|
|
214
|
+
className={cls}
|
|
164
215
|
onClick={(e) => { e.preventDefault(); e.stopPropagation(); onClick() }}
|
|
165
216
|
aria-label={active ? 'Remove favorite' : 'Favorite'}
|
|
166
217
|
title={active ? 'Remove favorite' : 'Favorite'}
|
|
@@ -170,9 +221,251 @@ function StarBtn({ active, onClick }) {
|
|
|
170
221
|
)
|
|
171
222
|
}
|
|
172
223
|
|
|
224
|
+
/* ─── Card Actions Menu ─── */
|
|
225
|
+
|
|
226
|
+
function CardActionsMenu({ typeLabel, onEdit, onDelete }) {
|
|
227
|
+
return (
|
|
228
|
+
<Menu.Root>
|
|
229
|
+
<Menu.Trigger
|
|
230
|
+
className={css.iconBtn}
|
|
231
|
+
onClick={(e) => { e.preventDefault(); e.stopPropagation() }}
|
|
232
|
+
aria-label="Actions"
|
|
233
|
+
render={<button />}
|
|
234
|
+
>
|
|
235
|
+
<KebabHorizontalIcon size={16} />
|
|
236
|
+
</Menu.Trigger>
|
|
237
|
+
<Menu.Portal>
|
|
238
|
+
<Menu.Positioner className={css.actionsMenuPositioner} side="bottom" alignment="end">
|
|
239
|
+
<Menu.Popup className={css.actionsMenu} onClick={(e) => { e.preventDefault(); e.stopPropagation() }}>
|
|
240
|
+
<Menu.Item
|
|
241
|
+
className={css.actionsMenuItem}
|
|
242
|
+
onClick={(e) => { e.preventDefault(); e.stopPropagation(); onEdit() }}
|
|
243
|
+
render={<button />}
|
|
244
|
+
>
|
|
245
|
+
<PencilIcon size={16} />
|
|
246
|
+
Edit {typeLabel}
|
|
247
|
+
</Menu.Item>
|
|
248
|
+
<Menu.Item
|
|
249
|
+
className={css.actionsMenuItemDanger}
|
|
250
|
+
onClick={(e) => { e.preventDefault(); e.stopPropagation(); onDelete() }}
|
|
251
|
+
render={<button />}
|
|
252
|
+
>
|
|
253
|
+
<TrashIcon size={16} />
|
|
254
|
+
Delete {typeLabel}
|
|
255
|
+
</Menu.Item>
|
|
256
|
+
</Menu.Popup>
|
|
257
|
+
</Menu.Positioner>
|
|
258
|
+
</Menu.Portal>
|
|
259
|
+
</Menu.Root>
|
|
260
|
+
)
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/* ─── Edit Artifact Modal ─── */
|
|
264
|
+
|
|
265
|
+
function EditArtifactModal({ item, dirName, basePath, onClose }) {
|
|
266
|
+
const [name, setName] = useState(item.name || '')
|
|
267
|
+
const [description, setDescription] = useState(item.description || '')
|
|
268
|
+
const [author, setAuthor] = useState(
|
|
269
|
+
item.author
|
|
270
|
+
? (Array.isArray(item.author) ? item.author.join(', ') : item.author)
|
|
271
|
+
: ''
|
|
272
|
+
)
|
|
273
|
+
const [error, setError] = useState('')
|
|
274
|
+
const [submitting, setSubmitting] = useState(false)
|
|
275
|
+
const overlayRef = useRef(null)
|
|
276
|
+
|
|
277
|
+
const typeLabel = item.type === 'canvas' ? 'Canvas' : item.type === 'prototype' ? 'Prototype' : 'Component'
|
|
278
|
+
|
|
279
|
+
const handleSubmit = async (e) => {
|
|
280
|
+
e.preventDefault()
|
|
281
|
+
setError('')
|
|
282
|
+
setSubmitting(true)
|
|
283
|
+
|
|
284
|
+
const apiBase = (basePath || '/').replace(/\/+$/, '')
|
|
285
|
+
let endpoint
|
|
286
|
+
|
|
287
|
+
if (item.type === 'canvas') {
|
|
288
|
+
endpoint = `${apiBase}/_storyboard/canvas/update-meta`
|
|
289
|
+
} else if (item.type === 'prototype') {
|
|
290
|
+
endpoint = `${apiBase}/_storyboard/workshop/prototypes`
|
|
291
|
+
} else {
|
|
292
|
+
setError('Editing this type is not supported')
|
|
293
|
+
setSubmitting(false)
|
|
294
|
+
return
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const body = {
|
|
298
|
+
name: dirName,
|
|
299
|
+
title: name.trim(),
|
|
300
|
+
description: description.trim(),
|
|
301
|
+
author: author.trim(),
|
|
302
|
+
}
|
|
303
|
+
if (item.folder) body.folder = item.folder
|
|
304
|
+
|
|
305
|
+
try {
|
|
306
|
+
const res = await fetch(endpoint, {
|
|
307
|
+
method: 'PUT',
|
|
308
|
+
headers: { 'Content-Type': 'application/json' },
|
|
309
|
+
body: JSON.stringify(body),
|
|
310
|
+
})
|
|
311
|
+
if (!res.ok) {
|
|
312
|
+
const text = await res.text()
|
|
313
|
+
throw new Error(text || `Request failed (${res.status})`)
|
|
314
|
+
}
|
|
315
|
+
window.location.reload()
|
|
316
|
+
} catch (err) {
|
|
317
|
+
setError(err.message)
|
|
318
|
+
setSubmitting(false)
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
useEffect(() => {
|
|
323
|
+
const handleKey = (e) => { if (e.key === 'Escape') onClose() }
|
|
324
|
+
document.addEventListener('keydown', handleKey)
|
|
325
|
+
return () => document.removeEventListener('keydown', handleKey)
|
|
326
|
+
}, [onClose])
|
|
327
|
+
|
|
328
|
+
return (
|
|
329
|
+
<div className={css.modalOverlay} onClick={(e) => { if (e.target === overlayRef.current) onClose() }} ref={overlayRef}>
|
|
330
|
+
<div className={css.modalContent} onClick={(e) => e.stopPropagation()}>
|
|
331
|
+
<form onSubmit={handleSubmit}>
|
|
332
|
+
<div className={css.createFormHeader}>
|
|
333
|
+
<div className={css.createMenuTitle}>Edit {typeLabel}</div>
|
|
334
|
+
<button type="button" className={css.createFormClose} onClick={onClose} aria-label="Close">
|
|
335
|
+
<XIcon size={16} />
|
|
336
|
+
</button>
|
|
337
|
+
</div>
|
|
338
|
+
|
|
339
|
+
<div className={css.createFormField}>
|
|
340
|
+
<label className={css.createFormLabel}>Name</label>
|
|
341
|
+
<input
|
|
342
|
+
className={css.createFormInput}
|
|
343
|
+
value={name}
|
|
344
|
+
onChange={e => setName(e.target.value)}
|
|
345
|
+
autoFocus
|
|
346
|
+
/>
|
|
347
|
+
</div>
|
|
348
|
+
|
|
349
|
+
<div className={css.createFormField}>
|
|
350
|
+
<label className={css.createFormLabel}>Description</label>
|
|
351
|
+
<input
|
|
352
|
+
className={css.createFormInput}
|
|
353
|
+
value={description}
|
|
354
|
+
onChange={e => setDescription(e.target.value)}
|
|
355
|
+
placeholder="Optional description"
|
|
356
|
+
/>
|
|
357
|
+
</div>
|
|
358
|
+
|
|
359
|
+
<div className={css.createFormField}>
|
|
360
|
+
<label className={css.createFormLabel}>Author</label>
|
|
361
|
+
<input
|
|
362
|
+
className={css.createFormInput}
|
|
363
|
+
value={author}
|
|
364
|
+
onChange={e => setAuthor(e.target.value)}
|
|
365
|
+
placeholder="GitHub username(s), comma-separated"
|
|
366
|
+
/>
|
|
367
|
+
</div>
|
|
368
|
+
|
|
369
|
+
{error && <div className={css.createFormError}>{error}</div>}
|
|
370
|
+
|
|
371
|
+
<div className={css.modalActions}>
|
|
372
|
+
<button type="button" className={css.modalCancelBtn} onClick={onClose}>Cancel</button>
|
|
373
|
+
<button type="submit" className={css.modalSubmitBtn} disabled={submitting}>
|
|
374
|
+
{submitting ? 'Saving…' : 'Save Changes'}
|
|
375
|
+
</button>
|
|
376
|
+
</div>
|
|
377
|
+
</form>
|
|
378
|
+
</div>
|
|
379
|
+
</div>
|
|
380
|
+
)
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/* ─── Delete Artifact Modal ─── */
|
|
384
|
+
|
|
385
|
+
function DeleteArtifactModal({ item, dirName, basePath, typeLabel, onClose, onDeleted }) {
|
|
386
|
+
const [error, setError] = useState('')
|
|
387
|
+
const [deleting, setDeleting] = useState(false)
|
|
388
|
+
const overlayRef = useRef(null)
|
|
389
|
+
|
|
390
|
+
const handleDelete = async () => {
|
|
391
|
+
setError('')
|
|
392
|
+
setDeleting(true)
|
|
393
|
+
|
|
394
|
+
const apiBase = (basePath || '/').replace(/\/+$/, '')
|
|
395
|
+
let endpoint
|
|
396
|
+
|
|
397
|
+
if (item.type === 'canvas') {
|
|
398
|
+
endpoint = `${apiBase}/_storyboard/canvas/delete-canvas`
|
|
399
|
+
} else if (item.type === 'prototype') {
|
|
400
|
+
endpoint = `${apiBase}/_storyboard/workshop/prototypes`
|
|
401
|
+
} else {
|
|
402
|
+
setError('Deleting this type is not supported')
|
|
403
|
+
setDeleting(false)
|
|
404
|
+
return
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const body = { name: dirName }
|
|
408
|
+
if (item.folder) body.folder = item.folder
|
|
409
|
+
|
|
410
|
+
try {
|
|
411
|
+
const res = await fetch(endpoint, {
|
|
412
|
+
method: 'DELETE',
|
|
413
|
+
headers: { 'Content-Type': 'application/json' },
|
|
414
|
+
body: JSON.stringify(body),
|
|
415
|
+
})
|
|
416
|
+
if (!res.ok) {
|
|
417
|
+
const text = await res.text()
|
|
418
|
+
throw new Error(text || `Request failed (${res.status})`)
|
|
419
|
+
}
|
|
420
|
+
onDeleted?.()
|
|
421
|
+
onClose()
|
|
422
|
+
} catch (err) {
|
|
423
|
+
setError(err.message)
|
|
424
|
+
setDeleting(false)
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
useEffect(() => {
|
|
429
|
+
const handleKey = (e) => { if (e.key === 'Escape') onClose() }
|
|
430
|
+
document.addEventListener('keydown', handleKey)
|
|
431
|
+
return () => document.removeEventListener('keydown', handleKey)
|
|
432
|
+
}, [onClose])
|
|
433
|
+
|
|
434
|
+
return (
|
|
435
|
+
<div className={css.modalOverlay} onClick={(e) => { if (e.target === overlayRef.current) onClose() }} ref={overlayRef}>
|
|
436
|
+
<div className={css.modalContent} onClick={(e) => e.stopPropagation()}>
|
|
437
|
+
<div className={css.createFormHeader}>
|
|
438
|
+
<div className={css.createMenuTitle}>Delete {typeLabel}</div>
|
|
439
|
+
<button type="button" className={css.createFormClose} onClick={onClose} aria-label="Close">
|
|
440
|
+
<XIcon size={16} />
|
|
441
|
+
</button>
|
|
442
|
+
</div>
|
|
443
|
+
|
|
444
|
+
<p className={css.deleteMessage}>
|
|
445
|
+
Are you sure you want to delete <strong>{item.name}</strong>? This action cannot be undone.
|
|
446
|
+
</p>
|
|
447
|
+
|
|
448
|
+
{error && <div className={css.createFormError}>{error}</div>}
|
|
449
|
+
|
|
450
|
+
<div className={css.modalActions}>
|
|
451
|
+
<button type="button" className={css.modalCancelBtn} onClick={onClose}>Cancel</button>
|
|
452
|
+
<button
|
|
453
|
+
type="button"
|
|
454
|
+
className={css.deleteConfirmBtn}
|
|
455
|
+
onClick={handleDelete}
|
|
456
|
+
disabled={deleting}
|
|
457
|
+
>
|
|
458
|
+
{deleting ? 'Deleting…' : `Delete ${typeLabel}`}
|
|
459
|
+
</button>
|
|
460
|
+
</div>
|
|
461
|
+
</div>
|
|
462
|
+
</div>
|
|
463
|
+
)
|
|
464
|
+
}
|
|
465
|
+
|
|
173
466
|
/* ─── Artifact Card ─── */
|
|
174
467
|
|
|
175
|
-
function ArtifactCard({ item, basePath, starred, onToggleStar }) {
|
|
468
|
+
function ArtifactCard({ item, basePath, starred, onToggleStar, onItemDeleted }) {
|
|
176
469
|
const href = item.route ? withBase(basePath, item.route) : '#'
|
|
177
470
|
const isExternal = item.isExternal
|
|
178
471
|
|
|
@@ -189,36 +482,73 @@ function ArtifactCard({ item, basePath, starred, onToggleStar }) {
|
|
|
189
482
|
? (Array.isArray(item.author) ? item.author : [item.author])
|
|
190
483
|
: item.gitAuthor ? [item.gitAuthor] : []
|
|
191
484
|
|
|
485
|
+
const [showEdit, setShowEdit] = useState(false)
|
|
486
|
+
const [showDelete, setShowDelete] = useState(false)
|
|
487
|
+
|
|
488
|
+
// Extract dirName from item.id (format: "type:dirName")
|
|
489
|
+
const dirName = item.id.split(':').slice(1).join(':')
|
|
490
|
+
const typeLabel = item.type === 'canvas' ? 'Canvas' : item.type === 'prototype' ? 'Prototype' : 'Component'
|
|
491
|
+
const canEditDelete = item.type === 'canvas' || item.type === 'prototype'
|
|
492
|
+
|
|
192
493
|
return (
|
|
193
|
-
|
|
194
|
-
<
|
|
195
|
-
<
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
494
|
+
<>
|
|
495
|
+
<Tag className={css.card} {...linkProps} onClick={handleClick}>
|
|
496
|
+
<div className={css.cardHeader}>
|
|
497
|
+
<span className={css.cardBadge}>{getTypeLabel(item.type)}</span>
|
|
498
|
+
<div className={css.cardActions}>
|
|
499
|
+
{item.flows?.length > 0 && <FlowsDropdown flows={item.flows} basePath={basePath} />}
|
|
500
|
+
{item.pages?.length > 1 && <PagesDropdown pages={item.pages} basePath={basePath} />}
|
|
501
|
+
{canEditDelete && (
|
|
502
|
+
<CardActionsMenu
|
|
503
|
+
typeLabel={typeLabel}
|
|
504
|
+
onEdit={() => setShowEdit(true)}
|
|
505
|
+
onDelete={() => setShowDelete(true)}
|
|
506
|
+
/>
|
|
507
|
+
)}
|
|
207
508
|
</div>
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
{
|
|
509
|
+
</div>
|
|
510
|
+
<div className={css.cardBody}>
|
|
511
|
+
<div className={css.cardBodyContent}>
|
|
512
|
+
<div className={css.cardTitleRow}>
|
|
513
|
+
<div className={css.cardTitle}>
|
|
514
|
+
{item.name}
|
|
515
|
+
{isExternal && <span className={css.externalBadge}>↗</span>}
|
|
516
|
+
</div>
|
|
517
|
+
<StarBtn active={starred} onClick={() => onToggleStar(item.id)} inline />
|
|
518
|
+
</div>
|
|
519
|
+
{item.description && (
|
|
520
|
+
<div className={css.cardDescription}>{item.description}</div>
|
|
521
|
+
)}
|
|
522
|
+
<div className={css.cardFooter}>
|
|
523
|
+
<AvatarStack authors={authorList} />
|
|
524
|
+
<div className={css.cardMeta}>
|
|
525
|
+
{authorList.length > 0 && <span>{authorList.join(', ')}</span>}
|
|
526
|
+
{authorList.length > 0 && formatRelativeTime(item.lastModified) && <span className={css.cardMetaDot} />}
|
|
527
|
+
{formatRelativeTime(item.lastModified) && <span>{formatRelativeTime(item.lastModified)}</span>}
|
|
528
|
+
</div>
|
|
217
529
|
</div>
|
|
218
530
|
</div>
|
|
219
531
|
</div>
|
|
220
|
-
</
|
|
221
|
-
|
|
532
|
+
</Tag>
|
|
533
|
+
{showEdit && (
|
|
534
|
+
<EditArtifactModal
|
|
535
|
+
item={item}
|
|
536
|
+
dirName={dirName}
|
|
537
|
+
basePath={basePath}
|
|
538
|
+
onClose={() => setShowEdit(false)}
|
|
539
|
+
/>
|
|
540
|
+
)}
|
|
541
|
+
{showDelete && (
|
|
542
|
+
<DeleteArtifactModal
|
|
543
|
+
item={item}
|
|
544
|
+
dirName={dirName}
|
|
545
|
+
basePath={basePath}
|
|
546
|
+
typeLabel={typeLabel}
|
|
547
|
+
onClose={() => setShowDelete(false)}
|
|
548
|
+
onDeleted={() => onItemDeleted?.(item.id)}
|
|
549
|
+
/>
|
|
550
|
+
)}
|
|
551
|
+
</>
|
|
222
552
|
)
|
|
223
553
|
}
|
|
224
554
|
|
|
@@ -316,7 +646,7 @@ function PagesDropdown({ pages, basePath }) {
|
|
|
316
646
|
|
|
317
647
|
/* ─── Folder Section ─── */
|
|
318
648
|
|
|
319
|
-
function FolderSection({ folder, collapsed, onToggle, basePath, starred, onToggleStar }) {
|
|
649
|
+
function FolderSection({ folder, collapsed, onToggle, basePath, starred, onToggleStar, onItemDeleted }) {
|
|
320
650
|
return (
|
|
321
651
|
<section className={collapsed ? css.folderSectionCollapsed : css.folderSection}>
|
|
322
652
|
<button className={css.folderHeader} onClick={onToggle}>
|
|
@@ -337,6 +667,7 @@ function FolderSection({ folder, collapsed, onToggle, basePath, starred, onToggl
|
|
|
337
667
|
basePath={basePath}
|
|
338
668
|
starred={starred.has(item.id)}
|
|
339
669
|
onToggleStar={onToggleStar}
|
|
670
|
+
onItemDeleted={onItemDeleted}
|
|
340
671
|
/>
|
|
341
672
|
))}
|
|
342
673
|
</div>
|
|
@@ -545,7 +876,7 @@ function CreateMenu({ onClose, basePath }) {
|
|
|
545
876
|
const items = [
|
|
546
877
|
{ icon: <Icon name="canvas" size={18} />, title: 'Canvas', desc: 'Interactive board for prototypes, components, and documents' },
|
|
547
878
|
{ icon: <Icon name="prototype" size={18} />, title: 'Prototype', desc: 'Interactive page flow' },
|
|
548
|
-
{ icon: <Icon name="
|
|
879
|
+
{ icon: <Icon name="iconoir/-couple-solid" size={18} />, title: 'Component', desc: 'Reusable component' },
|
|
549
880
|
]
|
|
550
881
|
|
|
551
882
|
const moreItems = [
|
|
@@ -615,147 +946,134 @@ const NAV_ITEMS = [
|
|
|
615
946
|
|
|
616
947
|
const TAB_FILTERS = ['All', 'Recent', 'Starred']
|
|
617
948
|
|
|
618
|
-
/* ─── Branch
|
|
949
|
+
/* ─── Branch Navigation ─── */
|
|
619
950
|
|
|
620
|
-
function
|
|
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])
|
|
648
|
-
|
|
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
|
|
951
|
+
function BranchNav({ basePath }) {
|
|
656
952
|
const isLocalDev = typeof window !== 'undefined' && window.__SB_LOCAL_DEV__ === true
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
const { branches, currentBranch, branchBasePath, gitUser } = useBranches(basePath)
|
|
660
|
-
const [showAll, setShowAll] = useState(false)
|
|
953
|
+
const { branches, currentBranch, branchBasePath } = useBranches(basePath)
|
|
661
954
|
const [switching, setSwitching] = useState(null)
|
|
662
|
-
const [switchError, setSwitchError] = useState(null)
|
|
663
955
|
|
|
664
956
|
if (!branches || branches.length === 0) return null
|
|
665
957
|
|
|
666
|
-
const
|
|
958
|
+
const branchNames = branches.map(b => b.branch)
|
|
667
959
|
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
960
|
+
const navigate = async (branch) => {
|
|
961
|
+
if (switching) return
|
|
962
|
+
const target = branches.find(b => b.branch === branch)
|
|
963
|
+
const folder = target?.folder || (branch === 'main' ? '' : `branch--${branch}/`)
|
|
964
|
+
const directUrl = `${branchBasePath}${folder}`
|
|
672
965
|
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
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 || ''))
|
|
966
|
+
if (!isLocalDev) {
|
|
967
|
+
window.location.href = directUrl
|
|
968
|
+
return
|
|
969
|
+
}
|
|
681
970
|
|
|
682
|
-
|
|
971
|
+
// Local dev: ask server to spin up the branch, then navigate
|
|
683
972
|
setSwitching(branch)
|
|
684
|
-
const
|
|
685
|
-
|
|
973
|
+
const apiBase = (basePath || '/').replace(/\/$/, '')
|
|
974
|
+
try {
|
|
975
|
+
const res = await fetch(`${apiBase}/_storyboard/switch-branch`, {
|
|
976
|
+
method: 'POST',
|
|
977
|
+
headers: { 'Content-Type': 'application/json' },
|
|
978
|
+
body: JSON.stringify({ branch }),
|
|
979
|
+
})
|
|
980
|
+
const data = await res.json()
|
|
981
|
+
window.location.href = (res.ok && data.url) ? data.url : directUrl
|
|
982
|
+
} catch {
|
|
983
|
+
window.location.href = directUrl
|
|
984
|
+
}
|
|
686
985
|
}
|
|
687
986
|
|
|
688
987
|
return (
|
|
689
|
-
|
|
690
|
-
<
|
|
988
|
+
<>
|
|
989
|
+
<div className={css.branchNav}>
|
|
691
990
|
<GitBranchIcon size={14} />
|
|
692
|
-
<
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
>
|
|
707
|
-
<GitBranchIcon size={12} />
|
|
708
|
-
{b.branch}
|
|
709
|
-
</Menu.Item>
|
|
710
|
-
))}
|
|
711
|
-
</>
|
|
712
|
-
)}
|
|
991
|
+
<BranchSelect
|
|
992
|
+
branches={branchNames}
|
|
993
|
+
value={currentBranch}
|
|
994
|
+
onChange={(e) => navigate(e.target.value)}
|
|
995
|
+
disabled={!!switching}
|
|
996
|
+
/>
|
|
997
|
+
</div>
|
|
998
|
+
{switching && <div className={css.switchOverlay}>
|
|
999
|
+
<div className={css.switchSpinner} />
|
|
1000
|
+
<span>Starting {switching}…</span>
|
|
1001
|
+
</div>}
|
|
1002
|
+
</>
|
|
1003
|
+
)
|
|
1004
|
+
}
|
|
713
1005
|
|
|
714
|
-
|
|
715
|
-
<div className={css.branchSeparator} />
|
|
716
|
-
)}
|
|
1006
|
+
/* ─── User Settings Dialog ─── */
|
|
717
1007
|
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
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
|
-
)}
|
|
1008
|
+
function UserSettingsDialog({ open, onOpenChange, user, onRemoveToken }) {
|
|
1009
|
+
const hasToken = (() => {
|
|
1010
|
+
try { return !!localStorage.getItem(COMMENTS_TOKEN_KEY) } catch { return false }
|
|
1011
|
+
})()
|
|
1012
|
+
const scopes = user?.scopes || []
|
|
1013
|
+
const isFineGrained = hasToken && scopes.length === 0
|
|
737
1014
|
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
1015
|
+
return (
|
|
1016
|
+
<Dialog.Root open={open} onOpenChange={onOpenChange}>
|
|
1017
|
+
<Dialog.Portal>
|
|
1018
|
+
<Dialog.Backdrop className={css.settingsBackdrop} />
|
|
1019
|
+
<div className={css.settingsPopupWrap}>
|
|
1020
|
+
<Dialog.Popup className={css.settingsPopup}>
|
|
1021
|
+
<Dialog.Title className={css.settingsTitle}>Settings</Dialog.Title>
|
|
1022
|
+
<Dialog.Close className={css.settingsCloseBtn} aria-label="Close">×</Dialog.Close>
|
|
1023
|
+
|
|
1024
|
+
{/* GitHub connection section */}
|
|
1025
|
+
<div className={css.settingsSection}>
|
|
1026
|
+
<div className={css.settingsSectionHeader}>
|
|
1027
|
+
<ShieldLockIcon size={16} />
|
|
1028
|
+
<span>GitHub Connection</span>
|
|
1029
|
+
</div>
|
|
1030
|
+
|
|
1031
|
+
{hasToken ? (
|
|
1032
|
+
<div className={css.settingsTokenCard}>
|
|
1033
|
+
<div className={css.settingsTokenRow}>
|
|
1034
|
+
<span className={css.settingsTokenLabel}>Token</span>
|
|
1035
|
+
<code className={css.settingsTokenValue}>••••••••••••••••</code>
|
|
1036
|
+
</div>
|
|
1037
|
+
<div className={css.settingsTokenRow}>
|
|
1038
|
+
<span className={css.settingsTokenLabel}>Permissions</span>
|
|
1039
|
+
<span className={css.settingsTokenValue}>
|
|
1040
|
+
{isFineGrained
|
|
1041
|
+
? 'Fine-grained token'
|
|
1042
|
+
: scopes.map(s => <code key={s} className={css.settingsScope}>{s}</code>)
|
|
1043
|
+
}
|
|
1044
|
+
</span>
|
|
1045
|
+
</div>
|
|
1046
|
+
<button className={css.settingsRemoveBtn} onClick={onRemoveToken}>
|
|
1047
|
+
<TrashIcon size={14} />
|
|
1048
|
+
Remove token
|
|
1049
|
+
</button>
|
|
1050
|
+
</div>
|
|
1051
|
+
) : (
|
|
1052
|
+
<div className={css.settingsNoToken}>
|
|
1053
|
+
<p>No GitHub token configured.</p>
|
|
1054
|
+
<button
|
|
1055
|
+
className={css.settingsSignInBtn}
|
|
1056
|
+
onClick={() => {
|
|
1057
|
+
onOpenChange(false)
|
|
1058
|
+
document.dispatchEvent(new CustomEvent('storyboard:open-auth-modal'))
|
|
1059
|
+
}}
|
|
1060
|
+
>
|
|
1061
|
+
<MarkGithubIcon size={16} />
|
|
1062
|
+
Sign in with GitHub
|
|
1063
|
+
</button>
|
|
1064
|
+
</div>
|
|
1065
|
+
)}
|
|
1066
|
+
</div>
|
|
1067
|
+
</Dialog.Popup>
|
|
1068
|
+
</div>
|
|
1069
|
+
</Dialog.Portal>
|
|
1070
|
+
</Dialog.Root>
|
|
753
1071
|
)
|
|
754
1072
|
}
|
|
755
1073
|
|
|
756
1074
|
/* ─── Main Component ─── */
|
|
757
1075
|
|
|
758
|
-
export default function
|
|
1076
|
+
export default function Workspace({
|
|
759
1077
|
pageModules = {},
|
|
760
1078
|
basePath,
|
|
761
1079
|
title = 'Storyboard',
|
|
@@ -765,12 +1083,23 @@ export default function Viewfinder({
|
|
|
765
1083
|
}) {
|
|
766
1084
|
const shouldHideDefault = hideDefaultFlow ?? hideDefaultScene
|
|
767
1085
|
const themeAttrs = useToolbarTheme()
|
|
1086
|
+
const ghUser = useGitHubUser(basePath)
|
|
1087
|
+
const [settingsOpen, setSettingsOpen] = useState(false)
|
|
1088
|
+
|
|
1089
|
+
const handleRemoveToken = useCallback(() => {
|
|
1090
|
+
try {
|
|
1091
|
+
localStorage.removeItem(COMMENTS_TOKEN_KEY)
|
|
1092
|
+
localStorage.removeItem(COMMENTS_USER_KEY)
|
|
1093
|
+
} catch { /* ignore */ }
|
|
1094
|
+
document.dispatchEvent(new CustomEvent('storyboard:auth-changed'))
|
|
1095
|
+
setSettingsOpen(false)
|
|
1096
|
+
}, [])
|
|
768
1097
|
|
|
769
1098
|
// Build data index from real prototype/canvas/story data
|
|
770
1099
|
const knownRoutes = useMemo(() =>
|
|
771
1100
|
Object.keys(pageModules)
|
|
772
1101
|
.map(p => p.replace('/src/prototypes/', '').replace('.jsx', ''))
|
|
773
|
-
.filter(n => !n.startsWith('_') && n !== 'index' && n !== 'viewfinder'),
|
|
1102
|
+
.filter(n => !n.startsWith('_') && n !== 'index' && n !== 'workspace' && n !== 'viewfinder'),
|
|
774
1103
|
[pageModules],
|
|
775
1104
|
)
|
|
776
1105
|
|
|
@@ -860,20 +1189,23 @@ export default function Viewfinder({
|
|
|
860
1189
|
const [activeNav, setActiveNav] = useState('all')
|
|
861
1190
|
const [activeTab, setActiveTab] = useState('All')
|
|
862
1191
|
const [showCreate, setShowCreate] = useState(false)
|
|
1192
|
+
const isLocalDev = typeof window !== 'undefined' && window.__SB_LOCAL_DEV__ === true
|
|
863
1193
|
const [sidebarOpen, setSidebarOpen] = useState(false)
|
|
864
1194
|
const [groupByFolders, setGroupByFolders] = useState(() => {
|
|
865
1195
|
try { return localStorage.getItem(GROUP_BY_FOLDERS_KEY) !== 'false' } catch { return true }
|
|
866
1196
|
})
|
|
867
1197
|
const [collapsedFolders, setCollapsedFolders] = useState(new Set())
|
|
1198
|
+
const [hiddenItems, setHiddenItems] = useState(new Set())
|
|
868
1199
|
const { starred, toggle: toggleStar } = useStarred()
|
|
869
1200
|
const recentIds = useRecent()
|
|
870
1201
|
|
|
871
1202
|
// Filter by nav category
|
|
1203
|
+
const typeMap = { prototypes: 'prototype', canvases: 'canvas', components: 'component' }
|
|
872
1204
|
const navFiltered = useMemo(() => {
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
return
|
|
876
|
-
}, [allItems, activeNav])
|
|
1205
|
+
let filtered = activeNav === 'all' ? allItems : allItems.filter(i => i.type === typeMap[activeNav])
|
|
1206
|
+
if (hiddenItems.size > 0) filtered = filtered.filter(i => !hiddenItems.has(i.id))
|
|
1207
|
+
return filtered
|
|
1208
|
+
}, [allItems, activeNav, hiddenItems])
|
|
877
1209
|
|
|
878
1210
|
// Filter by tab
|
|
879
1211
|
const items = useMemo(() => {
|
|
@@ -940,16 +1272,24 @@ export default function Viewfinder({
|
|
|
940
1272
|
})
|
|
941
1273
|
}, [])
|
|
942
1274
|
|
|
1275
|
+
const handleItemDeleted = useCallback((itemId) => {
|
|
1276
|
+
setHiddenItems(prev => new Set(prev).add(itemId))
|
|
1277
|
+
}, [])
|
|
1278
|
+
|
|
943
1279
|
// Counts
|
|
1280
|
+
const visibleItems = useMemo(() =>
|
|
1281
|
+
hiddenItems.size > 0 ? allItems.filter(i => !hiddenItems.has(i.id)) : allItems
|
|
1282
|
+
, [allItems, hiddenItems])
|
|
1283
|
+
|
|
944
1284
|
const counts = useMemo(() => ({
|
|
945
|
-
all:
|
|
946
|
-
prototypes:
|
|
947
|
-
canvases:
|
|
948
|
-
components:
|
|
949
|
-
}), [
|
|
1285
|
+
all: visibleItems.length,
|
|
1286
|
+
prototypes: visibleItems.filter(i => i.type === 'prototype').length,
|
|
1287
|
+
canvases: visibleItems.filter(i => i.type === 'canvas').length,
|
|
1288
|
+
components: visibleItems.filter(i => i.type === 'component').length,
|
|
1289
|
+
}), [visibleItems])
|
|
950
1290
|
|
|
951
1291
|
// Starred items for sidebar
|
|
952
|
-
const starredItems = useMemo(() =>
|
|
1292
|
+
const starredItems = useMemo(() => visibleItems.filter(i => starred.has(i.id)), [visibleItems, starred])
|
|
953
1293
|
|
|
954
1294
|
const pageTitle = NAV_ITEMS.find(n => n.id === activeNav)?.label || 'All artifacts'
|
|
955
1295
|
|
|
@@ -972,19 +1312,21 @@ export default function Viewfinder({
|
|
|
972
1312
|
</div>
|
|
973
1313
|
</div>
|
|
974
1314
|
<div className={css.topActions}>
|
|
975
|
-
<
|
|
976
|
-
|
|
977
|
-
<Menu.
|
|
978
|
-
<
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
<Menu.
|
|
982
|
-
<Menu.
|
|
983
|
-
<
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
1315
|
+
<BranchNav basePath={basePath} />
|
|
1316
|
+
{isLocalDev && (
|
|
1317
|
+
<Menu.Root open={showCreate} onOpenChange={setShowCreate}>
|
|
1318
|
+
<Menu.Trigger className={css.createBtn}>
|
|
1319
|
+
<PlusIcon size={14} /> Create
|
|
1320
|
+
</Menu.Trigger>
|
|
1321
|
+
<Menu.Portal>
|
|
1322
|
+
<Menu.Positioner className={css.createDropdownPositioner} side="bottom" align="end" sideOffset={4}>
|
|
1323
|
+
<Menu.Popup className={css.createDropdown}>
|
|
1324
|
+
<CreateMenu onClose={() => setShowCreate(false)} basePath={basePath} />
|
|
1325
|
+
</Menu.Popup>
|
|
1326
|
+
</Menu.Positioner>
|
|
1327
|
+
</Menu.Portal>
|
|
1328
|
+
</Menu.Root>
|
|
1329
|
+
)}
|
|
988
1330
|
</div>
|
|
989
1331
|
</header>
|
|
990
1332
|
|
|
@@ -1027,16 +1369,41 @@ export default function Viewfinder({
|
|
|
1027
1369
|
))}
|
|
1028
1370
|
</div>
|
|
1029
1371
|
|
|
1030
|
-
{/* User profile /
|
|
1372
|
+
{/* User profile / settings */}
|
|
1031
1373
|
<div className={css.sidebarFooter}>
|
|
1032
|
-
|
|
1033
|
-
<
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1374
|
+
{ghUser ? (
|
|
1375
|
+
<div className={css.footerRow}>
|
|
1376
|
+
<button className={css.userBtn} onClick={() => setSettingsOpen(true)}>
|
|
1377
|
+
<img
|
|
1378
|
+
className={css.userAvatar}
|
|
1379
|
+
src={ghUser.avatarUrl || `https://github.com/${ghUser.login}.png?size=64`}
|
|
1380
|
+
alt={ghUser.login}
|
|
1381
|
+
width={32}
|
|
1382
|
+
height={32}
|
|
1383
|
+
/>
|
|
1384
|
+
<div className={css.userInfo}>
|
|
1385
|
+
<div className={css.userName}>{ghUser.login}</div>
|
|
1386
|
+
</div>
|
|
1387
|
+
</button>
|
|
1037
1388
|
</div>
|
|
1038
|
-
|
|
1389
|
+
) : (
|
|
1390
|
+
<div className={css.footerRow}>
|
|
1391
|
+
<button className={css.loginBtn} onClick={() => document.dispatchEvent(new CustomEvent('storyboard:open-auth-modal'))}>
|
|
1392
|
+
<span className={css.avatar}><MarkGithubIcon size={16} /></span>
|
|
1393
|
+
<div>
|
|
1394
|
+
<div className={css.userName}>Sign in</div>
|
|
1395
|
+
<div className={css.userSub}>Connect with GitHub</div>
|
|
1396
|
+
</div>
|
|
1397
|
+
</button>
|
|
1398
|
+
</div>
|
|
1399
|
+
)}
|
|
1039
1400
|
</div>
|
|
1401
|
+
<UserSettingsDialog
|
|
1402
|
+
open={settingsOpen}
|
|
1403
|
+
onOpenChange={setSettingsOpen}
|
|
1404
|
+
user={ghUser}
|
|
1405
|
+
onRemoveToken={handleRemoveToken}
|
|
1406
|
+
/>
|
|
1040
1407
|
</aside>
|
|
1041
1408
|
|
|
1042
1409
|
{/* ─── Main ─── */}
|
|
@@ -1071,7 +1438,7 @@ export default function Viewfinder({
|
|
|
1071
1438
|
{activeTab === 'Starred' && 'No starred items. Click ☆ on a card to star it.'}
|
|
1072
1439
|
{activeTab === 'All' && 'No items found. Create a prototype, canvas, or component to get started.'}
|
|
1073
1440
|
</div>
|
|
1074
|
-
) : groupByFolders && grouped && activeTab === 'All'
|
|
1441
|
+
) : groupByFolders && grouped && activeTab === 'All' ? (
|
|
1075
1442
|
<>
|
|
1076
1443
|
{grouped.folders.map(folder => (
|
|
1077
1444
|
<FolderSection
|
|
@@ -1082,6 +1449,7 @@ export default function Viewfinder({
|
|
|
1082
1449
|
basePath={basePath}
|
|
1083
1450
|
starred={starred}
|
|
1084
1451
|
onToggleStar={toggleStar}
|
|
1452
|
+
onItemDeleted={handleItemDeleted}
|
|
1085
1453
|
/>
|
|
1086
1454
|
))}
|
|
1087
1455
|
{grouped.ungrouped.length > 0 && (
|
|
@@ -1093,6 +1461,7 @@ export default function Viewfinder({
|
|
|
1093
1461
|
basePath={basePath}
|
|
1094
1462
|
starred={starred.has(item.id)}
|
|
1095
1463
|
onToggleStar={toggleStar}
|
|
1464
|
+
onItemDeleted={handleItemDeleted}
|
|
1096
1465
|
/>
|
|
1097
1466
|
))}
|
|
1098
1467
|
</div>
|
|
@@ -1107,6 +1476,7 @@ export default function Viewfinder({
|
|
|
1107
1476
|
basePath={basePath}
|
|
1108
1477
|
starred={starred.has(item.id)}
|
|
1109
1478
|
onToggleStar={toggleStar}
|
|
1479
|
+
onItemDeleted={handleItemDeleted}
|
|
1110
1480
|
/>
|
|
1111
1481
|
))}
|
|
1112
1482
|
</div>
|