@scaffold-ui-kit/preview 0.0.0-stage → 2.3.0

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 (350) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1868 -2
  3. package/application/host/bootstrap.ts +43 -0
  4. package/application/host/main.ts +31 -0
  5. package/application/host/shell-host.ts +47 -0
  6. package/application/index.html +12 -0
  7. package/application/lib/base-url.ts +15 -0
  8. package/application/lib/no-code-block.ts +7 -0
  9. package/application/lib/no-setup.ts +3 -0
  10. package/application/lib/no-views.ts +6 -0
  11. package/application/preview/code/code.html +12 -0
  12. package/application/preview/code/main.ts +89 -0
  13. package/application/preview/globals.ts +254 -0
  14. package/application/preview/html/main.ts +93 -0
  15. package/application/preview/html/preview.html +12 -0
  16. package/application/preview/react/main.tsx +146 -0
  17. package/application/preview/react/preview.html +12 -0
  18. package/application/preview/svelte/Preview.svelte +104 -0
  19. package/application/preview/svelte/main.ts +13 -0
  20. package/application/preview/svelte/preview.html +12 -0
  21. package/application/preview/svelte/story-props.ts +172 -0
  22. package/application/preview/view/main.ts +124 -0
  23. package/application/preview/view/view.html +12 -0
  24. package/application/preview/vue/main.ts +145 -0
  25. package/application/preview/vue/preview.html +12 -0
  26. package/application/shells/charcell/components/Application/index.css +37 -0
  27. package/application/shells/charcell/components/Application/index.tsx +65 -0
  28. package/application/shells/charcell/components/Bar/index.css +68 -0
  29. package/application/shells/charcell/components/Bar/index.tsx +60 -0
  30. package/application/shells/charcell/components/CodePane/index.css +60 -0
  31. package/application/shells/charcell/components/CodePane/index.tsx +113 -0
  32. package/application/shells/charcell/components/ComponentStage/index.css +26 -0
  33. package/application/shells/charcell/components/ComponentStage/index.tsx +110 -0
  34. package/application/shells/charcell/components/Control/index.css +20 -0
  35. package/application/shells/charcell/components/Control/index.tsx +118 -0
  36. package/application/shells/charcell/components/CopyLink/index.tsx +35 -0
  37. package/application/shells/charcell/components/Description/index.css +6 -0
  38. package/application/shells/charcell/components/Description/index.tsx +24 -0
  39. package/application/shells/charcell/components/EditorAddDialog/index.css +9 -0
  40. package/application/shells/charcell/components/EditorAddDialog/index.tsx +158 -0
  41. package/application/shells/charcell/components/EditorPanel/index.css +48 -0
  42. package/application/shells/charcell/components/EditorPanel/index.tsx +496 -0
  43. package/application/shells/charcell/components/EventsRow/index.css +42 -0
  44. package/application/shells/charcell/components/EventsRow/index.tsx +68 -0
  45. package/application/shells/charcell/components/HeadingRow/index.css +11 -0
  46. package/application/shells/charcell/components/HeadingRow/index.tsx +16 -0
  47. package/application/shells/charcell/components/Hint/index.css +16 -0
  48. package/application/shells/charcell/components/Hint/index.tsx +81 -0
  49. package/application/shells/charcell/components/HtmlWarnings/index.tsx +45 -0
  50. package/application/shells/charcell/components/KeyHints/index.css +27 -0
  51. package/application/shells/charcell/components/KeyHints/index.tsx +106 -0
  52. package/application/shells/charcell/components/KitCodeBlock/index.css +55 -0
  53. package/application/shells/charcell/components/KitCodeBlock/index.tsx +18 -0
  54. package/application/shells/charcell/components/Markdown/index.tsx +16 -0
  55. package/application/shells/charcell/components/Navigation/index.css +13 -0
  56. package/application/shells/charcell/components/Navigation/index.tsx +98 -0
  57. package/application/shells/charcell/components/NavigationSplit/index.css +16 -0
  58. package/application/shells/charcell/components/NavigationSplit/index.tsx +82 -0
  59. package/application/shells/charcell/components/OptionControls/index.tsx +93 -0
  60. package/application/shells/charcell/components/PaneStatus/index.css +7 -0
  61. package/application/shells/charcell/components/PaneStatus/index.tsx +47 -0
  62. package/application/shells/charcell/components/PaneWidth/index.tsx +45 -0
  63. package/application/shells/charcell/components/PreviewGrid/index.css +39 -0
  64. package/application/shells/charcell/components/PreviewGrid/index.tsx +344 -0
  65. package/application/shells/charcell/components/PreviewPane/index.css +127 -0
  66. package/application/shells/charcell/components/PreviewPane/index.tsx +182 -0
  67. package/application/shells/charcell/components/PromptBar/index.tsx +133 -0
  68. package/application/shells/charcell/components/ScenarioRow/index.css +36 -0
  69. package/application/shells/charcell/components/ScenarioRow/index.tsx +71 -0
  70. package/application/shells/charcell/components/Select/index.tsx +48 -0
  71. package/application/shells/charcell/components/SlotOutlet/index.css +7 -0
  72. package/application/shells/charcell/components/SlotOutlet/index.tsx +25 -0
  73. package/application/shells/charcell/components/StageBar/index.css +7 -0
  74. package/application/shells/charcell/components/StageBar/index.tsx +109 -0
  75. package/application/shells/charcell/components/StageSplit/index.css +11 -0
  76. package/application/shells/charcell/components/StageSplit/index.tsx +99 -0
  77. package/application/shells/charcell/components/StatusBar/index.tsx +30 -0
  78. package/application/shells/charcell/components/ViewControls/index.css +15 -0
  79. package/application/shells/charcell/components/ViewControls/index.tsx +58 -0
  80. package/application/shells/charcell/components/ViewHeading/index.css +13 -0
  81. package/application/shells/charcell/components/ViewHeading/index.tsx +8 -0
  82. package/application/shells/charcell/components/ViewStage/index.css +9 -0
  83. package/application/shells/charcell/components/ViewStage/index.tsx +25 -0
  84. package/application/shells/charcell/components/WrapToggle/index.tsx +27 -0
  85. package/application/shells/charcell/configuration.ts +45 -0
  86. package/application/shells/charcell/controller-context.ts +27 -0
  87. package/application/shells/charcell/fit-labels.ts +42 -0
  88. package/application/shells/charcell/html-warnings.ts +18 -0
  89. package/application/shells/charcell/index.tsx +82 -0
  90. package/application/shells/charcell/page-loading.html +57 -0
  91. package/application/shells/charcell/placement.ts +24 -0
  92. package/application/shells/charcell/shell-options.ts +23 -0
  93. package/application/shells/charcell/shell.css +37 -0
  94. package/application/shells/charcell/shell.tsx +25 -0
  95. package/application/shells/charcell/split-reset.ts +47 -0
  96. package/application/shells/charcell/stacked-rows.ts +117 -0
  97. package/application/shells/charcell/stage-toggles.ts +93 -0
  98. package/application/types/manifest.d.ts +6 -0
  99. package/application/types/preview-configuration.d.ts +10 -0
  100. package/application/types/preview-shell.d.ts +7 -0
  101. package/application/types/react-registry.d.ts +15 -0
  102. package/application/types/vue-registry.d.ts +6 -0
  103. package/dist/build.d.ts +24 -0
  104. package/dist/build.js +34 -0
  105. package/dist/check.d.ts +48 -0
  106. package/dist/check.js +781 -0
  107. package/dist/cli.d.ts +2 -0
  108. package/dist/cli.js +88 -0
  109. package/dist/context.d.ts +179 -0
  110. package/dist/context.js +783 -0
  111. package/dist/controller/catalog.d.ts +339 -0
  112. package/dist/controller/catalog.js +367 -0
  113. package/dist/controller/create-preview-controller.d.ts +532 -0
  114. package/dist/controller/create-preview-controller.js +1707 -0
  115. package/dist/controller/editor-tree.d.ts +161 -0
  116. package/dist/controller/editor-tree.js +373 -0
  117. package/dist/controller/html-source.d.ts +62 -0
  118. package/dist/controller/html-source.js +61 -0
  119. package/dist/controller/index.d.ts +9 -0
  120. package/dist/controller/index.js +12 -0
  121. package/dist/controller/persistence.d.ts +191 -0
  122. package/dist/controller/persistence.js +315 -0
  123. package/dist/controller/regeneration.d.ts +30 -0
  124. package/dist/controller/regeneration.js +27 -0
  125. package/dist/controller/source.d.ts +80 -0
  126. package/dist/controller/source.js +236 -0
  127. package/dist/controller/store.d.ts +27 -0
  128. package/dist/controller/store.js +40 -0
  129. package/dist/declarations/application/shells/charcell/components/Application/index.d.ts +2 -0
  130. package/dist/declarations/application/shells/charcell/components/Bar/index.d.ts +11 -0
  131. package/dist/declarations/application/shells/charcell/components/CodePane/index.d.ts +5 -0
  132. package/dist/declarations/application/shells/charcell/components/ComponentStage/index.d.ts +2 -0
  133. package/dist/declarations/application/shells/charcell/components/Control/index.d.ts +9 -0
  134. package/dist/declarations/application/shells/charcell/components/CopyLink/index.d.ts +1 -0
  135. package/dist/declarations/application/shells/charcell/components/Description/index.d.ts +5 -0
  136. package/dist/declarations/application/shells/charcell/components/EditorAddDialog/index.d.ts +6 -0
  137. package/dist/declarations/application/shells/charcell/components/EditorPanel/index.d.ts +2 -0
  138. package/dist/declarations/application/shells/charcell/components/EventsRow/index.d.ts +4 -0
  139. package/dist/declarations/application/shells/charcell/components/HeadingRow/index.d.ts +6 -0
  140. package/dist/declarations/application/shells/charcell/components/Hint/index.d.ts +11 -0
  141. package/dist/declarations/application/shells/charcell/components/HtmlWarnings/index.d.ts +5 -0
  142. package/dist/declarations/application/shells/charcell/components/KeyHints/index.d.ts +2 -0
  143. package/dist/declarations/application/shells/charcell/components/KitCodeBlock/index.d.ts +5 -0
  144. package/dist/declarations/application/shells/charcell/components/Markdown/index.d.ts +3 -0
  145. package/dist/declarations/application/shells/charcell/components/Navigation/index.d.ts +8 -0
  146. package/dist/declarations/application/shells/charcell/components/NavigationSplit/index.d.ts +7 -0
  147. package/dist/declarations/application/shells/charcell/components/OptionControls/index.d.ts +7 -0
  148. package/dist/declarations/application/shells/charcell/components/PaneStatus/index.d.ts +2 -0
  149. package/dist/declarations/application/shells/charcell/components/PaneWidth/index.d.ts +4 -0
  150. package/dist/declarations/application/shells/charcell/components/PreviewGrid/index.d.ts +7 -0
  151. package/dist/declarations/application/shells/charcell/components/PreviewPane/index.d.ts +9 -0
  152. package/dist/declarations/application/shells/charcell/components/PromptBar/index.d.ts +1 -0
  153. package/dist/declarations/application/shells/charcell/components/ScenarioRow/index.d.ts +2 -0
  154. package/dist/declarations/application/shells/charcell/components/Select/index.d.ts +16 -0
  155. package/dist/declarations/application/shells/charcell/components/SlotOutlet/index.d.ts +5 -0
  156. package/dist/declarations/application/shells/charcell/components/StageBar/index.d.ts +2 -0
  157. package/dist/declarations/application/shells/charcell/components/StageSplit/index.d.ts +17 -0
  158. package/dist/declarations/application/shells/charcell/components/StatusBar/index.d.ts +1 -0
  159. package/dist/declarations/application/shells/charcell/components/ViewControls/index.d.ts +9 -0
  160. package/dist/declarations/application/shells/charcell/components/ViewHeading/index.d.ts +4 -0
  161. package/dist/declarations/application/shells/charcell/components/ViewStage/index.d.ts +4 -0
  162. package/dist/declarations/application/shells/charcell/components/WrapToggle/index.d.ts +1 -0
  163. package/dist/declarations/application/shells/charcell/configuration.d.ts +31 -0
  164. package/dist/declarations/application/shells/charcell/controller-context.d.ts +9 -0
  165. package/dist/declarations/application/shells/charcell/fit-labels.d.ts +18 -0
  166. package/dist/declarations/application/shells/charcell/html-warnings.d.ts +5 -0
  167. package/dist/declarations/application/shells/charcell/index.d.ts +39 -0
  168. package/dist/declarations/application/shells/charcell/placement.d.ts +7 -0
  169. package/dist/declarations/application/shells/charcell/shell-options.d.ts +15 -0
  170. package/dist/declarations/application/shells/charcell/shell.d.ts +9 -0
  171. package/dist/declarations/application/shells/charcell/split-reset.d.ts +30 -0
  172. package/dist/declarations/application/shells/charcell/stacked-rows.d.ts +58 -0
  173. package/dist/declarations/application/shells/charcell/stage-toggles.d.ts +28 -0
  174. package/dist/declarations/src/controller/catalog.d.ts +339 -0
  175. package/dist/declarations/src/controller/create-preview-controller.d.ts +532 -0
  176. package/dist/declarations/src/controller/editor-tree.d.ts +161 -0
  177. package/dist/declarations/src/controller/html-source.d.ts +62 -0
  178. package/dist/declarations/src/controller/index.d.ts +9 -0
  179. package/dist/declarations/src/controller/persistence.d.ts +191 -0
  180. package/dist/declarations/src/controller/regeneration.d.ts +30 -0
  181. package/dist/declarations/src/controller/source.d.ts +80 -0
  182. package/dist/declarations/src/controller/store.d.ts +27 -0
  183. package/dist/declarations/src/emit/common.d.ts +103 -0
  184. package/dist/declarations/src/emit/manifest.d.ts +138 -0
  185. package/dist/declarations/src/emit/source.d.ts +87 -0
  186. package/dist/declarations/src/lib/jsx-roots.d.ts +22 -0
  187. package/dist/declarations/src/lib/module-of-source.d.ts +18 -0
  188. package/dist/declarations/src/lib/naming.d.ts +29 -0
  189. package/dist/declarations/src/lib/react-spelling.d.ts +68 -0
  190. package/dist/declarations/src/lib/scenario-story.d.ts +41 -0
  191. package/dist/declarations/src/lib/slot-fill.d.ts +15 -0
  192. package/dist/declarations/src/lib/types.d.ts +741 -0
  193. package/dist/declarations/src/options.d.ts +224 -0
  194. package/dist/declarations/src/protocol.d.ts +541 -0
  195. package/dist/declarations/src/react-view.d.ts +22 -0
  196. package/dist/declarations/src/react.d.ts +17 -0
  197. package/dist/declarations/src/shell.d.ts +99 -0
  198. package/dist/declarations/src/views.d.ts +152 -0
  199. package/dist/development-server.d.ts +121 -0
  200. package/dist/development-server.js +331 -0
  201. package/dist/emit/common.d.ts +103 -0
  202. package/dist/emit/common.js +170 -0
  203. package/dist/emit/html-source.d.ts +46 -0
  204. package/dist/emit/html-source.js +322 -0
  205. package/dist/emit/manifest.d.ts +138 -0
  206. package/dist/emit/manifest.js +74 -0
  207. package/dist/emit/react.d.ts +16 -0
  208. package/dist/emit/react.js +210 -0
  209. package/dist/emit/scratch.d.ts +40 -0
  210. package/dist/emit/scratch.js +116 -0
  211. package/dist/emit/source.d.ts +87 -0
  212. package/dist/emit/source.js +789 -0
  213. package/dist/emit/svelte.d.ts +9 -0
  214. package/dist/emit/svelte.js +177 -0
  215. package/dist/emit/vue.d.ts +2 -0
  216. package/dist/emit/vue.js +152 -0
  217. package/dist/enumerate.d.ts +2 -0
  218. package/dist/enumerate.js +162 -0
  219. package/dist/generate.d.ts +43 -0
  220. package/dist/generate.js +302 -0
  221. package/dist/html-stylesheet.d.ts +43 -0
  222. package/dist/html-stylesheet.js +145 -0
  223. package/dist/index.d.ts +18 -0
  224. package/dist/index.js +21 -0
  225. package/dist/landing.d.ts +16 -0
  226. package/dist/landing.js +45 -0
  227. package/dist/lib/exported-types.d.ts +42 -0
  228. package/dist/lib/exported-types.js +86 -0
  229. package/dist/lib/html-attributes.d.ts +2 -0
  230. package/dist/lib/html-attributes.js +111 -0
  231. package/dist/lib/html-warnings.d.ts +19 -0
  232. package/dist/lib/html-warnings.js +79 -0
  233. package/dist/lib/jsx-roots.d.ts +22 -0
  234. package/dist/lib/jsx-roots.js +243 -0
  235. package/dist/lib/module-of-source.d.ts +18 -0
  236. package/dist/lib/module-of-source.js +24 -0
  237. package/dist/lib/naming.d.ts +29 -0
  238. package/dist/lib/naming.js +56 -0
  239. package/dist/lib/parse-type.d.ts +32 -0
  240. package/dist/lib/parse-type.js +103 -0
  241. package/dist/lib/react-spelling.d.ts +68 -0
  242. package/dist/lib/react-spelling.js +197 -0
  243. package/dist/lib/scenario-story.d.ts +41 -0
  244. package/dist/lib/scenario-story.js +84 -0
  245. package/dist/lib/slot-fill.d.ts +15 -0
  246. package/dist/lib/slot-fill.js +92 -0
  247. package/dist/lib/template-files.d.ts +14 -0
  248. package/dist/lib/template-files.js +25 -0
  249. package/dist/lib/types.d.ts +741 -0
  250. package/dist/lib/types.js +13 -0
  251. package/dist/lib/void-elements.d.ts +7 -0
  252. package/dist/lib/void-elements.js +13 -0
  253. package/dist/load.d.ts +37 -0
  254. package/dist/load.js +82 -0
  255. package/dist/model.d.ts +44 -0
  256. package/dist/model.js +464 -0
  257. package/dist/options.d.ts +224 -0
  258. package/dist/options.js +223 -0
  259. package/dist/prop-types.d.ts +26 -0
  260. package/dist/prop-types.js +102 -0
  261. package/dist/props-from.d.ts +84 -0
  262. package/dist/props-from.js +510 -0
  263. package/dist/protocol.d.ts +541 -0
  264. package/dist/protocol.js +819 -0
  265. package/dist/react-view.d.ts +22 -0
  266. package/dist/react-view.js +36 -0
  267. package/dist/react.d.ts +17 -0
  268. package/dist/react.js +23 -0
  269. package/dist/sass-importers.d.ts +26 -0
  270. package/dist/sass-importers.js +32 -0
  271. package/dist/shell-acceptance.d.ts +21 -0
  272. package/dist/shell-acceptance.js +37 -0
  273. package/dist/shell-page.d.ts +36 -0
  274. package/dist/shell-page.js +90 -0
  275. package/dist/shell-peers.d.ts +38 -0
  276. package/dist/shell-peers.js +61 -0
  277. package/dist/shell.d.ts +99 -0
  278. package/dist/shell.js +52 -0
  279. package/dist/shells/charcell/page.d.ts +8 -0
  280. package/dist/shells/charcell/page.js +69 -0
  281. package/dist/story-source.d.ts +113 -0
  282. package/dist/story-source.js +271 -0
  283. package/dist/story.d.ts +5 -0
  284. package/dist/story.js +28 -0
  285. package/dist/testing.d.ts +218 -0
  286. package/dist/testing.js +284 -0
  287. package/dist/views.d.ts +152 -0
  288. package/dist/views.js +7 -0
  289. package/dist/vite-configuration.d.ts +114 -0
  290. package/dist/vite-configuration.js +578 -0
  291. package/package.json +123 -3
  292. package/src/build.ts +51 -0
  293. package/src/check.ts +1062 -0
  294. package/src/cli.ts +123 -0
  295. package/src/context.ts +1168 -0
  296. package/src/controller/catalog.ts +719 -0
  297. package/src/controller/create-preview-controller.ts +2683 -0
  298. package/src/controller/editor-tree.ts +530 -0
  299. package/src/controller/html-source.ts +114 -0
  300. package/src/controller/index.ts +105 -0
  301. package/src/controller/persistence.ts +403 -0
  302. package/src/controller/regeneration.ts +55 -0
  303. package/src/controller/source.ts +333 -0
  304. package/src/controller/store.ts +55 -0
  305. package/src/development-server.ts +442 -0
  306. package/src/emit/common.ts +201 -0
  307. package/src/emit/html-source.ts +402 -0
  308. package/src/emit/manifest.ts +241 -0
  309. package/src/emit/react.ts +263 -0
  310. package/src/emit/scratch.ts +175 -0
  311. package/src/emit/source.ts +1094 -0
  312. package/src/emit/svelte.ts +230 -0
  313. package/src/emit/vue.ts +194 -0
  314. package/src/enumerate.ts +220 -0
  315. package/src/generate.ts +443 -0
  316. package/src/html-stylesheet.ts +173 -0
  317. package/src/index.ts +60 -0
  318. package/src/landing.ts +58 -0
  319. package/src/lib/exported-types.ts +114 -0
  320. package/src/lib/html-attributes.ts +113 -0
  321. package/src/lib/html-warnings.ts +81 -0
  322. package/src/lib/jsx-roots.ts +238 -0
  323. package/src/lib/module-of-source.ts +26 -0
  324. package/src/lib/naming.ts +65 -0
  325. package/src/lib/parse-type.ts +143 -0
  326. package/src/lib/react-spelling.ts +235 -0
  327. package/src/lib/scenario-story.ts +97 -0
  328. package/src/lib/slot-fill.ts +102 -0
  329. package/src/lib/template-files.ts +31 -0
  330. package/src/lib/types.ts +778 -0
  331. package/src/lib/void-elements.ts +15 -0
  332. package/src/load.ts +144 -0
  333. package/src/model.ts +568 -0
  334. package/src/options.ts +435 -0
  335. package/src/prop-types.ts +133 -0
  336. package/src/props-from.ts +678 -0
  337. package/src/protocol.ts +1135 -0
  338. package/src/react-view.ts +45 -0
  339. package/src/react.ts +34 -0
  340. package/src/sass-importers.ts +45 -0
  341. package/src/shell-acceptance.ts +45 -0
  342. package/src/shell-page.ts +133 -0
  343. package/src/shell-peers.ts +70 -0
  344. package/src/shell.ts +194 -0
  345. package/src/shells/charcell/page.ts +82 -0
  346. package/src/story-source.ts +391 -0
  347. package/src/story.ts +42 -0
  348. package/src/testing.ts +497 -0
  349. package/src/views.ts +183 -0
  350. package/src/vite-configuration.ts +675 -0
