@dfosco/storyboard 0.7.1 → 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 (254) hide show
  1. package/dist/storyboard-ui.css +1 -1
  2. package/dist/storyboard-ui.js +13907 -10816
  3. package/dist/storyboard-ui.js.map +1 -1
  4. package/dist/tailwind.css +1 -1
  5. package/package.json +138 -128
  6. package/scaffold/AGENTS.md +9 -7
  7. package/scaffold/README.md +18 -0
  8. package/scaffold/deploy.yml +21 -0
  9. package/scaffold/gitignore +8 -14
  10. package/scaffold/preview.yml +40 -0
  11. package/scaffold/{manifest.json → scaffold.config.json} +73 -3
  12. package/scaffold/scripts/aggregate-artifacts.mjs +257 -0
  13. package/scaffold/skills/canvas/SKILL.md +1 -1
  14. package/scaffold/skills/migrate-0.5.0/SKILL.md +1 -1
  15. package/scaffold/skills/ship/SKILL.md +2 -2
  16. package/scaffold/skills/worktree/SKILL.md +1 -1
  17. package/scaffold/src/_prototype.jsx +17 -0
  18. package/scaffold/src/_story.jsx +13 -0
  19. package/scaffold/src/library/README.md +34 -0
  20. package/scaffold/src/library/_app.jsx +53 -0
  21. package/scaffold/src/library/_app.module.css +8 -0
  22. package/scaffold/src/library/create.jsx +10 -0
  23. package/scaffold/src/library/index.jsx +14 -0
  24. package/scaffold/src/library/mount.jsx +52 -0
  25. package/scaffold/src/library/prototypes-entry.jsx +57 -0
  26. package/scaffold/src/library/routes.jsx +205 -0
  27. package/scaffold/src/library/viewfinder.jsx +13 -0
  28. package/scaffold/src/library/workspace.jsx +17 -0
  29. package/scripts/aggregate-artifacts.mjs +257 -0
  30. package/src/core/artifact/operations.js +19 -13
  31. package/src/core/autosync/server.test.js +1 -1
  32. package/src/core/canvas/hot-pool.js +10 -11
  33. package/src/core/canvas/materializer.js +39 -13
  34. package/src/core/canvas/server.js +324 -69
  35. package/src/core/canvas/server.test.js +365 -2
  36. package/src/core/canvas/terminal-server.js +144 -57
  37. package/src/core/canvas/undoRedo.js +174 -0
  38. package/src/core/canvas/undoRedo.test.js +250 -0
  39. package/src/core/cli/buildManifest.js +34 -0
  40. package/src/core/cli/canvasRedo.js +44 -0
  41. package/src/core/cli/canvasUndo.js +40 -0
  42. package/src/core/cli/create.js +116 -18
  43. package/src/core/cli/dev.js +23 -35
  44. package/src/core/cli/index.js +21 -0
  45. package/src/core/cli/schemas.js +2 -3
  46. package/src/core/cli/serverUrl.js +24 -0
  47. package/src/core/cli/setup.js +14 -25
  48. package/src/core/cli/terminal-welcome.js +12 -6
  49. package/src/core/cli/updateVersion.js +42 -2
  50. package/src/core/cli/workleaf.js +140 -0
  51. package/src/core/comments/api.js +1 -1
  52. package/src/core/comments/queries.js +3 -0
  53. package/src/core/comments/ui/CommentWindow.jsx +3 -2
  54. package/src/core/data/artifactIndex.js +235 -0
  55. package/src/core/data/artifactIndex.test.js +203 -0
  56. package/src/core/data/artifactManifest.js +290 -0
  57. package/src/core/data/artifactManifest.test.js +207 -0
  58. package/src/core/data/viewfinder.js +47 -7
  59. package/src/core/index.js +6 -1
  60. package/src/core/lib/components/ui/avatar/avatar-fallback.jsx +2 -2
  61. package/src/core/lib/components/ui/avatar/avatar-image.jsx +2 -2
  62. package/src/core/lib/components/ui/avatar/avatar.jsx +2 -2
  63. package/src/core/lib/components/ui/checkbox/checkbox.jsx +12 -9
  64. package/src/core/lib/components/ui/dialog/dialog-content.jsx +45 -31
  65. package/src/core/lib/components/ui/dialog/dialog-trigger.jsx +9 -5
  66. package/src/core/lib/components/ui/dialog/dialog.jsx +12 -3
  67. package/src/core/lib/components/ui/dialog/dialog.module.css +118 -0
  68. package/src/core/lib/components/ui/dialog/index.js +4 -34
  69. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-group.jsx +3 -3
  70. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.jsx +19 -9
  71. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-content.jsx +8 -8
  72. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.jsx +2 -2
  73. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-group.jsx +2 -2
  74. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-item.jsx +15 -4
  75. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-portal.jsx +2 -2
  76. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-radio-group.jsx +2 -2
  77. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.jsx +14 -7
  78. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-separator.jsx +2 -2
  79. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.jsx +8 -4
  80. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.jsx +4 -4
  81. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-sub.jsx +2 -2
  82. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-trigger.jsx +8 -2
  83. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu.jsx +2 -2
  84. package/src/core/lib/components/ui/label/label.jsx +1 -2
  85. package/src/core/lib/components/ui/panel/panel-close.jsx +7 -4
  86. package/src/core/lib/components/ui/panel/panel-content.jsx +7 -9
  87. package/src/core/lib/components/ui/panel/panel-title.jsx +3 -3
  88. package/src/core/lib/components/ui/panel/panel.jsx +11 -2
  89. package/src/core/lib/components/ui/separator/separator.jsx +6 -4
  90. package/src/core/lib/components/ui/tooltip/tooltip-content.jsx +7 -6
  91. package/src/core/lib/components/ui/tooltip/tooltip-trigger.jsx +13 -6
  92. package/src/core/lib/components/ui/tooltip/tooltip.jsx +5 -5
  93. package/src/core/mountStoryboardCore.js +234 -74
  94. package/src/core/mountStoryboardCore.test.js +174 -0
  95. package/src/core/rename-watcher/watcher.js +329 -36
  96. package/src/core/rename-watcher/watcher.test.js +253 -0
  97. package/src/core/scaffold/fragments.js +287 -0
  98. package/src/core/scaffold/fragments.test.js +401 -0
  99. package/src/core/scaffold/index.js +291 -0
  100. package/src/core/scaffold/migrateLegacyGitignore.js +157 -0
  101. package/src/core/scaffold/scaffold-integration.test.js +247 -0
  102. package/src/core/scaffold.js +4 -97
  103. package/src/core/stores/configSchema.js +41 -2
  104. package/src/core/stores/recentArtifacts.js +49 -2
  105. package/src/core/stores/recentArtifacts.test.js +59 -6
  106. package/src/core/stores/themeBootstrap.js +264 -0
  107. package/src/core/stores/themeBootstrap.test.js +211 -0
  108. package/src/core/stores/themeStore.test.js +154 -0
  109. package/src/core/stores/themeStore.ts +235 -220
  110. package/src/core/styles/generateThemesCss.js +63 -0
  111. package/src/core/styles/tailwind.css +12 -4
  112. package/src/core/styles/workspace-chrome.css +30 -0
  113. package/src/core/tools/handlers/paletteTheme.js +11 -8
  114. package/src/core/ui/ActionMenuButton.jsx +2 -1
  115. package/src/core/ui/CanvasCreateMenu.jsx +1 -19
  116. package/src/core/ui/CoreUIBar.jsx +10 -7
  117. package/src/core/ui/ThemeMenuButton.jsx +45 -30
  118. package/src/core/vite/server-plugin.js +13 -0
  119. package/src/core/workleaf/cleanup.js +102 -0
  120. package/src/core/workleaf/create.js +199 -0
  121. package/src/core/workleaf/delete.js +101 -0
  122. package/src/core/workleaf/list.js +57 -0
  123. package/src/core/workleaf/merge.js +238 -0
  124. package/src/core/workleaf/paths.js +146 -0
  125. package/src/core/workleaf/paths.test.js +230 -0
  126. package/src/core/workshop/features/createCanvas/CreateCanvasForm.jsx +28 -21
  127. package/src/core/workshop/features/createFlow/server.js +5 -1
  128. package/src/core/workshop/features/createPrototype/server.js +5 -1
  129. package/src/core/workshop/features/templateIndex.js +28 -1
  130. package/src/core/worktree/port.js +48 -5
  131. package/src/core/worktree/port.test.js +42 -0
  132. package/src/internals/ArtifactForm/ArtifactForm.jsx +173 -112
  133. package/src/internals/ArtifactForm/ArtifactForm.module.css +48 -66
  134. package/src/internals/ArtifactForm/artifactSchemas.js +40 -5
  135. package/src/internals/BaseUiForm/BaseUiForm.jsx +293 -0
  136. package/src/internals/BaseUiForm/BaseUiForm.module.css +542 -0
  137. package/src/internals/BranchesDropdown/BranchesDropdown.jsx +69 -0
  138. package/src/internals/BranchesDropdown/BranchesDropdown.module.css +84 -0
  139. package/src/internals/CommandPalette/CommandPalette.jsx +25 -19
  140. package/src/internals/CommandPalette/CreateArtifactForm.jsx +150 -0
  141. package/src/internals/CommandPalette/CreateDialog.jsx +20 -110
  142. package/src/internals/CommandPalette/WidgetArtifactDialog.jsx +1 -1
  143. package/src/internals/CommandPalette/createHelpers.js +30 -0
  144. package/src/internals/CreatePage/CreatePage.jsx +57 -0
  145. package/src/internals/CreatePage/CreatePage.module.css +34 -0
  146. package/src/internals/Dialog.jsx +51 -0
  147. package/src/internals/SimpleWorkspace/SimpleWorkspace.jsx +533 -0
  148. package/src/internals/SimpleWorkspace/SimpleWorkspace.module.css +323 -0
  149. package/src/internals/Tooltip.jsx +39 -0
  150. package/src/internals/Viewfinder.jsx +196 -48
  151. package/src/internals/Viewfinder.module.css +72 -1
  152. package/src/internals/canvas/CanvasPage.jsx +267 -124
  153. package/src/internals/canvas/CanvasPage.multiselect.test.jsx +39 -26
  154. package/src/internals/canvas/PageSelector.jsx +15 -1
  155. package/src/internals/canvas/PageSelector.module.css +14 -0
  156. package/src/internals/canvas/canvasApi.js +40 -5
  157. package/src/internals/canvas/isolation/createIsolationMiddleware.js +68 -0
  158. package/src/internals/canvas/isolation/mountIsolationApp.jsx +88 -0
  159. package/src/internals/canvas/isolation/prototypeRoutes.jsx +131 -0
  160. package/src/internals/canvas/isolation/prototypesEntry.jsx +28 -0
  161. package/src/internals/canvas/isolation/storiesEntry.jsx +20 -0
  162. package/src/internals/canvas/isolation/storyRoutes.jsx +55 -0
  163. package/src/internals/canvas/useUndoRedo.js +75 -60
  164. package/src/internals/canvas/useUndoRedo.test.js +46 -178
  165. package/src/internals/canvas/widgets/ExpandedPaneTopBar.jsx +1 -1
  166. package/src/internals/canvas/widgets/FrozenTerminalOverlay.jsx +30 -21
  167. package/src/internals/canvas/widgets/ImageWidget.jsx +7 -2
  168. package/src/internals/canvas/widgets/LinkPreview.module.css +2 -2
  169. package/src/internals/canvas/widgets/PrototypeEmbed.jsx +69 -10
  170. package/src/internals/canvas/widgets/PrototypeEmbed.test.jsx +114 -0
  171. package/src/internals/canvas/widgets/StorySetWidget.jsx +8 -4
  172. package/src/internals/canvas/widgets/StoryWidget.jsx +21 -38
  173. package/src/internals/canvas/widgets/TerminalReadWidget.jsx +13 -15
  174. package/src/internals/canvas/widgets/WidgetChrome.jsx +1 -1
  175. package/src/internals/canvas/widgets/WidgetChrome.module.css +15 -0
  176. package/src/internals/canvas/widgets/embedTheme.js +2 -2
  177. package/src/internals/canvas/widgets/expandUtils.js +19 -19
  178. package/src/internals/canvas/widgets/normalizeLegacyEmbedSrc.js +74 -0
  179. package/src/internals/context.jsx +22 -1
  180. package/src/internals/context.test.jsx +47 -0
  181. package/src/internals/hooks/useArtifactIndex.js +31 -0
  182. package/src/internals/hooks/useArtifactIndex.test.js +45 -0
  183. package/src/internals/hooks/usePrototypeReloadGuard.js +14 -8
  184. package/src/internals/index.js +6 -0
  185. package/src/internals/loadConsumerWrapper.js +60 -0
  186. package/src/internals/story/ComponentSetPage.jsx +75 -34
  187. package/src/internals/story/StoryPage.jsx +16 -25
  188. package/src/internals/vite/data-plugin.js +313 -148
  189. package/src/internals/vite/data-plugin.test.js +11 -11
  190. package/{scaffold/storyboard.config.json → storyboard.config.json} +25 -0
  191. package/terminal.config.json +1 -1
  192. package/src/core/lib/components/ui/card/card-action.jsx +0 -11
  193. package/src/core/lib/components/ui/card/card-content.jsx +0 -11
  194. package/src/core/lib/components/ui/card/card-description.jsx +0 -11
  195. package/src/core/lib/components/ui/card/card-footer.jsx +0 -11
  196. package/src/core/lib/components/ui/card/card-header.jsx +0 -19
  197. package/src/core/lib/components/ui/card/card-title.jsx +0 -11
  198. package/src/core/lib/components/ui/card/card.jsx +0 -17
  199. package/src/core/lib/components/ui/card/index.js +0 -25
  200. package/src/core/lib/components/ui/collapsible/collapsible-content.jsx +0 -7
  201. package/src/core/lib/components/ui/collapsible/collapsible-trigger.jsx +0 -7
  202. package/src/core/lib/components/ui/collapsible/collapsible.jsx +0 -7
  203. package/src/core/lib/components/ui/collapsible/index.js +0 -13
  204. package/src/core/lib/components/ui/dialog/dialog-close.jsx +0 -7
  205. package/src/core/lib/components/ui/dialog/dialog-description.jsx +0 -15
  206. package/src/core/lib/components/ui/dialog/dialog-footer.jsx +0 -23
  207. package/src/core/lib/components/ui/dialog/dialog-header.jsx +0 -11
  208. package/src/core/lib/components/ui/dialog/dialog-overlay.jsx +0 -15
  209. package/src/core/lib/components/ui/dialog/dialog-portal.jsx +0 -4
  210. package/src/core/lib/components/ui/dialog/dialog-title.jsx +0 -15
  211. package/src/core/lib/components/ui/popover/index.js +0 -28
  212. package/src/core/lib/components/ui/popover/popover-close.jsx +0 -7
  213. package/src/core/lib/components/ui/popover/popover-content.jsx +0 -22
  214. package/src/core/lib/components/ui/popover/popover-description.jsx +0 -11
  215. package/src/core/lib/components/ui/popover/popover-header.jsx +0 -11
  216. package/src/core/lib/components/ui/popover/popover-portal.jsx +0 -4
  217. package/src/core/lib/components/ui/popover/popover-title.jsx +0 -11
  218. package/src/core/lib/components/ui/popover/popover-trigger.jsx +0 -8
  219. package/src/core/lib/components/ui/popover/popover.jsx +0 -4
  220. package/src/core/lib/components/ui/select/index.js +0 -37
  221. package/src/core/lib/components/ui/select/select-content.jsx +0 -30
  222. package/src/core/lib/components/ui/select/select-group-heading.jsx +0 -17
  223. package/src/core/lib/components/ui/select/select-group.jsx +0 -15
  224. package/src/core/lib/components/ui/select/select-item.jsx +0 -26
  225. package/src/core/lib/components/ui/select/select-label.jsx +0 -11
  226. package/src/core/lib/components/ui/select/select-portal.jsx +0 -4
  227. package/src/core/lib/components/ui/select/select-scroll-down-button.jsx +0 -18
  228. package/src/core/lib/components/ui/select/select-scroll-up-button.jsx +0 -18
  229. package/src/core/lib/components/ui/select/select-separator.jsx +0 -15
  230. package/src/core/lib/components/ui/select/select-trigger.jsx +0 -25
  231. package/src/core/lib/components/ui/select/select.jsx +0 -4
  232. package/src/core/lib/components/ui/sheet/index.js +0 -34
  233. package/src/core/lib/components/ui/sheet/sheet-close.jsx +0 -7
  234. package/src/core/lib/components/ui/sheet/sheet-content.jsx +0 -35
  235. package/src/core/lib/components/ui/sheet/sheet-description.jsx +0 -15
  236. package/src/core/lib/components/ui/sheet/sheet-footer.jsx +0 -11
  237. package/src/core/lib/components/ui/sheet/sheet-header.jsx +0 -11
  238. package/src/core/lib/components/ui/sheet/sheet-overlay.jsx +0 -15
  239. package/src/core/lib/components/ui/sheet/sheet-portal.jsx +0 -4
  240. package/src/core/lib/components/ui/sheet/sheet-title.jsx +0 -15
  241. package/src/core/lib/components/ui/sheet/sheet-trigger.jsx +0 -7
  242. package/src/core/lib/components/ui/sheet/sheet.jsx +0 -4
  243. package/src/core/lib/components/ui/toggle/index.js +0 -8
  244. package/src/core/lib/components/ui/toggle/toggle.jsx +0 -36
  245. package/src/core/lib/components/ui/toggle-group/index.js +0 -10
  246. package/src/core/lib/components/ui/toggle-group/toggle-group-item.jsx +0 -29
  247. package/src/core/lib/components/ui/toggle-group/toggle-group.jsx +0 -43
  248. package/src/internals/canvas/StoryErrorBoundary.jsx +0 -47
  249. package/src/internals/canvas/componentIsolate.jsx +0 -144
  250. package/src/internals/canvas/componentSetIsolate.jsx +0 -300
  251. package/src/internals/canvas/widgets/InlineStoryRenderer.jsx +0 -129
  252. package/src/internals/canvas/widgets/InlineStoryRenderer.test.jsx +0 -68
  253. package/src/primer/ThemeSync.jsx +0 -73
  254. package/src/primer/index.js +0 -1
