@dfosco/storyboard 0.6.3 → 0.6.5
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 +1 -1
- package/scaffold/AGENTS.md +7 -7
- package/scaffold/skills/canvas/SKILL.md +1 -1
- package/scaffold/skills/migrate-0.5.0/SKILL.md +1 -1
- package/scaffold/skills/ship/SKILL.md +2 -2
- package/scaffold/skills/worktree/SKILL.md +1 -1
- package/src/core/autosync/server.test.js +1 -1
- package/src/core/canvas/hot-pool.js +10 -11
- package/src/core/canvas/terminal-server.js +144 -57
- package/src/core/cli/dev.js +17 -3
- package/src/core/cli/setup.js +12 -11
- package/src/core/cli/terminal-welcome.js +12 -6
- package/src/core/data/viewfinder.js +47 -7
- package/src/core/rename-watcher/watcher.js +10 -3
- package/src/internals/Viewfinder.jsx +57 -8
- package/src/internals/Viewfinder.module.css +53 -7
- package/src/internals/canvas/CanvasPage.jsx +9 -0
- package/src/internals/canvas/PageSelector.jsx +15 -1
- package/src/internals/canvas/PageSelector.module.css +14 -0
- package/src/internals/canvas/widgets/FrozenTerminalOverlay.jsx +30 -21
- package/src/internals/canvas/widgets/LinkPreview.module.css +2 -2
- package/src/internals/canvas/widgets/PrototypeEmbed.jsx +31 -3
- package/src/internals/canvas/widgets/StorySetWidget.jsx +16 -4
- package/src/internals/canvas/widgets/TerminalReadWidget.jsx +13 -15
- package/src/internals/canvas/widgets/WidgetChrome.module.css +15 -0
- package/src/internals/context.jsx +2 -1
- package/src/internals/vite/data-plugin.js +102 -74
- package/src/internals/vite/data-plugin.test.js +11 -11
- package/terminal.config.json +1 -1
|
@@ -133,6 +133,8 @@ export function buildPrototypeIndex(knownRoutes = []) {
|
|
|
133
133
|
const raw = getPrototypeMetadata(name)
|
|
134
134
|
const meta = raw?.meta || raw || {}
|
|
135
135
|
const isExternal = Boolean(raw?.url)
|
|
136
|
+
const folder = raw?.folder || null
|
|
137
|
+
const isPrivate = raw?._isPrivate || false
|
|
136
138
|
protoMap[name] = {
|
|
137
139
|
name: meta.title || name,
|
|
138
140
|
dirName: name,
|
|
@@ -144,9 +146,10 @@ export function buildPrototypeIndex(knownRoutes = []) {
|
|
|
144
146
|
team: meta.team || null,
|
|
145
147
|
tags: meta.tags || null,
|
|
146
148
|
hideFlows: meta.hideFlows ?? raw?.hideFlows ?? true,
|
|
147
|
-
folder
|
|
149
|
+
folder,
|
|
148
150
|
isExternal,
|
|
149
151
|
externalUrl: isExternal ? raw.url : null,
|
|
152
|
+
isPrivate,
|
|
150
153
|
flows: [],
|
|
151
154
|
}
|
|
152
155
|
}
|
|
@@ -202,6 +205,7 @@ export function buildPrototypeIndex(knownRoutes = []) {
|
|
|
202
205
|
dirName: folderName,
|
|
203
206
|
description: meta.description || null,
|
|
204
207
|
icon: meta.icon || null,
|
|
208
|
+
isPrivate: !!raw?._isPrivate,
|
|
205
209
|
prototypes: [],
|
|
206
210
|
}
|
|
207
211
|
}
|
|
@@ -218,6 +222,7 @@ export function buildPrototypeIndex(knownRoutes = []) {
|
|
|
218
222
|
dirName: proto.folder,
|
|
219
223
|
description: null,
|
|
220
224
|
icon: null,
|
|
225
|
+
isPrivate: false,
|
|
221
226
|
prototypes: [proto],
|
|
222
227
|
}
|
|
223
228
|
} else {
|
|
@@ -243,25 +248,56 @@ export function buildPrototypeIndex(knownRoutes = []) {
|
|
|
243
248
|
const pageName = data.title || toTitleCase(fileSegment)
|
|
244
249
|
|
|
245
250
|
const pageEntry = {
|
|
251
|
+
id: canvasId,
|
|
246
252
|
name: pageName,
|
|
247
253
|
route: data._route || `/canvas/${canvasId}`,
|
|
254
|
+
isPrivate: !!data._isPrivate,
|
|
248
255
|
}
|
|
249
256
|
|
|
250
257
|
// If this canvas belongs to a group we've already seen, add as a page
|
|
251
258
|
if (group && seenGroups.has(group)) {
|
|
252
259
|
const idx = seenGroups.get(group)
|
|
253
|
-
|
|
260
|
+
const existing = canvasEntries[idx]
|
|
261
|
+
if (!existing.pages) {
|
|
254
262
|
// Retroactively add the first page
|
|
255
|
-
const firstId =
|
|
263
|
+
const firstId = existing.dirName
|
|
256
264
|
const firstData = getCanvasData(firstId)
|
|
257
265
|
const firstSegment = firstId.split('/').pop()
|
|
258
266
|
const firstName = firstData?.title || toTitleCase(firstSegment)
|
|
259
|
-
|
|
267
|
+
existing.pages = [{
|
|
268
|
+
id: firstId,
|
|
269
|
+
name: firstName,
|
|
270
|
+
route: existing.route,
|
|
271
|
+
isPrivate: !!firstData?._isPrivate,
|
|
272
|
+
}]
|
|
260
273
|
}
|
|
261
|
-
|
|
274
|
+
// If the existing card is private but this incoming page is public,
|
|
275
|
+
// promote the public page to be the card's public face. The private
|
|
276
|
+
// sibling stays as one of the group's pages (with its eye-off icon
|
|
277
|
+
// in the dropdown). Without this swap, the workspace card would
|
|
278
|
+
// inherit the title + privacy of whichever page happened to come
|
|
279
|
+
// first in iteration order.
|
|
280
|
+
const incomingIsPrivate = !!data._isPrivate || canvasId.split('/').includes('drafts')
|
|
281
|
+
if (existing.isPrivate && !incomingIsPrivate) {
|
|
282
|
+
existing.name = meta?.title || data.title || canvasId
|
|
283
|
+
existing.dirName = canvasId
|
|
284
|
+
existing.description = meta?.description || data.description || null
|
|
285
|
+
existing.route = data._route || `/canvas/${canvasId}`
|
|
286
|
+
existing.folder = data._folder || existing.folder
|
|
287
|
+
existing.isPrivate = false
|
|
288
|
+
existing.author = meta?.author || data.author || existing.author
|
|
289
|
+
existing.gitAuthor = data.gitAuthor || existing.gitAuthor
|
|
290
|
+
existing._canvasMeta = meta || existing._canvasMeta
|
|
291
|
+
}
|
|
292
|
+
existing.pages.push(pageEntry)
|
|
262
293
|
continue
|
|
263
294
|
}
|
|
264
295
|
|
|
296
|
+
// Canvas is "private" when its parsed data carries _isPrivate (from the
|
|
297
|
+
// data plugin) or — fallback — when its canonical ID contains a `drafts`
|
|
298
|
+
// path segment.
|
|
299
|
+
const isPrivate = data._isPrivate
|
|
300
|
+
|| canvasId.split('/').includes('drafts')
|
|
265
301
|
const entry = {
|
|
266
302
|
name: meta?.title || data.title || canvasId,
|
|
267
303
|
dirName: canvasId,
|
|
@@ -269,6 +305,7 @@ export function buildPrototypeIndex(knownRoutes = []) {
|
|
|
269
305
|
route: data._route || `/canvas/${canvasId}`,
|
|
270
306
|
folder: data._folder || null,
|
|
271
307
|
isCanvas: true,
|
|
308
|
+
isPrivate,
|
|
272
309
|
author: meta?.author || data.author || null,
|
|
273
310
|
gitAuthor: data.gitAuthor || null,
|
|
274
311
|
_canvasMeta: meta || null,
|
|
@@ -287,8 +324,10 @@ export function buildPrototypeIndex(knownRoutes = []) {
|
|
|
287
324
|
if (key) orderMap.set(key, idx)
|
|
288
325
|
})
|
|
289
326
|
entry.pages.sort((a, b) => {
|
|
290
|
-
const
|
|
291
|
-
const
|
|
327
|
+
const aKey = a.id ?? a.name
|
|
328
|
+
const bKey = b.id ?? b.name
|
|
329
|
+
const aIdx = orderMap.has(aKey) ? orderMap.get(aKey) : Infinity
|
|
330
|
+
const bIdx = orderMap.has(bKey) ? orderMap.get(bKey) : Infinity
|
|
292
331
|
return aIdx - bIdx
|
|
293
332
|
})
|
|
294
333
|
}
|
|
@@ -305,6 +344,7 @@ export function buildPrototypeIndex(knownRoutes = []) {
|
|
|
305
344
|
dirName: canvas.folder,
|
|
306
345
|
description: null,
|
|
307
346
|
icon: null,
|
|
347
|
+
isPrivate: false,
|
|
308
348
|
prototypes: [],
|
|
309
349
|
canvases: [canvas],
|
|
310
350
|
}
|
|
@@ -78,11 +78,16 @@ function scanDirectory(root, watchEntry, config) {
|
|
|
78
78
|
|
|
79
79
|
/**
|
|
80
80
|
* Compute the route path for a prototype file (relative to src/prototypes/).
|
|
81
|
-
* Mirrors the route regex in src/routes.jsx
|
|
81
|
+
* Mirrors the route regex in src/routes.jsx — strips `.folder/` segments,
|
|
82
|
+
* `drafts/` segments (scratch dirs share the public URL of their non-draft
|
|
83
|
+
* sibling), and trailing file extension.
|
|
82
84
|
*/
|
|
83
85
|
function prototypeRoute(relPath) {
|
|
84
86
|
let route = relPath
|
|
85
87
|
.replace(/[^/]*\.folder\//g, '')
|
|
88
|
+
.split('/')
|
|
89
|
+
.filter((seg) => seg !== 'drafts')
|
|
90
|
+
.join('/')
|
|
86
91
|
.replace(/\.(jsx|tsx|mdx)$/, '')
|
|
87
92
|
.replace(/\/index$/, '')
|
|
88
93
|
|
|
@@ -92,12 +97,14 @@ function prototypeRoute(relPath) {
|
|
|
92
97
|
|
|
93
98
|
/**
|
|
94
99
|
* Compute the route path for a canvas file (relative to src/canvas/).
|
|
95
|
-
* Uses toCanvasId() for proper folder handling
|
|
100
|
+
* Uses toCanvasId() for proper folder handling, then strips `drafts/`
|
|
101
|
+
* segments so moves in/out of `drafts/` are no-ops.
|
|
96
102
|
*/
|
|
97
103
|
function canvasRoute(relPath) {
|
|
98
104
|
// relPath is relative to src/canvas/, prepend prefix for toCanvasId()
|
|
99
105
|
const canvasId = toCanvasId('src/canvas/' + relPath)
|
|
100
|
-
|
|
106
|
+
const stripped = canvasId.split('/').filter((seg) => seg !== 'drafts').join('/')
|
|
107
|
+
return '/canvas/' + stripped
|
|
101
108
|
}
|
|
102
109
|
|
|
103
110
|
function computeRoute(relPath, watchType) {
|
|
@@ -6,9 +6,10 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { useState, useEffect, useRef, useMemo, useCallback, useSyncExternalStore } from 'react'
|
|
8
8
|
import { buildPrototypeIndex, listStories, getStoryData, BranchSelect, getCustomerModeConfig } from '../core/index.js'
|
|
9
|
-
import { MarkGithubIcon, GitBranchIcon, ChevronDownIcon, ChevronRightIcon, PlusIcon, StarIcon, StarFillIcon, ThreeBarsIcon, XIcon, StackIcon, TrashIcon, ShieldLockIcon, KebabHorizontalIcon, PencilIcon } from '@primer/octicons-react'
|
|
9
|
+
import { MarkGithubIcon, GitBranchIcon, ChevronDownIcon, ChevronRightIcon, PlusIcon, StarIcon, StarFillIcon, ThreeBarsIcon, XIcon, StackIcon, TrashIcon, ShieldLockIcon, KebabHorizontalIcon, PencilIcon, EyeClosedIcon } from '@primer/octicons-react'
|
|
10
10
|
import { Menu } from '@base-ui/react/menu'
|
|
11
11
|
import { Dialog } from '@base-ui/react/dialog'
|
|
12
|
+
import { Tooltip } from '@primer/react'
|
|
12
13
|
import Icon from './Icon.jsx'
|
|
13
14
|
import { useBranches } from './BranchBar/useBranches.js'
|
|
14
15
|
import css from './Viewfinder.module.css'
|
|
@@ -467,9 +468,23 @@ function ArtifactCard({ item, basePath, starred, onToggleStar, onItemDeleted })
|
|
|
467
468
|
|
|
468
469
|
return (
|
|
469
470
|
<>
|
|
470
|
-
<Tag className={css.card} {...linkProps} onClick={handleClick}>
|
|
471
|
+
<Tag className={`${css.card}${item.isPrivate ? ' ' + css.cardPrivate : ''}`} {...linkProps} onClick={handleClick}>
|
|
471
472
|
<div className={css.cardHeader}>
|
|
472
|
-
<
|
|
473
|
+
<div className={css.cardBadgeGroup}>
|
|
474
|
+
<span className={css.cardBadge}>{getTypeLabel(item.type)}</span>
|
|
475
|
+
{item.isPrivate && (
|
|
476
|
+
<Tooltip text={`${typeLabel} not published, only visible to you`} direction="n">
|
|
477
|
+
<button
|
|
478
|
+
type="button"
|
|
479
|
+
className={css.cardPrivateBadge}
|
|
480
|
+
aria-label={`Private ${typeLabel.toLowerCase()}`}
|
|
481
|
+
onClick={(e) => { e.preventDefault(); e.stopPropagation() }}
|
|
482
|
+
>
|
|
483
|
+
<EyeClosedIcon size={12} />
|
|
484
|
+
</button>
|
|
485
|
+
</Tooltip>
|
|
486
|
+
)}
|
|
487
|
+
</div>
|
|
473
488
|
<div className={css.cardActions}>
|
|
474
489
|
<StarBtn active={starred} onClick={() => onToggleStar(item.id)} inline />
|
|
475
490
|
{item.flows?.length > 1 && <FlowsDropdown flows={item.flows} basePath={basePath} />}
|
|
@@ -609,8 +624,12 @@ function PagesDropdown({ pages, basePath }) {
|
|
|
609
624
|
<a
|
|
610
625
|
href={withBase(basePath, page.route)}
|
|
611
626
|
className={css.flowsItemLink}
|
|
627
|
+
title={page.isPrivate ? 'Local-only page — gitignored, dev-only' : undefined}
|
|
612
628
|
>
|
|
613
629
|
{page.name}
|
|
630
|
+
{page.isPrivate && (
|
|
631
|
+
<EyeClosedIcon size={12} className={css.flowsItemPrivateIcon} />
|
|
632
|
+
)}
|
|
614
633
|
</a>
|
|
615
634
|
</Menu.Item>
|
|
616
635
|
))}
|
|
@@ -624,17 +643,36 @@ function PagesDropdown({ pages, basePath }) {
|
|
|
624
643
|
/* ─── Folder Section ─── */
|
|
625
644
|
|
|
626
645
|
function FolderSection({ folder, collapsed, onToggle, basePath, starred, onToggleStar, onItemDeleted }) {
|
|
646
|
+
const sectionClass = collapsed ? css.folderSectionCollapsed : css.folderSection
|
|
627
647
|
return (
|
|
628
|
-
<section className={
|
|
629
|
-
<
|
|
648
|
+
<section className={`${sectionClass}${folder.isPrivate ? ' ' + css.folderSectionPrivate : ''}`}>
|
|
649
|
+
<div
|
|
650
|
+
className={css.folderHeader}
|
|
651
|
+
role="button"
|
|
652
|
+
tabIndex={0}
|
|
653
|
+
onClick={onToggle}
|
|
654
|
+
onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onToggle() } }}
|
|
655
|
+
>
|
|
630
656
|
<Icon name={collapsed ? 'folder' : 'folder-open'} size={16} className={css.folderIcon} />
|
|
631
657
|
<span className={css.folderName}>{folder.name}</span>
|
|
658
|
+
{folder.isPrivate && (
|
|
659
|
+
<Tooltip text="Folder not published, only visible to you" direction="n">
|
|
660
|
+
<button
|
|
661
|
+
type="button"
|
|
662
|
+
className={css.folderPrivateBadge}
|
|
663
|
+
aria-label="Private folder"
|
|
664
|
+
onClick={(e) => { e.preventDefault(); e.stopPropagation() }}
|
|
665
|
+
>
|
|
666
|
+
<EyeClosedIcon size={12} />
|
|
667
|
+
</button>
|
|
668
|
+
</Tooltip>
|
|
669
|
+
)}
|
|
632
670
|
<span className={css.folderCount}>{folder.items.length}</span>
|
|
633
671
|
<ChevronRightIcon
|
|
634
672
|
size={14}
|
|
635
673
|
className={collapsed ? css.folderChevron : css.folderChevronExpanded}
|
|
636
674
|
/>
|
|
637
|
-
</
|
|
675
|
+
</div>
|
|
638
676
|
{!collapsed && (
|
|
639
677
|
<div className={css.grid}>
|
|
640
678
|
{folder.items.map(item => (
|
|
@@ -1164,6 +1202,7 @@ function WorkspaceImpl({
|
|
|
1164
1202
|
folder: proto.folder,
|
|
1165
1203
|
description: proto.description,
|
|
1166
1204
|
flows: proto.flows || [],
|
|
1205
|
+
isPrivate: !!proto.isPrivate,
|
|
1167
1206
|
})
|
|
1168
1207
|
}
|
|
1169
1208
|
|
|
@@ -1187,6 +1226,7 @@ function WorkspaceImpl({
|
|
|
1187
1226
|
folder: canvas.folder,
|
|
1188
1227
|
description: canvas.description,
|
|
1189
1228
|
pages: canvas.pages || null,
|
|
1229
|
+
isPrivate: !!canvas.isPrivate,
|
|
1190
1230
|
})
|
|
1191
1231
|
}
|
|
1192
1232
|
|
|
@@ -1200,9 +1240,10 @@ function WorkspaceImpl({
|
|
|
1200
1240
|
for (const name of storyNames) {
|
|
1201
1241
|
const data = getStoryData(name)
|
|
1202
1242
|
if (!data) continue
|
|
1243
|
+
const displayName = (name.split('/').pop() || name).replace(/^~/, '')
|
|
1203
1244
|
items.push({
|
|
1204
1245
|
id: `component:${name}`,
|
|
1205
|
-
name:
|
|
1246
|
+
name: displayName.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' '),
|
|
1206
1247
|
type: 'component',
|
|
1207
1248
|
author: null,
|
|
1208
1249
|
gitAuthor: null,
|
|
@@ -1212,6 +1253,7 @@ function WorkspaceImpl({
|
|
|
1212
1253
|
externalUrl: null,
|
|
1213
1254
|
folder: null,
|
|
1214
1255
|
description: null,
|
|
1256
|
+
isPrivate: !!data._isPrivate || name.split('/').includes('drafts'),
|
|
1215
1257
|
})
|
|
1216
1258
|
}
|
|
1217
1259
|
|
|
@@ -1298,6 +1340,7 @@ function WorkspaceImpl({
|
|
|
1298
1340
|
const folders = Object.entries(folderItems).map(([dirName, fItems]) => ({
|
|
1299
1341
|
dirName,
|
|
1300
1342
|
name: folderMeta[dirName]?.name || dirName,
|
|
1343
|
+
isPrivate: !!folderMeta[dirName]?.isPrivate || dirName === 'drafts',
|
|
1301
1344
|
items: fItems,
|
|
1302
1345
|
}))
|
|
1303
1346
|
folders.sort((a, b) => {
|
|
@@ -1357,7 +1400,13 @@ function WorkspaceImpl({
|
|
|
1357
1400
|
>
|
|
1358
1401
|
{sidebarOpen ? <XIcon size={18} /> : <ThreeBarsIcon size={18} />}
|
|
1359
1402
|
</button>
|
|
1360
|
-
<
|
|
1403
|
+
<img
|
|
1404
|
+
src={`${basePath || '/'}favicon.svg`}
|
|
1405
|
+
alt=""
|
|
1406
|
+
width={48}
|
|
1407
|
+
height={48}
|
|
1408
|
+
className={css.logo}
|
|
1409
|
+
/>
|
|
1361
1410
|
<div>
|
|
1362
1411
|
<div className={css.appName}>{title}</div>
|
|
1363
1412
|
{subtitle && <div className={css.appSubtitle}>{subtitle}</div>}
|
|
@@ -30,13 +30,8 @@
|
|
|
30
30
|
.logo {
|
|
31
31
|
width: 48px;
|
|
32
32
|
height: 48px;
|
|
33
|
-
background: var(--bgColor-emphasis, #313131);
|
|
34
|
-
border-radius: 8px;
|
|
35
33
|
transform: rotate(-1deg);
|
|
36
|
-
display:
|
|
37
|
-
align-items: center;
|
|
38
|
-
justify-content: center;
|
|
39
|
-
color: var(--fgColor-onEmphasis, #fff);
|
|
34
|
+
display: block;
|
|
40
35
|
flex-shrink: 0;
|
|
41
36
|
}
|
|
42
37
|
|
|
@@ -642,6 +637,14 @@
|
|
|
642
637
|
color: inherit;
|
|
643
638
|
}
|
|
644
639
|
|
|
640
|
+
.cardPrivate {
|
|
641
|
+
opacity: 0.8;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
.cardPrivate:hover {
|
|
645
|
+
opacity: 1;
|
|
646
|
+
}
|
|
647
|
+
|
|
645
648
|
.cardThumb {
|
|
646
649
|
height: 140px;
|
|
647
650
|
background: var(--bgColor-muted, #f5f5f5);
|
|
@@ -663,6 +666,22 @@
|
|
|
663
666
|
color: var(--fgColor-muted, #555);
|
|
664
667
|
}
|
|
665
668
|
|
|
669
|
+
.cardPrivateBadge {
|
|
670
|
+
display: inline-flex;
|
|
671
|
+
align-items: center;
|
|
672
|
+
justify-content: center;
|
|
673
|
+
color: var(--fgColor-muted, #656d76);
|
|
674
|
+
background: none;
|
|
675
|
+
border: none;
|
|
676
|
+
padding: 2px;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
.cardBadgeGroup {
|
|
680
|
+
display: flex;
|
|
681
|
+
align-items: center;
|
|
682
|
+
gap: 6px;
|
|
683
|
+
}
|
|
684
|
+
|
|
666
685
|
/* Icon buttons (star, flows, etc.) */
|
|
667
686
|
|
|
668
687
|
.iconBtn {
|
|
@@ -1036,6 +1055,25 @@
|
|
|
1036
1055
|
flex: 1;
|
|
1037
1056
|
}
|
|
1038
1057
|
|
|
1058
|
+
.folderSectionPrivate {
|
|
1059
|
+
opacity: 0.8;
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
.folderSectionPrivate:hover {
|
|
1063
|
+
opacity: 1;
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
.folderPrivateBadge {
|
|
1067
|
+
display: inline-flex;
|
|
1068
|
+
align-items: center;
|
|
1069
|
+
justify-content: center;
|
|
1070
|
+
color: var(--fgColor-muted, #656d76);
|
|
1071
|
+
background: none;
|
|
1072
|
+
border: none;
|
|
1073
|
+
padding: 2px;
|
|
1074
|
+
margin-right: 6px;
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1039
1077
|
.folderCount {
|
|
1040
1078
|
font-size: 14px;
|
|
1041
1079
|
font-weight: 400;
|
|
@@ -1119,11 +1157,19 @@
|
|
|
1119
1157
|
}
|
|
1120
1158
|
|
|
1121
1159
|
.flowsItemLink {
|
|
1122
|
-
display:
|
|
1160
|
+
display: flex;
|
|
1161
|
+
align-items: center;
|
|
1162
|
+
gap: 6px;
|
|
1123
1163
|
color: inherit;
|
|
1124
1164
|
text-decoration: none;
|
|
1125
1165
|
}
|
|
1126
1166
|
|
|
1167
|
+
.flowsItemPrivateIcon {
|
|
1168
|
+
color: var(--fgColor-muted, #656d76);
|
|
1169
|
+
flex-shrink: 0;
|
|
1170
|
+
margin-left: auto;
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1127
1173
|
/* Avatar stack */
|
|
1128
1174
|
|
|
1129
1175
|
.avatarStack {
|
|
@@ -1636,6 +1636,15 @@ export default function CanvasPage({ canvasId: canvasIdProp, name, siblingPages
|
|
|
1636
1636
|
: [],
|
|
1637
1637
|
createdAt: snapshot.createdAt ?? null,
|
|
1638
1638
|
updatedAt: snapshot.updatedAt ?? null,
|
|
1639
|
+
// PR-specific metadata (state, branches, diff stats)
|
|
1640
|
+
state: snapshot.state ?? null,
|
|
1641
|
+
merged: typeof snapshot.merged === 'boolean' ? snapshot.merged : null,
|
|
1642
|
+
draft: typeof snapshot.draft === 'boolean' ? snapshot.draft : null,
|
|
1643
|
+
baseRef: typeof snapshot.baseRef === 'string' ? snapshot.baseRef : null,
|
|
1644
|
+
headRef: typeof snapshot.headRef === 'string' ? snapshot.headRef : null,
|
|
1645
|
+
additions: typeof snapshot.additions === 'number' ? snapshot.additions : null,
|
|
1646
|
+
deletions: typeof snapshot.deletions === 'number' ? snapshot.deletions : null,
|
|
1647
|
+
changedFiles: typeof snapshot.changedFiles === 'number' ? snapshot.changedFiles : null,
|
|
1639
1648
|
fetchedAt: new Date().toISOString(),
|
|
1640
1649
|
},
|
|
1641
1650
|
}
|
|
@@ -497,11 +497,25 @@ export default function PageSelector({ currentName, pages: initialPages, isLocal
|
|
|
497
497
|
) : (
|
|
498
498
|
<a
|
|
499
499
|
href={getPageHref(page)}
|
|
500
|
-
className={styles.itemLink}
|
|
500
|
+
className={`${styles.itemLink} ${page.isPrivate ? styles.itemPrivate : ''}`}
|
|
501
501
|
onClick={(e) => handleItemClick(page, e)}
|
|
502
502
|
onDoubleClick={(e) => e.stopPropagation()}
|
|
503
|
+
title={page.isPrivate ? 'Local-only page — gitignored, dev-only' : undefined}
|
|
503
504
|
>
|
|
504
505
|
<span className={styles.itemContent}>{page.title}</span>
|
|
506
|
+
{page.isPrivate && (
|
|
507
|
+
<svg
|
|
508
|
+
className={styles.privateIcon}
|
|
509
|
+
width="14"
|
|
510
|
+
height="14"
|
|
511
|
+
viewBox="0 0 16 16"
|
|
512
|
+
fill="currentColor"
|
|
513
|
+
aria-hidden="true"
|
|
514
|
+
>
|
|
515
|
+
<path d="M.143 2.31a.75.75 0 0 1 1.047-.167l14.5 10.5a.75.75 0 1 1-.88 1.214l-2.248-1.628C11.346 13.19 9.792 14 8 14c-1.981 0-3.67-.992-4.933-2.078C1.797 10.832.88 9.577.43 8.9a1.62 1.62 0 0 1 0-1.797c.353-.533 1.068-1.495 2.062-2.42L.31 3.357A.75.75 0 0 1 .143 2.31Zm1.536 5.622A.12.12 0 0 0 1.659 8c0 .021.006.045.02.068.387.582 1.211 1.703 2.31 2.646C5.1 11.668 6.42 12.5 8 12.5c1.286 0 2.413-.55 3.359-1.252L9.81 10.13a2.5 2.5 0 0 1-3.498-3.498L4.388 5.226c-1.014.91-1.749 1.886-2.108 2.426Zm6.728 1.42-1.668-1.208a1 1 0 0 0 1.36 1.207Z" />
|
|
516
|
+
<path d="M8 3.5c-.387 0-.74.057-1.063.145L5.793 2.503A7.84 7.84 0 0 1 8 2.207c1.981 0 3.67.992 4.933 2.078 1.27 1.09 2.187 2.345 2.637 3.022a1.62 1.62 0 0 1 0 1.795c-.247.371-.622.86-1.118 1.384L13.39 9.4c.402-.43.706-.83.91-1.137a.121.121 0 0 0 .012-.022.122.122 0 0 0-.012-.023c-.387-.582-1.211-1.703-2.31-2.646C10.9 4.733 9.58 3.9 8 3.9Zm.482 2.067-.044-.011a.748.748 0 0 0 .044.011Zm-.482 0c-.272 0-.5.228-.5.5 0 .272.228.5.5.5h.022l1.024.741A2.5 2.5 0 0 0 8 5.5Z" />
|
|
517
|
+
</svg>
|
|
518
|
+
)}
|
|
505
519
|
</a>
|
|
506
520
|
)}
|
|
507
521
|
{!isEditing && isLocalDev && (
|
|
@@ -166,6 +166,20 @@
|
|
|
166
166
|
background: var(--bgColor-accent-muted, #ddf4ff);
|
|
167
167
|
}
|
|
168
168
|
|
|
169
|
+
.itemPrivate {
|
|
170
|
+
opacity: 0.55;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.itemPrivate:hover {
|
|
174
|
+
opacity: 1;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
.privateIcon {
|
|
178
|
+
color: var(--fgColor-muted, #656d76);
|
|
179
|
+
flex-shrink: 0;
|
|
180
|
+
margin-left: 4px;
|
|
181
|
+
}
|
|
182
|
+
|
|
169
183
|
.separator {
|
|
170
184
|
height: 1px;
|
|
171
185
|
background: var(--borderColor-default, rgba(0, 0, 0, 0.15));
|
|
@@ -48,32 +48,41 @@ export default function FrozenTerminalOverlay({ widgetId, onActivate }) {
|
|
|
48
48
|
|
|
49
49
|
async function fetchSnapshot() {
|
|
50
50
|
const baseUrl = getBaseUrl()
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
if (!res.ok) continue
|
|
51
|
+
// Try the dev-server REST endpoint first, then fall back to the static
|
|
52
|
+
// consolidated index (production builds / no server).
|
|
53
|
+
const restUrl = `${baseUrl}_storyboard/canvas/terminal-snapshot/${widgetId}`
|
|
54
|
+
const indexUrl = `${baseUrl}_storyboard/terminal-snapshots/agents.snapshot.json`
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
const res = await fetch(restUrl)
|
|
58
|
+
if (res.ok) {
|
|
60
59
|
const data = await res.json()
|
|
61
60
|
if (cancelled) return
|
|
62
61
|
const text = data.paneContent || data.content || data.output || ''
|
|
63
|
-
if (
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
} else {
|
|
70
|
-
setPlainText(stripAnsi(text))
|
|
62
|
+
if (text) {
|
|
63
|
+
const converter = await getConverter()
|
|
64
|
+
if (cancelled) return
|
|
65
|
+
if (converter) setHtml(converter.toHtml(text))
|
|
66
|
+
else setPlainText(stripAnsi(text))
|
|
67
|
+
return
|
|
71
68
|
}
|
|
72
|
-
return
|
|
73
|
-
} catch {
|
|
74
|
-
continue
|
|
75
69
|
}
|
|
76
|
-
}
|
|
70
|
+
} catch { /* fall through to static index */ }
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
const res = await fetch(indexUrl)
|
|
74
|
+
if (!res.ok) return
|
|
75
|
+
const data = await res.json()
|
|
76
|
+
if (cancelled) return
|
|
77
|
+
const entry = data?.agents?.[widgetId]
|
|
78
|
+
if (!entry) return
|
|
79
|
+
const text = entry.paneContent || entry.content || entry.output || ''
|
|
80
|
+
if (!text) return
|
|
81
|
+
const converter = await getConverter()
|
|
82
|
+
if (cancelled) return
|
|
83
|
+
if (converter) setHtml(converter.toHtml(text))
|
|
84
|
+
else setPlainText(stripAnsi(text))
|
|
85
|
+
} catch { /* empty */ }
|
|
77
86
|
}
|
|
78
87
|
|
|
79
88
|
fetchSnapshot()
|
|
@@ -54,13 +54,41 @@ export default forwardRef(function PrototypeEmbed({ id: widgetId, props, onUpdat
|
|
|
54
54
|
|
|
55
55
|
const basePath = (import.meta.env.BASE_URL || '/').replace(/\/$/, '')
|
|
56
56
|
const baseSegment = basePath.replace(/^\//, '')
|
|
57
|
+
// Internal prototype iframes load the isolated prototypes.html entry
|
|
58
|
+
// (see .agents/plans/vite-isolation.md). prototypes.html uses
|
|
59
|
+
// createHashRouter so the prototype route lives in the URL hash —
|
|
60
|
+
// any path /MyProto/SignupForm becomes prototypes.html#/MyProto/SignupForm.
|
|
61
|
+
// External http(s) URLs are left alone.
|
|
57
62
|
const rawSrc = useMemo(() => {
|
|
58
63
|
if (!src) return ''
|
|
59
64
|
if (/^https?:\/\//.test(src)) return src
|
|
60
65
|
const cleaned = src.replace(/^\/branch--[^/]+/, '')
|
|
61
|
-
|
|
62
|
-
if (baseSegment && cleaned.startsWith(
|
|
63
|
-
|
|
66
|
+
let normalized
|
|
67
|
+
if (baseSegment && cleaned.startsWith(basePath)) normalized = cleaned
|
|
68
|
+
else if (baseSegment && cleaned.startsWith(baseSegment)) normalized = `/${cleaned}`
|
|
69
|
+
else normalized = `${basePath}${cleaned}`
|
|
70
|
+
// Strip basePath so we can split path/query/hash and re-anchor on
|
|
71
|
+
// prototypes.html. Any pre-existing #hash on the prototype URL is
|
|
72
|
+
// preserved by appending it after the route hash with an extra `#`,
|
|
73
|
+
// so the inner router still parses /MyProto correctly.
|
|
74
|
+
const withoutBase = baseSegment && normalized.startsWith(basePath)
|
|
75
|
+
? normalized.slice(basePath.length) || '/'
|
|
76
|
+
: normalized
|
|
77
|
+
const hashIdx = withoutBase.indexOf('#')
|
|
78
|
+
const innerHash = hashIdx >= 0 ? withoutBase.slice(hashIdx + 1) : ''
|
|
79
|
+
const pathAndQuery = hashIdx >= 0 ? withoutBase.slice(0, hashIdx) : withoutBase
|
|
80
|
+
const routeHash = pathAndQuery.startsWith('/') ? pathAndQuery : `/${pathAndQuery}`
|
|
81
|
+
const suffix = innerHash ? `#${innerHash}` : ''
|
|
82
|
+
// Extract the prototype name (first path segment) so the dev iframe can
|
|
83
|
+
// narrow its router to just that prototype's subtree — broken siblings
|
|
84
|
+
// never appear in the matched lazy() chain. The consumer's
|
|
85
|
+
// prototypes-entry.jsx reads ?proto= and calls getRoutesForProto(); if
|
|
86
|
+
// it doesn't (older scaffold), the param is harmlessly ignored and the
|
|
87
|
+
// full route tree loads. Prod builds skip the param entirely.
|
|
88
|
+
const pathOnly = pathAndQuery.split('?')[0]
|
|
89
|
+
const protoName = pathOnly.split('/').filter(Boolean)[0] || ''
|
|
90
|
+
const queryStr = (import.meta.env.DEV && protoName) ? `?proto=${encodeURIComponent(protoName)}` : ''
|
|
91
|
+
return `${basePath}/prototypes.html${queryStr}#${routeHash}${suffix}`
|
|
64
92
|
}, [src, basePath, baseSegment])
|
|
65
93
|
|
|
66
94
|
const scale = zoom / 100
|
|
@@ -31,11 +31,23 @@ function resolveStorySetUrl(storyId, layout, selected, density, theme) {
|
|
|
31
31
|
const story = getStoryData(storyId)
|
|
32
32
|
if (!story?._storyModule) return ''
|
|
33
33
|
const base = (import.meta.env.BASE_URL || '/').replace(/\/$/, '')
|
|
34
|
+
|
|
35
|
+
// In dev, prefer the dedicated isolate-set middleware: it serves a
|
|
36
|
+
// minimal HTML shell + dynamic-imports just this one story module, so a
|
|
37
|
+
// broken story cannot poison the canvas SPA's module graph. The
|
|
38
|
+
// middleware doesn't exist in deployed builds, so production falls
|
|
39
|
+
// through to the real story page with `_sb_component_set`.
|
|
40
|
+
if (import.meta.env.DEV) {
|
|
41
|
+
const params = new URLSearchParams()
|
|
42
|
+
params.set('module', story._storyModule)
|
|
43
|
+
if (layout) params.set('layout', layout)
|
|
44
|
+
if (selected) params.set('selected', selected)
|
|
45
|
+
if (density) params.set('density', density)
|
|
46
|
+
if (theme) params.set('theme', theme)
|
|
47
|
+
return `${base}/_storyboard/canvas/isolate-set?${params}`
|
|
48
|
+
}
|
|
49
|
+
|
|
34
50
|
const params = new URLSearchParams()
|
|
35
|
-
// Route via the real story page (works in dev AND prod). The dev-only
|
|
36
|
-
// `_storyboard/canvas/isolate-set` middleware doesn't exist in deployed
|
|
37
|
-
// builds, so we mount ComponentSetPage at the story's route with
|
|
38
|
-
// `_sb_component_set` instead. `_sb_embed` keeps the canvas chrome off.
|
|
39
51
|
params.set('_sb_embed', '')
|
|
40
52
|
params.set('_sb_component_set', '')
|
|
41
53
|
if (layout) params.set('layout', layout)
|
|
@@ -55,26 +55,24 @@ export default function TerminalReadWidget({ id, props }) {
|
|
|
55
55
|
const canvasId = getCanvasId()
|
|
56
56
|
if (!canvasId) { setFailed(true); return }
|
|
57
57
|
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
`${baseUrl}_storyboard/terminal-snapshots/${id}.snapshot.json`,
|
|
68
|
-
`${baseUrl}_storyboard/terminal-snapshots/${canvasId.replace(/\//g, '--')}/${id}.json`,
|
|
69
|
-
]
|
|
70
|
-
|
|
71
|
-
for (const url of urls) {
|
|
58
|
+
const indexUrl = `${baseUrl}_storyboard/terminal-snapshots/agents.snapshot.json`
|
|
59
|
+
const legacyUrl = `${baseUrl}_storyboard/terminal-snapshots/${canvasId.replace(/\//g, '--')}/${id}.json`
|
|
60
|
+
const restUrl = `${baseUrl}_storyboard/canvas/terminal-snapshot/${id}`
|
|
61
|
+
|
|
62
|
+
const tryUrls = isProduction()
|
|
63
|
+
? [{ url: indexUrl, fromIndex: true }, { url: legacyUrl, fromIndex: false }]
|
|
64
|
+
: [{ url: restUrl, fromIndex: false }, { url: indexUrl, fromIndex: true }, { url: legacyUrl, fromIndex: false }]
|
|
65
|
+
|
|
66
|
+
for (const { url, fromIndex } of tryUrls) {
|
|
72
67
|
try {
|
|
73
68
|
const res = await fetch(url)
|
|
74
69
|
if (!res.ok) continue
|
|
75
70
|
const data = await res.json()
|
|
76
71
|
if (cancelled) return
|
|
77
|
-
const
|
|
72
|
+
const entry = fromIndex ? data?.agents?.[id] : data
|
|
73
|
+
if (!entry) continue
|
|
74
|
+
const text = entry.paneContent || entry.content || entry.output || ''
|
|
75
|
+
if (!text) continue
|
|
78
76
|
setContent(text)
|
|
79
77
|
|
|
80
78
|
const converter = await getConverter()
|