@dfosco/storyboard-core 4.2.2 → 4.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/storyboard-ui.css +1 -1
- package/dist/storyboard-ui.js +7973 -8418
- package/dist/storyboard-ui.js.map +1 -1
- package/package.json +2 -12
- package/scaffold/AGENTS.md +432 -0
- package/scaffold/gitignore +64 -0
- package/scaffold/manifest.json +13 -8
- package/src/ActionMenuButton.jsx +1 -1
- package/src/AutosyncMenuButton.jsx +1 -1
- package/src/CanvasAgentsMenu.jsx +1 -1
- package/src/CanvasCreateMenu.jsx +1 -1
- package/src/CanvasSnap.jsx +1 -1
- package/src/CanvasZoomToFit.jsx +1 -1
- package/src/CommandMenu.jsx +2 -2
- package/src/CommandPalette.jsx +1 -1
- package/src/CommandPaletteTrigger.jsx +1 -1
- package/src/CommentsMenuButton.jsx +1 -1
- package/src/CoreUIBar.jsx +18 -2
- package/src/CreateMenuButton.jsx +1 -1
- package/src/HideChromeTrigger.jsx +1 -1
- package/src/{svelte-plugin-ui/components/Icon.jsx → Icon.jsx} +8 -10
- package/src/ThemeMenuButton.jsx +1 -1
- package/src/comments/ui/authModal.js +1 -1
- package/src/configSchema.js +2 -0
- package/src/configStore.js +1 -1
- package/src/devtools-consumer.js +2 -2
- package/src/index.js +3 -3
- package/src/mountStoryboardCore.js +3 -3
- package/src/sidepanel.css +1 -1
- package/src/toolbarConfigStore.js +1 -1
- package/src/ui/design-modes.ts +4 -51
- package/src/ui/viewfinder.ts +4 -55
- package/src/ui-entry.js +5 -5
- package/src/vite/server-plugin.js +9 -0
- package/src/workshop/features/createFlow/index.js +1 -1
- package/src/workshop/features/createPrototype/index.js +1 -1
- package/src/workshop/features/registry-server.js +1 -1
- package/src/workshop/ui/mount.ts +3 -65
- package/scaffold/svelte.config.js +0 -1
- package/src/svelte-plugin-ui/__tests__/ModeSwitch.test.ts +0 -75
- package/src/svelte-plugin-ui/__tests__/ToolbarShell.test.ts +0 -126
- package/src/svelte-plugin-ui/__tests__/designModes.test.ts +0 -58
- package/src/svelte-plugin-ui/__tests__/modeStore.test.ts +0 -53
- package/src/svelte-plugin-ui/__tests__/mount.test.ts +0 -29
- package/src/svelte-plugin-ui/components/Icon.css +0 -11
- package/src/svelte-plugin-ui/components/ModeSwitch.css +0 -90
- package/src/svelte-plugin-ui/components/ModeSwitch.jsx +0 -47
- package/src/svelte-plugin-ui/components/ToolbarShell.css +0 -80
- package/src/svelte-plugin-ui/components/ToolbarShell.jsx +0 -84
- package/src/svelte-plugin-ui/components/Viewfinder.css +0 -412
- package/src/svelte-plugin-ui/components/Viewfinder.jsx +0 -513
- package/src/svelte-plugin-ui/index.ts +0 -20
- package/src/svelte-plugin-ui/mount.ts +0 -120
- package/src/svelte-plugin-ui/stores/modeStore.ts +0 -91
- package/src/svelte-plugin-ui/stores/toolStore.ts +0 -71
- package/src/svelte-plugin-ui/stores/types.ts +0 -55
- package/src/svelte-plugin-ui/styles/base.css +0 -69
|
@@ -1,513 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Viewfinder — prototype index and flow dashboard.
|
|
3
|
-
*
|
|
4
|
-
* Full-page component that lists prototypes as expandable groups,
|
|
5
|
-
* each showing its flows. Global flows (not belonging to any prototype)
|
|
6
|
-
* appear as an "Other flows" group.
|
|
7
|
-
*
|
|
8
|
-
* Mounted via mountViewfinder() from the viewfinder plugin entry point.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { useState, useMemo, useEffect, useCallback } from 'react'
|
|
12
|
-
import './Viewfinder.css'
|
|
13
|
-
import { buildPrototypeIndex } from '../../viewfinder.js'
|
|
14
|
-
import { getLocal, setLocal } from '../../localStorage.js'
|
|
15
|
-
import { Icon } from './Icon.jsx'
|
|
16
|
-
|
|
17
|
-
const VIEW_MODE_KEY = 'viewfinder.viewMode'
|
|
18
|
-
const EXPANDED_KEY = 'viewfinder.expanded'
|
|
19
|
-
|
|
20
|
-
function loadExpanded() {
|
|
21
|
-
const raw = getLocal(EXPANDED_KEY)
|
|
22
|
-
if (!raw) return {}
|
|
23
|
-
try { return JSON.parse(raw) } catch { return {} }
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function formatName(name) {
|
|
27
|
-
return name
|
|
28
|
-
.split('-')
|
|
29
|
-
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
30
|
-
.join(' ')
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function placeholderSvg(name) {
|
|
34
|
-
const h = (function hashStr(s) {
|
|
35
|
-
let v = 0
|
|
36
|
-
for (let i = 0; i < s.length; i++) v = ((v << 5) - v + s.charCodeAt(i)) | 0
|
|
37
|
-
return Math.abs(v)
|
|
38
|
-
})(name)
|
|
39
|
-
|
|
40
|
-
let rects = ''
|
|
41
|
-
for (let i = 0; i < 12; i++) {
|
|
42
|
-
const s = h * (i + 1)
|
|
43
|
-
const x = (s * 7 + i * 31) % 320
|
|
44
|
-
const y = (s * 13 + i * 17) % 200
|
|
45
|
-
const w = 20 + (s * (i + 3)) % 80
|
|
46
|
-
const ht = 8 + (s * (i + 7)) % 40
|
|
47
|
-
const opacity = 0.06 + ((s * (i + 2)) % 20) / 100
|
|
48
|
-
const fill = i % 3 === 0 ? 'var(--sb--placeholder-accent)' : i % 3 === 1 ? 'var(--sb--placeholder-fg)' : 'var(--sb--placeholder-muted)'
|
|
49
|
-
rects += `<rect x="${x}" y="${y}" width="${w}" height="${ht}" rx="2" fill="${fill}" opacity="${opacity}" />`
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
let lines = ''
|
|
53
|
-
for (let i = 0; i < 6; i++) {
|
|
54
|
-
const s = h * (i + 5)
|
|
55
|
-
const y = 10 + (s % 180)
|
|
56
|
-
lines += `<line x1="0" y1="${y}" x2="320" y2="${y}" stroke="var(--sb--placeholder-grid)" stroke-width="0.5" opacity="0.4" />`
|
|
57
|
-
}
|
|
58
|
-
for (let i = 0; i < 8; i++) {
|
|
59
|
-
const s = h * (i + 9)
|
|
60
|
-
const x = 10 + (s % 300)
|
|
61
|
-
lines += `<line x1="${x}" y1="0" x2="${x}" y2="200" stroke="var(--sb--placeholder-grid)" stroke-width="0.5" opacity="0.3" />`
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
return `<svg viewBox="0 0 320 200" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><rect width="320" height="200" fill="var(--sb--placeholder-bg)" />${lines}${rects}</svg>`
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/* ── Author block ── */
|
|
68
|
-
function AuthorBlock({ author, gitAuthor }) {
|
|
69
|
-
if (author) {
|
|
70
|
-
const authors = Array.isArray(author) ? author : [author]
|
|
71
|
-
return (
|
|
72
|
-
<div className="author">
|
|
73
|
-
<span className="authorAvatars">
|
|
74
|
-
{authors.map((a) => (
|
|
75
|
-
<img key={a} src={`https://github.com/${a}.png?size=48`} alt={a} className="authorAvatar" />
|
|
76
|
-
))}
|
|
77
|
-
</span>
|
|
78
|
-
<span className="authorName">{authors.join(', ')}</span>
|
|
79
|
-
</div>
|
|
80
|
-
)
|
|
81
|
-
}
|
|
82
|
-
if (gitAuthor) {
|
|
83
|
-
return <p className="authorPlain">{gitAuthor}</p>
|
|
84
|
-
}
|
|
85
|
-
return null
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/* ── Proto entry ── */
|
|
89
|
-
function ProtoEntry({ proto, isExpanded, toggle, withBase, showThumbnails }) {
|
|
90
|
-
if (proto.isExternal) {
|
|
91
|
-
return (
|
|
92
|
-
<section className="protoGroup">
|
|
93
|
-
<a className="listItem" href={proto.externalUrl} target="_blank" rel="noopener noreferrer">
|
|
94
|
-
<div className="cardBody">
|
|
95
|
-
<p className="protoName">
|
|
96
|
-
{proto.icon && <span className="protoIcon">{proto.icon}</span>}
|
|
97
|
-
{proto.name}
|
|
98
|
-
<span className="externalBadge">
|
|
99
|
-
<Icon size={12} color="var(--fgColor-muted)" name="primer/link-external" offsetY={-2} />
|
|
100
|
-
external
|
|
101
|
-
</span>
|
|
102
|
-
</p>
|
|
103
|
-
{proto.description && <p className="protoDesc">{proto.description}</p>}
|
|
104
|
-
<AuthorBlock author={proto.author} gitAuthor={proto.gitAuthor} />
|
|
105
|
-
</div>
|
|
106
|
-
</a>
|
|
107
|
-
</section>
|
|
108
|
-
)
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
if (proto.hideFlows && proto.flows.length === 1) {
|
|
112
|
-
return (
|
|
113
|
-
<section className="protoGroup">
|
|
114
|
-
<a className="listItem" href={withBase(proto.flows[0].route)}>
|
|
115
|
-
<div className="cardBody">
|
|
116
|
-
<p className={`protoName${proto.dirName === '__global__' ? ' otherflows' : ''}`}>
|
|
117
|
-
{proto.icon && <span className="protoIcon">{proto.icon}</span>}
|
|
118
|
-
{proto.name}
|
|
119
|
-
</p>
|
|
120
|
-
{proto.description && <p className="protoDesc">{proto.description}</p>}
|
|
121
|
-
<AuthorBlock author={proto.author} gitAuthor={proto.gitAuthor} />
|
|
122
|
-
</div>
|
|
123
|
-
</a>
|
|
124
|
-
</section>
|
|
125
|
-
)
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
if (proto.flows.length > 0) {
|
|
129
|
-
const expanded = isExpanded(proto.dirName)
|
|
130
|
-
return (
|
|
131
|
-
<section className="protoGroup">
|
|
132
|
-
<button
|
|
133
|
-
className="listItem protoHeader"
|
|
134
|
-
onClick={() => toggle(proto.dirName)}
|
|
135
|
-
aria-expanded={expanded}
|
|
136
|
-
>
|
|
137
|
-
<div className="cardBody">
|
|
138
|
-
<p className={`protoName${proto.dirName === '__global__' ? ' otherflows' : ''}`}>
|
|
139
|
-
{proto.icon && <span className="protoIcon">{proto.icon}</span>}
|
|
140
|
-
{proto.name}
|
|
141
|
-
<span className="protoChevron">
|
|
142
|
-
{expanded
|
|
143
|
-
? <Icon size={12} color="var(--fgColor-disabled)" name="primer/chevron-down" offsetY={-3} offsetX={2} />
|
|
144
|
-
: <Icon size={12} color="var(--fgColor-disabled)" name="primer/chevron-right" offsetY={-3} offsetX={2} />
|
|
145
|
-
}
|
|
146
|
-
</span>
|
|
147
|
-
</p>
|
|
148
|
-
{proto.description && <p className="protoDesc">{proto.description}</p>}
|
|
149
|
-
<AuthorBlock author={proto.author} gitAuthor={proto.gitAuthor} />
|
|
150
|
-
</div>
|
|
151
|
-
</button>
|
|
152
|
-
|
|
153
|
-
{!(proto.hideFlows && proto.flows.length === 1) && expanded && proto.flows.length > 0 && (
|
|
154
|
-
<div className="flowList">
|
|
155
|
-
{proto.flows.map((flow) => (
|
|
156
|
-
<a key={flow.key} href={withBase(flow.route)} className="listItem flowItem">
|
|
157
|
-
{showThumbnails && (
|
|
158
|
-
<div className="thumbnail" dangerouslySetInnerHTML={{ __html: placeholderSvg(flow.key) }} />
|
|
159
|
-
)}
|
|
160
|
-
<div className="cardBody">
|
|
161
|
-
<p className="protoName">{flow.meta?.title || formatName(flow.name)}</p>
|
|
162
|
-
{flow.meta?.description && <p className="flowDesc">{flow.meta.description}</p>}
|
|
163
|
-
</div>
|
|
164
|
-
</a>
|
|
165
|
-
))}
|
|
166
|
-
</div>
|
|
167
|
-
)}
|
|
168
|
-
</section>
|
|
169
|
-
)
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
// Prototype with no flows — navigates directly
|
|
173
|
-
return (
|
|
174
|
-
<section className="protoGroup">
|
|
175
|
-
<a className="listItem" href={withBase(`/${proto.dirName}`)}>
|
|
176
|
-
<div className="cardBody">
|
|
177
|
-
<p className={`protoName${proto.dirName === '__global__' ? ' otherflows' : ''}`}>
|
|
178
|
-
{proto.icon && <span className="protoIcon">{proto.icon}</span>}
|
|
179
|
-
{proto.name}
|
|
180
|
-
</p>
|
|
181
|
-
{proto.description && <p className="protoDesc">{proto.description}</p>}
|
|
182
|
-
<AuthorBlock author={proto.author} gitAuthor={proto.gitAuthor} />
|
|
183
|
-
</div>
|
|
184
|
-
</a>
|
|
185
|
-
</section>
|
|
186
|
-
)
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
/* ── Canvas entry ── */
|
|
190
|
-
function CanvasEntry({ canvas, withBase }) {
|
|
191
|
-
const authors = canvas.author
|
|
192
|
-
? (Array.isArray(canvas.author) ? canvas.author : [canvas.author])
|
|
193
|
-
: null
|
|
194
|
-
|
|
195
|
-
return (
|
|
196
|
-
<section className="protoGroup">
|
|
197
|
-
<a className="listItem" href={withBase(canvas.route)}>
|
|
198
|
-
<div className="cardBody">
|
|
199
|
-
<p className="protoName">
|
|
200
|
-
<span className="protoIcon">{canvas.icon || ''}</span>
|
|
201
|
-
{canvas.name}
|
|
202
|
-
</p>
|
|
203
|
-
{canvas.description && <p className="protoDesc">{canvas.description}</p>}
|
|
204
|
-
{authors ? (
|
|
205
|
-
<div className="author">
|
|
206
|
-
<span className="authorAvatars">
|
|
207
|
-
{authors.map((a) => (
|
|
208
|
-
<img key={a} src={`https://github.com/${a}.png?size=48`} alt={a} className="authorAvatar" />
|
|
209
|
-
))}
|
|
210
|
-
</span>
|
|
211
|
-
<span className="authorName">{authors.join(', ')}</span>
|
|
212
|
-
</div>
|
|
213
|
-
) : canvas.gitAuthor ? (
|
|
214
|
-
<p className="authorPlain">{canvas.gitAuthor}</p>
|
|
215
|
-
) : null}
|
|
216
|
-
</div>
|
|
217
|
-
</a>
|
|
218
|
-
</section>
|
|
219
|
-
)
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
/* ── Folder section ── */
|
|
223
|
-
function FolderSection({ folder, isExpanded, toggle, renderItems, itemKey = 'prototypes' }) {
|
|
224
|
-
const key = `folder:${folder.dirName}`
|
|
225
|
-
const expanded = isExpanded(key)
|
|
226
|
-
const items = folder[itemKey] || []
|
|
227
|
-
|
|
228
|
-
return (
|
|
229
|
-
<section className={`folderGroup${expanded ? ' folderGroupOpen' : ''}`}>
|
|
230
|
-
<button
|
|
231
|
-
className="folderHeader"
|
|
232
|
-
onClick={() => toggle(key)}
|
|
233
|
-
aria-expanded={expanded}
|
|
234
|
-
>
|
|
235
|
-
<p className="folderName">
|
|
236
|
-
<span>
|
|
237
|
-
{expanded
|
|
238
|
-
? <Icon size={20} offsetY={-1.5} name="folder-open" color="#54aeff" />
|
|
239
|
-
: <Icon size={20} offsetY={-1.5} name="folder" color="#54aeff" />
|
|
240
|
-
}
|
|
241
|
-
</span>
|
|
242
|
-
{folder.name}
|
|
243
|
-
</p>
|
|
244
|
-
{folder.description && <p className="folderDesc">{folder.description}</p>}
|
|
245
|
-
</button>
|
|
246
|
-
{expanded && items.length > 0 && (
|
|
247
|
-
<div className="folderContent">
|
|
248
|
-
{renderItems(items)}
|
|
249
|
-
</div>
|
|
250
|
-
)}
|
|
251
|
-
</section>
|
|
252
|
-
)
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
/* ── Main Viewfinder component ── */
|
|
256
|
-
export function Viewfinder({
|
|
257
|
-
title = 'Storyboard',
|
|
258
|
-
subtitle = '',
|
|
259
|
-
basePath = '/',
|
|
260
|
-
knownRoutes = [],
|
|
261
|
-
showThumbnails = false,
|
|
262
|
-
hideDefaultFlow = false,
|
|
263
|
-
}) {
|
|
264
|
-
const prototypeIndex = useMemo(() => buildPrototypeIndex(knownRoutes), [knownRoutes])
|
|
265
|
-
|
|
266
|
-
const globalFlows = useMemo(() => {
|
|
267
|
-
return hideDefaultFlow
|
|
268
|
-
? prototypeIndex.globalFlows.filter((f) => f.key !== 'default')
|
|
269
|
-
: prototypeIndex.globalFlows
|
|
270
|
-
}, [prototypeIndex, hideDefaultFlow])
|
|
271
|
-
|
|
272
|
-
const ungroupedProtos = prototypeIndex.prototypes
|
|
273
|
-
const folders = prototypeIndex.folders || []
|
|
274
|
-
|
|
275
|
-
const otherFlows = useMemo(() => {
|
|
276
|
-
if (globalFlows.length === 0) return null
|
|
277
|
-
return {
|
|
278
|
-
name: 'Other flows',
|
|
279
|
-
dirName: '__global__',
|
|
280
|
-
description: null,
|
|
281
|
-
author: null,
|
|
282
|
-
gitAuthor: null,
|
|
283
|
-
lastModified: null,
|
|
284
|
-
icon: null,
|
|
285
|
-
team: null,
|
|
286
|
-
tags: null,
|
|
287
|
-
flows: globalFlows,
|
|
288
|
-
}
|
|
289
|
-
}, [globalFlows])
|
|
290
|
-
|
|
291
|
-
const totalProtos = ungroupedProtos.length + folders.reduce((sum, f) => sum + f.prototypes.length, 0)
|
|
292
|
-
|
|
293
|
-
// Sorting
|
|
294
|
-
const [sortBy, setSortBy] = useState('updated')
|
|
295
|
-
const sortedProtos = prototypeIndex.sorted?.[sortBy]?.prototypes ?? ungroupedProtos
|
|
296
|
-
const _sortedFolders = prototypeIndex.sorted?.[sortBy]?.folders ?? folders
|
|
297
|
-
void _sortedFolders
|
|
298
|
-
|
|
299
|
-
// Canvases
|
|
300
|
-
const ungroupedCanvases = prototypeIndex.canvases || []
|
|
301
|
-
const sortedCanvases = prototypeIndex.sorted?.[sortBy]?.canvases ?? ungroupedCanvases
|
|
302
|
-
const totalCanvases = ungroupedCanvases.length + folders.reduce((sum, f) => sum + (f.canvases?.length || 0), 0)
|
|
303
|
-
|
|
304
|
-
// View mode
|
|
305
|
-
const [viewMode, setViewMode] = useState(() =>
|
|
306
|
-
getLocal(VIEW_MODE_KEY) === 'canvases' ? 'canvases' : 'prototypes'
|
|
307
|
-
)
|
|
308
|
-
|
|
309
|
-
useEffect(() => {
|
|
310
|
-
setLocal(VIEW_MODE_KEY, viewMode)
|
|
311
|
-
}, [viewMode])
|
|
312
|
-
|
|
313
|
-
// Canvas folders
|
|
314
|
-
const canvasFolders = useMemo(() => {
|
|
315
|
-
const src = prototypeIndex.sorted?.[sortBy]?.folders ?? folders
|
|
316
|
-
return src
|
|
317
|
-
.filter((f) => f.canvases && f.canvases.length > 0)
|
|
318
|
-
.map((f) => ({ ...f, prototypes: [], canvases: f.canvases }))
|
|
319
|
-
}, [prototypeIndex, sortBy, folders])
|
|
320
|
-
|
|
321
|
-
// Proto-only folders
|
|
322
|
-
const protoOnlyFolders = useMemo(() => {
|
|
323
|
-
const src = prototypeIndex.sorted?.[sortBy]?.folders ?? folders
|
|
324
|
-
return src
|
|
325
|
-
.filter((f) => f.prototypes.length > 0)
|
|
326
|
-
.map((f) => ({ ...f, canvases: [] }))
|
|
327
|
-
}, [prototypeIndex, sortBy, folders])
|
|
328
|
-
|
|
329
|
-
// Expanded state
|
|
330
|
-
const [expanded, setExpanded] = useState(loadExpanded)
|
|
331
|
-
|
|
332
|
-
const isExpandedFn = useCallback((dirName) => expanded[dirName] ?? false, [expanded])
|
|
333
|
-
|
|
334
|
-
const toggleFn = useCallback((dirName) => {
|
|
335
|
-
setExpanded((prev) => {
|
|
336
|
-
const next = { ...prev, [dirName]: !(prev[dirName] ?? false) }
|
|
337
|
-
setLocal(EXPANDED_KEY, JSON.stringify(next))
|
|
338
|
-
return next
|
|
339
|
-
})
|
|
340
|
-
}, [])
|
|
341
|
-
|
|
342
|
-
// URL helpers
|
|
343
|
-
const withBase = useCallback((route) => {
|
|
344
|
-
const normalizedRoute = route.startsWith('/') ? route : `/${route}`
|
|
345
|
-
const normalizedBase = (basePath || '/').replace(/\/+$/, '')
|
|
346
|
-
if (!normalizedBase || normalizedBase === '/') return normalizedRoute
|
|
347
|
-
return `${normalizedBase}${normalizedRoute}`.replace(/\/+/g, '/')
|
|
348
|
-
}, [basePath])
|
|
349
|
-
|
|
350
|
-
// Branch switching
|
|
351
|
-
const branches = (typeof window !== 'undefined' && Array.isArray(window.__SB_BRANCHES__))
|
|
352
|
-
? window.__SB_BRANCHES__
|
|
353
|
-
: null
|
|
354
|
-
|
|
355
|
-
const branchBasePath = (basePath || '/storyboard-source/').replace(/\/branch--[^/]*\/$/, '/')
|
|
356
|
-
const currentBranch = useMemo(() => {
|
|
357
|
-
const m = (basePath || '').match(/\/branch--([^/]+)\/?$/)
|
|
358
|
-
return m ? m[1] : 'main'
|
|
359
|
-
}, [basePath])
|
|
360
|
-
|
|
361
|
-
function handleBranchChange(e) {
|
|
362
|
-
const folder = e.target.value
|
|
363
|
-
if (folder) {
|
|
364
|
-
window.location.href = `${branchBasePath}${folder}/`
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
return (
|
|
369
|
-
<div className="container">
|
|
370
|
-
<header className="header">
|
|
371
|
-
<div className="headerTop">
|
|
372
|
-
<div>
|
|
373
|
-
<h1 className="title">{title}</h1>
|
|
374
|
-
{subtitle && <p className="subtitle">{subtitle}</p>}
|
|
375
|
-
</div>
|
|
376
|
-
</div>
|
|
377
|
-
<div className="controlsRow">
|
|
378
|
-
{/* View mode toggle */}
|
|
379
|
-
<div className="sortToggle">
|
|
380
|
-
<button
|
|
381
|
-
className={`sortButton${viewMode === 'prototypes' ? ' sortButtonActive' : ''}`}
|
|
382
|
-
onClick={() => setViewMode('prototypes')}
|
|
383
|
-
>
|
|
384
|
-
Prototypes
|
|
385
|
-
</button>
|
|
386
|
-
<button
|
|
387
|
-
className={`sortButton${viewMode === 'canvases' ? ' sortButtonActive' : ''}`}
|
|
388
|
-
onClick={() => setViewMode('canvases')}
|
|
389
|
-
>
|
|
390
|
-
Canvas
|
|
391
|
-
</button>
|
|
392
|
-
</div>
|
|
393
|
-
{/* Sort toggle — hidden for now */}
|
|
394
|
-
<div className="sortToggle" style={{ display: 'none' }}>
|
|
395
|
-
<button
|
|
396
|
-
className={`sortButton${sortBy === 'updated' ? ' sortButtonActive' : ''}`}
|
|
397
|
-
onClick={() => setSortBy('updated')}
|
|
398
|
-
>
|
|
399
|
-
<Icon name="primer/clock" size={14} color="var(--fgColor-muted)" />
|
|
400
|
-
Last updated
|
|
401
|
-
</button>
|
|
402
|
-
<button
|
|
403
|
-
className={`sortButton${sortBy === 'title' ? ' sortButtonActive' : ''}`}
|
|
404
|
-
onClick={() => setSortBy('title')}
|
|
405
|
-
>
|
|
406
|
-
<Icon name="primer/sort-asc" size={14} color="var(--fgColor-muted)" />
|
|
407
|
-
Title A–Z
|
|
408
|
-
</button>
|
|
409
|
-
</div>
|
|
410
|
-
{branches && branches.length > 0 && (
|
|
411
|
-
<div className="branchDropdown">
|
|
412
|
-
<span className="branchIcon">
|
|
413
|
-
<Icon size={16} color="var(--fgColor-muted)" offsetY={-1} offsetX={2} name="primer/git-branch" />
|
|
414
|
-
</span>
|
|
415
|
-
<select
|
|
416
|
-
className="branchSelect"
|
|
417
|
-
onChange={handleBranchChange}
|
|
418
|
-
aria-label="Switch branch"
|
|
419
|
-
defaultValue=""
|
|
420
|
-
>
|
|
421
|
-
<option value="" disabled>{currentBranch}</option>
|
|
422
|
-
{branches.map((b) => (
|
|
423
|
-
<option key={b.folder} value={b.folder}>{b.branch}</option>
|
|
424
|
-
))}
|
|
425
|
-
</select>
|
|
426
|
-
</div>
|
|
427
|
-
)}
|
|
428
|
-
</div>
|
|
429
|
-
</header>
|
|
430
|
-
|
|
431
|
-
{viewMode === 'prototypes' && totalProtos === 0 && folders.length === 0 ? (
|
|
432
|
-
<p className="empty">No flows found. Add a <code>*.flow.json</code> file to get started.</p>
|
|
433
|
-
) : viewMode === 'canvases' && totalCanvases === 0 ? (
|
|
434
|
-
<p className="empty">No canvases found. Add a <code>*.canvas.jsonl</code> file to get started.</p>
|
|
435
|
-
) : (
|
|
436
|
-
<div className="list">
|
|
437
|
-
{viewMode === 'prototypes' ? (
|
|
438
|
-
<>
|
|
439
|
-
{protoOnlyFolders.map((folder) => (
|
|
440
|
-
<FolderSection
|
|
441
|
-
key={folder.dirName}
|
|
442
|
-
folder={folder}
|
|
443
|
-
isExpanded={isExpandedFn}
|
|
444
|
-
toggle={toggleFn}
|
|
445
|
-
renderItems={(protos) =>
|
|
446
|
-
protos.map((proto) => (
|
|
447
|
-
<ProtoEntry
|
|
448
|
-
key={proto.dirName}
|
|
449
|
-
proto={proto}
|
|
450
|
-
isExpanded={isExpandedFn}
|
|
451
|
-
toggle={toggleFn}
|
|
452
|
-
withBase={withBase}
|
|
453
|
-
showThumbnails={showThumbnails}
|
|
454
|
-
/>
|
|
455
|
-
))
|
|
456
|
-
}
|
|
457
|
-
/>
|
|
458
|
-
))}
|
|
459
|
-
|
|
460
|
-
{sortedProtos.map((proto) => (
|
|
461
|
-
<ProtoEntry
|
|
462
|
-
key={proto.dirName}
|
|
463
|
-
proto={proto}
|
|
464
|
-
isExpanded={isExpandedFn}
|
|
465
|
-
toggle={toggleFn}
|
|
466
|
-
withBase={withBase}
|
|
467
|
-
showThumbnails={showThumbnails}
|
|
468
|
-
/>
|
|
469
|
-
))}
|
|
470
|
-
|
|
471
|
-
{otherFlows && (
|
|
472
|
-
<ProtoEntry
|
|
473
|
-
proto={otherFlows}
|
|
474
|
-
isExpanded={isExpandedFn}
|
|
475
|
-
toggle={toggleFn}
|
|
476
|
-
withBase={withBase}
|
|
477
|
-
showThumbnails={showThumbnails}
|
|
478
|
-
/>
|
|
479
|
-
)}
|
|
480
|
-
</>
|
|
481
|
-
) : (
|
|
482
|
-
<>
|
|
483
|
-
<div className="canvasWarning">
|
|
484
|
-
<Icon size={14} name="primer/alert" color="#9a6700" offsetY={-1} />
|
|
485
|
-
<span>Canvas is an experimental feature. Use with caution.</span>
|
|
486
|
-
</div>
|
|
487
|
-
{canvasFolders.map((folder) => (
|
|
488
|
-
<FolderSection
|
|
489
|
-
key={folder.dirName}
|
|
490
|
-
folder={folder}
|
|
491
|
-
isExpanded={isExpandedFn}
|
|
492
|
-
toggle={toggleFn}
|
|
493
|
-
itemKey="canvases"
|
|
494
|
-
renderItems={(canvases) =>
|
|
495
|
-
canvases.map((canvas) => (
|
|
496
|
-
<CanvasEntry key={canvas.dirName} canvas={canvas} withBase={withBase} />
|
|
497
|
-
))
|
|
498
|
-
}
|
|
499
|
-
/>
|
|
500
|
-
))}
|
|
501
|
-
|
|
502
|
-
{sortedCanvases.map((canvas) => (
|
|
503
|
-
<CanvasEntry key={canvas.dirName} canvas={canvas} withBase={withBase} />
|
|
504
|
-
))}
|
|
505
|
-
</>
|
|
506
|
-
)}
|
|
507
|
-
</div>
|
|
508
|
-
)}
|
|
509
|
-
</div>
|
|
510
|
-
)
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
export default Viewfinder
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Svelte plugin UI — Svelte component framework for core plugin UIs.
|
|
3
|
-
*
|
|
4
|
-
* Part of @dfosco/storyboard-core. Provides mount utilities, shared styles,
|
|
5
|
-
* and reusable components that any storyboard core plugin can use,
|
|
6
|
-
* independent of the prototype app's frontend framework.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
// Mount utility
|
|
10
|
-
export { mountSveltePlugin, injectStyles, type PluginHandle } from './mount.js'
|
|
11
|
-
|
|
12
|
-
// Stores
|
|
13
|
-
export { modeState, switchMode } from './stores/modeStore.js'
|
|
14
|
-
|
|
15
|
-
// Type re-exports
|
|
16
|
-
export type { ModeState } from './stores/modeStore.js'
|
|
17
|
-
export type { ModeConfig, ModeToolConfig } from './stores/types.js'
|
|
18
|
-
|
|
19
|
-
// Viewfinder
|
|
20
|
-
export { mountViewfinder, unmountViewfinder, type ViewfinderProps } from './plugins/viewfinder.js'
|
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Generic mount utility for React plugin components.
|
|
3
|
-
*
|
|
4
|
-
* Provides a framework-agnostic way to mount React components into any
|
|
5
|
-
* DOM target. Handles shared style injection (idempotent) and returns
|
|
6
|
-
* a destroy handle for cleanup.
|
|
7
|
-
*
|
|
8
|
-
* Usage:
|
|
9
|
-
* import { mountSveltePlugin } from '@dfosco/storyboard-core/svelte-plugin-ui'
|
|
10
|
-
* import MyComponent from './MyComponent.jsx'
|
|
11
|
-
*
|
|
12
|
-
* const handle = mountSveltePlugin(document.body, MyComponent, { someProp: 'value' })
|
|
13
|
-
* // later: handle.destroy()
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
import { createElement, type ComponentType } from 'react'
|
|
17
|
-
import { createRoot, type Root } from 'react-dom/client'
|
|
18
|
-
|
|
19
|
-
const STYLE_ID = 'sb-svelte-ui-styles'
|
|
20
|
-
|
|
21
|
-
let stylesInjected = false
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Check whether the shared base styles are already present in the document
|
|
25
|
-
* (e.g. bundled by Vite's CSS code-splitting for the ui-entry chunk).
|
|
26
|
-
*/
|
|
27
|
-
function stylesAlreadyLoaded(): boolean {
|
|
28
|
-
if (typeof document === 'undefined') return false
|
|
29
|
-
// Check for an existing sb- custom property which is defined in the
|
|
30
|
-
// base stylesheet. If any <style> or <link> already defines it, the
|
|
31
|
-
// styles are present and we can skip dynamic injection.
|
|
32
|
-
try {
|
|
33
|
-
const val = getComputedStyle(document.documentElement).getPropertyValue('--sb--bg')
|
|
34
|
-
if (val && val.trim()) return true
|
|
35
|
-
} catch { /* SSR or non-standard env — fall through */ }
|
|
36
|
-
return false
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Inject shared base styles (Tachyons + sb-* tokens) into <head>.
|
|
41
|
-
* Idempotent — only injects once per page. Returns a promise that
|
|
42
|
-
* resolves when the stylesheet is ready.
|
|
43
|
-
*/
|
|
44
|
-
export function injectStyles(): Promise<void> {
|
|
45
|
-
if (stylesInjected) return Promise.resolve()
|
|
46
|
-
if (typeof document === 'undefined') return Promise.resolve()
|
|
47
|
-
if (document.getElementById(STYLE_ID)) {
|
|
48
|
-
stylesInjected = true
|
|
49
|
-
return Promise.resolve()
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// If Vite already bundled the styles into the page, skip the <link>.
|
|
53
|
-
if (stylesAlreadyLoaded()) {
|
|
54
|
-
stylesInjected = true
|
|
55
|
-
return Promise.resolve()
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
return new Promise<void>((resolve) => {
|
|
59
|
-
const link = document.createElement('link')
|
|
60
|
-
link.id = STYLE_ID
|
|
61
|
-
link.rel = 'stylesheet'
|
|
62
|
-
link.href = new URL('../styles/tailwind.css', import.meta.url).href
|
|
63
|
-
link.onload = () => resolve()
|
|
64
|
-
link.onerror = () => resolve() // resolve anyway so the UI still appears
|
|
65
|
-
document.head.appendChild(link)
|
|
66
|
-
stylesInjected = true
|
|
67
|
-
})
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
export interface PluginHandle {
|
|
71
|
-
/** Remove the component and its wrapper from the DOM */
|
|
72
|
-
destroy: () => void
|
|
73
|
-
/** The wrapper element containing the React component */
|
|
74
|
-
element: HTMLElement
|
|
75
|
-
/** Resolves when all styles are loaded and the component is ready to show */
|
|
76
|
-
ready: Promise<void>
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Mount a React component into a DOM target.
|
|
81
|
-
*
|
|
82
|
-
* @param target - DOM element to append the component wrapper to
|
|
83
|
-
* @param ComponentClass - React component
|
|
84
|
-
* @param props - Props to pass to the component
|
|
85
|
-
* @returns Handle with destroy() method and a `ready` promise
|
|
86
|
-
*/
|
|
87
|
-
export function mountSveltePlugin<T extends Record<string, unknown>>(
|
|
88
|
-
target: HTMLElement,
|
|
89
|
-
ComponentClass: ComponentType<T>,
|
|
90
|
-
props?: T,
|
|
91
|
-
): PluginHandle {
|
|
92
|
-
const stylesReady = injectStyles()
|
|
93
|
-
|
|
94
|
-
const wrapper = document.createElement('div')
|
|
95
|
-
wrapper.classList.add('sb-plugin-root')
|
|
96
|
-
wrapper.style.display = 'contents'
|
|
97
|
-
target.appendChild(wrapper)
|
|
98
|
-
|
|
99
|
-
const root: Root = createRoot(wrapper)
|
|
100
|
-
root.render(createElement(ComponentClass, props ?? ({} as T)))
|
|
101
|
-
|
|
102
|
-
return {
|
|
103
|
-
element: wrapper,
|
|
104
|
-
ready: stylesReady,
|
|
105
|
-
destroy() {
|
|
106
|
-
root.unmount()
|
|
107
|
-
wrapper.remove()
|
|
108
|
-
},
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Reset style injection state. Only for use in tests.
|
|
114
|
-
*/
|
|
115
|
-
export function _resetStyles(): void {
|
|
116
|
-
stylesInjected = false
|
|
117
|
-
if (typeof document !== 'undefined') {
|
|
118
|
-
document.getElementById(STYLE_ID)?.remove()
|
|
119
|
-
}
|
|
120
|
-
}
|