@dfosco/storyboard 0.7.0 → 0.8.1

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.
Files changed (258) hide show
  1. package/assets/favicon.svg +12 -0
  2. package/dist/storyboard-ui.css +1 -1
  3. package/dist/storyboard-ui.js +13907 -10816
  4. package/dist/storyboard-ui.js.map +1 -1
  5. package/dist/tailwind.css +1 -1
  6. package/package.json +138 -127
  7. package/scaffold/AGENTS.md +9 -7
  8. package/scaffold/README.md +18 -0
  9. package/scaffold/deploy.yml +21 -0
  10. package/scaffold/gitignore +8 -14
  11. package/scaffold/preview.yml +40 -0
  12. package/scaffold/{manifest.json → scaffold.config.json} +73 -3
  13. package/scaffold/scripts/aggregate-artifacts.mjs +257 -0
  14. package/scaffold/skills/canvas/SKILL.md +1 -1
  15. package/scaffold/skills/migrate-0.5.0/SKILL.md +1 -1
  16. package/scaffold/skills/ship/SKILL.md +2 -2
  17. package/scaffold/skills/worktree/SKILL.md +1 -1
  18. package/scaffold/src/_prototype.jsx +17 -0
  19. package/scaffold/src/_story.jsx +13 -0
  20. package/scaffold/src/library/README.md +34 -0
  21. package/scaffold/src/library/_app.jsx +53 -0
  22. package/scaffold/src/library/_app.module.css +8 -0
  23. package/scaffold/src/library/create.jsx +10 -0
  24. package/scaffold/src/library/index.jsx +14 -0
  25. package/scaffold/src/library/mount.jsx +52 -0
  26. package/scaffold/src/library/prototypes-entry.jsx +57 -0
  27. package/scaffold/src/library/routes.jsx +205 -0
  28. package/scaffold/src/library/viewfinder.jsx +13 -0
  29. package/scaffold/src/library/workspace.jsx +17 -0
  30. package/scripts/aggregate-artifacts.mjs +257 -0
  31. package/src/core/artifact/operations.js +19 -13
  32. package/src/core/autosync/server.test.js +1 -1
  33. package/src/core/canvas/githubEmbeds.js +12 -0
  34. package/src/core/canvas/githubEmbeds.test.js +71 -2
  35. package/src/core/canvas/hot-pool.js +10 -11
  36. package/src/core/canvas/materializer.js +39 -13
  37. package/src/core/canvas/server.js +324 -69
  38. package/src/core/canvas/server.test.js +365 -2
  39. package/src/core/canvas/terminal-server.js +144 -57
  40. package/src/core/canvas/undoRedo.js +174 -0
  41. package/src/core/canvas/undoRedo.test.js +250 -0
  42. package/src/core/cli/buildManifest.js +34 -0
  43. package/src/core/cli/canvasRedo.js +44 -0
  44. package/src/core/cli/canvasUndo.js +40 -0
  45. package/src/core/cli/create.js +116 -18
  46. package/src/core/cli/dev.js +23 -35
  47. package/src/core/cli/index.js +21 -0
  48. package/src/core/cli/schemas.js +2 -3
  49. package/src/core/cli/serverUrl.js +24 -0
  50. package/src/core/cli/setup.js +14 -25
  51. package/src/core/cli/terminal-welcome.js +12 -6
  52. package/src/core/cli/updateVersion.js +42 -2
  53. package/src/core/cli/workleaf.js +140 -0
  54. package/src/core/comments/api.js +1 -1
  55. package/src/core/comments/queries.js +3 -0
  56. package/src/core/comments/ui/CommentWindow.jsx +3 -2
  57. package/src/core/data/artifactIndex.js +235 -0
  58. package/src/core/data/artifactIndex.test.js +203 -0
  59. package/src/core/data/artifactManifest.js +290 -0
  60. package/src/core/data/artifactManifest.test.js +207 -0
  61. package/src/core/data/viewfinder.js +47 -7
  62. package/src/core/index.js +6 -1
  63. package/src/core/lib/components/ui/avatar/avatar-fallback.jsx +2 -2
  64. package/src/core/lib/components/ui/avatar/avatar-image.jsx +2 -2
  65. package/src/core/lib/components/ui/avatar/avatar.jsx +2 -2
  66. package/src/core/lib/components/ui/checkbox/checkbox.jsx +12 -9
  67. package/src/core/lib/components/ui/dialog/dialog-content.jsx +45 -31
  68. package/src/core/lib/components/ui/dialog/dialog-trigger.jsx +9 -5
  69. package/src/core/lib/components/ui/dialog/dialog.jsx +12 -3
  70. package/src/core/lib/components/ui/dialog/dialog.module.css +118 -0
  71. package/src/core/lib/components/ui/dialog/index.js +4 -34
  72. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-group.jsx +3 -3
  73. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.jsx +19 -9
  74. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-content.jsx +8 -8
  75. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.jsx +2 -2
  76. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-group.jsx +2 -2
  77. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-item.jsx +15 -4
  78. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-portal.jsx +2 -2
  79. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-radio-group.jsx +2 -2
  80. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.jsx +14 -7
  81. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-separator.jsx +2 -2
  82. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.jsx +8 -4
  83. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.jsx +4 -4
  84. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-sub.jsx +2 -2
  85. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-trigger.jsx +8 -2
  86. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu.jsx +2 -2
  87. package/src/core/lib/components/ui/label/label.jsx +1 -2
  88. package/src/core/lib/components/ui/panel/panel-close.jsx +7 -4
  89. package/src/core/lib/components/ui/panel/panel-content.jsx +7 -9
  90. package/src/core/lib/components/ui/panel/panel-title.jsx +3 -3
  91. package/src/core/lib/components/ui/panel/panel.jsx +11 -2
  92. package/src/core/lib/components/ui/separator/separator.jsx +6 -4
  93. package/src/core/lib/components/ui/tooltip/tooltip-content.jsx +7 -6
  94. package/src/core/lib/components/ui/tooltip/tooltip-trigger.jsx +13 -6
  95. package/src/core/lib/components/ui/tooltip/tooltip.jsx +5 -5
  96. package/src/core/mountStoryboardCore.js +234 -74
  97. package/src/core/mountStoryboardCore.test.js +174 -0
  98. package/src/core/rename-watcher/watcher.js +329 -36
  99. package/src/core/rename-watcher/watcher.test.js +253 -0
  100. package/src/core/scaffold/fragments.js +287 -0
  101. package/src/core/scaffold/fragments.test.js +401 -0
  102. package/src/core/scaffold/index.js +291 -0
  103. package/src/core/scaffold/migrateLegacyGitignore.js +157 -0
  104. package/src/core/scaffold/scaffold-integration.test.js +247 -0
  105. package/src/core/scaffold.js +4 -97
  106. package/src/core/stores/configSchema.js +41 -2
  107. package/src/core/stores/recentArtifacts.js +49 -2
  108. package/src/core/stores/recentArtifacts.test.js +59 -6
  109. package/src/core/stores/themeBootstrap.js +264 -0
  110. package/src/core/stores/themeBootstrap.test.js +211 -0
  111. package/src/core/stores/themeStore.test.js +154 -0
  112. package/src/core/stores/themeStore.ts +235 -220
  113. package/src/core/styles/generateThemesCss.js +63 -0
  114. package/src/core/styles/tailwind.css +12 -4
  115. package/src/core/styles/workspace-chrome.css +30 -0
  116. package/src/core/tools/handlers/paletteTheme.js +11 -8
  117. package/src/core/ui/ActionMenuButton.jsx +2 -1
  118. package/src/core/ui/CanvasCreateMenu.jsx +1 -19
  119. package/src/core/ui/CoreUIBar.jsx +10 -7
  120. package/src/core/ui/ThemeMenuButton.jsx +45 -30
  121. package/src/core/vite/server-plugin.js +36 -0
  122. package/src/core/workleaf/cleanup.js +102 -0
  123. package/src/core/workleaf/create.js +199 -0
  124. package/src/core/workleaf/delete.js +101 -0
  125. package/src/core/workleaf/list.js +57 -0
  126. package/src/core/workleaf/merge.js +238 -0
  127. package/src/core/workleaf/paths.js +146 -0
  128. package/src/core/workleaf/paths.test.js +230 -0
  129. package/src/core/workshop/features/createCanvas/CreateCanvasForm.jsx +28 -21
  130. package/src/core/workshop/features/createFlow/server.js +5 -1
  131. package/src/core/workshop/features/createPrototype/server.js +5 -1
  132. package/src/core/workshop/features/templateIndex.js +28 -1
  133. package/src/core/worktree/port.js +48 -5
  134. package/src/core/worktree/port.test.js +42 -0
  135. package/src/internals/ArtifactForm/ArtifactForm.jsx +173 -112
  136. package/src/internals/ArtifactForm/ArtifactForm.module.css +48 -66
  137. package/src/internals/ArtifactForm/artifactSchemas.js +40 -5
  138. package/src/internals/BaseUiForm/BaseUiForm.jsx +293 -0
  139. package/src/internals/BaseUiForm/BaseUiForm.module.css +542 -0
  140. package/src/internals/BranchesDropdown/BranchesDropdown.jsx +69 -0
  141. package/src/internals/BranchesDropdown/BranchesDropdown.module.css +84 -0
  142. package/src/internals/CommandPalette/CommandPalette.jsx +25 -19
  143. package/src/internals/CommandPalette/CreateArtifactForm.jsx +150 -0
  144. package/src/internals/CommandPalette/CreateDialog.jsx +20 -110
  145. package/src/internals/CommandPalette/WidgetArtifactDialog.jsx +1 -1
  146. package/src/internals/CommandPalette/createHelpers.js +30 -0
  147. package/src/internals/CreatePage/CreatePage.jsx +57 -0
  148. package/src/internals/CreatePage/CreatePage.module.css +34 -0
  149. package/src/internals/Dialog.jsx +51 -0
  150. package/src/internals/SimpleWorkspace/SimpleWorkspace.jsx +533 -0
  151. package/src/internals/SimpleWorkspace/SimpleWorkspace.module.css +323 -0
  152. package/src/internals/Tooltip.jsx +39 -0
  153. package/src/internals/Viewfinder.jsx +196 -48
  154. package/src/internals/Viewfinder.module.css +72 -1
  155. package/src/internals/canvas/CanvasPage.jsx +267 -124
  156. package/src/internals/canvas/CanvasPage.multiselect.test.jsx +39 -26
  157. package/src/internals/canvas/PageSelector.jsx +15 -1
  158. package/src/internals/canvas/PageSelector.module.css +14 -0
  159. package/src/internals/canvas/canvasApi.js +40 -5
  160. package/src/internals/canvas/isolation/createIsolationMiddleware.js +68 -0
  161. package/src/internals/canvas/isolation/mountIsolationApp.jsx +88 -0
  162. package/src/internals/canvas/isolation/prototypeRoutes.jsx +131 -0
  163. package/src/internals/canvas/isolation/prototypesEntry.jsx +28 -0
  164. package/src/internals/canvas/isolation/storiesEntry.jsx +20 -0
  165. package/src/internals/canvas/isolation/storyRoutes.jsx +55 -0
  166. package/src/internals/canvas/useUndoRedo.js +75 -60
  167. package/src/internals/canvas/useUndoRedo.test.js +46 -178
  168. package/src/internals/canvas/widgets/ExpandedPaneTopBar.jsx +1 -1
  169. package/src/internals/canvas/widgets/FrozenTerminalOverlay.jsx +30 -21
  170. package/src/internals/canvas/widgets/ImageWidget.jsx +7 -2
  171. package/src/internals/canvas/widgets/LinkPreview.jsx +45 -1
  172. package/src/internals/canvas/widgets/LinkPreview.module.css +71 -0
  173. package/src/internals/canvas/widgets/PrototypeEmbed.jsx +69 -10
  174. package/src/internals/canvas/widgets/PrototypeEmbed.test.jsx +114 -0
  175. package/src/internals/canvas/widgets/StorySetWidget.jsx +8 -4
  176. package/src/internals/canvas/widgets/StoryWidget.jsx +21 -38
  177. package/src/internals/canvas/widgets/TerminalReadWidget.jsx +13 -15
  178. package/src/internals/canvas/widgets/WidgetChrome.jsx +1 -1
  179. package/src/internals/canvas/widgets/WidgetChrome.module.css +15 -0
  180. package/src/internals/canvas/widgets/embedTheme.js +2 -2
  181. package/src/internals/canvas/widgets/expandUtils.js +19 -19
  182. package/src/internals/canvas/widgets/normalizeLegacyEmbedSrc.js +74 -0
  183. package/src/internals/context.jsx +22 -1
  184. package/src/internals/context.test.jsx +47 -0
  185. package/src/internals/hooks/useArtifactIndex.js +31 -0
  186. package/src/internals/hooks/useArtifactIndex.test.js +45 -0
  187. package/src/internals/hooks/usePrototypeReloadGuard.js +14 -8
  188. package/src/internals/index.js +6 -0
  189. package/src/internals/loadConsumerWrapper.js +60 -0
  190. package/src/internals/story/ComponentSetPage.jsx +75 -34
  191. package/src/internals/story/StoryPage.jsx +16 -25
  192. package/src/internals/vite/data-plugin.js +313 -148
  193. package/src/internals/vite/data-plugin.test.js +11 -11
  194. package/{scaffold/storyboard.config.json → storyboard.config.json} +25 -0
  195. package/terminal.config.json +1 -1
  196. package/src/core/lib/components/ui/card/card-action.jsx +0 -11
  197. package/src/core/lib/components/ui/card/card-content.jsx +0 -11
  198. package/src/core/lib/components/ui/card/card-description.jsx +0 -11
  199. package/src/core/lib/components/ui/card/card-footer.jsx +0 -11
  200. package/src/core/lib/components/ui/card/card-header.jsx +0 -19
  201. package/src/core/lib/components/ui/card/card-title.jsx +0 -11
  202. package/src/core/lib/components/ui/card/card.jsx +0 -17
  203. package/src/core/lib/components/ui/card/index.js +0 -25
  204. package/src/core/lib/components/ui/collapsible/collapsible-content.jsx +0 -7
  205. package/src/core/lib/components/ui/collapsible/collapsible-trigger.jsx +0 -7
  206. package/src/core/lib/components/ui/collapsible/collapsible.jsx +0 -7
  207. package/src/core/lib/components/ui/collapsible/index.js +0 -13
  208. package/src/core/lib/components/ui/dialog/dialog-close.jsx +0 -7
  209. package/src/core/lib/components/ui/dialog/dialog-description.jsx +0 -15
  210. package/src/core/lib/components/ui/dialog/dialog-footer.jsx +0 -23
  211. package/src/core/lib/components/ui/dialog/dialog-header.jsx +0 -11
  212. package/src/core/lib/components/ui/dialog/dialog-overlay.jsx +0 -15
  213. package/src/core/lib/components/ui/dialog/dialog-portal.jsx +0 -4
  214. package/src/core/lib/components/ui/dialog/dialog-title.jsx +0 -15
  215. package/src/core/lib/components/ui/popover/index.js +0 -28
  216. package/src/core/lib/components/ui/popover/popover-close.jsx +0 -7
  217. package/src/core/lib/components/ui/popover/popover-content.jsx +0 -22
  218. package/src/core/lib/components/ui/popover/popover-description.jsx +0 -11
  219. package/src/core/lib/components/ui/popover/popover-header.jsx +0 -11
  220. package/src/core/lib/components/ui/popover/popover-portal.jsx +0 -4
  221. package/src/core/lib/components/ui/popover/popover-title.jsx +0 -11
  222. package/src/core/lib/components/ui/popover/popover-trigger.jsx +0 -8
  223. package/src/core/lib/components/ui/popover/popover.jsx +0 -4
  224. package/src/core/lib/components/ui/select/index.js +0 -37
  225. package/src/core/lib/components/ui/select/select-content.jsx +0 -30
  226. package/src/core/lib/components/ui/select/select-group-heading.jsx +0 -17
  227. package/src/core/lib/components/ui/select/select-group.jsx +0 -15
  228. package/src/core/lib/components/ui/select/select-item.jsx +0 -26
  229. package/src/core/lib/components/ui/select/select-label.jsx +0 -11
  230. package/src/core/lib/components/ui/select/select-portal.jsx +0 -4
  231. package/src/core/lib/components/ui/select/select-scroll-down-button.jsx +0 -18
  232. package/src/core/lib/components/ui/select/select-scroll-up-button.jsx +0 -18
  233. package/src/core/lib/components/ui/select/select-separator.jsx +0 -15
  234. package/src/core/lib/components/ui/select/select-trigger.jsx +0 -25
  235. package/src/core/lib/components/ui/select/select.jsx +0 -4
  236. package/src/core/lib/components/ui/sheet/index.js +0 -34
  237. package/src/core/lib/components/ui/sheet/sheet-close.jsx +0 -7
  238. package/src/core/lib/components/ui/sheet/sheet-content.jsx +0 -35
  239. package/src/core/lib/components/ui/sheet/sheet-description.jsx +0 -15
  240. package/src/core/lib/components/ui/sheet/sheet-footer.jsx +0 -11
  241. package/src/core/lib/components/ui/sheet/sheet-header.jsx +0 -11
  242. package/src/core/lib/components/ui/sheet/sheet-overlay.jsx +0 -15
  243. package/src/core/lib/components/ui/sheet/sheet-portal.jsx +0 -4
  244. package/src/core/lib/components/ui/sheet/sheet-title.jsx +0 -15
  245. package/src/core/lib/components/ui/sheet/sheet-trigger.jsx +0 -7
  246. package/src/core/lib/components/ui/sheet/sheet.jsx +0 -4
  247. package/src/core/lib/components/ui/toggle/index.js +0 -8
  248. package/src/core/lib/components/ui/toggle/toggle.jsx +0 -36
  249. package/src/core/lib/components/ui/toggle-group/index.js +0 -10
  250. package/src/core/lib/components/ui/toggle-group/toggle-group-item.jsx +0 -29
  251. package/src/core/lib/components/ui/toggle-group/toggle-group.jsx +0 -43
  252. package/src/internals/canvas/StoryErrorBoundary.jsx +0 -47
  253. package/src/internals/canvas/componentIsolate.jsx +0 -144
  254. package/src/internals/canvas/componentSetIsolate.jsx +0 -300
  255. package/src/internals/canvas/widgets/InlineStoryRenderer.jsx +0 -129
  256. package/src/internals/canvas/widgets/InlineStoryRenderer.test.jsx +0 -68
  257. package/src/primer/ThemeSync.jsx +0 -73
  258. package/src/primer/index.js +0 -1
