@dfosco/storyboard 0.6.4 → 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 +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 +50 -7
- package/src/internals/Viewfinder.module.css +52 -1
- 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 => (
|
|
@@ -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) => {
|
|
@@ -637,6 +637,14 @@
|
|
|
637
637
|
color: inherit;
|
|
638
638
|
}
|
|
639
639
|
|
|
640
|
+
.cardPrivate {
|
|
641
|
+
opacity: 0.8;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
.cardPrivate:hover {
|
|
645
|
+
opacity: 1;
|
|
646
|
+
}
|
|
647
|
+
|
|
640
648
|
.cardThumb {
|
|
641
649
|
height: 140px;
|
|
642
650
|
background: var(--bgColor-muted, #f5f5f5);
|
|
@@ -658,6 +666,22 @@
|
|
|
658
666
|
color: var(--fgColor-muted, #555);
|
|
659
667
|
}
|
|
660
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
|
+
|
|
661
685
|
/* Icon buttons (star, flows, etc.) */
|
|
662
686
|
|
|
663
687
|
.iconBtn {
|
|
@@ -1031,6 +1055,25 @@
|
|
|
1031
1055
|
flex: 1;
|
|
1032
1056
|
}
|
|
1033
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
|
+
|
|
1034
1077
|
.folderCount {
|
|
1035
1078
|
font-size: 14px;
|
|
1036
1079
|
font-weight: 400;
|
|
@@ -1114,11 +1157,19 @@
|
|
|
1114
1157
|
}
|
|
1115
1158
|
|
|
1116
1159
|
.flowsItemLink {
|
|
1117
|
-
display:
|
|
1160
|
+
display: flex;
|
|
1161
|
+
align-items: center;
|
|
1162
|
+
gap: 6px;
|
|
1118
1163
|
color: inherit;
|
|
1119
1164
|
text-decoration: none;
|
|
1120
1165
|
}
|
|
1121
1166
|
|
|
1167
|
+
.flowsItemPrivateIcon {
|
|
1168
|
+
color: var(--fgColor-muted, #656d76);
|
|
1169
|
+
flex-shrink: 0;
|
|
1170
|
+
margin-left: auto;
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1122
1173
|
/* Avatar stack */
|
|
1123
1174
|
|
|
1124
1175
|
.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,
|