@dfosco/storyboard 0.6.4 → 0.6.6
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 +4 -6
- 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 +55 -14
- package/src/internals/Viewfinder.module.css +58 -2
- 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/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
|
@@ -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 => (
|
|
@@ -1093,6 +1131,8 @@ function WorkspaceImpl({
|
|
|
1093
1131
|
basePath,
|
|
1094
1132
|
title = 'Storyboard',
|
|
1095
1133
|
subtitle,
|
|
1134
|
+
logo,
|
|
1135
|
+
logoIcon = 'iconoir/key-command',
|
|
1096
1136
|
showAllArtifacts = false,
|
|
1097
1137
|
showPrototypes = true,
|
|
1098
1138
|
showCanvases = true,
|
|
@@ -1164,6 +1204,7 @@ function WorkspaceImpl({
|
|
|
1164
1204
|
folder: proto.folder,
|
|
1165
1205
|
description: proto.description,
|
|
1166
1206
|
flows: proto.flows || [],
|
|
1207
|
+
isPrivate: !!proto.isPrivate,
|
|
1167
1208
|
})
|
|
1168
1209
|
}
|
|
1169
1210
|
|
|
@@ -1187,6 +1228,7 @@ function WorkspaceImpl({
|
|
|
1187
1228
|
folder: canvas.folder,
|
|
1188
1229
|
description: canvas.description,
|
|
1189
1230
|
pages: canvas.pages || null,
|
|
1231
|
+
isPrivate: !!canvas.isPrivate,
|
|
1190
1232
|
})
|
|
1191
1233
|
}
|
|
1192
1234
|
|
|
@@ -1200,9 +1242,10 @@ function WorkspaceImpl({
|
|
|
1200
1242
|
for (const name of storyNames) {
|
|
1201
1243
|
const data = getStoryData(name)
|
|
1202
1244
|
if (!data) continue
|
|
1245
|
+
const displayName = (name.split('/').pop() || name).replace(/^~/, '')
|
|
1203
1246
|
items.push({
|
|
1204
1247
|
id: `component:${name}`,
|
|
1205
|
-
name:
|
|
1248
|
+
name: displayName.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' '),
|
|
1206
1249
|
type: 'component',
|
|
1207
1250
|
author: null,
|
|
1208
1251
|
gitAuthor: null,
|
|
@@ -1212,6 +1255,7 @@ function WorkspaceImpl({
|
|
|
1212
1255
|
externalUrl: null,
|
|
1213
1256
|
folder: null,
|
|
1214
1257
|
description: null,
|
|
1258
|
+
isPrivate: !!data._isPrivate || name.split('/').includes('drafts'),
|
|
1215
1259
|
})
|
|
1216
1260
|
}
|
|
1217
1261
|
|
|
@@ -1298,6 +1342,7 @@ function WorkspaceImpl({
|
|
|
1298
1342
|
const folders = Object.entries(folderItems).map(([dirName, fItems]) => ({
|
|
1299
1343
|
dirName,
|
|
1300
1344
|
name: folderMeta[dirName]?.name || dirName,
|
|
1345
|
+
isPrivate: !!folderMeta[dirName]?.isPrivate || dirName === 'drafts',
|
|
1301
1346
|
items: fItems,
|
|
1302
1347
|
}))
|
|
1303
1348
|
folders.sort((a, b) => {
|
|
@@ -1357,13 +1402,9 @@ function WorkspaceImpl({
|
|
|
1357
1402
|
>
|
|
1358
1403
|
{sidebarOpen ? <XIcon size={18} /> : <ThreeBarsIcon size={18} />}
|
|
1359
1404
|
</button>
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
width={48}
|
|
1364
|
-
height={48}
|
|
1365
|
-
className={css.logo}
|
|
1366
|
-
/>
|
|
1405
|
+
{logo
|
|
1406
|
+
? <div className={`${css.logo} smooth-corners`}>{logo}</div>
|
|
1407
|
+
: <div className={`${css.logo} smooth-corners`}><Icon name={logoIcon} size={22} color="#fff" /></div>}
|
|
1367
1408
|
<div>
|
|
1368
1409
|
<div className={css.appName}>{title}</div>
|
|
1369
1410
|
{subtitle && <div className={css.appSubtitle}>{subtitle}</div>}
|
|
@@ -30,8 +30,13 @@
|
|
|
30
30
|
.logo {
|
|
31
31
|
width: 48px;
|
|
32
32
|
height: 48px;
|
|
33
|
+
background: var(--bgColor-emphasis, #313131);
|
|
34
|
+
border-radius: 8px;
|
|
33
35
|
transform: rotate(-1deg);
|
|
34
|
-
display:
|
|
36
|
+
display: flex;
|
|
37
|
+
align-items: center;
|
|
38
|
+
justify-content: center;
|
|
39
|
+
color: var(--fgColor-onEmphasis, #fff);
|
|
35
40
|
flex-shrink: 0;
|
|
36
41
|
}
|
|
37
42
|
|
|
@@ -637,6 +642,14 @@
|
|
|
637
642
|
color: inherit;
|
|
638
643
|
}
|
|
639
644
|
|
|
645
|
+
.cardPrivate {
|
|
646
|
+
opacity: 0.8;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
.cardPrivate:hover {
|
|
650
|
+
opacity: 1;
|
|
651
|
+
}
|
|
652
|
+
|
|
640
653
|
.cardThumb {
|
|
641
654
|
height: 140px;
|
|
642
655
|
background: var(--bgColor-muted, #f5f5f5);
|
|
@@ -658,6 +671,22 @@
|
|
|
658
671
|
color: var(--fgColor-muted, #555);
|
|
659
672
|
}
|
|
660
673
|
|
|
674
|
+
.cardPrivateBadge {
|
|
675
|
+
display: inline-flex;
|
|
676
|
+
align-items: center;
|
|
677
|
+
justify-content: center;
|
|
678
|
+
color: var(--fgColor-muted, #656d76);
|
|
679
|
+
background: none;
|
|
680
|
+
border: none;
|
|
681
|
+
padding: 2px;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
.cardBadgeGroup {
|
|
685
|
+
display: flex;
|
|
686
|
+
align-items: center;
|
|
687
|
+
gap: 6px;
|
|
688
|
+
}
|
|
689
|
+
|
|
661
690
|
/* Icon buttons (star, flows, etc.) */
|
|
662
691
|
|
|
663
692
|
.iconBtn {
|
|
@@ -1031,6 +1060,25 @@
|
|
|
1031
1060
|
flex: 1;
|
|
1032
1061
|
}
|
|
1033
1062
|
|
|
1063
|
+
.folderSectionPrivate {
|
|
1064
|
+
opacity: 0.8;
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
.folderSectionPrivate:hover {
|
|
1068
|
+
opacity: 1;
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
.folderPrivateBadge {
|
|
1072
|
+
display: inline-flex;
|
|
1073
|
+
align-items: center;
|
|
1074
|
+
justify-content: center;
|
|
1075
|
+
color: var(--fgColor-muted, #656d76);
|
|
1076
|
+
background: none;
|
|
1077
|
+
border: none;
|
|
1078
|
+
padding: 2px;
|
|
1079
|
+
margin-right: 6px;
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1034
1082
|
.folderCount {
|
|
1035
1083
|
font-size: 14px;
|
|
1036
1084
|
font-weight: 400;
|
|
@@ -1114,11 +1162,19 @@
|
|
|
1114
1162
|
}
|
|
1115
1163
|
|
|
1116
1164
|
.flowsItemLink {
|
|
1117
|
-
display:
|
|
1165
|
+
display: flex;
|
|
1166
|
+
align-items: center;
|
|
1167
|
+
gap: 6px;
|
|
1118
1168
|
color: inherit;
|
|
1119
1169
|
text-decoration: none;
|
|
1120
1170
|
}
|
|
1121
1171
|
|
|
1172
|
+
.flowsItemPrivateIcon {
|
|
1173
|
+
color: var(--fgColor-muted, #656d76);
|
|
1174
|
+
flex-shrink: 0;
|
|
1175
|
+
margin-left: auto;
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1122
1178
|
/* Avatar stack */
|
|
1123
1179
|
|
|
1124
1180
|
.avatarStack {
|
|
@@ -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()
|
|
@@ -149,6 +149,21 @@
|
|
|
149
149
|
top: calc(100% + 10px);
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
+
/* Invisible hit-area that bridges the 10px gap between the widget and the
|
|
153
|
+
toolbar. Keeps the parent .chromeContainer in :hover (and React mouseenter)
|
|
154
|
+
when the pointer crosses the padding, and — crucially — stops the pointer
|
|
155
|
+
from falling through to widgets stacked underneath, so the toolbar of an
|
|
156
|
+
overlapping widget on top stays interactive. */
|
|
157
|
+
.toolbar::before {
|
|
158
|
+
content: '';
|
|
159
|
+
position: absolute;
|
|
160
|
+
left: 0;
|
|
161
|
+
right: 0;
|
|
162
|
+
bottom: 100%;
|
|
163
|
+
height: 10px;
|
|
164
|
+
pointer-events: auto;
|
|
165
|
+
}
|
|
166
|
+
|
|
152
167
|
/* Trigger dot — positioned in the toolbar, visible at rest */
|
|
153
168
|
.triggerDot {
|
|
154
169
|
width: 6px;
|
|
@@ -70,6 +70,7 @@ function getCanvasGroupMap() {
|
|
|
70
70
|
name,
|
|
71
71
|
route,
|
|
72
72
|
title: data?.title || name.split('/').pop(),
|
|
73
|
+
isPrivate: !!data?._isPrivate,
|
|
73
74
|
_canvasMeta: data?._canvasMeta || null,
|
|
74
75
|
})
|
|
75
76
|
}
|
|
@@ -428,7 +429,7 @@ function StoryboardProviderInner({ flowName, sceneName, recordName, recordParam,
|
|
|
428
429
|
const _hmrTick = canvasIndexKey
|
|
429
430
|
const siblingPages = group
|
|
430
431
|
? getCanvasGroupMap().get(group) || []
|
|
431
|
-
: [{ name: canvasId, route: canvasData?._route || `/canvas/${canvasId}`, title: canvasData?.title || canvasId.split('/').pop() }]
|
|
432
|
+
: [{ name: canvasId, route: canvasData?._route || `/canvas/${canvasId}`, title: canvasData?.title || canvasId.split('/').pop(), isPrivate: !!canvasData?._isPrivate }]
|
|
432
433
|
const canvasMeta = canvasData?._canvasMeta || null
|
|
433
434
|
const canvasValue = {
|
|
434
435
|
data: null,
|