@@ -583,3 +583,74 @@
583
583
  .expandedUrl:hover {
584
584
  text-decoration: underline;
585
585
  }
586
+
587
+ /* ── PR metadata (state badge + branch refs + diff stats) ──────────── */
588
+
589
+ .prMeta {
590
+ display: flex;
591
+ align-items: center;
592
+ gap: 8px;
593
+ flex-wrap: wrap;
594
+ font-size: 12px;
595
+ color: var(--fgColor-muted, #656d76);
596
+ min-width: 0;
597
+ }
598
+
599
+ .prStateBadge {
600
+ display: inline-flex;
601
+ align-items: center;
602
+ gap: 6px;
603
+ padding: 5px 10px 5px 9px;
604
+ border-radius: 2em;
605
+ font-size: 12px;
606
+ font-weight: 500;
607
+ line-height: 1;
608
+ color: var(--fgColor-onEmphasis, #ffffff);
609
+ white-space: nowrap;
610
+ }
611
+
612
+ .prStateOpen { background: var(--bgColor-open-emphasis, #1f883d); }
613
+ .prStateMerged { background: var(--bgColor-done-emphasis, #8250df); }
614
+ .prStateClosed { background: var(--bgColor-closed-emphasis, #cf222e); }
615
+ .prStateDraft {
616
+ background: var(--bgColor-neutral-emphasis, #59636e);
617
+ }
618
+
619
+ .prBranchRef {
620
+ display: inline-flex;
621
+ align-items: center;
622
+ gap: 4px;
623
+ padding: 2px 6px;
624
+ border-radius: 4px;
625
+ background: var(--bgColor-muted, #f6f8fa);
626
+ color: var(--fgColor-default, #1f2328);
627
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
628
+ font-size: 11px;
629
+ max-width: 240px;
630
+ overflow: hidden;
631
+ }
632
+
633
+ .prBranchName {
634
+ overflow: hidden;
635
+ text-overflow: ellipsis;
636
+ white-space: nowrap;
637
+ max-width: 96px;
638
+ }
639
+
640
+ .prBranchArrow {
641
+ color: var(--fgColor-muted, #656d76);
642
+ font-family: -apple-system, system-ui, sans-serif;
643
+ flex-shrink: 0;
644
+ }
645
+
646
+ .prDiffStat {
647
+ display: inline-flex;
648
+ align-items: center;
649
+ gap: 4px;
650
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
651
+ font-size: 12px;
652
+ font-weight: 600;
653
+ }
654
+
655
+ .prDiffAdd { color: var(--fgColor-success, #1a7f37); }
656
+ .prDiffDel { color: var(--fgColor-danger, #cf222e); }
@@ -7,6 +7,7 @@ import { readProp, prototypeEmbedSchema } from './widgetProps.js'
7
7
  import { getEmbedChromeVars } from './embedTheme.js'
8
8
  import { useIframeDevLogs } from './iframeDevLogs.js'
9
9
  import { findAllConnectedSplitTargets, getSplitPaneLabel, buildPaneForWidget, buildSplitLayout } from './expandUtils.js'
10
+ import { normalizeLegacyEmbedSrc } from './normalizeLegacyEmbedSrc.js'
10
11
  import ExpandedPane from './ExpandedPane.jsx'
11
12
  import styles from './PrototypeEmbed.module.css'
12
13
  import overlayStyles from './embedOverlay.module.css'
@@ -46,21 +47,76 @@ function resolveCanvasThemeFromStorage() {
46
47
  const HEADER_HEIGHT = 37
47
48
 
48
49
  export default forwardRef(function PrototypeEmbed({ id: widgetId, props, onUpdate, resizable }, ref) {
49
- const src = readProp(props, 'src', prototypeEmbedSchema)
50
+ const rawStoredSrc = readProp(props, 'src', prototypeEmbedSchema)
51
+ const src = normalizeLegacyEmbedSrc(rawStoredSrc)
50
52
  const width = readProp(props, 'width', prototypeEmbedSchema) || 800
51
53
  const height = readProp(props, 'height', prototypeEmbedSchema) || 600
52
54
  const zoom = readProp(props, 'zoom', prototypeEmbedSchema) || 100
53
55
  const label = readProp(props, 'label', prototypeEmbedSchema) || src
54
56
 
57
+ // Self-healing write-back: if normalization changed the src, persist the
58
+ // clean form so the canvas JSONL stops carrying the broken URL. Without
59
+ // this, every render keeps recovering at runtime but the file stays
60
+ // poisoned and any consumer on an older library still sees the bug.
61
+ useEffect(() => {
62
+ if (!onUpdate) return
63
+ if (typeof rawStoredSrc !== 'string' || rawStoredSrc === src) return
64
+ onUpdate({ src })
65
+ }, [rawStoredSrc, src, onUpdate])
66
+
55
67
  const basePath = (import.meta.env.BASE_URL || '/').replace(/\/$/, '')
56
68
  const baseSegment = basePath.replace(/^\//, '')
57
- const rawSrc = useMemo(() => {
58
- if (!src) return ''
59
- if (/^https?:\/\//.test(src)) return src
69
+ // Two URLs are derived from `src`:
70
+ // - rawSrc the iframe URL. In DEV this routes through the isolated
71
+ // prototypes.html entry so a broken prototype's transform/HMR errors
72
+ // stay inside the iframe (see .agents/plans/vite-isolation.md):
73
+ // /MyProto/SignupForm becomes prototypes.html/MyProto/SignupForm.
74
+ // Routing happens in the *pathname* (createBrowserRouter), not in the
75
+ // hash, so the hash and search stay free for storyboard URL state
76
+ // (`?flow=...` and `#key=value` overrides). In PROD it loads the
77
+ // prototype path directly through the canvas SPA (prototypes.html is
78
+ // a build-time isolation artifact that must not leak into deployed URLs).
79
+ // - externalSrc — the URL used by "Open in new tab". Always direct
80
+ // (`${basePath}/<protoPath>`), never prototypes.html, even in dev —
81
+ // opening prototypes.html/... in a fresh tab is a leaky surprise
82
+ // for users navigating from the canvas.
83
+ // External http(s) URLs are left alone in both cases. basePath already
84
+ // carries the /branch--xxx/ prefix on branch deploys, so both work for
85
+ // main and branch deploys alike.
86
+ const { rawSrc, externalSrc } = useMemo(() => {
87
+ if (!src) return { rawSrc: '', externalSrc: '' }
88
+ if (/^https?:\/\//.test(src)) return { rawSrc: src, externalSrc: src }
60
89
  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}`
90
+ let normalized
91
+ if (baseSegment && cleaned.startsWith(basePath)) normalized = cleaned
92
+ else if (baseSegment && cleaned.startsWith(baseSegment)) normalized = `/${cleaned}`
93
+ else normalized = `${basePath}${cleaned}`
94
+ // Strip basePath so we can split path/query/hash cleanly and
95
+ // re-anchor for whichever mode we're in. Any pre-existing #hash on
96
+ // the original src is preserved.
97
+ const withoutBase = baseSegment && normalized.startsWith(basePath)
98
+ ? normalized.slice(basePath.length) || '/'
99
+ : normalized
100
+ const hashIdx = withoutBase.indexOf('#')
101
+ const innerHash = hashIdx >= 0 ? withoutBase.slice(hashIdx + 1) : ''
102
+ const pathAndQuery = hashIdx >= 0 ? withoutBase.slice(0, hashIdx) : withoutBase
103
+ const routePath = pathAndQuery.startsWith('/') ? pathAndQuery : `/${pathAndQuery}`
104
+ const suffix = innerHash ? `#${innerHash}` : ''
105
+ // Direct path through the canvas SPA — used in prod for the iframe and
106
+ // always for "Open in new tab".
107
+ const directUrl = `${basePath}${routePath}${suffix}`
108
+ if (import.meta.env.PROD) {
109
+ return { rawSrc: directUrl, externalSrc: directUrl }
110
+ }
111
+ // Dev iframe: prototypes.html with the route as a sub-path. The library
112
+ // middleware (`data-plugin.js`) serves the same HTML shell for
113
+ // `/prototypes.html/*` so a hard reload / open-in-new-tab works. The
114
+ // prototype name (first path segment) narrows getRoutesForProto so a
115
+ // broken sibling prototype can't poison the lazy() chain. Hash and
116
+ // search stay attached so storyboard URL state (`#key=value` overrides,
117
+ // `?flow=name`) survives the redirect through the isolation entry.
118
+ const iframeUrl = `${basePath}/prototypes.html${routePath}${suffix}`
119
+ return { rawSrc: iframeUrl, externalSrc: directUrl }
64
120
  }, [src, basePath, baseSegment])
65
121
 
66
122
  const scale = zoom / 100
@@ -268,7 +324,10 @@ export default forwardRef(function PrototypeEmbed({ id: widgetId, props, onUpdat
268
324
  function handleMessage(e) {
269
325
  if (e.source !== iframeRef.current?.contentWindow) return
270
326
  if (e.data?.type !== 'storyboard:embed:navigate') return
271
- const newSrc = e.data.src
327
+ // Defensive: older consumer embeds reported `/prototypes.html#/route`
328
+ // here (loader path + hash). Normalize to the inner route so we never
329
+ // persist the broken form.
330
+ const newSrc = normalizeLegacyEmbedSrc(e.data.src)
272
331
  if (newSrc && newSrc !== src) {
273
332
  const originalSrc = readProp(props, 'originalSrc', prototypeEmbedSchema)
274
333
  onUpdate?.({ src: newSrc, originalSrc: originalSrc || src })
@@ -295,7 +354,7 @@ export default forwardRef(function PrototypeEmbed({ id: widgetId, props, onUpdat
295
354
  } else if (actionId === 'split-screen') {
296
355
  setExpandMode('split')
297
356
  } else if (actionId === 'open-external') {
298
- if (rawSrc) window.open(rawSrc, '_blank', 'noopener')
357
+ if (externalSrc) window.open(externalSrc, '_blank', 'noopener')
299
358
  } else if (actionId === 'refresh-frame') {
300
359
  const iframe = iframeRef.current
301
360
  if (iframe) {
@@ -310,7 +369,7 @@ export default forwardRef(function PrototypeEmbed({ id: widgetId, props, onUpdat
310
369
  onUpdate?.({ zoom: Math.max(25, zoom - step) })
311
370
  }
312
371
  },
313
- }), [rawSrc, zoom, onUpdate])
372
+ }), [externalSrc, zoom, onUpdate])
314
373
 
315
374
  function handlePickRoute(route) {
316
375
  onUpdate?.({ src: route })
@@ -1,5 +1,6 @@
1
1
  import { describe, expect, it } from 'vitest'
2
2
  import { getEmbedChromeVars } from './embedTheme.js'
3
+ import { normalizeLegacyEmbedSrc } from './normalizeLegacyEmbedSrc.js'
3
4
 
4
5
  describe('getEmbedChromeVars', () => {
5
6
  it('follows toolbar theme variants for embed edit chrome', () => {
@@ -8,3 +9,116 @@ describe('getEmbedChromeVars', () => {
8
9
  expect(getEmbedChromeVars('dark_dimmed')['--bgColor-default']).toBe('#212830')
9
10
  })
10
11
  })
12
+
13
+ describe('normalizeLegacyEmbedSrc', () => {
14
+ it('passes through clean canvas-app routes unchanged', () => {
15
+ expect(normalizeLegacyEmbedSrc('/cq-org-enablement')).toBe('/cq-org-enablement')
16
+ expect(normalizeLegacyEmbedSrc('/MyProto/SignupForm')).toBe('/MyProto/SignupForm')
17
+ expect(normalizeLegacyEmbedSrc('/cq-org-enablement#x=1')).toBe('/cq-org-enablement#x=1')
18
+ })
19
+
20
+ it('passes through external http(s) URLs unchanged', () => {
21
+ expect(normalizeLegacyEmbedSrc('https://example.com/foo')).toBe('https://example.com/foo')
22
+ })
23
+
24
+ it('passes through empty/falsy values', () => {
25
+ expect(normalizeLegacyEmbedSrc('')).toBe('')
26
+ expect(normalizeLegacyEmbedSrc(null)).toBe(null)
27
+ expect(normalizeLegacyEmbedSrc(undefined)).toBe(undefined)
28
+ })
29
+
30
+ describe('legacy hash-form: /prototypes.html#/<route>', () => {
31
+ it('strips loader prefix and keeps inner route', () => {
32
+ expect(normalizeLegacyEmbedSrc('/prototypes.html#/cq-org-enablement')).toBe('/cq-org-enablement')
33
+ })
34
+
35
+ it('keeps query strings inside inner', () => {
36
+ expect(normalizeLegacyEmbedSrc('/prototypes.html#/cq-org-enablement?cqConfirmOpen=true'))
37
+ .toBe('/cq-org-enablement?cqConfirmOpen=true')
38
+ })
39
+
40
+ it('handles non-root basePath', () => {
41
+ expect(normalizeLegacyEmbedSrc('/storyboard/prototypes.html#/MyProto')).toBe('/MyProto')
42
+ })
43
+
44
+ it('returns "/" for empty inner', () => {
45
+ expect(normalizeLegacyEmbedSrc('/prototypes.html#')).toBe('/')
46
+ })
47
+
48
+ it('prefixes "/" if inner missing leading slash', () => {
49
+ expect(normalizeLegacyEmbedSrc('/prototypes.html#MyProto')).toBe('/MyProto')
50
+ })
51
+ })
52
+
53
+ describe('legacy URLSearchParams-clobbered form (real-world 0.6.13/0.6.14 bug)', () => {
54
+ // Real bug: storyboard's session.writeHash did URLSearchParams.toString()
55
+ // on the entire hash, which URL-encoded the route slash to %2F and turned
56
+ // /MyProto into a key with an empty value. Real persisted srcs from the
57
+ // wild look like /prototypes.html#%2Fcq-org-enablement=&cqConfirmOpen=true
58
+ it('recovers route from URLSearchParams-mangled hash', () => {
59
+ expect(
60
+ normalizeLegacyEmbedSrc('/prototypes.html#%2Fcq-org-enablement=&cqConfirmOpen=true'),
61
+ ).toBe('/cq-org-enablement#cqConfirmOpen=true')
62
+ })
63
+
64
+ it('recovers route with no surviving storyboard state', () => {
65
+ expect(normalizeLegacyEmbedSrc('/prototypes.html#%2Fcq-org-enablement=')).toBe('/cq-org-enablement')
66
+ })
67
+
68
+ it('recovers route with multiple storyboard params', () => {
69
+ expect(
70
+ normalizeLegacyEmbedSrc(
71
+ '/prototypes.html#%2Fcq-org-enablement=&cqEnabled=null&cqFlashDismissed=null&cqConfirmOpen=true',
72
+ ),
73
+ ).toBe('/cq-org-enablement#cqEnabled=null&cqFlashDismissed=null&cqConfirmOpen=true')
74
+ })
75
+
76
+ it('recovers nested route', () => {
77
+ expect(
78
+ normalizeLegacyEmbedSrc('/prototypes.html#%2FMyProto%2FSignupForm=&x=1'),
79
+ ).toBe('/MyProto/SignupForm#x=1')
80
+ })
81
+
82
+ it('handles non-root basePath', () => {
83
+ expect(
84
+ normalizeLegacyEmbedSrc('/storyboard/prototypes.html#%2FMyProto=&x=1'),
85
+ ).toBe('/MyProto#x=1')
86
+ })
87
+ })
88
+
89
+ describe('defensive path-form: /prototypes.html/<route>', () => {
90
+ it('strips loader prefix', () => {
91
+ expect(normalizeLegacyEmbedSrc('/prototypes.html/MyProto')).toBe('/MyProto')
92
+ })
93
+
94
+ it('handles nested routes', () => {
95
+ expect(normalizeLegacyEmbedSrc('/prototypes.html/MyProto/SignupForm')).toBe('/MyProto/SignupForm')
96
+ })
97
+
98
+ it('handles non-root basePath', () => {
99
+ expect(normalizeLegacyEmbedSrc('/storyboard/prototypes.html/MyProto')).toBe('/MyProto')
100
+ })
101
+ })
102
+
103
+ describe('all-encompassing regression — no legacy shape can produce a /prototypes.html src', () => {
104
+ const cases = [
105
+ '/prototypes.html#/foo',
106
+ '/prototypes.html#/foo?bar=1',
107
+ '/prototypes.html#',
108
+ '/prototypes.html#%2Ffoo=&bar=1',
109
+ '/prototypes.html#%2Ffoo%2Fbar=&x=1',
110
+ '/prototypes.html/foo',
111
+ '/prototypes.html/foo/bar',
112
+ '/storyboard/prototypes.html#/foo',
113
+ '/storyboard/prototypes.html#%2Ffoo=&x=1',
114
+ '/storyboard/prototypes.html/foo',
115
+ ]
116
+ for (const input of cases) {
117
+ it(`recovers ${input}`, () => {
118
+ const result = normalizeLegacyEmbedSrc(input)
119
+ expect(result).not.toMatch(/prototypes\.html/)
120
+ expect(result.startsWith('/')).toBe(true)
121
+ })
122
+ }
123
+ })
124
+ })
@@ -31,18 +31,22 @@ 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
+ // Route through the shared iframe-isolation entry in dev so a broken
36
+ // .story.jsx can't poison the canvas SPA's module graph. The real story
37
+ // route handles it in prod (no isolation middleware in deployed builds).
34
38
  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
39
  params.set('_sb_embed', '')
40
40
  params.set('_sb_component_set', '')
41
41
  if (layout) params.set('layout', layout)
42
42
  if (selected) params.set('selected', selected)
43
43
  if (density) params.set('density', density)
44
44
  if (theme) params.set('theme', theme)
45
+
45
46
  const route = story._route || `/components/${storyId}`
47
+ if (import.meta.env.DEV) {
48
+ return `${base}/stories.html${route}?${params}`
49
+ }
46
50
  return `${base}${route}?${params}`
47
51
  }
48
52
 
@@ -11,7 +11,6 @@
11
11
  import { forwardRef, useImperativeHandle, useRef, useCallback, useState, useEffect, useMemo } from 'react'
12
12
  import { getStoryData } from '../../../core/index.js'
13
13
  import { useThemeState, useThemeSyncTargets } from '../../hooks/useThemeState.js'
14
- import { getConfig } from '../../../core/stores/configStore.js'
15
14
  import { createInspectorHighlighter } from '../../../core/inspector/highlighter.js'
16
15
  import Icon from '../../Icon.jsx'
17
16
  import WidgetWrapper from './WidgetWrapper.jsx'
@@ -20,7 +19,6 @@ import { useIframeDevLogs } from './iframeDevLogs.js'
20
19
  import { findAllConnectedSplitTargets, getSplitPaneLabel, buildPaneForWidget, buildSplitLayout, buildSecondaryIframeUrl } from './expandUtils.js'
21
20
  import { useExpandOverride } from './useExpandOverride.js'
22
21
  import ExpandedPane from './ExpandedPane.jsx'
23
- import InlineStoryRenderer from './InlineStoryRenderer.jsx'
24
22
  import styles from './StoryWidget.module.css'
25
23
  import overlayStyles from './embedOverlay.module.css'
26
24
 
@@ -28,30 +26,24 @@ function ComponentIcon({ size = 36 }) {
28
26
  return <Icon name="iconoir/keyframe" size={size} />
29
27
  }
30
28
 
31
- function isInlineStoriesEnabled() {
32
- try {
33
- if (typeof window !== 'undefined') {
34
- const params = new URLSearchParams(window.location.search)
35
- if (params.has('_sb_inline_stories')) {
36
- const v = params.get('_sb_inline_stories')
37
- return v !== '0' && v !== 'false'
38
- }
39
- }
40
- } catch { /* */ }
41
- try {
42
- return Boolean(getConfig('canvas')?.inlineStories)
43
- } catch { return false }
44
- }
45
-
46
29
  function resolveStoryUrl(storyId, exportName, theme) {
47
30
  const story = getStoryData(storyId)
48
31
  if (!story?._storyModule) return ''
49
32
  const base = (import.meta.env.BASE_URL || '/').replace(/\/$/, '')
33
+
34
+ // In dev, route through the isolated `stories.html` entry so a broken
35
+ // .story.jsx's transform/HMR errors stay inside the iframe (the canvas
36
+ // chrome can't be poisoned by a sibling story's module graph). In prod
37
+ // there is no isolation middleware — the real SPA route handles it.
50
38
  const params = new URLSearchParams()
51
- params.set('module', story._storyModule)
52
39
  if (exportName) params.set('export', exportName)
40
+ params.set('_sb_embed', '')
53
41
  if (theme) params.set('theme', theme)
54
- return `${base}/_storyboard/canvas/isolate?${params}`
42
+
43
+ if (import.meta.env.DEV) {
44
+ return `${base}/stories.html${story._route || `/components/${storyId}`}?${params}`
45
+ }
46
+ return `${base}${story._route || `/components/${storyId}`}?${params}`
55
47
  }
56
48
 
57
49
  const _storySourcesCache = {}
@@ -205,15 +197,10 @@ export default forwardRef(function StoryWidget({ id: widgetId, props, onUpdate,
205
197
  [storyId, exportName, storyIndexKey, effectiveTheme],
206
198
  )
207
199
 
208
- const inlineEnabled = isInlineStoriesEnabled()
209
-
210
- // When paused and not interactive, freeze the iframe src to prevent reloads
211
- const effectiveSrc = iframeSrc
212
-
213
200
  useIframeDevLogs({
214
201
  widget: 'StoryWidget',
215
- loaded: !inlineEnabled && interactive && !showCode && Boolean(effectiveSrc),
216
- src: effectiveSrc,
202
+ loaded: interactive && !showCode && Boolean(iframeSrc),
203
+ src: iframeSrc,
217
204
  })
218
205
 
219
206
  const displayName = exportName ? `${storyId} / ${exportName}` : storyId
@@ -231,7 +218,7 @@ export default forwardRef(function StoryWidget({ id: widgetId, props, onUpdate,
231
218
  )
232
219
  }
233
220
 
234
- if (!inlineEnabled && !effectiveSrc) {
221
+ if (!iframeSrc) {
235
222
  return (
236
223
  <WidgetWrapper>
237
224
  <div className={styles.container} ref={containerRef}>
@@ -279,17 +266,13 @@ export default forwardRef(function StoryWidget({ id: widgetId, props, onUpdate,
279
266
  ) : (
280
267
  <>
281
268
  <div className={styles.content}>
282
- {inlineEnabled ? (
283
- <InlineStoryRenderer storyId={storyId} exportName={exportName} />
284
- ) : (
285
- <iframe
286
- ref={iframeRef}
287
- src={effectiveSrc}
288
- className={styles.iframe}
289
- title={displayName}
290
- onLoad={(e) => e.target.blur()}
291
- />
292
- )}
269
+ <iframe
270
+ ref={iframeRef}
271
+ src={iframeSrc}
272
+ className={styles.iframe}
273
+ title={displayName}
274
+ onLoad={(e) => e.target.blur()}
275
+ />
293
276
  </div>
294
277
 
295
278
  {!interactive && (
@@ -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()
@@ -1,5 +1,5 @@
1
1
  import { useState, useCallback, useRef, useEffect, useSyncExternalStore } from 'react'
2
- import { Tooltip } from '@primer/react'
2
+ import Tooltip from '../../Tooltip.jsx'
3
3
  import { CheckIcon } from '@primer/octicons-react'
4
4
  import { getConnectorConfig, getInteractGate } from './widgetConfig.js'
5
5
  import { ICON_REGISTRY } from './widgetIcons.jsx'
@@ -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;
@@ -2,7 +2,7 @@
2
2
  * Resolve the effective canvas theme from the core theme store.
3
3
  * Respects the canvas-specific theme sync toggle.
4
4
  */
5
- import { getTheme, getThemeSyncTargets } from '../../../core/index.js'
5
+ import { getTheme, getSurfaceSync } from '../../../core/index.js'
6
6
 
7
7
  function resolveSystem() {
8
8
  if (typeof window === 'undefined') return 'light'
@@ -10,7 +10,7 @@ function resolveSystem() {
10
10
  }
11
11
 
12
12
  export function resolveCanvasTheme() {
13
- const sync = getThemeSyncTargets()
13
+ const sync = getSurfaceSync()
14
14
  if (!sync.canvas) return 'light'
15
15
  const theme = getTheme()
16
16
  return theme === 'system' ? resolveSystem() : theme
@@ -498,31 +498,31 @@ export function buildSecondaryIframeUrl(widget) {
498
498
  const exportName = widget.props?.exportName
499
499
  if (!storyId) return null
500
500
  const storyData = getStoryData(storyId)
501
- if (storyData?._storyModule) {
502
- const params = new URLSearchParams()
503
- params.set('module', storyData._storyModule)
504
- if (exportName) params.set('export', exportName)
505
- return `${baseClean}/_storyboard/canvas/isolate?${params}`
506
- }
507
- return null
501
+ if (!storyData?._storyModule) return null
502
+ const route = storyData._route || `/components/${storyId}`
503
+ const params = new URLSearchParams()
504
+ if (exportName) params.set('export', exportName)
505
+ params.set('_sb_embed', '')
506
+ // Dev: route through the shared isolation entry. Prod: direct SPA route.
507
+ if (import.meta.env.DEV) return `${baseClean}/stories.html${route}?${params}`
508
+ return `${baseClean}${route}?${params}`
508
509
  }
509
510
 
510
511
  if (widget.type === 'component-set') {
511
512
  const storyId = widget.props?.storyId
512
513
  if (!storyId) return null
513
514
  const storyData = getStoryData(storyId)
514
- if (storyData?._storyModule) {
515
- const params = new URLSearchParams()
516
- params.set('_sb_embed', '')
517
- params.set('_sb_component_set', '')
518
- const layout = widget.props?.layout
519
- if (layout) params.set('layout', layout)
520
- const selected = widget.props?.selected
521
- if (selected) params.set('selected', selected)
522
- const route = storyData._route || `/components/${storyId}`
523
- return `${baseClean}${route}?${params}`
524
- }
525
- return null
515
+ if (!storyData?._storyModule) return null
516
+ const params = new URLSearchParams()
517
+ params.set('_sb_embed', '')
518
+ params.set('_sb_component_set', '')
519
+ const layout = widget.props?.layout
520
+ if (layout) params.set('layout', layout)
521
+ const selected = widget.props?.selected
522
+ if (selected) params.set('selected', selected)
523
+ const route = storyData._route || `/components/${storyId}`
524
+ if (import.meta.env.DEV) return `${baseClean}/stories.html${route}?${params}`
525
+ return `${baseClean}${route}?${params}`
526
526
  }
527
527
 
528
528
  return null