@@ -0,0 +1,1135 @@
1
+ // The messages the preview host and its iframes exchange over postMessage, the
2
+ // flag an iframe sets once it can receive them, and the URLs the iframes load.
3
+ // The host, the five iframe entries and the testing helper all speak through
4
+ // this module, so the vocabulary lives in one place. Browser-safe - no node
5
+ // imports; type-only imports keep it free of runtime dependencies.
6
+
7
+ import type { OptionValue, PreviewOption } from './options.js';
8
+ import type {
9
+ CodeBlockContent,
10
+ ViewContextComponent,
11
+ ViewContextSource,
12
+ ViewFramework,
13
+ } from './views.js';
14
+
15
+ /**
16
+ * What a story pane renders in: a scripted target, which a component preview
17
+ * iframe runs the story's script in, or `html`, the markup the story renders
18
+ * to with no JavaScript of its own.
19
+ */
20
+ export const PREVIEW_TARGETS = ['react', 'vue', 'svelte', 'html'] as const;
21
+
22
+ /** What a story pane renders in: one of {@link PREVIEW_TARGETS}. */
23
+ export type PreviewTarget = (typeof PREVIEW_TARGETS)[number];
24
+
25
+ /** A target a component preview iframe runs a story's script in: every
26
+ * target but `html`. */
27
+ export type PreviewScriptedTarget = Exclude<PreviewTarget, 'html'>;
28
+
29
+ /** The scripted targets, in the preview's order: {@link PREVIEW_TARGETS}
30
+ * without `html`. */
31
+ export const PREVIEW_SCRIPTED_TARGETS = [
32
+ 'react',
33
+ 'vue',
34
+ 'svelte',
35
+ ] as const satisfies readonly PreviewScriptedTarget[];
36
+
37
+ /** The `type` of each message, keyed by a short name. */
38
+ export const PREVIEW_MESSAGE = {
39
+ /** Iframe → host: the iframe listens for messages now (sent on each load). */
40
+ ready: 'preview:ready',
41
+ /** Host → iframe: mount a story by id with its root props. */
42
+ setStory: 'preview:set-story',
43
+ /** Host → iframe: replace the mounted story's props in place. */
44
+ props: 'preview:props',
45
+ /** Host → iframe: the merged option values to apply to the document. */
46
+ globals: 'preview:globals',
47
+ /** Host → view iframe: the catalog a custom view builds its context from. */
48
+ catalog: 'preview:catalog',
49
+ /** View iframe → host: open a component story, as a navigation click does. */
50
+ select: 'preview:select',
51
+ /** Iframe → host: the size of what the iframe shows, sent once it renders
52
+ * and again on each change. */
53
+ contentSize: 'preview:content-size',
54
+ /** Host → html iframe: the markup to show, or why there is none. */
55
+ markup: 'preview:markup',
56
+ /** Iframe → host: the story threw as it rendered, or, with no message,
57
+ * the story it mounts since renders. */
58
+ renderError: 'preview:render-error',
59
+ /** Host → code iframe: the source the kit's code block shows. */
60
+ code: 'preview:code',
61
+ /** Iframe → host: the story called one of its events. */
62
+ event: 'preview:event',
63
+ /** Iframe → host: its first content has mounted and painted, so the host
64
+ * can show the iframe. Sent once per document. */
65
+ shown: 'preview:shown',
66
+ } as const;
67
+
68
+ /** The `type` of a preview message. */
69
+ export type PreviewMessageType =
70
+ (typeof PREVIEW_MESSAGE)[keyof typeof PREVIEW_MESSAGE];
71
+
72
+ /** Sent by an iframe once its message listener is attached. */
73
+ export interface PreviewReadyMessage {
74
+ type: typeof PREVIEW_MESSAGE.ready;
75
+ }
76
+
77
+ /** Sent by an iframe once its first content has mounted and painted. */
78
+ export interface PreviewShownMessage {
79
+ type: typeof PREVIEW_MESSAGE.shown;
80
+ }
81
+
82
+ /** Mounts the story `id` afresh with `props` as its root props. */
83
+ export interface PreviewSetStoryMessage {
84
+ type: typeof PREVIEW_MESSAGE.setStory;
85
+ id: string;
86
+ props: Record<string, unknown>;
87
+ /** The story's events: the function props a pane records the calls of,
88
+ * reporting each as a `preview:event`. */
89
+ events?: string[];
90
+ /** The story's two-way props and the event each reports its new value
91
+ * through. A pane may take a value a story's given prop reports as that
92
+ * prop at once, as the host does after it (see {@link withWriteBacks}). */
93
+ bindings?: PreviewWriteBack[];
94
+ /** Which of the host's story mounts this is: a pane tags each event it
95
+ * records with it, so the host can tell an event of a story it has since
96
+ * replaced (see {@link withEventRecorders}). */
97
+ mount?: number;
98
+ }
99
+
100
+ /** A two-way prop and the event prop it reports each new value through. */
101
+ export interface PreviewWriteBack {
102
+ /** The two-way prop. */
103
+ prop: string;
104
+ /** The event prop called with each new value. */
105
+ changeProp: string;
106
+ }
107
+
108
+ /** Replaces the mounted story's root props, keeping its component instance. */
109
+ export interface PreviewPropsMessage {
110
+ type: typeof PREVIEW_MESSAGE.props;
111
+ props: Record<string, unknown>;
112
+ /** The newest of the receiving pane's own write-backs the host had taken
113
+ * when it sent the props (the `write` of the pane's events, see
114
+ * {@link PendingWriteBacks}): a pane keeps a later write of its own over
115
+ * the value these props hold. Absent, the pane takes the props as they
116
+ * are. */
117
+ answers?: number;
118
+ }
119
+
120
+ /** The option values (global and story-scoped, merged) the iframe applies. */
121
+ export interface PreviewGlobalsMessage {
122
+ type: typeof PREVIEW_MESSAGE.globals;
123
+ options: Record<string, OptionValue>;
124
+ }
125
+
126
+ /** The catalog a custom view reads: the kit's name, its sources and its stories. */
127
+ export interface PreviewCatalogPayload {
128
+ kitName: string;
129
+ sources: ViewContextSource[];
130
+ components: ViewContextComponent[];
131
+ }
132
+
133
+ /** Delivers the {@link PreviewCatalogPayload} to the view iframe. */
134
+ export interface PreviewCatalogMessage extends PreviewCatalogPayload {
135
+ type: typeof PREVIEW_MESSAGE.catalog;
136
+ }
137
+
138
+ /** Asks the host to open a component story, optionally with root props. */
139
+ export interface PreviewSelectMessage {
140
+ type: typeof PREVIEW_MESSAGE.select;
141
+ id: string;
142
+ props?: Record<string, unknown>;
143
+ }
144
+
145
+ /** The size of what a preview iframe shows, in whole CSS pixels. */
146
+ export interface PreviewContentSize {
147
+ width: number;
148
+ height: number;
149
+ }
150
+
151
+ /**
152
+ * Reports the size of what an iframe shows: its story or view, with the
153
+ * padding, borders and margins of the elements around it, rounded up to
154
+ * whole CSS pixels, so an iframe of that size shows it all.
155
+ */
156
+ export interface PreviewContentSizeMessage extends PreviewContentSize {
157
+ type: typeof PREVIEW_MESSAGE.contentSize;
158
+ }
159
+
160
+ /**
161
+ * The html iframe's story: its `markup`, or the `reason` it has none. With
162
+ * neither, the markup is being printed, and the iframe keeps what it shows.
163
+ */
164
+ export interface PreviewMarkupMessage {
165
+ type: typeof PREVIEW_MESSAGE.markup;
166
+ markup?: string;
167
+ /** The styles of the components the markup renders, shown with it. */
168
+ stylesheet?: string;
169
+ reason?: string;
170
+ }
171
+
172
+ /**
173
+ * Reports what an iframe's story threw as it rendered (`message`), shown in
174
+ * the story's place; with no `message`, the story mounted since renders.
175
+ */
176
+ export interface PreviewRenderErrorMessage {
177
+ type: typeof PREVIEW_MESSAGE.renderError;
178
+ message?: string;
179
+ }
180
+
181
+ /** The source a code iframe's block shows: a target's, in its language, or
182
+ * why it has none; with neither, it is being printed. */
183
+ export interface PreviewCodeMessage extends CodeBlockContent {
184
+ type: typeof PREVIEW_MESSAGE.code;
185
+ }
186
+
187
+ /** Reports a story's call of one of its events: its `name`, a readable
188
+ * summary of what it was called with, and its first argument as `value`
189
+ * when that is plain data (what JSON holds), such as the new value a
190
+ * two-way prop's change callback reports. */
191
+ export interface PreviewEventMessage {
192
+ type: typeof PREVIEW_MESSAGE.event;
193
+ name: string;
194
+ payload: string;
195
+ value?: unknown;
196
+ /** The `mount` of the set-story message the pane was showing. */
197
+ mount?: number;
198
+ /** The number of the newest value the pane wrote back itself since its
199
+ * last report, when it wrote one (see {@link PendingWriteBacks}): the
200
+ * host answers it on the pane's next props message. */
201
+ write?: number;
202
+ }
203
+
204
+ /** Any message the host and the iframes exchange. */
205
+ export type PreviewMessage =
206
+ | PreviewReadyMessage
207
+ | PreviewSetStoryMessage
208
+ | PreviewPropsMessage
209
+ | PreviewGlobalsMessage
210
+ | PreviewCatalogMessage
211
+ | PreviewSelectMessage
212
+ | PreviewContentSizeMessage
213
+ | PreviewMarkupMessage
214
+ | PreviewRenderErrorMessage
215
+ | PreviewCodeMessage
216
+ | PreviewEventMessage
217
+ | PreviewShownMessage;
218
+
219
+ /** The message whose `type` is `Type`. */
220
+ export type PreviewMessageOf<Type extends PreviewMessageType> = Extract<
221
+ PreviewMessage,
222
+ { type: Type }
223
+ >;
224
+
225
+ const MESSAGE_TYPES = new Set<string>(Object.values(PREVIEW_MESSAGE));
226
+
227
+ /**
228
+ * Read posted data as a preview message: the data itself when it is an object
229
+ * whose `type` is one of the preview's, otherwise `undefined`. Only the `type`
230
+ * is checked; a receiver validates the payload it relies on.
231
+ *
232
+ * @param data - A `MessageEvent`'s `data`.
233
+ * @returns The data as a {@link PreviewMessage}, or `undefined`.
234
+ */
235
+ export function readPreviewMessage(data: unknown): PreviewMessage | undefined {
236
+ if (!data || typeof data !== 'object') return undefined;
237
+ const type = (data as { type?: unknown }).type;
238
+ return typeof type === 'string' && MESSAGE_TYPES.has(type)
239
+ ? (data as PreviewMessage)
240
+ : undefined;
241
+ }
242
+
243
+ /**
244
+ * Whether posted data is a preview message of one type.
245
+ *
246
+ * @param data - A `MessageEvent`'s `data`.
247
+ * @param type - The message type to match.
248
+ * @returns `true` when `data` is an object whose `type` is `type`.
249
+ */
250
+ export function isPreviewMessage<Type extends PreviewMessageType>(
251
+ data: unknown,
252
+ type: Type
253
+ ): data is PreviewMessageOf<Type> {
254
+ return readPreviewMessage(data)?.type === type;
255
+ }
256
+
257
+ /** @returns A {@link PreviewReadyMessage}. */
258
+ export function readyMessage(): PreviewReadyMessage {
259
+ return { type: PREVIEW_MESSAGE.ready };
260
+ }
261
+
262
+ /**
263
+ * @param id - The story's id.
264
+ * @param props - Its root props.
265
+ * @param events - The function props a pane records the calls of.
266
+ * @param bindings - The two-way props and the events they report through.
267
+ * @param mount - Which of the host's story mounts this is.
268
+ * @returns A {@link PreviewSetStoryMessage}.
269
+ */
270
+ export function setStoryMessage(
271
+ id: string,
272
+ props: Record<string, unknown>,
273
+ events?: string[],
274
+ bindings?: readonly PreviewWriteBack[],
275
+ mount?: number
276
+ ): PreviewSetStoryMessage {
277
+ return {
278
+ type: PREVIEW_MESSAGE.setStory,
279
+ id,
280
+ props,
281
+ ...(mount !== undefined ? { mount } : {}),
282
+ ...(events && events.length > 0 ? { events } : {}),
283
+ ...(bindings && bindings.length > 0
284
+ ? {
285
+ bindings: bindings.map(({ prop, changeProp }) => ({
286
+ prop,
287
+ changeProp,
288
+ })),
289
+ }
290
+ : {}),
291
+ };
292
+ }
293
+
294
+ /**
295
+ * The story's props with each two-way prop's change event writing the value
296
+ * it reports back as that prop at once, through `write`, while the story
297
+ * gives the prop: what the host does with the value after it hears of the
298
+ * call, done inside the pane, so a framework that restores a controlled
299
+ * input to its prop's value as the event ends (React) finds the new value
300
+ * there and keeps the caret where it was. The host's own write-back follows
301
+ * with the same value. Only plain data is written, as the host takes only
302
+ * plain data; a prop the story leaves out stays the component's own.
303
+ *
304
+ * @param props - The story's root props, its events' recorders in place.
305
+ * @param bindings - The two-way props and the events they report through.
306
+ * @param write - Takes a two-way prop's new value.
307
+ * @returns The props, each given two-way prop's change event writing back.
308
+ * @example
309
+ * ```ts
310
+ * withWriteBacks(props, [{ prop: 'value', changeProp: 'onValueChange' }], (prop, value) =>
311
+ * setProps((current) => ({ ...current, [prop]: value }))
312
+ * );
313
+ * ```
314
+ */
315
+ export function withWriteBacks(
316
+ props: Record<string, unknown>,
317
+ bindings: readonly PreviewWriteBack[] = [],
318
+ write: (prop: string, value: unknown) => void
319
+ ): Record<string, unknown> {
320
+ // The given two-way props by the change prop they report through: one
321
+ // change prop may report several, each written back.
322
+ const byChangeProp = new Map<string, string[]>();
323
+ for (const { prop, changeProp } of bindings) {
324
+ if (props[prop] === undefined || changeProp === prop) continue;
325
+ byChangeProp.set(changeProp, [
326
+ ...(byChangeProp.get(changeProp) ?? []),
327
+ prop,
328
+ ]);
329
+ }
330
+ if (byChangeProp.size === 0) return props;
331
+ const next = { ...props };
332
+ for (const [changeProp, written] of byChangeProp) {
333
+ const own = props[changeProp];
334
+ next[changeProp] = (...parameters: unknown[]) => {
335
+ const copy = plainValue(parameters[0]);
336
+ if ('value' in copy) for (const prop of written) write(prop, copy.value);
337
+ return typeof own === 'function' ? own(...parameters) : undefined;
338
+ };
339
+ }
340
+ return next;
341
+ }
342
+
343
+ /**
344
+ * The values a pane has written back itself (see {@link withWriteBacks}) and
345
+ * the host has not yet answered, per two-way prop, oldest first. Each write
346
+ * gets a number, counting up for the life of the pane, which the next event
347
+ * the pane reports carries (`write`); each props message from the host
348
+ * names the newest write it had taken when it sent them (`answers`). Props
349
+ * the host sent before it took a write hold an older value for that prop,
350
+ * whether they answer the write's own echo or a change of another control,
351
+ * so `reconcile` keeps the pane's newest value over them.
352
+ *
353
+ * @example
354
+ * ```ts
355
+ * const pending = new PendingWriteBacks();
356
+ * pending.wrote('value', 'ab'); // 1
357
+ * pending.wrote('value', 'abc'); // 2
358
+ * pending.reconcile({ value: 'ab' }, 1); // { value: 'abc' }, still waiting for 2
359
+ * pending.reconcile({ value: 'abc' }, 2); // { value: 'abc' }, nothing waiting
360
+ * ```
361
+ */
362
+ export class PendingWriteBacks {
363
+ private readonly pending = new Map<
364
+ string,
365
+ { write: number; value: unknown }[]
366
+ >();
367
+ private count = 0;
368
+ private unreported: number | undefined;
369
+
370
+ /**
371
+ * Note a value the pane has written back for a prop.
372
+ *
373
+ * @param prop - The two-way prop.
374
+ * @param value - The value, plain data.
375
+ * @returns The write's number.
376
+ */
377
+ wrote(prop: string, value: unknown): number {
378
+ this.count += 1;
379
+ const values = this.pending.get(prop) ?? [];
380
+ values.push({ write: this.count, value });
381
+ this.pending.set(prop, values);
382
+ this.unreported = this.count;
383
+ return this.count;
384
+ }
385
+
386
+ /**
387
+ * The number of the newest write since the last call, for the event the
388
+ * pane reports next to carry; `undefined` when there has been none.
389
+ *
390
+ * @returns The write's number, or `undefined`.
391
+ */
392
+ takeUnreported(): number | undefined {
393
+ const write = this.unreported;
394
+ this.unreported = undefined;
395
+ return write;
396
+ }
397
+
398
+ /**
399
+ * The host's props as the pane takes them. The writes up to `answers` are
400
+ * done with: the host had them when it sent the props. A two-way prop the
401
+ * host gives with a later write still waiting keeps the newest value the
402
+ * pane wrote. Without `answers` the props are taken as they are, and
403
+ * every write is done with.
404
+ *
405
+ * @param props - The props the host sends.
406
+ * @param answers - The newest of the pane's writes the host had taken.
407
+ * @returns The props to take.
408
+ */
409
+ reconcile(
410
+ props: Record<string, unknown>,
411
+ answers?: number
412
+ ): Record<string, unknown> {
413
+ if (answers === undefined) {
414
+ this.pending.clear();
415
+ return props;
416
+ }
417
+ let taken = props;
418
+ for (const [prop, values] of this.pending) {
419
+ const waiting = values.filter((written) => written.write > answers);
420
+ if (waiting.length === 0) {
421
+ this.pending.delete(prop);
422
+ continue;
423
+ }
424
+ this.pending.set(prop, waiting);
425
+ // A prop the host leaves out is no longer given: the pane writes it
426
+ // back no more, and the component keeps its own value.
427
+ if (props[prop] === undefined) continue;
428
+ taken = { ...taken, [prop]: waiting[waiting.length - 1].value };
429
+ }
430
+ return taken;
431
+ }
432
+
433
+ /** Forget every write, as a new story starts. The count goes on. */
434
+ clear(): void {
435
+ this.pending.clear();
436
+ this.unreported = undefined;
437
+ }
438
+ }
439
+
440
+ /**
441
+ * The story's props with a recorder for each of its events: a function that
442
+ * reports its call to the host as a `preview:event`, the event's name and a
443
+ * readable summary of what it was called with. A function the props hold
444
+ * already for an event is called too.
445
+ *
446
+ * @param props - The story's root props.
447
+ * @param events - The function props to record the calls of.
448
+ * @param target - The pane's window.
449
+ * @param mount - The `mount` of the set-story message the pane shows, which
450
+ * each report carries.
451
+ * @param writes - The pane's own write-backs: a report carries the number of
452
+ * the newest write since the last one (`write`), for the host to answer.
453
+ * @returns The props, an event's recorder in its place.
454
+ */
455
+ export function withEventRecorders(
456
+ props: Record<string, unknown>,
457
+ events: readonly string[] = [],
458
+ target: Window = window,
459
+ mount?: number,
460
+ writes?: PendingWriteBacks
461
+ ): Record<string, unknown> {
462
+ if (events.length === 0) return props;
463
+ const recorded = { ...props };
464
+ for (const name of events) {
465
+ const own = props[name];
466
+ recorded[name] = (...parameters: unknown[]) => {
467
+ const write = writes?.takeUnreported();
468
+ const report: PreviewEventMessage = {
469
+ type: PREVIEW_MESSAGE.event,
470
+ name,
471
+ payload: summarizeArguments(parameters),
472
+ ...plainValue(parameters[0]),
473
+ ...(mount !== undefined ? { mount } : {}),
474
+ ...(write !== undefined ? { write } : {}),
475
+ };
476
+ target.parent?.postMessage(report, '*');
477
+ return typeof own === 'function' ? own(...parameters) : undefined;
478
+ };
479
+ }
480
+ return recorded;
481
+ }
482
+
483
+ /** An event's first argument as `{ value }`, a copy, when it is plain data:
484
+ * `null`, a boolean, a finite number, a string, or an array or plain object
485
+ * of these. Anything else, such as a browser event or an element, gives
486
+ * nothing. */
487
+ function plainValue(value: unknown): { value?: unknown } {
488
+ return isPlainData(value, 0)
489
+ ? { value: JSON.parse(JSON.stringify(value)) }
490
+ : {};
491
+ }
492
+
493
+ /**
494
+ * Whether a value is plain data, which survives `postMessage` and JSON as it
495
+ * is: `null`, a boolean, a finite number, a string, or an array or plain
496
+ * object of these.
497
+ *
498
+ * @param value - The value.
499
+ * @param depth - How deep the value sits in the one being read.
500
+ * @returns `true` when the value is plain data.
501
+ */
502
+ export function isPlainData(value: unknown, depth = 0): boolean {
503
+ if (value === null || typeof value === 'string' || typeof value === 'boolean')
504
+ return true;
505
+ if (typeof value === 'number') return Number.isFinite(value);
506
+ if (typeof value !== 'object' || depth > 32) return false;
507
+ if (Array.isArray(value))
508
+ return value.every((entry) => isPlainData(entry, depth + 1));
509
+ const prototype = Object.getPrototypeOf(value);
510
+ if (prototype !== Object.prototype && prototype !== null) return false;
511
+ return Object.values(value).every((entry) => isPlainData(entry, depth + 1));
512
+ }
513
+
514
+ /** A readable, one-line summary of what an event was called with. */
515
+ export function summarizeArguments(parameters: unknown[]): string {
516
+ return parameters.map(summarizeValue).join(', ');
517
+ }
518
+
519
+ function summarizeValue(value: unknown): string {
520
+ if (value === undefined) return 'undefined';
521
+ if (typeof value === 'function') return 'function';
522
+ if (typeof Event !== 'undefined' && value instanceof Event)
523
+ return `${value.constructor.name} ${value.type}`;
524
+ // React hands a handler its own wrapper of the browser's event, which
525
+ // holds the event itself: summarized alike, it reads as Vue's and
526
+ // Svelte's do.
527
+ if (
528
+ typeof Event !== 'undefined' &&
529
+ typeof value === 'object' &&
530
+ value !== null &&
531
+ (value as { nativeEvent?: unknown }).nativeEvent instanceof Event
532
+ )
533
+ return summarizeValue((value as { nativeEvent: Event }).nativeEvent);
534
+ if (typeof Element !== 'undefined' && value instanceof Element)
535
+ return `<${value.tagName.toLowerCase()}>`;
536
+ try {
537
+ const text = JSON.stringify(value);
538
+ return text.length > 200 ? `${text.slice(0, 197)}...` : text;
539
+ } catch {
540
+ // An object JSON cannot hold, such as one that refers to itself, by
541
+ // the name of what made it.
542
+ return typeof value === 'object' && value !== null
543
+ ? (value.constructor?.name ?? 'Object')
544
+ : String(value);
545
+ }
546
+ }
547
+
548
+ /**
549
+ * @param props - The mounted story's new root props.
550
+ * @param answers - The newest of the receiving pane's write-backs the host
551
+ * has taken.
552
+ * @returns A {@link PreviewPropsMessage}.
553
+ */
554
+ export function propsMessage(
555
+ props: Record<string, unknown>,
556
+ answers?: number
557
+ ): PreviewPropsMessage {
558
+ return {
559
+ type: PREVIEW_MESSAGE.props,
560
+ props,
561
+ ...(answers !== undefined ? { answers } : {}),
562
+ };
563
+ }
564
+
565
+ /**
566
+ * @param options - The option values, keyed by option id.
567
+ * @returns A {@link PreviewGlobalsMessage}.
568
+ */
569
+ export function globalsMessage(
570
+ options: Record<string, OptionValue>
571
+ ): PreviewGlobalsMessage {
572
+ return { type: PREVIEW_MESSAGE.globals, options };
573
+ }
574
+
575
+ /**
576
+ * @param catalog - The kit's name, sources and browseable stories.
577
+ * @returns A {@link PreviewCatalogMessage}.
578
+ */
579
+ export function catalogMessage(
580
+ catalog: PreviewCatalogPayload
581
+ ): PreviewCatalogMessage {
582
+ return { type: PREVIEW_MESSAGE.catalog, ...catalog };
583
+ }
584
+
585
+ /**
586
+ * @param id - The component story to open.
587
+ * @param props - Root props to open it with, over the component's defaults.
588
+ * @returns A {@link PreviewSelectMessage}.
589
+ */
590
+ export function selectMessage(
591
+ id: string,
592
+ props?: Record<string, unknown>
593
+ ): PreviewSelectMessage {
594
+ return { type: PREVIEW_MESSAGE.select, id, props };
595
+ }
596
+
597
+ /**
598
+ * @param content - The target's source, its language, or why it has none.
599
+ * @returns A {@link PreviewCodeMessage}.
600
+ */
601
+ export function codeMessage(content: CodeBlockContent): PreviewCodeMessage {
602
+ return { type: PREVIEW_MESSAGE.code, ...content };
603
+ }
604
+
605
+ /**
606
+ * Tell the host what the iframe's story threw as it rendered, or, with no
607
+ * message, that the story mounted since renders.
608
+ *
609
+ * @param message - The error's message; omitted when a new story mounts.
610
+ * @param target - The iframe's window.
611
+ */
612
+ export function reportRenderError(
613
+ message?: string,
614
+ target: Window = window
615
+ ): void {
616
+ const report: PreviewRenderErrorMessage = {
617
+ type: PREVIEW_MESSAGE.renderError,
618
+ };
619
+ if (message !== undefined) report.message = message;
620
+ (target as unknown as Record<string, unknown>)[PREVIEW_RENDER_ERROR_FLAG] =
621
+ message ?? null;
622
+ target.parent?.postMessage(report, '*');
623
+ }
624
+
625
+ /**
626
+ * @param size - The content's width and height in CSS pixels.
627
+ * @returns A {@link PreviewContentSizeMessage}.
628
+ */
629
+ export function contentSizeMessage(
630
+ size: PreviewContentSize
631
+ ): PreviewContentSizeMessage {
632
+ return {
633
+ type: PREVIEW_MESSAGE.contentSize,
634
+ width: size.width,
635
+ height: size.height,
636
+ };
637
+ }
638
+
639
+ /**
640
+ * @param story - The markup to show, the reason there is none, or neither
641
+ * while it is being printed.
642
+ * @returns A {@link PreviewMarkupMessage}.
643
+ */
644
+ export function markupMessage(
645
+ story:
646
+ | { markup: string; stylesheet?: string }
647
+ | { reason: string }
648
+ | Record<string, never>
649
+ ): PreviewMarkupMessage {
650
+ return { type: PREVIEW_MESSAGE.markup, ...story };
651
+ }
652
+
653
+ /**
654
+ * Read posted data as a content size: its width and height when both are
655
+ * finite numbers of zero or more, otherwise `undefined`.
656
+ *
657
+ * @param message - A {@link PreviewContentSizeMessage} as it was received.
658
+ * @returns The size, or `undefined` for a malformed message.
659
+ */
660
+ export function readContentSize(
661
+ message: PreviewContentSizeMessage
662
+ ): PreviewContentSize | undefined {
663
+ const { width, height } = message as { width?: unknown; height?: unknown };
664
+ const valid = (value: unknown): value is number =>
665
+ typeof value === 'number' && Number.isFinite(value) && value >= 0;
666
+ return valid(width) && valid(height) ? { width, height } : undefined;
667
+ }
668
+
669
+ /** The window property an iframe sets to `true` once it can receive messages,
670
+ * for a test that loads the iframe's page on its own (where the ready message
671
+ * has no parent to reach). */
672
+ export const PREVIEW_READY_FLAG = '__previewReady';
673
+
674
+ /** The window property a story iframe sets to the ids of the stories it can
675
+ * mount, for a test helper to reject an id that would mount nothing. */
676
+ export const PREVIEW_STORIES_FLAG = '__previewStories';
677
+
678
+ /** The window property a preview iframe sets to the kit's options, each with
679
+ * the values a choice option offers, for a test helper to reject an id or a
680
+ * value the iframe would ignore. */
681
+ export const PREVIEW_OPTIONS_FLAG = '__previewOptions';
682
+
683
+ /** The window property a story iframe sets to the message of what its story
684
+ * threw as it rendered, or to `null` while the story mounted since renders,
685
+ * for a test that loads the iframe's page on its own (where the report has no
686
+ * parent to reach). The message is a string, and may be empty. */
687
+ export const PREVIEW_RENDER_ERROR_FLAG = '__previewRenderError';
688
+
689
+ /** The window property a story iframe sets to the id of the story it last
690
+ * finished mounting, or to the empty string once a story has unmounted with
691
+ * none set after it. Set after the story's first render and its effects, so
692
+ * a test that loads the iframe's page on its own can tell a story that
693
+ * rendered nothing from one still mounting. */
694
+ export const PREVIEW_MOUNTED_STORY_FLAG = '__previewMountedStory';
695
+
696
+ /** The id of the element each preview iframe mounts its story or view in. */
697
+ export const PREVIEW_MOUNT_ELEMENT_ID = 'app';
698
+
699
+ /**
700
+ * Announce that an iframe shows its first content: post a
701
+ * {@link PreviewShownMessage} to the parent window once the content mounted
702
+ * now has painted, two animation frames on. Call it after the iframe mounts
703
+ * the first content the host sends; a later call in the same document sends
704
+ * nothing.
705
+ *
706
+ * @param target - The iframe's window.
707
+ */
708
+ export function announcePreviewShown(target: Window = window): void {
709
+ const flags = target as unknown as Record<string, unknown>;
710
+ if (flags[PREVIEW_SHOWN_FLAG]) return;
711
+ flags[PREVIEW_SHOWN_FLAG] = true;
712
+ target.requestAnimationFrame(() =>
713
+ target.requestAnimationFrame(() => {
714
+ if (target.parent !== target)
715
+ target.parent.postMessage({ type: PREVIEW_MESSAGE.shown }, '*');
716
+ })
717
+ );
718
+ }
719
+
720
+ /**
721
+ * Record under {@link PREVIEW_MOUNTED_STORY_FLAG} that the iframe has finished
722
+ * mounting a story. Call it once the framework has committed the mount,
723
+ * whatever the story rendered.
724
+ *
725
+ * @param id - The story's id, or the empty string after an unmount.
726
+ * @param target - The iframe's window.
727
+ */
728
+ export function announceStoryMounted(
729
+ id: string,
730
+ target: Window = window
731
+ ): void {
732
+ (target as unknown as Record<string, unknown>)[PREVIEW_MOUNTED_STORY_FLAG] =
733
+ id;
734
+ }
735
+
736
+ /** Set on an iframe's window once it has announced its first content. */
737
+ const PREVIEW_SHOWN_FLAG = '__previewShown';
738
+
739
+ /**
740
+ * List the stories an iframe can mount under {@link PREVIEW_STORIES_FLAG}.
741
+ * Call it before {@link announcePreviewReady}, so a test that waits for the
742
+ * iframe finds the list.
743
+ *
744
+ * @param ids - The ids of the stories.
745
+ * @param target - The iframe's window.
746
+ */
747
+ export function announcePreviewStories(
748
+ ids: readonly string[],
749
+ target: Window = window
750
+ ): void {
751
+ (target as unknown as Record<string, unknown>)[PREVIEW_STORIES_FLAG] = [
752
+ ...ids,
753
+ ];
754
+ }
755
+
756
+ /** An option as a preview iframe lists it: its id and, for a choice, the
757
+ * values it offers; a boolean option has no `values`. */
758
+ export interface PreviewListedOption {
759
+ /** The option's id. */
760
+ id: string;
761
+ /** The values of a choice option. */
762
+ values?: string[];
763
+ }
764
+
765
+ /**
766
+ * List the kit's options under {@link PREVIEW_OPTIONS_FLAG}.
767
+ *
768
+ * @param options - The options.
769
+ * @param target - The iframe's window.
770
+ */
771
+ export function announcePreviewOptions(
772
+ options: readonly Pick<PreviewOption, 'id' | 'values'>[],
773
+ target: Window = window
774
+ ): void {
775
+ (target as unknown as Record<string, unknown>)[PREVIEW_OPTIONS_FLAG] =
776
+ options.map(
777
+ ({ id, values }): PreviewListedOption =>
778
+ values ? { id, values: values.map((choice) => choice.value) } : { id }
779
+ );
780
+ }
781
+
782
+ /**
783
+ * Announce that an iframe listens for messages: set {@link PREVIEW_READY_FLAG}
784
+ * and post a {@link PreviewReadyMessage} to the parent window. Call it right
785
+ * after attaching the message listener.
786
+ *
787
+ * @param target - The iframe's window.
788
+ */
789
+ export function announcePreviewReady(target: Window = window): void {
790
+ (target as unknown as Record<string, unknown>)[PREVIEW_READY_FLAG] = true;
791
+ // A pane opened on its own, with no host, gives itself the story its
792
+ // address names, as a host would.
793
+ const standalone =
794
+ target.parent === target ? standaloneMessages(target.location.search) : [];
795
+ if (standalone.length > 0) {
796
+ for (const message of standalone) target.postMessage(message, '*');
797
+ return;
798
+ }
799
+ target.parent?.postMessage(readyMessage(), '*');
800
+ }
801
+
802
+ /** The pixel value of a computed length such as `12px`; `0` for anything else. */
803
+ function pixels(value: string): number {
804
+ const number = parseFloat(value);
805
+ return Number.isFinite(number) ? number : 0;
806
+ }
807
+
808
+ /** Whether the mount's height may follow the layout around it rather than its
809
+ * content: it holds a minimum height of its own, or it is a flex or grid
810
+ * item, which the container may stretch. */
811
+ function heightFollowsLayout(mount: HTMLElement, view: Window): boolean {
812
+ if (pixels(view.getComputedStyle(mount).minHeight) > 0) return true;
813
+ const parent = mount.parentElement;
814
+ if (!parent) return false;
815
+ const display = view.getComputedStyle(parent).display;
816
+ return /flex|grid/.test(display);
817
+ }
818
+
819
+ /** The height the mount's children span, each with its vertical margins:
820
+ * fixed-position children overlay the page and are left out. */
821
+ function childrenHeight(mount: HTMLElement, view: Window): number {
822
+ let top = Infinity;
823
+ let bottom = -Infinity;
824
+ const range = mount.ownerDocument.createRange();
825
+ for (const node of mount.childNodes) {
826
+ if (node.nodeType === 1) {
827
+ const element = node as Element;
828
+ const style = view.getComputedStyle(element);
829
+ if (style.display === 'none' || style.position === 'fixed') continue;
830
+ const rectangle = element.getBoundingClientRect();
831
+ top = Math.min(top, rectangle.top - pixels(style.marginTop));
832
+ bottom = Math.max(bottom, rectangle.bottom + pixels(style.marginBottom));
833
+ } else if (node.nodeType === 3 && node.textContent?.trim()) {
834
+ range.selectNodeContents(node);
835
+ const rectangle = range.getBoundingClientRect();
836
+ top = Math.min(top, rectangle.top);
837
+ bottom = Math.max(bottom, rectangle.bottom);
838
+ }
839
+ }
840
+ return bottom > top ? bottom - top : 0;
841
+ }
842
+
843
+ /**
844
+ * Measure what an iframe shows: the mount element's own box, with the
845
+ * padding, borders and margins of the mount and every element around it up
846
+ * to the body, rounded up to whole CSS pixels. Nothing in it depends on the
847
+ * height of the iframe, so a host that sizes the iframe to it does not make
848
+ * it grow: a wrapper's `min-height: 100vh` adds nothing but its padding.
849
+ * Where the mount's own height may follow the layout around it (a minimum
850
+ * height of its own, or a flex or grid item), its children's extent stands
851
+ * in for it.
852
+ *
853
+ * @param mount - The element the story or view is mounted in.
854
+ * @returns The content's width and height.
855
+ */
856
+ export function measureContentSize(mount: HTMLElement): PreviewContentSize {
857
+ const view = mount.ownerDocument.defaultView;
858
+ if (!view) return { width: 0, height: 0 };
859
+ const box = mount.getBoundingClientRect();
860
+ const mountStyle = view.getComputedStyle(mount);
861
+ let width = box.width;
862
+ let height = heightFollowsLayout(mount, view)
863
+ ? childrenHeight(mount, view) +
864
+ pixels(mountStyle.paddingTop) +
865
+ pixels(mountStyle.paddingBottom) +
866
+ pixels(mountStyle.borderTopWidth) +
867
+ pixels(mountStyle.borderBottomWidth)
868
+ : box.height;
869
+ const root = mount.ownerDocument.documentElement;
870
+ for (
871
+ let element: HTMLElement | null = mount;
872
+ element && element !== root;
873
+ element = element.parentElement
874
+ ) {
875
+ const style = view.getComputedStyle(element);
876
+ height += pixels(style.marginTop) + pixels(style.marginBottom);
877
+ width += pixels(style.marginLeft) + pixels(style.marginRight);
878
+ if (element === mount) continue;
879
+ height +=
880
+ pixels(style.paddingTop) +
881
+ pixels(style.paddingBottom) +
882
+ pixels(style.borderTopWidth) +
883
+ pixels(style.borderBottomWidth);
884
+ width +=
885
+ pixels(style.paddingLeft) +
886
+ pixels(style.paddingRight) +
887
+ pixels(style.borderLeftWidth) +
888
+ pixels(style.borderRightWidth);
889
+ }
890
+ return { width: Math.ceil(width), height: Math.ceil(height) };
891
+ }
892
+
893
+ const reporting = new WeakMap<HTMLElement, () => void>();
894
+
895
+ /**
896
+ * Report the size of what an iframe shows to the parent window as a
897
+ * {@link PreviewContentSizeMessage}: once now, and again whenever it
898
+ * changes, at most once per animation frame and only when the size differs
899
+ * from the last one reported. It watches the mount, its children and the
900
+ * elements around it for a change of size, the mount's content for a change
901
+ * of nodes or text, the elements around it for a change of class or style,
902
+ * and the window for a resize. Call it once the message listener is
903
+ * attached; a second call for the same mount returns the first one's stop.
904
+ *
905
+ * @param mount - The element the story or view is mounted in.
906
+ * @param target - The iframe's window.
907
+ * @returns A function that stops the reports.
908
+ */
909
+ export function reportContentSize(
910
+ mount: HTMLElement,
911
+ target: Window = window
912
+ ): () => void {
913
+ const running = reporting.get(mount);
914
+ if (running) return running;
915
+ const root = mount.ownerDocument.documentElement;
916
+ let pending: number | undefined;
917
+ let last = '';
918
+
919
+ function report(): void {
920
+ pending = undefined;
921
+ const size = measureContentSize(mount);
922
+ const key = `${size.width}x${size.height}`;
923
+ if (key === last) return;
924
+ last = key;
925
+ target.parent?.postMessage(contentSizeMessage(size), '*');
926
+ }
927
+
928
+ function schedule(): void {
929
+ if (pending === undefined) pending = target.requestAnimationFrame(report);
930
+ }
931
+
932
+ const observed = new Set<Element>();
933
+ const resizeObserver =
934
+ typeof ResizeObserver === 'function'
935
+ ? new ResizeObserver(schedule)
936
+ : undefined;
937
+ const surroundings = new MutationObserver(schedule);
938
+
939
+ function observeElements(): void {
940
+ const elements: Element[] = [...mount.children];
941
+ for (
942
+ let element: HTMLElement | null = mount;
943
+ element && element !== root;
944
+ element = element.parentElement
945
+ ) {
946
+ elements.push(element);
947
+ }
948
+ for (const element of elements) {
949
+ if (observed.has(element)) continue;
950
+ observed.add(element);
951
+ resizeObserver?.observe(element, { box: 'border-box' });
952
+ }
953
+ for (const element of [...observed]) {
954
+ if (elements.includes(element)) continue;
955
+ observed.delete(element);
956
+ resizeObserver?.unobserve(element);
957
+ }
958
+ }
959
+
960
+ const content = new MutationObserver(() => {
961
+ observeElements();
962
+ schedule();
963
+ });
964
+ content.observe(mount, {
965
+ childList: true,
966
+ subtree: true,
967
+ characterData: true,
968
+ });
969
+ for (
970
+ let element: HTMLElement | null = mount;
971
+ element;
972
+ element = element.parentElement
973
+ ) {
974
+ surroundings.observe(element, {
975
+ attributes: true,
976
+ attributeFilter: ['class', 'style'],
977
+ });
978
+ }
979
+ observeElements();
980
+ target.addEventListener('resize', schedule);
981
+ schedule();
982
+
983
+ const stop = (): void => {
984
+ if (pending !== undefined) target.cancelAnimationFrame(pending);
985
+ pending = undefined;
986
+ resizeObserver?.disconnect();
987
+ content.disconnect();
988
+ surroundings.disconnect();
989
+ target.removeEventListener('resize', schedule);
990
+ reporting.delete(mount);
991
+ };
992
+ reporting.set(mount, stop);
993
+ return stop;
994
+ }
995
+
996
+ /** The query parameter that names the view the view iframe renders. */
997
+ export const VIEW_PARAMETER = 'view';
998
+
999
+ /** The query parameter that names the framework of the view the view iframe
1000
+ * renders, so the iframe loads that framework's setup module before the view. */
1001
+ export const VIEW_FRAMEWORK_PARAMETER = 'framework';
1002
+
1003
+ /** The query parameter that names the target whose source a code iframe
1004
+ * shows. */
1005
+ export const CODE_TARGET_PARAMETER = 'target';
1006
+
1007
+ /** The query parameter a component iframe's URL carries so that changing it
1008
+ * reloads the iframe. */
1009
+ export const RELOAD_PARAMETER = 'r';
1010
+
1011
+ /**
1012
+ * The URL of a story pane's iframe: a target's, or the html one's.
1013
+ *
1014
+ * @param target - What the iframe renders in.
1015
+ * @param reload - A counter whose change reloads the iframe; omitted, the URL
1016
+ * carries no reload parameter.
1017
+ * @param baseUrl - The URL the preview is served under, ending in `/`; the
1018
+ * root of the origin, `/`, by default.
1019
+ * @returns The page's URL, such as `/preview/vue/preview.html?r=0`.
1020
+ */
1021
+ export function componentFrameUrl(
1022
+ target: PreviewTarget,
1023
+ reload?: number,
1024
+ baseUrl = '/'
1025
+ ): string {
1026
+ const path = `${baseUrl}preview/${target}/preview.html`;
1027
+ return reload === undefined ? path : `${path}?${RELOAD_PARAMETER}=${reload}`;
1028
+ }
1029
+
1030
+ /** The query parameters a target's pane, opened on its own, reads its
1031
+ * story from: the story's id, its root props and the option values, the
1032
+ * latter two as JSON. */
1033
+ export const STANDALONE_PARAMETERS = {
1034
+ story: 'story',
1035
+ props: 'props',
1036
+ options: 'options',
1037
+ } as const;
1038
+
1039
+ /**
1040
+ * The URL of a target's pane opened on its own, outside the preview: it
1041
+ * shows `story` with its props under its option values, as the preview's pane
1042
+ * does, with no host to follow.
1043
+ *
1044
+ * @param target - The target to render the story in.
1045
+ * @param story - The story's id, root props and option values.
1046
+ * @param baseUrl - The URL the preview is served under, ending in `/`.
1047
+ * @returns The pane's URL.
1048
+ */
1049
+ export function standaloneFrameUrl(
1050
+ target: PreviewScriptedTarget,
1051
+ story: {
1052
+ id: string;
1053
+ props: Record<string, unknown>;
1054
+ options: Record<string, OptionValue>;
1055
+ },
1056
+ baseUrl = '/'
1057
+ ): string {
1058
+ const query = new URLSearchParams({
1059
+ [STANDALONE_PARAMETERS.story]: story.id,
1060
+ [STANDALONE_PARAMETERS.props]: JSON.stringify(story.props),
1061
+ [STANDALONE_PARAMETERS.options]: JSON.stringify(story.options),
1062
+ });
1063
+ return `${componentFrameUrl(target, undefined, baseUrl)}?${query}`;
1064
+ }
1065
+
1066
+ /**
1067
+ * The messages a pane opened on its own gives itself, from its address: the
1068
+ * story to mount and the option values. None when the address names no story.
1069
+ *
1070
+ * @param search - The pane's `location.search`.
1071
+ * @returns A set-story and a globals message, or none.
1072
+ */
1073
+ export function standaloneMessages(search: string): PreviewMessage[] {
1074
+ const query = new URLSearchParams(search);
1075
+ const id = query.get(STANDALONE_PARAMETERS.story);
1076
+ if (!id) return [];
1077
+ const parsed = (name: string): Record<string, unknown> => {
1078
+ try {
1079
+ const value: unknown = JSON.parse(query.get(name) ?? '{}');
1080
+ return value && typeof value === 'object' && !Array.isArray(value)
1081
+ ? (value as Record<string, unknown>)
1082
+ : {};
1083
+ } catch {
1084
+ return {};
1085
+ }
1086
+ };
1087
+ return [
1088
+ setStoryMessage(id, parsed(STANDALONE_PARAMETERS.props)),
1089
+ globalsMessage(
1090
+ parsed(STANDALONE_PARAMETERS.options) as Record<string, OptionValue>
1091
+ ),
1092
+ ];
1093
+ }
1094
+
1095
+ /**
1096
+ * The URL of a code iframe: the kit's code block, showing one target's
1097
+ * source.
1098
+ *
1099
+ * @param target - The target whose source the block shows.
1100
+ * @param framework - The block's framework; a framework other than `'dom'`
1101
+ * is named in the URL, so the iframe loads its setup module first.
1102
+ * @param baseUrl - The URL the preview is served under, ending in `/`.
1103
+ * @returns The page's URL, such as `/preview/code/code.html?target=vue`.
1104
+ */
1105
+ export function codeFrameUrl(
1106
+ target: PreviewTarget,
1107
+ framework: ViewFramework = 'dom',
1108
+ baseUrl = '/'
1109
+ ): string {
1110
+ const path = `${baseUrl}preview/code/code.html?${CODE_TARGET_PARAMETER}=${target}`;
1111
+ return framework === 'dom'
1112
+ ? path
1113
+ : `${path}&${VIEW_FRAMEWORK_PARAMETER}=${framework}`;
1114
+ }
1115
+
1116
+ /**
1117
+ * The URL of the view iframe rendering one custom view.
1118
+ *
1119
+ * @param viewId - The view's id.
1120
+ * @param framework - The view's framework; a framework other than `'dom'` is
1121
+ * named in the URL, so the iframe loads its setup module ahead of the views.
1122
+ * @param baseUrl - The URL the preview is served under, ending in `/`; the
1123
+ * root of the origin, `/`, by default.
1124
+ * @returns The page's URL, such as `/preview/view/view.html?view=tokens`.
1125
+ */
1126
+ export function viewFrameUrl(
1127
+ viewId: string,
1128
+ framework: ViewFramework = 'dom',
1129
+ baseUrl = '/'
1130
+ ): string {
1131
+ const path = `${baseUrl}preview/view/view.html?${VIEW_PARAMETER}=${viewId}`;
1132
+ return framework === 'dom'
1133
+ ? path
1134
+ : `${path}&${VIEW_FRAMEWORK_PARAMETER}=${framework}`;
1135
+ }