@@ -1,300 +0,0 @@
1
- /**
2
- * Canvas Component-Set Isolate — lightweight iframe entry point.
3
- *
4
- * Renders ALL named exports from a .story.jsx module in a grid layout,
5
- * bypassing the full SPA bootstrap (router, StoryboardProvider, data index).
6
- *
7
- * This is the component-set equivalent of componentIsolate.jsx, which renders
8
- * a single export. By avoiding the full app bootstrap, component-set widgets
9
- * load significantly faster — especially important since each widget is an
10
- * iframe that would otherwise need to initialize the entire app.
11
- *
12
- * Query params:
13
- * module — absolute or base-relative path to the .story.jsx file
14
- * layout — "horizontal" (default) | "vertical"
15
- * selected — export name of the currently selected cell
16
- * theme — canvas theme (light / dark / dark_dimmed)
17
- */
18
- import { createElement, Component as ReactComponent, useState, useEffect, useLayoutEffect, useCallback, useRef } from 'react'
19
- import { createRoot } from 'react-dom/client'
20
- import { ThemeProvider, BaseStyles } from '@primer/react'
21
-
22
- // ── Primer Primitives CSS (required for CSS variables) ──────────────
23
- import '@primer/primitives/dist/css/base/size/size.css'
24
- import '@primer/primitives/dist/css/base/typography/typography.css'
25
- import '@primer/primitives/dist/css/base/motion/motion.css'
26
- import '@primer/primitives/dist/css/functional/size/border.css'
27
- import '@primer/primitives/dist/css/functional/size/breakpoints.css'
28
- import '@primer/primitives/dist/css/functional/size/size-coarse.css'
29
- import '@primer/primitives/dist/css/functional/size/size-fine.css'
30
- import '@primer/primitives/dist/css/functional/size/size.css'
31
- import '@primer/primitives/dist/css/functional/size/viewport.css'
32
- import '@primer/primitives/dist/css/functional/typography/typography.css'
33
- import '@primer/primitives/dist/css/functional/themes/light.css'
34
- import '@primer/primitives/dist/css/functional/themes/light-colorblind.css'
35
- import '@primer/primitives/dist/css/functional/themes/dark.css'
36
- import '@primer/primitives/dist/css/functional/themes/dark-colorblind.css'
37
- import '@primer/primitives/dist/css/functional/themes/dark-high-contrast.css'
38
- import '@primer/primitives/dist/css/functional/themes/dark-dimmed.css'
39
-
40
- import styles from '../story/ComponentSetPage.module.css'
41
-
42
- // ── Error Boundary ──────────────────────────────────────────────────
43
- class IsolateErrorBoundary extends ReactComponent {
44
- constructor(props) {
45
- super(props)
46
- this.state = { error: null }
47
- }
48
- static getDerivedStateFromError(error) {
49
- return { error }
50
- }
51
- render() {
52
- if (this.state.error) {
53
- return createElement('div', { style: errorStyle },
54
- createElement('strong', null, this.props.name || 'Component'),
55
- createElement('br'),
56
- String(this.state.error.message || this.state.error),
57
- )
58
- }
59
- return this.props.children
60
- }
61
- }
62
-
63
- const errorStyle = {
64
- padding: '16px',
65
- color: '#cf222e',
66
- fontFamily: 'system-ui, -apple-system, sans-serif',
67
- fontSize: '13px',
68
- lineHeight: 1.5,
69
- whiteSpace: 'pre-wrap',
70
- wordBreak: 'break-word',
71
- }
72
-
73
- // ── Resolve module path ─────────────────────────────────────────────
74
- function resolveModulePath(raw) {
75
- if (!raw) return raw
76
- if (/^[a-zA-Z][a-zA-Z\d+\-.]*:/.test(raw)) return raw
77
- if (!raw.startsWith('/')) return raw
78
- const base = (import.meta.env?.BASE_URL || '/').replace(/\/$/, '')
79
- if (!base) return raw
80
- if (raw.startsWith(base)) return raw
81
- return `${base}${raw}`
82
- }
83
-
84
- // ── Component-Set Grid (mirrors ComponentSetPage UI) ────────────────
85
- function ComponentSetGrid({ exports, layout, density, initialSelected }) {
86
- const [selected, setSelected] = useState(initialSelected)
87
- const gridRef = useRef(null)
88
-
89
- const handleSelect = useCallback((exportName) => {
90
- const next = exportName === selected ? '' : exportName
91
- setSelected(next)
92
-
93
- // Update URL without navigation
94
- const params = new URLSearchParams(window.location.search)
95
- if (next) params.set('selected', next)
96
- else params.delete('selected')
97
- window.history.replaceState(null, '', `${window.location.pathname}?${params}`)
98
-
99
- // Notify parent widget
100
- if (window.parent !== window) {
101
- window.parent.postMessage({
102
- type: 'storyboard:component-set:select',
103
- exportName: next || null,
104
- }, '*')
105
- }
106
- }, [selected])
107
-
108
- // Post a sensible initial widget size to the parent. Each cell sizes to
109
- // its own content via CSS; here we measure individual cells (which have
110
- // intrinsic widths thanks to `minmax` + the iframe being narrow at first)
111
- // and arrange them in a near-square grid to compute an initial widget size.
112
- useLayoutEffect(() => {
113
- const grid = gridRef.current
114
- if (!grid || !exports) return
115
-
116
- let posted = false
117
- function measureAndPost() {
118
- if (posted || window.parent === window) return
119
- const cells = grid.querySelectorAll(`.${styles.cell}`)
120
- if (cells.length === 0) return
121
- let maxW = 0
122
- let maxH = 0
123
- cells.forEach((cell) => {
124
- const r = cell.getBoundingClientRect()
125
- if (r.width > maxW) maxW = r.width
126
- if (r.height > maxH) maxH = r.height
127
- })
128
- if (maxW < 10 || maxH < 10) return
129
- const count = cells.length
130
- const cols = Math.max(1, Math.min(count, Math.ceil(Math.sqrt(count))))
131
- const rows = Math.ceil(count / cols)
132
- const gap = 16
133
- const pad = 32 // 16px padding on both sides
134
- const width = Math.ceil(maxW * cols + gap * (cols - 1) + pad)
135
- const height = Math.ceil(maxH * rows + gap * (rows - 1) + pad)
136
- posted = true
137
- window.parent.postMessage({
138
- type: 'storyboard:component-set:initial-size',
139
- width,
140
- height,
141
- }, '*')
142
- }
143
-
144
- // Re-post on grid size changes too (content height changes as cells reflow).
145
- // Measure from cell bounding boxes (not scrollWidth/scrollHeight) so the
146
- // reported content size reflects the *natural* extent of the cells —
147
- // scrollWidth always matches the container width when the widget is
148
- // wider than needed, which would defeat width-snapping on resize end.
149
- function postContentSize() {
150
- if (window.parent === window) return
151
- const cells = grid.querySelectorAll(`.${styles.cell}`)
152
- if (!cells.length) return
153
- const gridRect = grid.getBoundingClientRect()
154
- let maxRight = 0
155
- let maxBottom = 0
156
- cells.forEach((cell) => {
157
- const r = cell.getBoundingClientRect()
158
- const right = r.right - gridRect.left
159
- const bottom = r.bottom - gridRect.top
160
- if (right > maxRight) maxRight = right
161
- if (bottom > maxBottom) maxBottom = bottom
162
- })
163
- const pad = 16 // mirror grid padding
164
- window.parent.postMessage({
165
- type: 'storyboard:component-set:content-size',
166
- width: Math.ceil(maxRight + pad),
167
- height: Math.ceil(maxBottom + pad),
168
- }, '*')
169
- }
170
- const ro = new ResizeObserver(postContentSize)
171
- ro.observe(grid)
172
-
173
- // Measure after layout + fonts settle
174
- requestAnimationFrame(() => requestAnimationFrame(measureAndPost))
175
- document.fonts.ready.then(() => requestAnimationFrame(measureAndPost))
176
-
177
- return () => ro.disconnect()
178
- }, [exports, layout, density])
179
-
180
- // Signal snapshot-ready
181
- useEffect(() => {
182
- document.fonts.ready.then(() => {
183
- requestAnimationFrame(() => requestAnimationFrame(() => {
184
- window.__sbSnapshotReady?.()
185
- }))
186
- })
187
- }, [exports])
188
-
189
- const exportNames = Object.keys(exports)
190
-
191
- // eslint-disable-next-line react-hooks/refs -- ref assigned to DOM element, not read during render
192
- return createElement('div', {
193
- ref: gridRef,
194
- className: styles.grid,
195
- 'data-layout': layout,
196
- 'data-density': density,
197
- },
198
- exportNames.map((exportName) => {
199
- const Component = exports[exportName]
200
- const isSelected = exportName === selected
201
- const cellStyle = typeof Component.minHeight === 'number'
202
- ? { '--cell-min-h': `${Component.minHeight}px` }
203
- : undefined
204
- return createElement('div', {
205
- key: exportName,
206
- className: styles.cell,
207
- 'data-selected': isSelected || undefined,
208
- style: cellStyle,
209
- },
210
- createElement('button', {
211
- className: styles.cellLabel,
212
- onClick: () => handleSelect(exportName),
213
- 'data-selected': isSelected || undefined,
214
- 'aria-pressed': isSelected,
215
- },
216
- createElement('span', { className: styles.cellRadio, 'data-selected': isSelected || undefined }),
217
- createElement('span', { className: styles.cellName }, exportName),
218
- ),
219
- createElement('div', { className: styles.cellContent, 'data-cell-content': '' },
220
- createElement(IsolateErrorBoundary, { name: exportName },
221
- createElement(Component),
222
- ),
223
- ),
224
- )
225
- }),
226
- )
227
- }
228
-
229
- // ── Main ────────────────────────────────────────────────────────────
230
- const params = new URLSearchParams(window.location.search)
231
- const modulePath = params.get('module')
232
- const layoutParam = params.get('layout') || 'auto'
233
- const layout = layoutParam === 'horizontal' ? 'wide' : layoutParam === 'vertical' ? 'tall' : layoutParam
234
- const density = params.get('density') || 'comfy'
235
- const selected = params.get('selected') || ''
236
- const theme = params.get('theme') || 'light'
237
-
238
- const colorMode = theme.startsWith('dark') ? 'night' : 'day'
239
-
240
- document.documentElement.setAttribute('data-color-mode', theme.startsWith('dark') ? 'dark' : 'light')
241
- document.documentElement.setAttribute('data-dark-theme', theme.startsWith('dark') ? theme : '')
242
- document.documentElement.setAttribute('data-light-theme', theme.startsWith('dark') ? '' : theme || 'light')
243
- document.documentElement.setAttribute('data-sb-theme', theme || 'light')
244
-
245
- // Note: we deliberately do NOT install the canvas-hmr-guard here. The guard
246
- // drops BOTH `full-reload` and `update` payloads server-side, which means
247
- // edits to the story source file never reach this iframe — no Fast Refresh,
248
- // no reload, nothing. Story/component-set widgets are expected to refresh
249
- // when the underlying .story.jsx is edited, so we let HMR through.
250
- void import.meta.hot
251
-
252
- const root = createRoot(document.getElementById('root'))
253
-
254
- async function mount() {
255
- if (!modulePath) {
256
- root.render(createElement('div', { style: errorStyle }, 'Missing module param'))
257
- return
258
- }
259
-
260
- if (!modulePath.match(/\.story\.(jsx|tsx)$/)) {
261
- root.render(createElement('div', { style: errorStyle }, 'Invalid module path — only .story.jsx/.tsx files are allowed'))
262
- return
263
- }
264
-
265
- try {
266
- const resolved = resolveModulePath(modulePath)
267
- const mod = await import(/* @vite-ignore */ resolved)
268
-
269
- const namedExports = {}
270
- for (const [key, value] of Object.entries(mod)) {
271
- if (key !== 'default' && typeof value === 'function') {
272
- // Opt-out: showcase exports that already render every variant.
273
- if (value.componentSet === false) continue
274
- namedExports[key] = value
275
- }
276
- }
277
-
278
- if (Object.keys(namedExports).length === 0) {
279
- throw new Error('No named exports found in story module')
280
- }
281
-
282
- root.render(
283
- createElement(ThemeProvider, { colorMode },
284
- createElement(BaseStyles, null,
285
- createElement(ComponentSetGrid, { exports: namedExports, layout, density, initialSelected: selected }),
286
- ),
287
- ),
288
- )
289
- } catch (err) {
290
- root.render(
291
- createElement('div', { style: errorStyle },
292
- createElement('strong', null, 'Component Set'),
293
- createElement('br'),
294
- String(err.message || err),
295
- ),
296
- )
297
- }
298
- }
299
-
300
- mount()
@@ -1,129 +0,0 @@
1
- /**
2
- * InlineStoryRenderer — renders a story's named export directly inside the
3
- * canvas React tree (no iframe).
4
- *
5
- * Wrapped in:
6
- * - <ThemeProvider colorMode> + <BaseStyles> so the story's Primer theme can
7
- * differ from the canvas chrome theme.
8
- * - <StoryErrorBoundary> so a thrown render error stays scoped to this widget.
9
- *
10
- * Re-imports the module on `storyboard:story-index-changed` events so HMR-driven
11
- * story updates show without remounting the iframe shell.
12
- *
13
- * Note: stories using `useOverride` / `useFlowData` will share the canvas URL
14
- * hash. This is a known limitation of the inline path; iframe path remains
15
- * available via `canvas.inlineStories: false`.
16
- */
17
- import { useState, useEffect, useMemo } from 'react'
18
- import { ThemeProvider, BaseStyles } from '@primer/react'
19
- import { getStoryData } from '../../../core/index.js'
20
- import { StoryErrorBoundary } from '../StoryErrorBoundary.jsx'
21
-
22
- function resolveColorMode() {
23
- if (typeof document === 'undefined') return 'day'
24
- const attr = document.documentElement.getAttribute('data-color-mode')
25
- if (attr === 'dark') return 'night'
26
- if (attr === 'light') return 'day'
27
- if (typeof window !== 'undefined' && typeof window.matchMedia === 'function') {
28
- return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'night' : 'day'
29
- }
30
- return 'day'
31
- }
32
-
33
- export default function InlineStoryRenderer({ storyId, exportName }) {
34
- const [mod, setMod] = useState(null)
35
- const [error, setError] = useState(null)
36
- const [storyIndexKey, setStoryIndexKey] = useState(0)
37
- const [colorMode, setColorMode] = useState(() => resolveColorMode())
38
-
39
- // Track parent theme changes
40
- useEffect(() => {
41
- const onTheme = () => setColorMode(resolveColorMode())
42
- document.addEventListener('storyboard:theme:changed', onTheme)
43
- return () => document.removeEventListener('storyboard:theme:changed', onTheme)
44
- }, [])
45
-
46
- // Re-import on story index change (HMR / new exports added)
47
- useEffect(() => {
48
- const handler = () => setStoryIndexKey((k) => k + 1)
49
- document.addEventListener('storyboard:story-index-changed', handler)
50
- return () => document.removeEventListener('storyboard:story-index-changed', handler)
51
- }, [])
52
-
53
- // storyIndexKey forces re-lookup on HMR
54
- // eslint-disable-next-line react-hooks/exhaustive-deps
55
- const story = useMemo(() => getStoryData(storyId), [storyId, storyIndexKey])
56
- const lookupError = useMemo(() => {
57
- if (!story) return new Error(`Story "${storyId}" not found`)
58
- if (typeof story._storyImport !== 'function') return new Error(`Story "${storyId}" has no import function`)
59
- return null
60
- }, [story, storyId])
61
-
62
- useEffect(() => {
63
- if (lookupError) return
64
- let cancelled = false
65
- // eslint-disable-next-line react-hooks/set-state-in-effect
66
- setError(null)
67
- story._storyImport()
68
- .then((m) => { if (!cancelled) setMod(m) })
69
- .catch((err) => { if (!cancelled) { setError(err); setMod(null) } })
70
- return () => { cancelled = true }
71
- }, [story, lookupError, storyIndexKey])
72
-
73
- const displayedError = lookupError || error
74
-
75
- const Component = useMemo(() => {
76
- if (!mod) return null
77
- if (exportName) {
78
- const C = mod[exportName]
79
- return typeof C === 'function' ? C : null
80
- }
81
- // No exportName — render the first named function export
82
- for (const [k, v] of Object.entries(mod)) {
83
- if (k !== 'default' && typeof v === 'function') return v
84
- }
85
- return null
86
- }, [mod, exportName])
87
-
88
- const errorStyle = {
89
- padding: '16px',
90
- color: '#cf222e',
91
- fontFamily: 'system-ui, -apple-system, sans-serif',
92
- fontSize: '13px',
93
- lineHeight: 1.5,
94
- whiteSpace: 'pre-wrap',
95
- wordBreak: 'break-word',
96
- }
97
-
98
- if (displayedError) {
99
- return (
100
- <div style={errorStyle}>
101
- <strong>{exportName || storyId}</strong>
102
- <br />
103
- {String(displayedError.message || displayedError)}
104
- </div>
105
- )
106
- }
107
- if (!mod) return null
108
- if (!Component) {
109
- return (
110
- <div style={errorStyle}>
111
- <strong>{exportName || storyId}</strong>
112
- <br />
113
- {exportName ? `Export "${exportName}" not found or is not a component` : 'No named exports found in story module'}
114
- </div>
115
- )
116
- }
117
-
118
- return (
119
- <div style={{ contain: 'layout paint style', width: '100%', height: '100%', overflow: 'auto' }}>
120
- <ThemeProvider colorMode={colorMode}>
121
- <BaseStyles>
122
- <StoryErrorBoundary name={exportName || storyId} resetKey={`${storyId}:${exportName}:${storyIndexKey}`}>
123
- <Component />
124
- </StoryErrorBoundary>
125
- </BaseStyles>
126
- </ThemeProvider>
127
- </div>
128
- )
129
- }
@@ -1,68 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
2
- import { render, screen, act, waitFor } from '@testing-library/react'
3
- import InlineStoryRenderer from './InlineStoryRenderer.jsx'
4
-
5
- vi.mock('../../../core/index.js', () => ({
6
- getStoryData: vi.fn(),
7
- }))
8
-
9
- import { getStoryData } from '../../../core/index.js'
10
-
11
- function GoodComponent() { return <div data-testid="good">hello</div> }
12
- function ThrowingComponent() { throw new Error('boom') }
13
-
14
- describe('InlineStoryRenderer', () => {
15
- beforeEach(() => {
16
- vi.spyOn(console, 'error').mockImplementation(() => {})
17
- })
18
- afterEach(() => {
19
- vi.restoreAllMocks()
20
- getStoryData.mockReset()
21
- })
22
-
23
- it('renders the named export when found', async () => {
24
- getStoryData.mockReturnValue({
25
- _storyImport: () => Promise.resolve({ Default: GoodComponent }),
26
- })
27
- render(<InlineStoryRenderer storyId="my-story" exportName="Default" />)
28
- await waitFor(() => expect(screen.getByTestId('good')).toBeTruthy())
29
- })
30
-
31
- it('reports missing story', () => {
32
- getStoryData.mockReturnValue(null)
33
- render(<InlineStoryRenderer storyId="missing" exportName="Default" />)
34
- expect(screen.getByText(/Story "missing" not found/)).toBeTruthy()
35
- })
36
-
37
- it('reports missing export', async () => {
38
- getStoryData.mockReturnValue({
39
- _storyImport: () => Promise.resolve({ Other: GoodComponent }),
40
- })
41
- render(<InlineStoryRenderer storyId="s" exportName="Default" />)
42
- await waitFor(() => expect(screen.getByText(/Export "Default" not found/)).toBeTruthy())
43
- })
44
-
45
- it('contains thrown render errors via error boundary', async () => {
46
- getStoryData.mockReturnValue({
47
- _storyImport: () => Promise.resolve({ Default: ThrowingComponent }),
48
- })
49
- render(<InlineStoryRenderer storyId="bad" exportName="Default" />)
50
- await waitFor(() => expect(screen.getByText(/boom/)).toBeTruthy())
51
- })
52
-
53
- it('re-imports on storyboard:story-index-changed', async () => {
54
- let calls = 0
55
- getStoryData.mockImplementation(() => ({
56
- _storyImport: () => {
57
- calls += 1
58
- return Promise.resolve({ Default: GoodComponent })
59
- },
60
- }))
61
- render(<InlineStoryRenderer storyId="s" exportName="Default" />)
62
- await waitFor(() => expect(calls).toBe(1))
63
- act(() => {
64
- document.dispatchEvent(new Event('storyboard:story-index-changed'))
65
- })
66
- await waitFor(() => expect(calls).toBe(2))
67
- })
68
- })
@@ -1,73 +0,0 @@
1
- /**
2
- * ThemeSync — invisible React component that bridges the storyboard-core
3
- * toolbar theme switcher with Primer's ThemeProvider context.
4
- *
5
- * Listens for `storyboard:theme:changed` custom events dispatched by the
6
- * core theme store and calls setColorMode/setDayScheme/setNightScheme on
7
- * Primer's useTheme() hook accordingly.
8
- *
9
- * On mount it reads localStorage to initialize Primer to the correct
10
- * scheme before the Svelte CoreUIBar has loaded.
11
- *
12
- * When prototype sync is disabled (via "Apply theme to" settings), the
13
- * prototype is forced to light mode regardless of the selected theme.
14
- */
15
-
16
- import { useEffect } from 'react'
17
- import { useTheme } from '@primer/react'
18
-
19
- const THEME_STORAGE_KEY = 'sb-color-scheme'
20
- const THEME_SYNC_STORAGE_KEY = 'sb-theme-sync'
21
-
22
- const DEFAULT_SYNC = {
23
- prototype: true,
24
- toolbar: false,
25
- codeBoxes: true,
26
- }
27
-
28
- function readSyncTargets() {
29
- try {
30
- const raw = localStorage.getItem(THEME_SYNC_STORAGE_KEY)
31
- if (!raw) return DEFAULT_SYNC
32
- return { ...DEFAULT_SYNC, ...JSON.parse(raw) }
33
- } catch {
34
- return DEFAULT_SYNC
35
- }
36
- }
37
-
38
- function applyToPrimer(setColorMode, setDayScheme, setNightScheme, themeValue) {
39
- if (themeValue === 'system' || !themeValue) {
40
- setColorMode('auto')
41
- setDayScheme('light')
42
- setNightScheme('dark')
43
- } else {
44
- setColorMode('day')
45
- setDayScheme(themeValue)
46
- setNightScheme(themeValue)
47
- }
48
- }
49
-
50
- export default function ThemeSync() {
51
- const { setColorMode, setDayScheme, setNightScheme } = useTheme()
52
-
53
- // Restore saved theme on mount
54
- useEffect(() => {
55
- const saved = localStorage.getItem(THEME_STORAGE_KEY)
56
- const syncTargets = readSyncTargets()
57
- const prototypeTheme = syncTargets.prototype ? saved : 'light'
58
- applyToPrimer(setColorMode, setDayScheme, setNightScheme, prototypeTheme)
59
- }, []) // eslint-disable-line react-hooks/exhaustive-deps
60
-
61
- // Listen for theme changes from the Svelte CoreUIBar
62
- useEffect(() => {
63
- function handleThemeChanged(e) {
64
- const { prototypeTheme } = e.detail || {}
65
- if (typeof prototypeTheme !== 'string') return
66
- applyToPrimer(setColorMode, setDayScheme, setNightScheme, prototypeTheme)
67
- }
68
- document.addEventListener('storyboard:theme:changed', handleThemeChanged)
69
- return () => document.removeEventListener('storyboard:theme:changed', handleThemeChanged)
70
- }, [setColorMode, setDayScheme, setNightScheme])
71
-
72
- return null
73
- }
@@ -1 +0,0 @@
1
- export { default as ThemeSync } from './ThemeSync.jsx'