@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.
- package/package.json +10 -11
- package/src/AuthModal/AuthModal.jsx +6 -8
- 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 +480 -187
- package/src/CommandPalette/command-palette.css +142 -78
- package/src/Icon.jsx +157 -58
- package/src/Viewfinder.jsx +562 -207
- 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 +11 -7
- package/src/canvas/CanvasPage.jsx +739 -219
- package/src/canvas/CanvasPage.module.css +13 -15
- package/src/canvas/CanvasPage.multiselect.test.jsx +17 -6
- package/src/canvas/ConnectorLayer.jsx +121 -165
- 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/canvasReloadGuard.test.js +1 -1
- 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 +474 -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 +113 -5
- package/src/canvas/widgets/LinkPreview.module.css +127 -0
- package/src/canvas/widgets/MarkdownBlock.jsx +167 -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 -39
- 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 +73 -15
- package/src/canvas/widgets/TerminalReadWidget.jsx +146 -0
- package/src/canvas/widgets/TerminalReadWidget.module.css +94 -0
- package/src/canvas/widgets/TerminalWidget.jsx +445 -67
- package/src/canvas/widgets/TerminalWidget.module.css +271 -8
- package/src/canvas/widgets/TilesWidget.jsx +300 -0
- package/src/canvas/widgets/TilesWidget.module.css +133 -0
- package/src/canvas/widgets/WidgetChrome.jsx +74 -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 +560 -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 +48 -20
- package/src/hooks/useConfig.js +14 -0
- package/src/hooks/usePrototypeReloadGuard.js +64 -0
- package/src/hooks/useSceneData.js +1 -0
- package/src/hooks/useThemeState.test.js +1 -1
- 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 +407 -67
- package/src/vite/data-plugin.test.js +1 -1
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,
|
|
9
|
-
import { MarkGithubIcon, GitBranchIcon, ChevronDownIcon, ChevronRightIcon,
|
|
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-
|
|
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 }
|
|
@@ -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="
|
|
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={
|
|
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
|
-
|
|
194
|
-
<
|
|
195
|
-
<
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
{
|
|
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
|
-
</
|
|
221
|
-
|
|
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="
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
948
|
+
const branchNames = branches.map(b => b.branch)
|
|
667
949
|
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
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
|
-
|
|
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 || ''))
|
|
956
|
+
if (!isLocalDev) {
|
|
957
|
+
window.location.href = directUrl
|
|
958
|
+
return
|
|
959
|
+
}
|
|
681
960
|
|
|
682
|
-
|
|
961
|
+
// Local dev: ask server to spin up the branch, then navigate
|
|
683
962
|
setSwitching(branch)
|
|
684
|
-
const
|
|
685
|
-
|
|
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
|
-
|
|
690
|
-
<
|
|
978
|
+
<>
|
|
979
|
+
<div className={css.branchNav}>
|
|
691
980
|
<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
|
-
)}
|
|
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
|
-
|
|
715
|
-
<div className={css.branchSeparator} />
|
|
716
|
-
)}
|
|
996
|
+
/* ─── User Settings Dialog ─── */
|
|
717
997
|
|
|
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
|
-
)}
|
|
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
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
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
|
|
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
|
-
|
|
874
|
-
|
|
875
|
-
return
|
|
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:
|
|
946
|
-
prototypes:
|
|
947
|
-
canvases:
|
|
948
|
-
components:
|
|
949
|
-
}), [
|
|
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(() =>
|
|
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
|
-
<
|
|
976
|
-
|
|
977
|
-
<Menu.
|
|
978
|
-
<
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
<Menu.
|
|
982
|
-
<Menu.
|
|
983
|
-
<
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
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 /
|
|
1357
|
+
{/* User profile / settings */}
|
|
1031
1358
|
<div className={css.sidebarFooter}>
|
|
1032
|
-
|
|
1033
|
-
<
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
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
|
-
|
|
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'
|
|
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>
|