@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.
@@ -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
- return '/canvas/' + canvasId
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
- <span className={css.cardBadge}>{getTypeLabel(item.type)}</span>
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={collapsed ? css.folderSectionCollapsed : css.folderSection}>
629
- <button className={css.folderHeader} onClick={onToggle}>
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
- </button>
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: name.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' '),
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: block;
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
- const urls = [
52
- `${baseUrl}_storyboard/canvas/terminal-snapshot/${widgetId}`,
53
- `${baseUrl}_storyboard/terminal-snapshots/${widgetId}.snapshot.json`,
54
- ]
55
-
56
- for (const url of urls) {
57
- try {
58
- const res = await fetch(url)
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 (!text) continue
64
-
65
- const converter = await getConverter()
66
- if (cancelled) return
67
- if (converter) {
68
- setHtml(converter.toHtml(text))
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
- if (baseSegment && cleaned.startsWith(basePath)) return cleaned
62
- if (baseSegment && cleaned.startsWith(baseSegment)) return `/${cleaned}`
63
- return `${basePath}${cleaned}`
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 urls = isProduction()
59
- ? [
60
- // New flat format: <widgetId>.snapshot.json
61
- `${baseUrl}_storyboard/terminal-snapshots/${id}.snapshot.json`,
62
- // Legacy nested format: <canvasDir>/<widgetId>.json
63
- `${baseUrl}_storyboard/terminal-snapshots/${canvasId.replace(/\//g, '--')}/${id}.json`,
64
- ]
65
- : [
66
- `${baseUrl}_storyboard/canvas/terminal-snapshot/${id}`,
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 text = data.paneContent || data.content || data.output || ''
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,